Environment variables
Scope: which variables exist across repos and which must pair. Values
live in each repo's .env (gitignored) — this page is a registry of names and
rules, never values. Repo-local run instructions live in that repo's docs/run.md.
Source-checked against optolink-backend @ 29f8589 (
src/config/env.validation.ts,src/billing/config/billing-config.service.ts,src/main.ts,.env.example) and optolink-portal @ 7d31d45 (.env.example), 2026-10-07. SDK e2e vars verified in their suites (see table).
Backend (optolink-backend)
Boot-required — fails fast at startup, listing ALL missing:
DATABASE_URL · REDIS_URL · ADMIN_BEARER_TOKEN · CNAME_TARGET ·
CLERK_SECRET_KEY · CLERK_WEBHOOK_SECRET
Billing boot-required — a second fail-fast (BillingConfigService; empty strings count as missing; all ten must be present for the billing module):
STRIPE_SECRET_KEY · STRIPE_WEBHOOK_SECRET · PAYMOB_SECRET_KEY ·
PAYMOB_PUBLIC_KEY · PAYMOB_CARD_INTEGRATION_ID_WEB_3DS ·
PAYMOB_MOTO_INTEGRATION_ID · PAYMOB_HMAC_SECRET · PAYMOB_API_KEY ·
BACKEND_URL · CLIENT_URL
The two Paymob integration IDs are separate dashboard credentials (3DS
on-session vs MOTO off-session). Plan pricing is not env config — it lives
in the seeded plan_config table (see Pricing & plans).
Optional: PORT · NODE_ENV (unset/non-production registers the sandbox
module) · GEOIP_DB_PATH (unset → click geo fields null) · PORTAL_URL
(comma-separated CORS origins; default http://localhost:5173) ·
ACME_EMAIL + ACME_DIRECTORY_URL (SSL provisioning) · CLERK_OPS_ORG_ID
(unset → ops/admin routes deny everyone, fail-closed) · RATE_LIMIT_DISABLED
(test/CI escape hatch only — must NOT be set in production).
Portal (optolink-portal) — VITE_*, build-time inlined
VITE_API_BASE_URL · VITE_CLERK_PUBLISHABLE_KEY · VITE_CLERK_OPS_ORG_ID ·
VITE_SDK_DOCS_URL · VITE_PRODUCT_DOCS_URL · VITE_GEO_ENDPOINT (optional;
currency/gateway geo-IP — defaults to the shared Firebase geocode function;
EG → EGP/Paymob, else USD/Stripe).
SDK-local (test/e2e only — never product runtime)
| Var | Where | Value shape |
|---|---|---|
OPTOLINK_E2E_API_KEY | optolink-node tests/e2e.spec.ts | raw SERVER-tier opl_api_…; unset = self-skip |
OPTOLINK_E2E_SDK_KEY | optolink-android LiveE2eTest.kt, optolink-ios LiveE2eTests.swift | raw CLIENT-tier opl_sdk_…; unset = self-skip |
OPTOLINK_E2E_BASE_URL | all three | backend base URL; default http://localhost:3000 (iOS: loopback http only) |
OPTOLINK_API_KEY / OPTOLINK_ORG_KEY | optolink-flutter example/ | --dart-define at run time |
Pairing rules
These must agree within one environment or auth/billing fails in ways that look like code bugs:
- Clerk instance: backend
CLERK_SECRET_KEY(sk_…) and portalVITE_CLERK_PUBLISHABLE_KEY(pk_…) must belong to the same Clerk instance — never cross-point. Same forCLERK_OPS_ORG_ID↔VITE_CLERK_OPS_ORG_ID: the ops-org id from that same instance. Local dev and the deployed test stack run different Clerk dev instances; their keys are not interchangeable — see Clerk dev instance and Deployed test stack. - Backend base URLs:
BACKEND_URL+CLIENT_URL(billing webhooks and checkout redirects) and portalVITE_API_BASE_URLmust all point at the same backend/portal pair. - Key tiers: node e2e takes an
opl_api_(SERVER) key; android/iOS e2e takeopl_sdk_(CLIENT) — swapped tiers authenticate but 403 on the exercised endpoints, which reads as a test failure, not an auth one.
Gateway secrets stay in .env; gateway identifiers and amounts (price ids,
plan_config rows) are config data in source control, not secrets.