Environment Variables
This page is the reference for the environment variables that matter across the
three MVS Pay runtime surfaces. It is grounded in the actual code that reads
process.env, the ExternalSecret manifests under deploy/, and the
.env.example contracts — not in aspiration. Where a variable is optional,
defaulted, deferred, or only consumed in a non-obvious place, that is called out
explicitly.
URLs and domains are not environment variables in this platform. Per
packages/config/urls.ts, the rule is: env is for secrets, database DSNs, and
third-party credentials only — public hostnames are hardcoded and derived from
the environment suffix. The handful of NEXT_PUBLIC_* domain overrides below
exist for local development and CORS, not for production routing. See
Domain & DNS.
The three surfaces
There are three places code reads configuration, and they do not share a
single .env. Each surface has its own delivery mechanism.
The extensions service and the worker share the same Infisical scope name
(mvs-pay) inside @mvs/secrets (SECRETS_SCOPE = "mvs-pay" in
packages/temporal-workflows/src/worker/start.ts), but they pull from
different Infisical folder paths (mvs-pay-extensions/ vs
pay-crons-worker/). Several keys are duplicated across both folders on
purpose — there is no single source row for, e.g., MVS_PAY_DATABASE_URL.
Rotating it means updating it in every folder it appears.
How loading works per surface
UI (Vercel)
Next.js inlines NEXT_PUBLIC_* at build time, so changing one requires a
redeploy. Server-only vars (MVS_PAY_DATABASE_URL,
MVS_PAY_EXTENSIONS_INTERNAL_SECRET, RESEND_API_KEY, APPLE_PAY_*) are
read at request time. MVS_PAY_DATABASE_URL is also needed at build for
Prisma client generation (packages/mvs-pay/prisma.config.ts).
Extensions (Hono on EKS)
createHonoApp() calls injectSecretsToEnv() from @mvs/secrets when
isInfisicalConfigured() is true, then getConfig() validates everything
through a Zod schema. Missing optional values fall back to defaults; a
malformed value (e.g. a non-URL HYPERSWITCH_API_BASE_URL) throws at boot.
UI — apps/mvs-pay (Vercel)
The merchant/operator UI. Server-only secrets and a small set of build-time-inlined public keys.
/api/debug/secrets only returns data when NODE_ENV/VERCEL_ENV is
development and VERCEL_ENV is neither production nor preview
(app/api/debug/secrets/route.ts). It still must never be relied on as a
production diagnostic — treat it as a local-only convenience.
Apple Pay token lookup
The /.well-known/... route picks the env key at request time. Default key is
prod/dev based on NODE_ENV / INFISICAL_ENV / VERCEL_ENV; a ?domain=
query triggers a per-domain override lookup that falls back to the default.
A 404 is intentional when no token is configured — Apple’s verifier treats it as a clean “not set up” signal. See the Wallet Validation runbook and Wallets & Click to Pay.
Extensions — apps/mvs-pay-extensions (Hono on EKS)
Every variable here is read through getConfig() in
apps/mvs-pay-extensions/src/config/index.ts (Zod-validated) unless noted.
Defaults below are the schema defaults.
The single shared x-internal-secret model — not per-user JWTs — is a
deliberate, documented decision. Per-user identity is not propagated to PHI
audit trails yet (phiAudit.logAccess() logs userId: null). See
ADR-010 and
Authentication. The secret is rotated on a cadence —
see Secrets Rotation.
Worker — pay-crons-worker (Temporal on EKS)
Entrypoint packages/temporal-workflows/src/worker/start.ts. Registers the
mvs-pay workflows + activities on the pay-crons task queue. See the
Temporal Worker surface.
The TEMPORAL_TLS_CERT_PEM / TEMPORAL_TLS_KEY_PEM pair is optional in the
worker code (it falls back to a plaintext connection), but the Temporal cluster
is provisioned with TLS (see step 11 of
External Provisioning), so in any
real environment both are required.
Infisical layout
Production secrets live in the Infisical project mvs-pay, environment prod
(and dev / staging). The folder paths the ExternalSecrets and runbook
reference:
The ExternalSecret manifests are the authoritative mapping from Infisical keys to in-cluster env vars:
How Infisical, ESO, and Vercel fit together across the platform.
The cadence and steps for rotating the internal secret and provider keys.
What must exist outside the repo before any of this resolves.
Running each surface with .env.local and no Infisical.
Required vs optional at a glance
The minimum each surface needs to boot meaningfully (not just survive Zod):
UI minimum
MVS_PAY_DATABASE_URL(build + runtime)MVS_PAY_EXTENSIONS_URL+MVS_PAY_EXTENSIONS_INTERNAL_SECRETINFISICAL_ENV(correct env routing / cookies)- Session secret (
MVSPAY_SESSION_SECRETorBETTER_AUTH_SECRET) NEXT_PUBLIC_HYPERSWITCH_PUBLISHABLE_KEYfor live checkout- Email (
RESEND_API_KEY) and wallets (APPLE_PAY_*) are feature-gated and degrade honestly when absent.
Extensions minimum
MVS_PAY_DATABASE_URL,CONTROL_PLANE_DATABASE_URL,REDIS_URLMVS_PAY_EXTENSIONS_INTERNAL_SECRET(auth fails closed without it)HYPERSWITCH_ADMIN_API_KEY(+HYPERSWITCH_WEBHOOK_SECRETfor webhooks)INFISICAL_*for in-cluster secret bootstrap- Plaid (
PLAID_CLIENT_ID+PLAID_SECRET) only if instant payouts are enabled.
Worker minimum
TEMPORAL_ADDRESS,TEMPORAL_NAMESPACE,TEMPORAL_TLS_CERT_PEM,TEMPORAL_TLS_KEY_PEMMVS_PAY_DATABASE_URLHYPERSWITCH_API_BASE_URL+HYPERSWITCH_ADMIN_API_KEYfor activitiesMVS_PAY_EXTENSIONS_BASE_URLfor the success-rate reconcile activity.
Known gaps and caveats
- No central registry. There is no single typed schema covering all three
surfaces. Extensions has a Zod schema (
config/index.ts); the UI and worker readprocess.envad hoc. Adding a var means touching code, the relevant.env.example, and the relevant ExternalSecret. - Duplicate DSNs.
MVS_PAY_DATABASE_URL, theHYPERSWITCH_*, andINFISICAL_*keys are stored in both Infisical folders. Rotation must hit every copy. NEXT_PUBLIC_ROOT_DOMAINvsNEXT_PUBLIC_BASE_DOMAIN. These are two different variables read by two different files (proxy.tsvsurls.ts). Don’t assume one feeds the other.- Vercel ⇄ Infisical sync is not codified in this repo. The UI’s secrets are listed in Infisical’s provisioning runbook, but the mechanism that lands them in the Vercel project env is operator-side and not represented by an ExternalSecret. Confirm the actual sync path with the platform owner.
infrastructure/secrets-managementpage. It is registered indocs.ymlnavigation but the source.mdxmay not yet be present on disk — the cross-link above will 404 until that page is authored.