Environment Configuration
The Primebrick Backend reads configuration from two sources:
- Environment variables — see the strict split below
config_entriesdatabase table — runtime auth settings (OIDC, Redis, gateway)
Environment variables are loaded once at startup via dotenv/config. The
config_entries table is read at runtime and can be updated via the
admin API without restarting.
The env contract — read this first (CRITICAL)
The .env file is a closed set: it may contain ONLY the database
bootstrap pair DATABASE_URL + DATABASE_SCHEMA. assertDbEnv()
crashes the boot on ANY other key in the file — including a misspelled
DB_*/DATABASE_* name (fail-loud, never silently ignored).
A closed set of platform variables (NODE_ENV, E2E/debug knobs) is
supplied at process launch or via Docker ENV — never in .env, never
hardcoded, never in config_entries. Everything else — URLs, ports,
secrets — lives in the config_entries table (nats_url included: the
BE connects inside startServiceLifecycle(), after initDal()).
Quick start
Create .env with ONLY the DB pair:
Code
The defaults work out-of-the-box with the dockerized infrastructure from
infra/docker-compose.postgres.yml.
Required variables (.env — closed set)
| Variable | Description | Default |
|---|---|---|
DATABASE_URL | PostgreSQL connection string | (none — required) |
DATABASE_SCHEMA | Declared schema domain — CSV or JSON array (public,system,custom). [0] is the owned schema | (none — required) |
The backend crashes on startup if either is missing or empty, or if the
.env file contains any other key.
Platform variables (process env — never .env, never config)
| Variable | Description | Impacts |
|---|---|---|
NODE_ENV | production via Docker ENV or process launch — never .env, never hardcoded, never config_entries | production → session cookies carry Secure (HTTPS-only); allowedTrust accepts only release-signed DB patches. Any other value (or unset) → plain-HTTP dev cookies + dev/release patch trust. |
The HTTP port is fixed (3001, src/index.ts) — not configurable.
nats_url, frontend_url, OIDC/Casdoor values and every other runtime
setting live in config_entries (table below). UUID columns always
default to gen_random_uuid() (pgcrypto — hardcoded in the DAL entity
decorator, no override).
Debug toggles (process env only — dev/test only, never .env)
PB_<MODULE>_FORCE_EMPTY / PB_<MODULE>_FORCE_ERROR (1 to enable) —
consumed by DomainService.list to force empty/error states for E2E.
E2E_ADMIN_USERNAME / E2E_ADMIN_PASSWORD carry the ephemeral test
actor minted per run by the FE E2E harness. Do not enable in production.
(Demo customers seed via pnpm run db:seed:customers, not an env flag.)
Database-stored configuration
Most auth-related configuration is stored in the config_entries
table, not in environment variables. This allows runtime updates via the
admin API without restarting the server.
Each config row has a type column that drives typed coercion at the
API boundary and Frontend widget selection. The type vocabulary includes
string, text, boolean, bigint, number, money, badge, list,
url, secret, json, date, datetime, and time. See the
Config tables guide for the full vocabulary.
Config rows marked reserved = true cannot have their type or
type_config changed — only the value is editable. This protects
built-in settings from being re-typed in a way that would break the
system. Non-reserved rows allow value, type, and type_config updates.
| Config key | Description |
|---|---|
auth_mode | STANDALONE (API verifies JWTs via OIDC) or GATEWAY (trusted proxy injects identity headers) — validated at boot |
auth_roles_path | Dotted JWT path used to extract roles from any token shape (e.g. roles, realm_access.roles) — mandatory in all modes |
idp_type, idp_endpoint, idp_organization, idp_client_id, idp_client_secret | Casdoor™ IDP connection (login, org sync, user resolution) |
redis_url | Redis connection URL for caching and sessions |
oidc_issuer_url | OIDC issuer URL (defaults to Casdoor endpoint) |
oidc_client_id | OIDC client ID |
oidc_client_secret | OIDC client secret |
oidc_audience | Optional OIDC audience check (STANDALONE mode) |
service_client_shield_secret | Shared secret protecting service-to-service calls (client shield); value auto-rotated by the secret engine — this row is only the static fallback |
service_client_shield_secret_header, gateway_public_secret, gateway_public_secret_header, gateway_header_* | GATEWAY mode: shield secrets + the user-identity header names injected by the trusted proxy — all mandatory when auth_mode=GATEWAY |
enable_mfa, enable_webauthn, passkey_required, enable_email_verification_check | Auth feature flags (type: boolean) |
password_policy | Password policy rules for self-service changes |
invitation_expiry_days | Invitation expiry in days (type: bigint) |
mfa_challenge_token_ttl_seconds | MFA token TTL in seconds (type: bigint) |
mfa_challenge_signing_secret | HMAC secret signing MFA challenges — auto-generated if empty |
To update a config value, use the admin API:
Code
Local development setup
Prerequisites
- Docker Desktop running
- PostgreSQL + pgvector — start with:
Code
- Casdoor — included in the same compose file, starts automatically
Steps
-
Create
.envwith ONLY the DB pair (see "The env contract" above):Code -
Apply database migrations:
Code -
Set up Casdoor (creates the builtin client, admin user, and roles):
Code -
Start the dev server:
Code -
Verify health:
Code
Production checklist
-
NODE_ENV=productionvia DockerENV(image) or process launch — never.env - Set
DATABASE_URL+DATABASE_SCHEMAin.env(the only keys allowed there) - Update
config_entrieswith production values:nats_url,idp_*/oidc_*(production Casdoor),frontend_url, Redis, and gateway settings - Remove or disable all debug toggles