Skip to content

Configuration (env)

Everything is configured through environment variables. The canonical example is backend/.env.example.

Never commit secrets

backend/.env, the root .env and backend.env are all gitignored. When you add a variable, document it in the READMEs — never its value.

Core

Variable Example Purpose
PORT 8080 API listen port
ENVIRONMENT production Turns on strict production guards
DEBUG false Verbose logging
ALLOWED_ORIGINS https://open.gerege.mn CORS allow-list (comma-separated; * forbidden)
TRUSTED_PROXIES Reverse-proxy addresses

Database and Redis

Variable Purpose
DB_POSTGRE_DSN / DB_POSTGRE_URL Connection string
DB_MAX_OPEN_CONNS, DB_MAX_IDLE_CONNS, DB_CONN_MAX_LIFE_MINS Pool tuning
REDIS_HOST, REDIS_PASS, REDIS_EXPIRED Redis connection and TTL

Production DSNs must use sslmode=verify-full

The production guard requires it. The Docker Compose stack deliberately runs with ENVIRONMENT=development because its internal database has no TLS.

The API must not connect as a superuser

RLS only enforces when the app connects as a least-privilege role. A superuser or BYPASSRLS role fails the boot in production.

JWT and sessions

Variable Purpose
JWT_SECRET ≥32 characters. Changing it invalidates every session
JWT_EXPIRED, JWT_REFRESH_EXPIRED Access / refresh lifetimes
JWT_ISSUER Usually the app domain. Changing it invalidates all existing tokens

eID (Relying Party)

Variable Purpose
EID_BASE_URL eID Mongolia /v3 base (or the SSO sign relay)
EID_RP_UUID, EID_RP_SECRET RP credentials
SIGN_RELAY_TOKEN Shared token for signature relay (empty disables it)

Gerege SSO (RP side — this app as a client)

Variable Example Purpose
SSO_ISSUER https://sso.gerege.mn Defaults to this when unset
SSO_CLIENT_ID / SSO_CLIENT_SECRET Empty leaves the SSO flow inert
SSO_REDIRECT_URI https://open.gerege.mn/sso/callback Must be registered exactly on the SSO client
SSO_SCOPE openid profile email nationalid nationalid adds the citizen ID number
SSO_NATIVE_CLIENT_ID Client for the mobile (PKCE, public) flow
SSO_EID_PROXY_BASE_URL When set, the eID PKI surface goes through the SSO proxy

An unregistered client returns invalid_client

If SSO_CLIENT_ID is absent from the provider's client store, the authorize step returns {"error":"invalid_client"}. The redirect URI must match exactly too.

OIDC provider side (this app acting as a provider)

Variable Purpose
OAUTH_ISSUER e.g. https://open.gerege.mn. The provider is enabled only when this is set
SSO_STATE_KEY HMAC key for transient login/consent state (≥32 bytes)
SSO_FIRSTPARTY_CLIENTS First-party clients that skip the consent screen
SSO_ADMIN_API_KEYS, SSO_ADMIN_SUBS Admin API access

Sign-in surface (AUTH_MODE)

Whether the platform authenticates users itself or redirects to an upstream SSO is not a code difference — this single variable decides:

Value On the landing page and /login
provider The sign-in card (eID national ID/QR · Google) is rendered here
client Redirect to the upstream SSO (SSO_ISSUER)
AUTH_MODE=client      # this template's reference deployment — an SSO relying party
AUTH_MODE=provider    # an identity service such as sso.dgov.mn / sso.gerege.mn

Leave it empty and it is derived from whether SSO_CLIENT_ID is configured — so existing deployments need no change.

A typo is NOT a silent fallback

An unrecognised value makes the backend refuse to start. Otherwise the platform would quietly boot with a different sign-in surface than intended.

A SEPARATE axis from OAUTH_ISSUER

OAUTH_ISSUER answers "is this platform an issuer for other apps"; AUTH_MODE answers "where do this platform's users sign in". Both can be active at once — a chained setup.

The frontend reads its mode from the public GET /api/v1/site/auth (no authentication, no secrets), so there is no duplicated env on the frontend.

Interface languages

The platform ships with bundled translations for Mongolian + the six official UN languages (Arabic · Chinese · English · French · Russian · Spanish). All seven work immediately — with an empty database and without any translation step.

Arabic automatically gets <html dir="rtl">.

Variable Notes
No configuration needed; the languages arrive in the languages table as is_builtin

If you need more languages, a super admin adds them under Languages and fills the translations with Gemini — those are stored as a database overlay.

Third parties and storage

Variable Purpose
GEMINI_API_KEY AI pipeline. Without it /ai/* returns a genuine 500
GOOGLE_CLIENT_ID / SECRET Google linking (the button hides when empty)
VERIFY_API_BASE, VERIFY_API_KEY, VERIFY_CHANNEL Citizen / organisation verification
XYP_API_BASE, XYP_CLIENT_ID, XYP_CLIENT_SECRET State-registry lookups
GSPACE_* The app's own SFTP storage (per-user quota)
INTEGRATION_ENC_KEY ≥16 bytes. Encrypts OAuth tokens and super admin MFA

INTEGRATION_ENC_KEY is mandatory

Deployments require this key, and once set it must never change — rotating it breaks every previously encrypted value.

Observability

Variable Purpose
OTEL_EXPORTER, OTEL_SAMPLE_RATIO OpenTelemetry tracing
OBSERVABILITY_TOKEN Bearer token gating /metrics and /swagger in production

Frontend

Variable Purpose
BACKEND_URL The internal address the BFF calls (e.g. http://api:8080)

The name api can collide on a shared network

When several stacks share one Docker network, http://api:8080 may resolve to a different container and every /api/v1/* call turns into a 404. Pin BACKEND_URL to your own api container's full name in that case.