Configuration
Every setting lives in an environment variable using Nuxt's runtime-config convention:
NUXT_<KEY>— server-only, never sent to the browserNUXT_PUBLIC_<KEY>— exposed to the client
No rebuild is needed to change config — the app reads these at startup.
Configurator in the admin UI
Logged-in admins can view and export the running instance's env-var mapping at /admin/configurator — pre-filled values, one-click switch between docker-compose.yml, .env, and shell export formats, secrets redacted. See Admin panel → Configurator.
Required
| Variable | Description |
|---|---|
NUXT_DATABASE_URL | PostgreSQL: postgresql://… — SQLite: sqlite:./data.db or :memory: |
NUXT_ISSUER | OIDC issuer URL, must end with /oidc |
NUXT_APP_URL | Browser-reachable base URL |
NUXT_OIDC_CLIENTS_JSON | JSON array of OIDC client definitions (schema) |
NUXT_RP_ID | WebAuthn relying party domain (no scheme, no port) |
NUXT_RP_NAME | WebAuthn RP display name |
NUXT_RP_ORIGIN | WebAuthn origin URL, full scheme + port |
Optional — core
| Variable | Default | Description |
|---|---|---|
NUXT_JWT_SECRET | auto | ≥ 32 chars, HMAC for session JWTs. Auto-generated and persisted to data/jwt-secret.txt on first start. |
NUXT_TOTP_ENCRYPTION_KEY | auto | Encrypts authenticator-app (TOTP) secrets at rest. Auto-generated to data/totp-encryption-key.txt if unset; back it up with the data dir. Rotating it makes existing TOTP enrolments undecryptable (users must re-enrol). |
NUXT_JWT_PRIVATE_KEY | auto | RSA 2048 PKCS8 PEM. Auto-generated and persisted to data/jwt-private-key.pem. \n-escape for .env. |
NUXT_JWT_PRIVATE_KEY_PREVIOUS | (empty) | Retired signing key kept in the JWKS (verify-only) during a no-downtime key rotation. Same PEM format. Empty = single-key. |
NUXT_TRUST_PROXY | false | Set true behind a reverse proxy that terminates TLS and sets X-Forwarded-*. |
NUXT_COOKIE_SECURE | auto | Defaults to true when NODE_ENV=production. Set false over plain HTTP for local dev. |
NUXT_REGISTRATION_ENABLED | true | Set false to disable new user signups. |
NUXT_AUTO_ADMIN_FIRST_USER | true | First registered user is promoted to admin. |
NUXT_TWO_FACTOR_ENABLED | false | Enables email-code 2FA after password login. Passkeys and authenticator-app (TOTP) work as 2FA too. TOTP is per-user opt-in and needs no env var (it works even with this off / no SMTP). |
NUXT_REQUIRE_TWO_FACTOR | false | Force every account to have a second factor. A user with no usable factor is corralled to /setup-2fa (hard server-enforced gate) until they enrol TOTP or a passkey. Any factor satisfies it — including the emailed code — so forced enrolment only triggers when email-2FA isn't available. See Security → Mandatory two-factor. |
NUXT_REQUIRE_TWO_FACTOR_SCOPE | all | Who the mandatory-2FA gate applies to: all or admins (only admin accounts). Ignored when require-2FA is off. |
NUXT_REQUIRE_TWO_FACTOR_GRACE_DAYS | 0 | New-account grace (days) before the gate applies, measured from account creation. 0 = gate immediately. Accounts older than N days are gated regardless (no bypass window when the policy is enabled). |
NUXT_SUSPICIOUS_LOGIN_ALERTS | false | Email the user a "new sign-in" alert on a login from a new IP or device. Opt-in; requires SMTP. IP accuracy behind a proxy needs NUXT_TRUST_PROXY. See Security → New-sign-in alerts. |
NUXT_SUSPICIOUS_LOGIN_STEP_UP | false | Force an email-code challenge before granting the session when a login is flagged suspicious (new IP/device). Opt-in; requires SMTP. Independent of the alert. See Security → New-sign-in alerts. |
NUXT_APP_ROLES_REQUIRE_GRANT | false | Restrict the app-roles write API (PUT /api/apps/roles): only write for users who already have an OIDC grant with the calling client (else 404, uniform with an unknown user). See OIDC → App-managed roles. |
NUXT_OIDC_RESOURCE_AUDIENCE | (empty) | Comma-separated audience URIs (RFC 8707). Empty = all access tokens stay opaque. When set, a client requesting resource=<audience> gets a JWT access token. See OIDC → JWT access tokens. |
NUXT_OIDC_RESOURCE_SERVERS_JSON | (empty) | Richer JWT-AT resource config: JSON array of { audience, scopes?, accessTokenTTL? } giving a resource its own scope set and/or shorter token TTL (seconds). Merged with (overriding, per audience) NUXT_OIDC_RESOURCE_AUDIENCE. See OIDC → JWT access tokens. |
NUXT_BACKCHANNEL_LOGOUT_RETRY_DELAYS_MS | 2000,15000 | Comma-separated backoff delays (ms) before each back-channel-logout re-attempt after the inline first attempt. Empty = single attempt. Bounded + in-memory (lost on restart). |
NUXT_LOG_LEVEL | info | pino level: fatal error warn info debug trace. |
SMTP (optional — email features degrade when absent)
If NUXT_SMTP_HOST is empty, email verification and 2FA are disabled, users are auto-verified on registration, and password reset via email is unavailable. Useful for local dev without Mailcatcher.
| Variable | Example |
|---|---|
NUXT_SMTP_HOST | smtp.sendgrid.net / mailcatcher |
NUXT_SMTP_PORT | 587 / 1025 |
NUXT_SMTP_SECURE | true for port 465 |
NUXT_SMTP_USER | apikey |
NUXT_SMTP_PASS | <api-key> |
NUXT_SMTP_FROM | Schleuse <noreply@…> |
NUXT_SMTP_REQUIRE_TLS | auto (default) / true / false |
NUXT_SMTP_TLS_REJECT_UNAUTHORIZED | true (default) |
NUXT_EMAIL_RETRY_DELAYS_MS | 30000,120000 (default) |
Transport security
NUXT_SMTP_REQUIRE_TLS decides what happens on a non-465 port:
| Value | Behaviour |
|---|---|
auto (default) | Upgrade to STARTTLS whenever the relay advertises it. If the relay doesn't offer it — or rejects the command — delivery continues unencrypted. Never blocks mail. |
true | Require STARTTLS and a valid certificate. No upgrade means no delivery. |
false | Never attempt STARTTLS, even when advertised. Legacy behaviour; only for a relay that mis-advertises it. |
Ignored when NUXT_SMTP_SECURE=true — implicit TLS encrypts the session from the first byte.
Certificates are validated only where TLS is mandatory (true, or implicit TLS). Under auto they deliberately are not: nodemailer cannot fall back from a failed TLS handshake, so checking there would stop mail for every self-signed relay rather than downgrading. That is the standard opportunistic-security posture (RFC 7435) — encryption without authentication still beats cleartext, and the admin page labels it honestly as certificate not verified.
NUXT_SMTP_TLS_REJECT_UNAUTHORIZED=false relaxes validation in mandatory mode, for a self-hosted relay with a self-signed certificate. The hop stays encrypted against passive capture but is no longer proof against an active man-in-the-middle. Only a literal false disables it — a typo keeps validation on.
Check what your deployment actually negotiated on the admin Email & SMTP page; it reports the measured result, not the configured intent.
NUXT_EMAIL_RETRY_DELAYS_MS controls automatic retry of verification, password-reset and resend emails: a comma-separated list of backoff delays (in milliseconds) before each re-attempt. The default 30000,120000 means three attempts — immediate, then +30 s, then +2 min. Set it to an empty string to disable retry (a single attempt). Retries are in-memory and not durable: pending retries are lost if the process restarts. Time-sensitive 2FA login codes are never retried — a failed code send surfaces immediately so the user can request a fresh one.
Password policy
| Variable | Default | Notes |
|---|---|---|
NUXT_PASSWORD_POLICY | standard | Presets: simple (6+ chars), standard (8+ with upper/lower/number/special), strong (12+ with all) |
NUXT_PASSWORD_MIN_LENGTH | from preset | Override individual rule |
NUXT_PASSWORD_REQUIRE_UPPERCASE | from preset | |
NUXT_PASSWORD_REQUIRE_LOWERCASE | from preset | |
NUXT_PASSWORD_REQUIRE_NUMBER | from preset | |
NUXT_PASSWORD_REQUIRE_SPECIAL | from preset | |
NUXT_PASSWORD_HISTORY_COUNT | 0 | Prevent reuse of the last N passwords on change/reset. 0 disables it. See below. |
Password reuse prevention
Set NUXT_PASSWORD_HISTORY_COUNT=N (an integer ≥ 1) to reject a password change or reset that reuses one of the user's last N passwords — the current password counts as the most recent of the N. For example, 3 blocks the current password and the two before it.
0disables the check entirely, including allowing a change back to the current password.- Default coupling: when
NUXT_PASSWORD_HISTORY_COUNTis left unset, thestrongpassword preset turns reuse prevention on (count3) automatically; other presets default to0. Setting the variable explicitly (including0) always wins. - Only hashed retired passwords are stored (in a
password_historytable, argon2id like the live credential); they are deleted with the account and are never part of the GDPR data export. - Increasing N takes effect going forward; previously pruned history is not recovered.
Rate limiting
Format: maxAttempts/windowMinutes. In-memory per-IP counters (resets on restart — acceptable for a single-instance sidecar).
| Variable | Default |
|---|---|
NUXT_RATE_LIMIT_LOGIN | 10/15 |
NUXT_RATE_LIMIT_REGISTER | 10/60 |
NUXT_RATE_LIMIT_PASSWORD_RESET | 5/60 |
Observability (metrics)
| Variable | Default | Notes |
|---|---|---|
NUXT_METRICS_TOKEN | (unset) | Bearer token that enables GET /metrics (Prometheus). Unset → the endpoint returns 404 (feature off). When set it must be ≥ 32 characters (startup fails otherwise), and every scrape must send Authorization: Bearer <token> (constant-time compared; never logged). Treated as a secret — the Configurator never echoes its value. Rotate by changing the env var and restarting. |
Provisioning API
| Variable | Default | Notes |
|---|---|---|
NUXT_ADMIN_API_TOKENS_JSON | (unset) | JSON array of bearer tokens for the provisioning API: [{"id":"migration-importer","token":"…"}]. Unset or [] → every /api/provisioning/** route returns 404 (feature off). Each token must be ≥ 32 characters and each id a lowercase slug ([a-z0-9_-]); duplicate ids or tokens fail startup. The id is not secret — it labels the caller in the audit log. Treated as a secret: the Configurator never echoes the value. |
The id exists so rotation needs no coordinated restart: add a second entry, deploy, move your importer across, then remove the first — both are accepted in between.
WARNING
This token can create sign-in-capable accounts. Keep the endpoints off the public internet where you can, and remove the token once a migration is finished. It cannot set is_admin. See Migrating users in.
Metrics are in-memory counters/histograms (reset on restart): login success/failure, registrations, 2FA challenges, rate-limit hits, an HTTP request-duration histogram, and standard Node/process metrics. See Deployment for the scrape config and exposure guidance.
Localization
See Multilingual for the full story.
| Variable | Default | Description |
|---|---|---|
NUXT_DEFAULT_LOCALE | en | BCP-47 tag. Must be listed in NUXT_SUPPORTED_LOCALES. |
NUXT_SUPPORTED_LOCALES | en | Comma-separated allow-list (e.g. en,de,fr,it). The UI ships en, de, fr, it; any other tag is email-only. |
Theme
All optional — defaults produce a dark neon-modern look. Variables use NUXT_PUBLIC_THEME_* and are exposed to the client. See the THEMES.md ready-to-use presets.
| Variable | Default | Notes |
|---|---|---|
NUXT_PUBLIC_THEME_NAME | Schleuse | App name shown in the UI and page titles. |
NUXT_PUBLIC_THEME_PRIMARY_COLOR | #06d6a0 | Primary hex colour; the full 50–950 palette is auto-derived via HSL. |
NUXT_PUBLIC_THEME_BACKGROUND_COLOR | #0a0e17 | Page background hex colour. |
NUXT_PUBLIC_THEME_TEXT_COLOR | #f1f5f9 | Base text hex colour. |
NUXT_PUBLIC_THEME_BORDER_RADIUS | 12px | Global border radius (CSS length). |
NUXT_PUBLIC_THEME_DARK_MODE | true | Whether the theme is dark-first (true | false). |
NUXT_PUBLIC_THEME_SURFACE_COLOR | (auto) | Card / surface background. Auto-derived when empty. |
NUXT_PUBLIC_THEME_SURFACE_SECONDARY_COLOR | (auto) | Secondary surface background. Auto-derived when empty. |
NUXT_PUBLIC_THEME_BORDER_COLOR | (auto) | Border colour. Auto-derived when empty. |
NUXT_PUBLIC_THEME_MUTED_COLOR | (auto) | Muted / secondary text colour. Auto-derived when empty. |
NUXT_PUBLIC_THEME_LOGO_URL | (empty) | URL or path to a logo shown on the login pages and in the admin sidebar. Empty falls back to the bundled Schleuse mark — see the note below. |
NUXT_PUBLIC_THEME_BACKGROUND_IMAGE | (empty) | URL or path to a background image for the auth pages. |
NUXT_PUBLIC_THEME_BACKGROUND_LAYOUT | split | Auth-page layout with a background image: split (form left, image right) or full. |
Empty does not mean "no logo"
Leaving NUXT_PUBLIC_THEME_LOGO_URL empty shows the bundled Schleuse mark on the login pages, in the admin sidebar and as the browser-tab icon.
The one exception is deliberate: if you also set NUXT_PUBLIC_THEME_NAME to something other than Schleuse, the mark is not shown and your name is rendered as text instead. Schleuse will not put its own logo above your brand. To show your own mark, set NUXT_PUBLIC_THEME_LOGO_URL.
Email is separate — the mail header uses this variable only when you set it explicitly, and it needs an absolute URL to a PNG or JPEG. See Email templates.
Public UI flags
Also NUXT_PUBLIC_* (exposed to the client), but not part of the colour theme:
| Variable | Default | Notes |
|---|---|---|
NUXT_PUBLIC_CUSTOM_DISPLAY_NAME | false | Allow users to set a custom display name. When false, it is auto-composed from first + last name. |
NUXT_PUBLIC_HOME_REDIRECT_URL | (empty) | When set, visitors to / are redirected here (internal path or external URL). |
NUXT_PUBLIC_PASSWORD_HINT | (empty) | Override the auto-generated password-complexity hint shown under password fields. |
JWT keys
Both NUXT_JWT_SECRET and NUXT_JWT_PRIVATE_KEY are auto-generated on first start when not set. Generated values are persisted to the data/ directory:
| File | Contents | Purpose |
|---|---|---|
data/jwt-secret.txt | 48-byte random base64url | HMAC for session JWTs |
data/jwt-private-key.pem | RSA 2048 PKCS8 PEM | OIDC ID-token signing |
data/totp-encryption-key.txt | 48-byte random base64url | Encrypts TOTP secrets at rest |
Files are created with mode 0600. Mount /app/data as a Docker volume to persist keys across container restarts.
Priority: ENV variable > persisted file > generate new.
To generate keys manually:
# JWT secret (≥ 32 chars)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# RSA private key (newlines escaped for .env)
node -e "
const {generateKeyPairSync} = require('crypto');
const {privateKey} = generateKeyPairSync('rsa', {modulusLength: 2048});
const pem = privateKey.export({type:'pkcs8',format:'pem'});
console.log(pem.replace(/\n/g,'\\\\n'));
"Startup validation
The app validates the environment once at startup and refuses to boot on contradictory configuration — so operator typos surface immediately in the logs instead of as a mysterious outage months later. Fatal checks (the process exits with a clear level: fatal log naming the variable):
NUXT_ISSUERmust end with/oidc— the OIDC endpoints (discovery, authorize, token, jwks) are mounted there.NUXT_COOKIE_SECURE=truetogether with anhttp://NUXT_APP_URL— secure cookies are never sent over plain HTTP, so no session would survive. Use HTTPS, or setNUXT_COOKIE_SECURE=falsefor local HTTP.
Non-fatal (logged as level: warn, app still starts):
NUXT_APP_URLandNUXT_RP_ORIGINresolving to different origins — WebAuthn rejects assertions whose origin doesn't match the relying-party origin. Make them match unless the split is intentional.
These are in addition to the per-variable validation (URL shape, required values, secret length, etc.). If the container exits on boot, check the startup logs for a fatal line.