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).
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.
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.
Full operator procedure (the NOLOGIN → LOGIN 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.
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.
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.
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.