Environment Variables

Reference

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.

SurfaceCodeWhere env is setHow it loads
UIapps/mvs-pay (Next.js)Vercel project env vars (mvs-pay-app)process.env at build time (for NEXT_PUBLIC_* and Prisma generate) and at request time
Extensionsapps/mvs-pay-extensions (Hono)Infisical /apps/mvs-pay/mvs-pay-extensions/* → ESO → K8s SecretgetConfig() in apps/mvs-pay-extensions/src/config/index.ts; also @mvs/secrets injects at boot
Workerpay-crons-worker (packages/temporal-workflows)Infisical /apps/mvs-pay/pay-crons-worker/* → ESO → K8s Secretprocess.env, with @mvs/secrets bootstrap (see src/worker/start.ts)

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

1

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

2

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.

3

Worker (Temporal on EKS)

pay-crons-worker calls loadAppSecretsForRuntime() then reads process.env directly. Temporal connection vars have safe localhost defaults so the worker can run in dev without Infisical.

UI — apps/mvs-pay (Vercel)

The merchant/operator UI. Server-only secrets and a small set of build-time-inlined public keys.

VariablePurposeWhere setRequired
MVS_PAY_DATABASE_URLDSN for the dedicated mvs-pay extensions Postgres (Neon). Read by @mvs/mvs-pay’s Prisma client (packages/mvs-pay/src/client.ts) and required at build for prisma generate.Vercel + InfisicalRequired
MVS_PAY_EXTENSIONS_INTERNAL_SECRETShared service-to-service secret sent as the x-internal-secret header to the extensions service. Read in instrumentation.ts and app/api/debug/secrets/route.ts.Vercel + InfisicalRequired
MVS_PAY_EXTENSIONS_URLBase URL of the extensions service the SDK client points at. (MVS_PAY_EXTENSIONS_BASE_URL is accepted as a fallback in app/api/debug/secrets/route.ts.)VercelRequired
NEXT_PUBLIC_HYPERSWITCH_PUBLISHABLE_KEYPer-merchant Hyperswitch publishable key surfaced to the browser SDK via @mvs/mvs-pay-ui. Read in components/payment-flow.tsx. Inlined at build time.VercelRequired for live checkout
RESEND_API_KEYResend credential used by @mvs/mail to send payment-link emails. When unset, POST /api/payment-links/[id]/send returns an honest 503 (“Email is not configured”) rather than faking delivery.Vercel + InfisicalOptional (feature-gated)
RESEND_FROM_EMAILFrom-address for @mvs/mail (env, with optional DB-config override).Vercel + InfisicalOptional
APPLE_PAY_DOMAIN_ASSOCIATION_PRODApple Pay domain-association token served at /.well-known/apple-developer-merchantid-domain-association in production. See below.Vercel + InfisicalOptional (wallets)
APPLE_PAY_DOMAIN_ASSOCIATION_DEVSame token for non-production.Vercel + InfisicalOptional (wallets)
APPLE_PAY_DOMAIN_ASSOCIATION_<SLUG>Per-domain override for embed-host deployments. <SLUG> = host uppercased, non-alphanumerics → _ (e.g. partner.example.comPARTNER_EXAMPLE_COM).Vercel + InfisicalOptional
NEXT_PUBLIC_BASE_DOMAINOverrides the platform base domain (mvscloud.com) for local dev. Also feeds getValidBaseDomains() for CORS origin validation.Vercel (dev/preview)Optional
NEXT_PUBLIC_ROOT_DOMAINRead in proxy.ts for host/subdomain routing. Note this is distinct from NEXT_PUBLIC_BASE_DOMAINurls.ts does not read it.VercelOptional
CONTROL_PLANE_DATABASE_URLControl-plane Postgres DSN. Used by instrumentation.ts boot checks and required by the Playwright auth fixtures.Vercel + InfisicalRequired where control-plane access is needed
BETTER_AUTH_SECRET / MVSPAY_SESSION_SECRETSession-signing secret (lib/session-secret.ts prefers MVSPAY_SESSION_SECRET, falls back to BETTER_AUTH_SECRET).Vercel + InfisicalRequired for sessions
INFISICAL_ENVSelects dev / staging / prod. Drives getUrlEnvironment() in urls.ts, cookie domain, and the Apple Pay prod/dev token selection.VercelRequired for correct env routing
NEXT_PUBLIC_PAYMENTS_ENVIRONMENT"live" toggles the virtual-terminal live banner (app/(dashboard)/virtual-terminal/page.tsx).VercelOptional
NEXT_PUBLIC_MVS_PAY_INTERCOM_APP_ID / NEXT_PUBLIC_INTERCOM_APP_ID / NEXT_PUBLIC_INTERCOM_ENABLEDIntercom widget config (app/providers.tsx). Set NEXT_PUBLIC_INTERCOM_ENABLED=false to disable.VercelOptional
VERCEL_ENV / VERCEL_URL / VERCEL_BRANCH_URLSet automatically by Vercel. Used for env detection and to add preview deploy URLs to trusted origins.Vercel (auto)Auto
VERCEL_PREVIEW_ORIGINS_ALLOWEDWhen "true", accepts *.vercel.app / *.vercel.sh as trusted origins. Pulumi sets this on dev/preview only — must stay unset in production.Vercel (dev/preview)Optional

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

VariablePurposeWhere setRequired
HYPERSWITCH_API_BASE_URLHyperswitch router REST base URL. Defaults to https://hyperswitch.payments.internal. Must be a valid URL or boot fails.InfisicalOptional (defaulted)
HYPERSWITCH_ADMIN_API_KEYPrivileged admin key for merchant_account, merchant_connector_account, and routing operations. isHyperswitchConfigured() gates behaviour on its presence. Only this service and the worker hold it.InfisicalRequired for live ops
HYPERSWITCH_WEBHOOK_SECRETVerifies inbound Hyperswitch webhook signatures.InfisicalRequired for webhooks
HYPERSWITCH_DASHBOARD_URLDashboard deep-link base for cost-observability links (src/routes/cost-observability.ts, has a built-in default).InfisicalOptional
MVS_PAY_EXTENSIONS_INTERNAL_SECRETExpected value of the x-internal-secret header. Constant-time compared in src/middleware/auth.ts. If unset, every authed request 401s.InfisicalRequired
MVS_PAY_EXTENSIONS_PLATFORM_ADMIN_SECRETElevated secret for platform-admin-only routes (config.platformAdminSecret).InfisicalRequired for platform-admin routes
MVS_PAY_EXTENSIONS_CORS_ORIGINSComma-separated allowlist of production CORS origins. In development, CORS is *; otherwise defaults to pay/healthos/c2.mvscloud.com. Including * in non-dev throws at boot (src/app.ts).Infisical / envOptional (defaulted)
CONTROL_PLANE_DATABASE_URLControl-plane Postgres DSN (tenant + API-key lookup).InfisicalRequired where used
MVS_PAY_DATABASE_URLDedicated mvs-pay extensions Postgres DSN (Neon).InfisicalRequired
REDIS_URLRedis for the API-key cache and distributed rate limiting.InfisicalRequired in prod
INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET / INFISICAL_PROJECT_IDMachine-identity credentials. All three present ⇒ isInfisicalConfigured() true ⇒ secrets are injected at boot.Infisical / K8sRequired for in-cluster bootstrap
INFISICAL_ENVdev / staging / prod; also flips Plaid webhook prod handling (src/routes/webhooks-plaid.ts).InfisicalRequired
PLAID_CLIENT_IDPlaid client id (non-secret).InfisicalRequired for Plaid
PLAID_SECRETPlaid per-environment secret. isPlaidConfigured() needs both id + secret.InfisicalRequired for Plaid
PLAID_ENVsandbox (default) / development / production.InfisicalOptional (defaulted)
PLAID_WEBHOOK_URLAbsolute URL Plaid posts back to; must terminate at /webhooks/plaid in this service.InfisicalOptional
PLAID_WEBHOOK_SECRETOptional Plaid verification key.InfisicalOptional
PORTHTTP listen port. Default 3004.envOptional (defaulted)
NODE_ENVdevelopment / staging / production. Default development.envOptional (defaulted)
API_KEY_CACHE_TTL_SECONDSAPI-key cache TTL. Default 300.envOptional (defaulted)
TENANT_DB_POOL_SIZEPer-tenant DB pool size. Default 5.envOptional (defaulted)
DEFAULT_RATE_LIMIT / RATE_LIMIT_WINDOW_SECONDSLegacy/global rate-limit knobs. Defaults 100 / 60.envOptional (defaulted)
MVS_PAY_RATE_LIMIT_<ROUTE>_MAX / ..._WINDOW_SECPer-route rate limits (payments, refunds, payouts, disputes, webhooks) read in src/middleware/rate-limit.ts. Each has its own default (e.g. payments 600/60s, webhooks 200/1s).envOptional (defaulted)
DD_API_KEY / DD_SERVICE / DD_ENVDatadog APM/log/metric. DD_API_KEY is delivered via the ExternalSecret; service/env have defaults in .env.example.InfisicalOptional

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.

VariablePurposeWhere setRequired
TEMPORAL_ADDRESSTemporal frontend address. Default localhost:7233.InfisicalRequired in prod
TEMPORAL_NAMESPACETemporal namespace. Default default.InfisicalRequired in prod
TEMPORAL_TLS_CERT_PEMClient cert PEM for namespace mTLS. Optional locally; required against the prod Temporal cluster.InfisicalOptional / prod-required
TEMPORAL_TLS_KEY_PEMClient key PEM matching the cert.InfisicalOptional / prod-required
PAY_CRONS_TASK_QUEUETask queue name. Default pay-crons.envOptional (defaulted)
HYPERSWITCH_API_BASE_URLConsumed by worker activities (same meaning as extensions).InfisicalRequired for activities
HYPERSWITCH_ADMIN_API_KEYConsumed by worker activities.InfisicalRequired for activities
MVS_PAY_DATABASE_URLConsumed by @mvs/mvs-pay inside activities.InfisicalRequired
MVS_PAY_EXTENSIONS_BASE_URLBase URL the success-rate reconcile activity calls back into the extensions service (src/activities/success-rate-reconcile.ts).Infisical / envRequired for that activity
INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET / INFISICAL_PROJECT_ID / INFISICAL_ENVOptional Infisical bootstrap via loadAppSecretsForRuntime().Infisical / K8sOptional bootstrap
DD_API_KEYDatadog. Delivered via the worker ExternalSecret.InfisicalOptional

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:

/apps/mvs-pay/mvs-pay-extensions/ # HYPERSWITCH_*, MVS_PAY_DATABASE_URL,
# CONTROL_PLANE_DATABASE_URL, REDIS_URL,
# MVS_PAY_EXTENSIONS_INTERNAL_SECRET,
# MVS_PAY_EXTENSIONS_PLATFORM_ADMIN_SECRET,
# INFISICAL_*, DD_API_KEY
/apps/mvs-pay/pay-crons-worker/ # TEMPORAL_*, HYPERSWITCH_*,
# MVS_PAY_DATABASE_URL, INFISICAL_*, DD_API_KEY
/apps/mvs-pay/hyperswitch/ # HYPERSWITCH_DB_*, HYPERSWITCH_REDIS_*,
# HYPERSWITCH_*KEYS, HYPERSWITCH_ADMIN_API_KEY,
# HYPERSWITCH_WEBHOOK_SECRET
/apps/mvs-pay/legal/baa-status/* # signed:YYYY-MM-DD | pending | not-required
/apps/mvs-pay/wallets/{org}/apple_domain_association
/apps/mvs-pay/connectors/{org}/taxjar/

The ExternalSecret manifests are the authoritative mapping from Infisical keys to in-cluster env vars:

1# deploy/mvs-pay-extensions/base/externalsecret.yaml (excerpt)
2spec:
3 secretStoreRef:
4 name: infisical-payments
5 kind: ClusterSecretStore
6 data:
7 - secretKey: MVS_PAY_EXTENSIONS_INTERNAL_SECRET
8 remoteRef:
9 key: /apps/mvs-pay/mvs-pay-extensions/MVS_PAY_EXTENSIONS_INTERNAL_SECRET
10 - secretKey: MVS_PAY_DATABASE_URL
11 remoteRef:
12 key: /apps/mvs-pay/mvs-pay-extensions/MVS_PAY_DATABASE_URL
13 # ...refreshInterval: 1h, deletionPolicy: Retain

Required vs optional at a glance

The minimum each surface needs to boot meaningfully (not just survive Zod):

  • MVS_PAY_DATABASE_URL (build + runtime)
  • MVS_PAY_EXTENSIONS_URL + MVS_PAY_EXTENSIONS_INTERNAL_SECRET
  • INFISICAL_ENV (correct env routing / cookies)
  • Session secret (MVSPAY_SESSION_SECRET or BETTER_AUTH_SECRET)
  • NEXT_PUBLIC_HYPERSWITCH_PUBLISHABLE_KEY for live checkout
  • Email (RESEND_API_KEY) and wallets (APPLE_PAY_*) are feature-gated and degrade honestly when absent.
  • MVS_PAY_DATABASE_URL, CONTROL_PLANE_DATABASE_URL, REDIS_URL
  • MVS_PAY_EXTENSIONS_INTERNAL_SECRET (auth fails closed without it)
  • HYPERSWITCH_ADMIN_API_KEY (+ HYPERSWITCH_WEBHOOK_SECRET for webhooks)
  • INFISICAL_* for in-cluster secret bootstrap
  • Plaid (PLAID_CLIENT_ID + PLAID_SECRET) only if instant payouts are enabled.
  • TEMPORAL_ADDRESS, TEMPORAL_NAMESPACE, TEMPORAL_TLS_CERT_PEM, TEMPORAL_TLS_KEY_PEM
  • MVS_PAY_DATABASE_URL
  • HYPERSWITCH_API_BASE_URL + HYPERSWITCH_ADMIN_API_KEY for activities
  • MVS_PAY_EXTENSIONS_BASE_URL for 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 read process.env ad hoc. Adding a var means touching code, the relevant .env.example, and the relevant ExternalSecret.
  • Duplicate DSNs. MVS_PAY_DATABASE_URL, the HYPERSWITCH_*, and INFISICAL_* keys are stored in both Infisical folders. Rotation must hit every copy.
  • NEXT_PUBLIC_ROOT_DOMAIN vs NEXT_PUBLIC_BASE_DOMAIN. These are two different variables read by two different files (proxy.ts vs urls.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-management page. It is registered in docs.yml navigation but the source .mdx may not yet be present on disk — the cross-link above will 404 until that page is authored.