Skip to content

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.

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.
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

If you’d rather skip the wizard’s setup step, copy .env.example to .env and generate the secrets yourself:

Terminal window
# 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>"