Skip to main content

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).

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).

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)​

VarWhereValue shape
OPTOLINK_E2E_API_KEYoptolink-node tests/e2e.spec.tsraw SERVER-tier opl_api_…; unset = self-skip
OPTOLINK_E2E_SDK_KEYoptolink-android LiveE2eTest.kt, optolink-ios LiveE2eTests.swiftraw CLIENT-tier opl_sdk_…; unset = self-skip
OPTOLINK_E2E_BASE_URLall threebackend base URL; default http://localhost:3000 (iOS: loopback http only)
OPTOLINK_API_KEY / OPTOLINK_ORG_KEYoptolink-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:

  1. Clerk instance: backend CLERK_SECRET_KEY (sk_…) and portal VITE_CLERK_PUBLISHABLE_KEY (pk_…) must belong to the same Clerk instance — never cross-point. Same for CLERK_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.
  2. Backend base URLs: BACKEND_URL + CLIENT_URL (billing webhooks and checkout redirects) and portal VITE_API_BASE_URL must all point at the same backend/portal pair.
  3. Key tiers: node e2e takes an opl_api_ (SERVER) key; android/iOS e2e take opl_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.