Configuration
Xentium reads one .env file at the repo root. Normally you never touch it: the wizard
writes it on first run (with mode 0600). This section is for when you need to, e.g. in
CI, in a prebuilt container, or when you restore a config.
How the API decides what to boot: if a required variable is missing, it starts in
setup mode, a small server that only serves the install wizard. As soon as the wizard
has written the config, the full app starts in the same process on the same port. If the
variables are there, it boots normally. We validate the config with Zod at startup
(apps/api/src/config/env.ts), and the API refuses to run on an invalid one.
Required
Section titled “Required”| Variable | Notes |
|---|---|
DATABASE_URL |
e.g. postgresql://xentium:xentium@localhost:5432/xentium (matches our Docker defaults) |
COOKIE_SECRET |
at least 16 characters; signs cookies |
JWT_SECRET |
at least 32 characters; signs the HS256 access tokens |
DUMMY_ARGON2_HASH |
a throwaway argon2 hash. We verify against it when a login email doesn’t exist, so a failed login takes the same time either way and nobody can probe which accounts exist. |
Optional
Section titled “Optional”| Variable | Default |
|---|---|
NODE_ENV |
production. Development mode is opt-in: ./xentium.sh sets it, or put NODE_ENV=development in .env. |
PORT |
4000 |
APP_URL |
http://localhost:5173 |
REDIS_URL |
redis://localhost:6379 |
REDIS_KEY_PREFIX |
xentium: |
REFRESH_TOKEN_HMAC_SECRET |
falls back to JWT_SECRET if unset (the wizard generates its own) |
UPLOAD_DIR |
./var/uploads, relative to where the API process runs |
BOOTSTRAP_OFFICIAL_PLUGINS |
false. Set it to true to install the first-party plugins from plugins/official/ on startup; otherwise you install them from the ACP. |
LICENSE_SERVER_URL, LICENSE_*_KEY |
only needed for marketplace and licensing |
Manual setup
Section titled “Manual setup”If you’d rather skip the wizard’s setup step, copy .env.example to .env and generate
the secrets yourself:
# COOKIE_SECRET (>=16) and JWT_SECRET (>=32)echo "COOKIE_SECRET=$(openssl rand -hex 16)"echo "JWT_SECRET=$(openssl rand -hex 32)"
# DUMMY_ARGON2_HASH (any valid argon2 hash works)pnpm --filter @xentium/api exec node --input-type=commonjs \ -e "require('argon2').hash('xentium-dummy').then(h=>console.log('DUMMY_ARGON2_HASH='+h))"A complete dev .env:
NODE_ENV="development"DATABASE_URL="postgresql://xentium:xentium@localhost:5432/xentium"REDIS_URL="redis://localhost:6379"APP_URL="http://localhost:5173"COOKIE_SECRET="<openssl rand -hex 16>"JWT_SECRET="<openssl rand -hex 32>"DUMMY_ARGON2_HASH="<argon2 hash from the command above>"