Provider setup
Every provider in order, the environment variables each needs, where each key comes from, and the gotcha that costs you a session.
Per provider: what it gives you, the env vars, where each key comes from, and the gotcha that costs a session.
Supabase — database, auth, storage
| Var | Secret | Where |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | no | Project Settings → API |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY | yes | same page |
SUPABASE_SECRET_KEY | critical | same page — service role, bypasses RLS |
Set up first: everything downstream references the project ref.
Gotchas. The service key bypasses RLS entirely — treat it as a root password and confine it to server-only directories. Migration tooling that stamps its own timestamps instead of your committed filename will desync the migration ledger from your repo; apply a committed migration under its own version. Free-tier projects pause when idle, which reads as an outage.
Vercel — hosting
| Var | Secret | Where |
|---|---|---|
BLOB_READ_WRITE_TOKEN | yes | Storage → Blob (only if using blob storage) |
CRON_SECRET | yes | you generate it; guards cron routes |
Gotchas. Never deploy from the CLI in an unlinked directory — it creates a
stray project named after the folder and bills failed builds. Vars marked
Sensitive are write-only: env pull returns them as empty strings, and the
dashboard's masked placeholder is not the stored value. To fix a suspect value,
overwrite it; never ask anyone to read it back. An env change only takes effect
on a new deployment — a browser refresh does nothing.
Resend — transactional email
| Var | Secret | Where |
|---|---|---|
RESEND_API_KEY | yes | API Keys |
RESEND_FROM | no | your verified sender |
Scope matters: if the code touches contacts or broadcasts, sending_access
is not enough — it needs full_access.
Gotchas. An invalid key returns 400 "API key is invalid", not 401 —
easy to misread as a wrong endpoint. Domain verification needs DNS records and is
not instant; start it early. Send from a subdomain so marketing volume cannot
damage the root domain's reputation.
Stripe Connect — payments and payouts
| Var | Secret | Where |
|---|---|---|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | no | Developers → API keys |
STRIPE_SECRET_KEY | critical | same page |
STRIPE_WEBHOOK_SECRET | yes | per webhook endpoint |
STRIPE_WEBHOOK_SECRET_CONNECT | yes | Connect events use a separate endpoint and secret |
Gotchas. Connect events are a distinct endpoint with a distinct secret — missing that means silently unprocessed account events. Test and live keys are separate universes; a valid test key in a production slot returns 200 and is still wrong, so assert the prefix, not just the status. Never release a payout before the cancellation window closes.
Twilio — SMS
| Var | Secret | Where |
|---|---|---|
TWILIO_ACCOUNT_SID | yes | Console → Keys & Credentials, Live column |
TWILIO_AUTH_TOKEN | yes | same |
TWILIO_MESSAGING_SERVICE_SID | yes | Messaging → Services |
TWILIO_FROM_NUMBER | — | usually leave empty; see below |
Start A2P 10DLC registration on day one. Brand registration, campaign vetting, and per-campaign monthly fees, with approval measured in days to weeks.
Gotchas, all three of which we hit.
- Twilio shows Live and Test credentials side by side and it is easy to paste
the Test pair. Test credentials return
403code20008on every real resource, so SMS looks configured and silently does nothing. - A messaging service with no phone number attached fails to send even with valid credentials. Accounts accumulate near-identically-named services — verify which one actually holds the number.
- If the sending code prefers
FromoverMessagingServiceSid, setting a from number bypasses your verified A2P campaign and routes over a bare long code, inviting carrier filtering. Leave it empty and send through the service. - Prove production works by listing recent messages; delivered messages are the only conclusive evidence.
Google Maps
| Var | Secret | Where |
|---|---|---|
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY | yes | Cloud Console → Credentials (browser key, referrer-restricted) |
GOOGLE_MAPS_API_KEY | yes | separate server key, IP-restricted |
Gotchas. These must be two different keys with different restrictions. A
referrer-restricted browser key fails server-side with "API keys with referer
restrictions cannot be used with this API" — that is the key being wrong for the
call, so read error_message before concluding it is dead. Probe the API the key
is actually scoped for. Attribution and logo display are terms-of-service
requirements, not styling choices. See costs.md before writing any call.
Sentry and PostHog
| Var | Secret | Notes |
|---|---|---|
NEXT_PUBLIC_SENTRY_DSN | no | safe to expose |
SENTRY_AUTH_TOKEN | yes | build-time only — sourcemap upload; empty locally is correct |
NEXT_PUBLIC_POSTHOG_KEY | no | client key |
Add last. Neither blocks anything.
Webhook signing secrets
SEND_EMAIL_HOOK_SECRET, RESEND_EVENTS_WEBHOOK_SECRET, and similar verify
inbound requests. Nothing calls localhost, so empty locally is correct —
they only matter in deployed environments. Do not treat them as missing
configuration; a doctor that flags them as broken trains people to ignore it.