Environment variables
The complete reference — copy .env.example to .env and work through the tables. Columns say what breaks or degrades when a variable is missing.
Core (database, auth, URLs)
| Variable | Required | Notes — and what breaks without it |
|---|---|---|
DATABASE_URL | Yes | Supabase pooled connection string (port 6543, ?pgbouncer=true). Without it every query fails. |
DIRECT_URL | Yes | Supabase direct connection string (port 5432) — used by migrations and the seed, and also validated at runtime: a deployment with only DATABASE_URL works until someone uploads a file, then 500s. Set both. |
NEXT_PUBLIC_SUPABASE_URL | Yes* | Your Supabase project URL. *Optional only with AUTH_DISABLED=true. |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Yes* | Supabase anon key, same caveat. |
SUPABASE_SERVICE_ROLE_KEY | Yes in production | Powers file storage and team invites, and the speaker-portal session cookie’s signing key is derived from it — production refuses to start portal sessions without it. |
NEXT_PUBLIC_APP_URL | Yes in production | The deployment’s public URL. Every absolute link — magic links, portal links, email buttons, the Airtable App URL column — is built from it; production throws rather than pointing emails at localhost. Inlined into client bundles at build time. |
AUTH_DISABLED | Never in production | true skips Supabase auth so you can explore the admin locally. The speaker portal keeps its own token auth even then. |
| Variable | Required | Notes |
|---|---|---|
RESEND_API_KEY | For real email | Without it, development logs every email to the console (the only place local portal sign-in links appear). In production a missing key makes sends fail loudly unless EMAIL_MOCK_MODE=1 is set explicitly. |
EMAIL_FROM | In production with Resend | Sender in Name <address> form; also becomes the calendar-invite organizer. The fallback Resend sandbox sender only delivers to the account owner, so production requires this. |
EMAIL_MOCK_MODE | No | 1 lets a keyless production deployment mock email (one masked log line per send) instead of erroring — for demos only. |
Scheduled jobs
| Variable | Required | Notes |
|---|---|---|
CRON_SECRET | Yes in production | Bearer token guarding /api/cron/airtable-sync and /api/cron/task-reminders. Unset in production, the routes answer 401 and scheduled work silently stops; unset in development they stay open. Vercel sends it automatically once set. Generate: openssl rand -hex 32. |
TASK_REMINDER_WINDOW_HOURS | No (default 48) | How far ahead the nightly reminder looks for due tasks. Also overridable per-run with ?windowHours=. |
TASK_REMINDER_DEDUPE_HOURS | No (default 20) | Minimum gap before the same person is reminded again. ?dedupeHours= per run. |
Airtable (all optional)
| Variable | Required | Notes |
|---|---|---|
ENCRYPTION_KEY | For the in-app connect flow | Encrypts stored Airtable tokens at rest. Without it the connect form explains itself instead of storing anything; not needed for the env-var fallback. Losing or rotating it makes stored tokens unreadable — each event must reconnect. Generate: openssl rand -base64 32. |
AIRTABLE_API_KEY | For the env fallback | Personal access token (pat…) with data.records:read + data.records:write (add both schema.bases:* scopes to use Build the table). Works only together with AIRTABLE_BASE_ID. |
AIRTABLE_BASE_ID | For the env fallback | The app… segment of the base URL. |
AIRTABLE_SUBMISSIONS_TABLE | No (default “Submissions”) | Table name or id for the env-var fallback. |
AIRTABLE_OAUTH_CLIENT_ID | For OAuth | With the redirect URI, switches on the Connect Airtable button — both or neither. |
AIRTABLE_OAUTH_REDIRECT_URI | For OAuth | Must be https://your-app.example.com/api/airtable/oauth/callback, registered with Airtable byte-for-byte (no wildcards — so Vercel preview deployments can’t use OAuth; paste a token there instead). |
AIRTABLE_OAUTH_CLIENT_SECRET | If issued | The flow is PKCE-protected and works without it, but set it if Airtable issued one. |