Skip to content

Runbook: configuration reference (environment variables)

The authoritative reference for every environment variable the app reads — purpose, whether it is required, its default/fallback, and what breaks when it is unset. .env.example is the copy-paste template (the same variables, grouped identically); this page is the prose reference. Keep the two in sync when adding a variable.

  • The backend reads variables through a single readRuntimeEnv(name) helper (Bun/Node env), so any value present in the process environment works — .env* files, the shell, or the Kubernetes deployment.
  • EMERGENT_-prefixed aliases. Several externally-reachable URLs are read as readRuntimeEnv("X") || readRuntimeEnv("EMERGENT_X"), so either name is accepted. The reference lists the short name and marks the alias.
  • Unset is safe. Nothing here is required for the process to start. A missing value either (a) falls back to a https://localhost placeholder (syntactically valid URLs that are simply not externally verifiable) or (b) makes exactly one feature return 503 — never a crash, never silent wrong behavior. Set the relevant group for any real deployment.
  • Secrets (*_SECRET, *_TOKEN_SECRET, *_DATABASE_URL) are environment-owned and never committed. The migrations create signing roles NOLOGIN; their passwords are an operator step (see credential-issuer-keys.md).
Variable Required Notes
DATABASE_URL yes General app connection; also provisioning (encrypt-only Vault writes) and public JWKS/profile reads.
APP_ENV no development / production.
Variable Required Notes
VITE_SUPABASE_URL for frontend Supabase origin the SPA talks to.
VITE_SYNC_BASE_URL for frontend Sync/Electric base the SPA talks to.

Externally reachable origins embedded in credentials, emails, and OAuth redirects. Unset → https://localhost fallback. (alias = also read from EMERGENT_….)

Variable Alias Notes
API_PUBLIC_URL API origin; base for canonical images /img/:kind/:id, extracted wallet badge art /img/wallet-credential/:key, and the Badge Connect manifest apiBase.
LEARNER_WEB_PUBLIC_URL Learner SPA origin; LTI launch redirects and the Badge Connect client redirect_uri (…/wallet/connections/callback).
TEACHER_PORTAL_PUBLIC_URL Teacher SPA origin.
ADMIN_CONSOLE_PUBLIC_URL Admin SPA origin.
LRS_PUBLIC_URL xAPI LRS public base.
H5P_RUNTIME_PUBLIC_URL H5P runtime origin (cmi5/xAPI launches).
QTI_ITEM_ASSET_BASE_URL Base for QTI authored-item asset URLs (ADR-0023); falls back to the API origin.
CASE_PROVIDER_BASE_URL Externally reachable base for exported CASE framework URIs.
ONEROSTER_PROVIDER_BASE_URL Externally reachable base for the OneRoster provider.

Supabase storage (server-side object storage)

Section titled “Supabase storage (server-side object storage)”

Storage-backed features — entity + wallet badge images (ADR-0035 / ADR-0038), QTI item assets (ADR-0023), learner submissions (ADR-0027), External-Package runtimes — read/write private buckets server-side over the service_role key. The storage endpoint is derived as ${SUPABASE_URL}/storage/v1 unless SUPABASE_STORAGE_URL overrides it. Without a storage URL and a service-role key, those features return 503; the rest of the app is fine.

Variable Required Notes
SUPABASE_URL for storage Supabase base; storage URL derives from it.
SUPABASE_STORAGE_URL no Explicit storage endpoint override.
SUPABASE_SECRET_KEY for storage Server-side bucket read/write key.
SUPABASE_AUTH_ADMIN_URL, SUPABASE_HOST_INTERNAL_URL, SUPABASE_PUBLISHABLE_KEY for admin auth GoTrue admin endpoint + internal host for admin-console user management.

Credential signing & key custody (ADR-0015/0016/0017; capability-b ADR-0038)

Section titled “Credential signing & key custody (ADR-0015/0016/0017; capability-b ADR-0038)”

Full operator procedure (the NOLOGINLOGIN PASSWORD step, connection wiring, rotation): credential-issuer-keys.md. The credential_signing_role migration creates the credential_signer / holder_signer roles and their name-scoped decrypted views.

Variable Required Notes
CREDENTIAL_ISSUER_BASE_URL for issuance Issuer URI / JWKS base for issued OB 3.0 / CLR 2.0 credentials. Unset → https://localhost.
VITE_CREDENTIAL_ISSUER_BASE_URL frontend (build-time) The learner wallet reads its list from the sync rail and shows this as the issuer of OUR-issued credential cards (the issuer is server-derived, not a synced column). Mirror CREDENTIAL_ISSUER_BASE_URL — set both together, or issued cards fall back to https://localhost.
SIGNING_DATABASE_URL for issuance KeySigner connection as credential_signer — the only decrypt path for issuer Ed25519 secrets. Same DB as DATABASE_URL, different user. Unset → credential export cannot sign.
HOLDER_SIGNING_DATABASE_URL for presentations KeySigner holder path as holder_signer — the only decrypt path for per-learner VP keys. Separate user from the issuer signer. Unset → Verifiable Presentations cannot be signed.

Badge Connect host & client (capability-b; ADR-0038)

Section titled “Badge Connect host & client (capability-b; ADR-0038)”

emergent both hosts a Badge Connect / CLR API (OAuth2 authorization-server) and acts as a client pulling a learner’s credentials from an external host.

