Configuration
All configuration is environment variables. The app validates its environment
both at build time and on start, so every variable declared in src/env.ts
must be present; the image sets safe non-secret defaults for all of them.
Override the ones you care about.
With compose, set variables in .env next to docker-compose.yml. Only the
variables listed in the compose x-app-env/build anchors reach the containers.
Required in production
| Variable | Purpose |
|---|---|
DATABASE_URL | Postgres connection string, e.g. postgres://user:pass@host:5432/intranet. |
BETTER_AUTH_SECRET | Session/token signing secret. 32+ characters, high entropy. |
ORIGIN | Optional public base URL (scheme + host + port). See Origin. |
APP_ENV | development, staging, or production. |
OAUTH_* | Provider connection. See Single sign-on. |
Origin and CSRF
ORIGIN is optional. When it is unset, the app derives the origin per
request from the Host header; adapter-node defaults the protocol to https,
and behind a proxy you can set PROTOCOL_HEADER/HOST_HEADER (e.g.
x-forwarded-proto/x-forwarded-host) so the derived origin matches the public
URL. Because the image no longer bakes the origin, one image can be deployed to
any domain.
SvelteKit's cross-origin form check is disabled
(csrf.trustedOrigins: ['*']), and better-auth resolves its base URL from the
request (baseURL: { allowedHosts: ['*'] }). Authentication is still enforced
server-side.
Consequences:
-
Local compose works out of the box with no
ORIGINset. -
To pin a canonical origin (for example when serving over plain HTTP, where the derived protocol would default to
https), setORIGIN:# plain Dockerdocker run -e ORIGIN=https://intranet.example ...# compose, via .envPUBLIC_ORIGIN=https://intranet.example -
OIDC redirect URIs still default to the browser origin; if you set
ORIGIN, register that exact origin with the provider.
Variables
Core
| Variable | Default (image) | Notes |
|---|---|---|
APP_ENV | development | Enables staging guard when staging. |
DATABASE_URL | postgres://intranet:intranet@db:5432/intranet | Override in production. |
ORIGIN | (unset; derived from Host) | Optional. Pin a canonical origin if needed. |
BETTER_AUTH_SECRET | dev-only-change-me-… | Override. |
PORT / HOST | 3000 / 0.0.0.0 | Listen address. |
TEST_DATABASE_URL | …@db:5432/intranet_test | Must be present; used by integration tests. |
Sign-in (provider-agnostic)
See Single sign-on with Authentik for a worked example.
| Variable | Default | Notes |
|---|---|---|
OAUTH_PROVIDER_ID | sso | Key in the callback path: /api/auth/callback/<id>. |
OAUTH_PROVIDER_NAME | Single sign-on | Login button label. |
OAUTH_DISCOVERY_URL | (empty) | OIDC discovery document; preferred. |
OAUTH_AUTHORIZATION_URL | (empty) | Use instead of discovery for plain OAuth2. |
OAUTH_TOKEN_URL | (empty) | Plain OAuth2 token endpoint. |
OAUTH_USERINFO_URL | (empty) | Optional for plain OAuth2. |
OAUTH_CLIENT_ID | (empty) | |
OAUTH_CLIENT_SECRET | (empty) | |
OAUTH_SCOPES | openid profile email | |
OAUTH_PKCE | true | |
OAUTH_GROUPS_CLAIM | groups | Claim carrying group membership. |
OAUTH_ROLE_MAP | (empty) | JSON: provider group → app role. |
OAUTH_DEFAULT_ROLE | engineer | Role for unmapped identities. |
OAUTH_EXTRA_PROVIDERS | (empty) | JSON array of additional providers. |
If OAUTH_ROLE_MAP or OAUTH_GROUPS_CLAIM never reach the container, every
identity silently becomes OAUTH_DEFAULT_ROLE (engineer). Verify after
deploying by signing in and checking the role shown in the sidebar.
Staging isolation
APP_ENV=staging refuses production endpoints and restricts access to an
allowlist.
| Variable | Notes |
|---|---|
STAGING_ALLOWED_EMAILS | Comma-separated identities allowed on staging. |
PRODUCTION_DATABASE_URL | Production DB, for the isolation check. |
PRODUCTION_OAUTH_ISSUER | Production OAuth issuer, for the isolation check. |
Accounts and provisioning
There are no local passwords. On first provider login the app creates a local
user and sets the role from the group claim; every later login re-syncs it. The
/dev/login route mints a session directly and is handy for tests, but
production users sign in through the provider.
API credentials
The HTTP API uses two credential types, created in the admin UI:
- Personal tokens (
cxp_…) inherit the owner's role and scopes. - Service clients (
cxs_…+ secret) use OAuth 2.0 client credentials for machine-to-machine accounting access.
See the API reference linked in the navbar.