The full env-var matrix. Variables are grouped by service. The Where column points to every code location that reads the variable.
| Variable | Public? | Required in prod? | Set where |
|---|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
yes | yes | Vercel + local |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
yes | yes | Vercel + local |
SUPABASE_SERVICE_ROLE_KEY |
no | yes | Vercel only (never client) |
NEXT_PUBLIC_APP_URL |
yes | yes | Vercel only (see note) |
PAYMONGO_SECRET_KEY |
no | yes | Vercel only |
PAYMONGO_WEBHOOK_SECRET |
no | yes | Vercel only |
NEXT_PUBLIC_PAYMONGO_PUBLIC_KEY |
yes | yes | Vercel + local |
PAYMONGO_CURRENCY |
no | yes | Vercel only — plan currency (default usd; PHP-first platform) |
PAYMONGO_MONTHLY_AMOUNT |
no | yes | Vercel only — monthly price in smallest unit (default 800) |
PAYMONGO_ANNUAL_AMOUNT |
no | yes | Vercel only — annual price in smallest unit (default 8000) |
RESEND_API_KEY |
no | yes | Vercel only |
NEXT_PUBLIC_TURNSTILE_SITE_KEY |
yes | yes | Vercel + local |
NEXT_PUBLIC_TURNSTILE_TEST_BYPASS |
yes | no | Vercel Preview only |
NEXT_PUBLIC_TURNSTILE_DISABLED |
yes | no | (kill switch, see below) |
NEXT_PUBLIC_SENTRY_DSN |
yes | yes | Vercel only |
SENTRY_AUTH_TOKEN |
no | yes | GitHub Actions only (release upload) |
SENTRY_ORG |
no | yes | Vercel + GitHub Actions |
SENTRY_PROJECT |
no | yes | Vercel + GitHub Actions |
CRON_SECRET |
no | yes | Vercel only |
SUPABASE_ACCESS_TOKEN |
no | n/a | GitHub Actions (staging workflow) |
STAGING_SUPABASE_DB_URL |
no | n/a | GitHub Actions only |
PREVIEW_SUPABASE_DB_URL |
no | n/a | GitHub Actions only |
VERCEL_TOKEN |
no | n/a | GitHub Actions only |
PRODUCTION_APP_URL |
no | n/a | GitHub Actions (production release smoke) |
NEXT_PUBLIC_MAINTENANCE_BANNER |
yes | no | Vercel (fail-closed kill switch — see FEATURE-FLAGS.md) |
NEXT_PUBLIC_FEATURE_CSV_IMPORT |
yes | no | Vercel + local (fail-open kill switch) |
NEXT_PUBLIC_FEATURE_PIPELINE |
yes | no | Vercel + local (fail-open kill switch) |
NEXT_PUBLIC_FEATURE_BULK_ACTIONS |
yes | no | Vercel + local (fail-open kill switch) |
Public variables have the
NEXT_PUBLIC_prefix and are inlined into the browser bundle. Treat them as public — never put a secret in aNEXT_PUBLIC_var.
Where: src/lib/supabase/{client,server,middleware}.ts —
the Supabase project URL (e.g. https://fsxdduvwshirrheenmag.supabase.co).
Required: yes (every runtime).
Where: Same three files — the public anon key. RLS is the guarantee that this key can't read other users' data; never rely on the anon key being secret.
Required: yes.
Where: src/lib/paymongo.ts (for updating profiles.plan and
subscription_status via webhook), src/lib/resend.ts (for the daily digest
to query all users), src/app/api/cron/daily-digest/route.ts,
tests/e2e/fixtures/auth.ts (test-only).
Required: yes (server-only).
Security: this key bypasses RLS. It must never reach the
browser. Vercel will refuse to inline it in the client bundle
since it lacks the NEXT_PUBLIC_ prefix.
Where: .github/workflows/staging-promotion.yml,
.github/workflows/pr-gatekeeper.yml (for the supabase
CLI to link the project).
Required: CI only. A personal access token from https://supabase.com/dashboard/account/tokens.
Where: .github/workflows/staging-promotion.yml,
.github/workflows/pr-gatekeeper.yml (for supabase db push and
supabase gen types). Uses the session-mode pooler
(aws-1-ap-southeast-1.pooler.supabase.com:5432) to avoid
prepared-statement leaks from the transaction-mode pooler.
Required: CI only.
Where: .github/workflows/pr-gatekeeper.yml (for the
preview-deploy job to apply migrations to a per-PR preview DB).
Required: CI only.
Where: src/lib/auth.ts (getBrowserOrigin SSR fallback).
Required: yes, but only on the server. The browser
prefers window.location.origin, so a Vercel preview deploy
correctly uses its own URL even with this set to the production
domain.
The standard pattern is:
- Vercel Production: set to
https://agent-flow.app. - Vercel Preview: leave unset (so the SSR fallback is empty
and
window.location.originwins). - Local: set in
.env.localtohttp://localhost:3000.
A common bug: setting
NEXT_PUBLIC_APP_URLto the production domain in.env.local. This breaks magic-link emails from local dev (they'd link to production). Keep.env.localpointing atlocalhost.
Where: src/lib/paymongo.ts (lazy-init). Used for customer,
plan, subscription, and checkout-session creation.
Required: yes (production). The lazy init means the absence of this var in dev only causes runtime errors on first call, not build failures.
Format: sk_live_... in production, sk_test_... in dev.
Where: src/app/api/paymongo/webhook/route.ts. Used for
HMAC-SHA256 signature verification on incoming webhooks.
Required: yes (production). Get this from
https://dashboard.paymongo.com/webhooks after creating a webhook
endpoint that points to <origin>/api/paymongo/webhook.
Where: src/lib/paymongo.ts (lazy-init). Currently only the
secret key is used server-side; the public key is exposed for any
future client-side PayMongo widgets (e.g., inline card tokenization).
Required: no. Safe to leave empty until client-side PayMongo components are added.
Format: pk_live_... in production, pk_test_... in dev.
Where: src/lib/resend.ts (lazy-init). Used by
sendDailyDigest().
Required: yes (production). The domain agent-flow.app must
be verified in the Resend dashboard before the digest emails
will deliver; the sender address is
AgentFlow <daily@agentflow.app>.
Where: src/components/turnstile-widget.tsx — passed to
<Turnstile siteKey={...} />.
Required: yes (production). Get from https://dash.cloudflare.com/?to=/:account/turnstile.
Where: src/components/turnstile-widget.tsx. When set to
"true", the widget renders a fake that auto-fires
onSuccess("test-bypass-token"). Used in Vercel preview
deploys so e2e tests don't need to interact with the real
Cloudflare challenge.
Required: no. Set to "true" on the Vercel Preview
environment only. Never set on Production — Supabase's
captcha verification would either reject the test token (good)
or, if the test bypass token happens to be valid for the
project's secret key, skip the captcha check (bad).
Where: src/components/turnstile-widget.tsx. When set to
"true", the widget renders nothing and never calls onSuccess.
The submit button stays disabled because the page uses
captchaVerified to gate submission.
Required: no. Emergency kill switch — use only if Cloudflare is having an outage and Supabase is rejecting all auth requests due to missing/invalid captcha tokens. Never set in production unless you're OK with no bot protection on the auth pages.
The env-var feature flag system is described in detail in FEATURE-FLAGS.md (operational reference) and ADR-0001 (the decision and alternatives). This section is a quick lookup.
All four vars are fail-OPEN by default (a missing var, an
unknown value, or "" falls through to the code default) — except
the maintenance banner, which is fail-CLOSED (must be the literal
"true" to show).
Where: src/components/maintenance-banner.tsx (via
isMaintenanceBannerVisible()).
Required: no.
Values:
"true"— banner is visible on all dashboard pages- anything else (including missing) — banner is hidden
Default: hidden. Set this to "true" when you want users to
see "AgentFlow is in active development" + a link to /changelog.
Where: src/app/(dashboard)/leads/page.tsx (gates the "Import"
button).
Required: no.
Values:
"false"— CSV Import button is hidden- anything else (including missing) — button is shown
Default: shown. Set to "false" to hide the button without
removing the underlying import page.
Where: reserved — no consumer yet. Adding the pipeline view's gating is the natural next step.
Required: no.
Values:
"false"— pipeline view is hidden- anything else (including missing) — pipeline is shown
Default: shown.
Where: reserved — no consumer yet. Bulk select / bulk delete on the leads list is the natural next step.
Required: no.
Values:
"false"— bulk-actions UI is hidden- anything else (including missing) — bulk actions are shown
Default: shown.
Where: src/sentry.client.config.ts. Public DSN. The
Sentry SDK is initialized only in production (enabled: process.env.NODE_ENV === "production").
Required: yes (production).
Where: .github/workflows/production-release.yml —
npx @sentry/cli releases new ... and sentry-cli upload-sourcemaps.
Required: CI only. Create at
https://sentry.io/settings/account/api/auth-tokens/ with
project:releases and org:read scopes.
Where: next.config.mjs — withSentryConfig() is wrapped
conditionally. Both must be set for the Sentry build plugin to
run; otherwise the build falls through to plain next build.
Required: yes (production). Set on Vercel.
Where: src/app/api/cron/daily-digest/route.ts — checked
against the Authorization: Bearer <secret> header on incoming
requests. Returned 401 if missing or wrong.
Required: yes (production). The cron is invoked by Vercel Cron (or an external scheduler) with the secret in the header.
Where: .github/workflows/staging-promotion.yml —
vercel pull --environment=preview && vercel build && vercel deploy --prebuilt.
Required: CI only. A Vercel classic token from https://vercel.com/account/tokens. (OAuth-session logins cannot create tokens via the CLI.)
Where: .github/workflows/production-release.yml — used
by the smoke-tests job to hit the deployed app and verify it
returned 200. If unset, the job is skipped (this is why the
local agent-flow.app URL smoke tests were disabled in early
runs).
Required: CI only. Set to https://agent-flow.app.
The .env.local.example file is the template. Copy it to
.env.local and fill in real values. .env.local is gitignored.
# .env.local (template, do not commit)
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
NEXT_PUBLIC_APP_URL=http://localhost:3000
PAYMONGO_SECRET_KEY=
PAYMONGO_WEBHOOK_SECRET=
NEXT_PUBLIC_PAYMONGO_PUBLIC_KEY=
RESEND_API_KEY=
NEXT_PUBLIC_TURNSTILE_SITE_KEY=The actual .env.local typically also has:
NEXT_PUBLIC_TURNSTILE_TEST_BYPASS=true # for local e2eThere are three Vercel environments. Each gets a different set of secrets.
| Env | Domain | Has APP_URL? |
Captcha | Sentry |
|---|---|---|---|---|
| Production | agent-flow.app |
https://agent-flow.app |
real site key | enabled |
| Preview | *.vercel.app (per PR) |
unset (let window.location.origin win) |
real + TEST_BYPASS=true | disabled |
| Development | local / vercel dev | http://localhost:3000 |
real | disabled |
The Preview env is the only one with
NEXT_PUBLIC_TURNSTILE_TEST_BYPASS=true. This is what allows the
staging workflow's e2e tests to mint Supabase sessions without
interacting with the Cloudflare challenge.
The app uses lazy init for third-party SDKs and throws for infrastructure vars.
| SDK | Behavior on missing var |
|---|---|
PayMongo (src/lib/paymongo.ts) |
Lazy. First call throws if secret key is missing. Build succeeds. |
Resend (src/lib/resend.ts) |
Lazy. First call throws. Build succeeds. |
| Supabase browser client | Returns {} cast to SupabaseClient (safe on SSR). |
| Supabase server client | Throws on module load. |
| Supabase middleware client | Throws on module load. |
| Sentry | No-op in development; in production, if DSN is missing, Sentry console-warns and continues. |
| Turnstile | Component returns null if NEXT_PUBLIC_TURNSTILE_DISABLED=true; otherwise renders the real widget, which will display Cloudflare's own error if the site key is invalid. |
- DEPLOYMENT.md — where each env var is set in CI and on Vercel.
- AUTHENTICATION.md — how the Turnstile bypass and kill switch are wired.
- SECURITY.md — why
NEXT_PUBLIC_*doesn't mean "public to anyone but the app's own users."