Environment Variables
All configuration for Assessors Studio is driven by environment variables. The backend reads them at startup and validates them against a Zod schema; a malformed value fails fast with a descriptive error rather than producing surprising behavior at runtime.
Variables can be set in any of the usual places: a .env file next to the backend process, the environment section of a docker-compose.yml, a Kubernetes ConfigMap or Secret, or directly on the shell. The application does not care where a value comes from; it only reads the final environment.
| Variable | Default | Description |
|---|---|---|
DATABASE_PROVIDER | pglite | Database engine. Use pglite for embedded local development, postgres for production. |
DATABASE_URL | postgresql://localhost:5432/assessors_studio | PostgreSQL connection string. Used when DATABASE_PROVIDER=postgres. |
PGLITE_DATA_DIR | ./data/pglite | Directory for the embedded database files. Ignored when using PostgreSQL. |
JWT_SECRET | auto generated | Secret for signing session tokens. When unset the backend generates a secret on first run and stores it in the app_config table. Set explicitly (at least 32 characters) for multi replica deployments or to manage the key externally. Changing the value invalidates all active sessions. |
JWT_EXPIRY | 24h | Token lifetime. Accepts any value the ms library understands, for example 24h, 7d, 30m. |
PORT | 3001 | HTTP port the backend listens on. |
LOG_LEVEL | info | Verbosity of application logs. One of error, warn, info, debug. |
CORS_ORIGIN | http://localhost:5173 | Allowed CORS origin. The packaged container serves the SPA and the API on the same origin, so CORS is effectively a no op in that deployment. Set this when the API is called from a different origin, such as a Vite dev server during local work or a cross origin integration. |
APP_URL | http://localhost:5173 | Public base URL of the application (for example https://studio.example.com). Required for notification channels that include links back to the app. |
NODE_ENV | development | Runtime environment: development, production, or test. |
COOKIE_SECURE | auto | Whether to mark session cookies with the Secure attribute. auto (default) infers from the request scheme and NODE_ENV, setting Secure whenever the app is served over HTTPS or NODE_ENV=production. true forces Secure on every response. false disables it (only appropriate for plain HTTP local development). |
TRUST_PROXY_HOPS | 0 | Number of reverse proxy hops Express should trust for X-Forwarded-* header parsing. Leave at 0 for direct deployments. Set to 1 when a single TLS terminating ingress (nginx, ALB, Traefik) sits directly in front of the Node process. Required for correct client IP handling, rate limiting, Secure cookie inference, and audit logging in proxied deployments. |
Initial administrator account
Section titled “Initial administrator account”No environment variables are used to seed the initial administrator. The first account is created through the in browser setup wizard the first time the application is opened. When no users exist, the API responds to any request with a pointer to /setup. The SPA is always served and routes to the setup wizard automatically. Credentials are never written to configuration files, container images, or deployment manifests.
Registration
Section titled “Registration”| Variable | Default | Description |
|---|---|---|
REGISTRATION_MODE | disabled | Controls self service account creation. disabled rejects every request to POST /api/v1/auth/register. invite_only requires a valid unused invite token issued by an admin. open accepts any well formed registration and assigns the assessee role. |
ACCEPT_OPEN_REGISTRATION_RISK | false | Operator acknowledgement required to run REGISTRATION_MODE=open in production. When NODE_ENV=production and REGISTRATION_MODE=open, the backend refuses to start unless this is set to true (or 1). The flag is ignored outside production so local and CI setups remain friction free. Open mode accepts any well formed registration with no invite token; only enable it when you have other controls in place such as network gating, captcha, or email verification. |
INVITE_RETAIN_AFTER_TERMINAL_DAYS | 30 | Days to retain invites in consumed, revoked, or expired state before the periodic cleanup job purges them. |
Admins can always create users through the admin APIs, irrespective of mode. When running in invite_only mode, call POST /api/v1/admin/invites with an optional email, an intendedRole, and an optional expiresInHours (default 168). The plaintext token is returned once; the server stores only the SHA-256 hash. GET /api/v1/admin/invites lists invites and DELETE /api/v1/admin/invites/:id revokes one.
The register endpoint returns a generic 202 Accepted response for both successful and duplicate submissions so an attacker cannot enumerate accounts by probing it.
Mode changes leave a tamper evidence trail. The backend persists the active REGISTRATION_MODE to the app_config table on startup and emits an audit row whenever the runtime value differs from the last persisted value, so a silent toggle from disabled or invite_only to open is always recoverable from the audit log.
Authentication and lockout
Section titled “Authentication and lockout”| Variable | Default | Description |
|---|---|---|
LOGIN_MAX_FAILED_ATTEMPTS | 5 | Number of consecutive failed password attempts before the account is locked. Set to 0 to disable per account lockout (rate limiting still applies). |
LOGIN_LOCKOUT_DURATION_MINUTES | 15 | Minutes an account remains locked after exceeding LOGIN_MAX_FAILED_ATTEMPTS. The lock auto clears when the duration elapses. |
Per IP rate limiting on /api/v1/auth (10 attempts per 15 minutes) is always on outside NODE_ENV=test and is not configurable. The two layers compose: per account lockout protects a known username from a focused attacker, and per IP rate limiting protects against credential stuffing across a username list.
Sessions
Section titled “Sessions”| Variable | Default | Description |
|---|---|---|
SESSION_CLEANUP_INTERVAL_MINUTES | 15 | Minutes between runs of the scheduled session cleanup job. Set to 0 to disable scheduled cleanup. |
SESSION_RETAIN_EXPIRED_HOURS | 24 | Hours to retain expired session rows after they are no longer valid. The cleanup job deletes anything older than this window. |
Password policy
Section titled “Password policy”| Variable | Default | Description |
|---|---|---|
PASSWORD_MIN_LENGTH | 12 | Minimum password length. Hard floor of 8 and ceiling of 128 enforced by configuration validation. |
PASSWORD_HIBP_CHECK_ENABLED | false | When true, the backend performs a Have I Been Pwned k anonymity range check on every new password during registration, password change, and password reset. The full password is never sent over the wire, only the first five characters of its SHA-1 hash. |
PASSWORD_HIBP_TIMEOUT_MS | 3000 | Network timeout for the HIBP range API in milliseconds. On timeout the check fails open so a momentary network blip does not lock users out of password changes. |
Evidence storage
Section titled “Evidence storage”| Variable | Default | Description |
|---|---|---|
STORAGE_PROVIDER | database | Where new evidence attachments are stored: database or s3. |
UPLOAD_MAX_FILE_SIZE | 52428800 | Maximum upload size in bytes. Default is 50 MB. |
S3_BUCKET | Bucket name. Required when STORAGE_PROVIDER=s3. | |
S3_REGION | us-east-1 | AWS region or S3 compatible region. |
S3_ENDPOINT | Custom endpoint URL for MinIO, DigitalOcean Spaces, Backblaze B2, Cloudflare R2, and other S3 compatible providers. Leave unset for AWS S3. | |
S3_ACCESS_KEY_ID | Access key. Required when STORAGE_PROVIDER=s3 unless the process runs with ambient AWS credentials (IRSA, instance role). Prefer S3_ACCESS_KEY_ID_FILE in production. | |
S3_SECRET_ACCESS_KEY | Secret key. Required alongside S3_ACCESS_KEY_ID. Prefer S3_SECRET_ACCESS_KEY_FILE in production. | |
S3_ACCESS_KEY_ID_FILE | Path to a file whose contents are used as S3_ACCESS_KEY_ID. Intended for Docker Compose secrets mounted at /run/secrets/<name>. Overrides S3_ACCESS_KEY_ID when both are set. The backend refuses to start if the path is set but unreadable. | |
S3_SECRET_ACCESS_KEY_FILE | Same as above for the secret access key. | |
S3_FORCE_PATH_STYLE | false | Set to true for MinIO and other providers that require path style addressing. AWS S3 uses virtual host style by default. |
See Evidence Storage for guidance on choosing between database and object storage, and for migration scripts.
Webhook channel
Section titled “Webhook channel”| Variable | Default | Description |
|---|---|---|
WEBHOOK_ENABLED | true | Master toggle for the webhook notification channel. |
WEBHOOK_TIMEOUT | 10000 | HTTP timeout for webhook deliveries, in milliseconds. |
WEBHOOK_MAX_RETRIES | 5 | Maximum retry attempts for failed deliveries. |
WEBHOOK_DELIVERY_RETENTION_DAYS | 30 | Days to retain webhook delivery logs before automatic purge. |
Individual webhook targets are configured through the admin UI and stored (encrypted) in the database, not through environment variables.
Email channel (SMTP)
Section titled “Email channel (SMTP)”| Variable | Default | Description |
|---|---|---|
SMTP_ENABLED | false | Master toggle for the email notification channel. |
SMTP_HOST | SMTP server hostname. | |
SMTP_PORT | 587 | SMTP server port. |
SMTP_SECURE | false | Set to true for implicit TLS (typically port 465). |
SMTP_USER | Authentication username. | |
SMTP_PASS | Authentication password. | |
SMTP_FROM | Sender address. A common value is "Assessors Studio" <noreply@example.com>. | |
SMTP_TLS_REJECT_UNAUTHORIZED | true | Set to false only when using a self signed certificate in development. |
Chat channels
Section titled “Chat channels”| Variable | Default | Description |
|---|---|---|
SLACK_ENABLED | false | Enable Slack as a notification channel. |
TEAMS_ENABLED | false | Enable Microsoft Teams as a notification channel. |
MATTERMOST_ENABLED | false | Enable Mattermost as a notification channel. |
CHAT_TIMEOUT | 10000 | HTTP timeout for chat deliveries, in milliseconds. |
CHAT_DELIVERY_RETENTION_DAYS | 30 | Days to retain chat delivery logs before automatic purge. |
Individual chat destinations (channel, webhook URL) are configured through the admin UI.
Prometheus metrics
Section titled “Prometheus metrics”| Variable | Default | Description |
|---|---|---|
METRICS_ENABLED | false | Master toggle for the /metrics endpoint. |
METRICS_TOKEN | Bearer token required to scrape metrics. Leave empty for unauthenticated access. Recommended for any production installation that exposes metrics to a shared collector. | |
METRICS_PREFIX | cdxa_ | Prefix applied to all exported metric names. |
METRICS_DOMAIN_REFRESH_INTERVAL | 60 | Seconds between domain gauge refreshes. |
See Metrics and Monitoring for the metric catalog and alerting recommendations.
Encryption at rest
Section titled “Encryption at rest”| Variable | Default | Description |
|---|---|---|
MASTER_ENCRYPTION_KEY | 256 bit key encoded as 64 hex characters. Required in production when REQUIRE_ENCRYPTION=true. When unset, the encryption service operates in passthrough mode (values stored as plaintext). | |
REQUIRE_ENCRYPTION | false | When true, the application refuses to start if MASTER_ENCRYPTION_KEY is missing or too short. Production deployments should always set this to true. |
OLD_MASTER_ENCRYPTION_KEY | Only used by the npm run rekey-master CLI during master key rotation. Set this to the previous 64 character hex key so the script can decrypt existing values before re encrypting with the new key. Remove from the environment once the rekey completes. |
Generate a master key with:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"See Encryption at Rest for the key hierarchy, rotation procedure, and admin visibility.
Identity providers
Section titled “Identity providers”OpenID Connect is on the roadmap. When it ships, the OIDC_* environment variables will be documented here. Until then, the platform authenticates with local username and password only.
Putting it together
Section titled “Putting it together”A minimal production environment looks like:
DATABASE_PROVIDER=postgresDATABASE_URL=postgresql://assessors:<password>@db:5432/assessors_studioSTORAGE_PROVIDER=s3S3_BUCKET=my-evidenceS3_REGION=us-east-1S3_ACCESS_KEY_ID=<key>S3_SECRET_ACCESS_KEY=<secret>MASTER_ENCRYPTION_KEY=<64 hex chars>REQUIRE_ENCRYPTION=trueJWT_SECRET=<32+ chars>APP_URL=https://studio.example.comCORS_ORIGIN=https://studio.example.comSMTP_ENABLED=trueSMTP_HOST=smtp.example.comSMTP_PORT=587SMTP_USER=<user>SMTP_PASS=<pass>SMTP_FROM="Assessors Studio <noreply@example.com>"METRICS_ENABLED=trueMETRICS_TOKEN=<random 32 char>NODE_ENV=productionLOG_LEVEL=infoEverything not listed above is optional or defaults to a safe value for production. Review the full table when you are configuring an integration you have not configured before.