Variable Required Notes
BADGE_CONNECT_TOKEN_SECRET for host and client Dual role: the HS256 secret the OAuth2 AS host mints/verifies access tokens with, and the AES-256-GCM key the wallet client uses to seal its PKCE authorization-flow state. Long random value. Unset → the host returns 503 and the wallet cannot start an external-host pull.
BADGE_CONNECT_AUTHORIZATION_URL no The learner-web consent page the host manifest advertises as authorizationUrl. Falls back to ${API_PUBLIC_URL}/oauth/authorize.

Each signs the short-lived capability token for one feature’s serve proxy. Long random values; unset → only that feature returns 503.

Variable Feature
WEB_CONTENT_DELIVERY_TOKEN_SECRET web-content delivery (ADR-0022)
PACKAGE_RUNTIME_TOKEN_SECRET static-package runtime serving, cmi5 Captivate (ADR-0032)
ONEROSTER_PROVIDER_TOKEN_SECRET OneRoster provider OAuth2 token endpoint
Variable Required Notes
EMAIL_SENDER_TYPE no mailgun (default) or console (dev).
MAILGUN_BASE_URL, MAILGUN_REGION for mailgun Mailgun endpoint config.
SEND_EMAIL_HOOK_SECRETS for auth emails Verifies the Supabase auth “send email” webhook signature; strict mode throws without it (so auth emails are never sent unverified).
Variable Required Notes
ELECTRIC_URL for sync Electric service URL (electric-proxy target).
ELECTRIC_SECRET / ELECTRIC_API_TOKEN for proxy One of the two must be set for the electric-proxy routes.
COMMON_DATA_ELECTRIC_SERVICE_HOST / _PORT cluster Internal Electric service host/port (set by the deploy).
EMERGENT_SYNC_MUTATION_RLS_ENABLED no Defaults to enabled; "false" bypasses RLS on sync mutations (dev only).
EMERGENT_SYNC_MUTATION_REQUEST_PATH no Override the in-DB mutation request path (default /api/mutations, the canonical batch-write path).
Variable Required Notes
EMERGENT_LTI_TOOL_ENABLED no Enables the LTI tool routes.
LTI_TOOL_KEY_ID no kid for the tool’s signing key (else a default).
EMERGENT_LTI_TOOL_ALLOW_EPHEMERAL_KEYS no Dev only: allow ephemeral (non-persisted) keys.

emergent acting as a platform that launches into external tools (the inverse of the tool role above). The /lti/platform/* endpoints are unset-safe — each returns 503 until configured, so none of these are required to boot.

Variable Required Notes
LTI_PLATFORM_ISSUER for launches The platform iss stamped on every launch, and the base of the JWKS URL. Falls back to API_PUBLIC_URL (then EMERGENT_PUBLIC_API_URL).
LTI_PLATFORM_TOKEN_SECRET for NRPS/AGS Long random HS256 secret; mints and verifies the service (NRPS/AGS) access tokens — emergent is both the authorization server and the resource server.
LTI_PLATFORM_PRIVATE_JWK_JSON / LTI_PLATFORM_PUBLIC_JWK_JSON for launches The RS256 id_token signing keypair (JWK JSON). The public half is served at /lti/platform/jwks for tools to verify launches. Set both together.
EMERGENT_LTI_PLATFORM_ALLOW_EPHEMERAL_KEYS no Dev only: generate an ephemeral RS256 keypair when the JWKs are unset (rotates per restart — fine for a local spike, breaks JWKS caching).

Generate the platform signing keypair (the env values above mirror the tool side’s key custody — no Vault, no DB):

Terminal window
bun -e '
import { generateKeyPair, exportJWK, calculateJwkThumbprint } from "jose";
const { publicKey, privateKey } = await generateKeyPair("RS256", { extractable: true });
const pub = await exportJWK(publicKey), priv = await exportJWK(privateKey);
const kid = await calculateJwkThumbprint(pub);
pub.kid = priv.kid = kid; pub.use = "sig"; pub.alg = "RS256";
console.log("LTI_PLATFORM_PRIVATE_JWK_JSON=" + JSON.stringify(priv));
console.log("LTI_PLATFORM_PUBLIC_JWK_JSON=" + JSON.stringify(pub));
'

For local dev, .env.development.local already ships a stable generated keypair + token secret and an LTI_PLATFORM_ISSUER pointing at the loc API origin, so the platform endpoints work out of the box. In a real deployment, set LTI_PLATFORM_TOKEN_SECRET and the JWK pair from a secret store (the same way the credential-signing keys are handled — see credential-issuer-keys.md); LTI_PLATFORM_ISSUER falls back to API_PUBLIC_URL, so it only needs setting if the platform issuer differs from the API origin.

Variable Required Notes
CMI5_API_INTERNAL_URL, CMI5_LRS_INTERNAL_URL cluster Internal cmi5 fetch/state + LRS endpoints (set by the deploy).
CMI5_LAUNCH_SESSION_TTL_SECONDS no Launch-session lifetime (else a built-in default).
EMERGENT_XAPI_ACTOR_HOME_PAGE no xAPI actor account homePage (default https://emergent.app).
Variable Notes
EMERGENT_SEED_PROFILE Which deterministic seed profile to load.
EMERGENT_SEED_VALUE drizzle-seed RNG seed.
EMERGENT_SMALL_SEED_DEFAULT_PASSWORD / _TEACHER_PASSWORD / _LEARNER_PASSWORD Passwords for seeded accounts (small profile).