Local dev and deploy
Lesson 18 · about 15 minutes · what config.toml actually controls, what the three CLI commands do, and which parts of it never reach prod.
The local stack is a faithful copy of prod in some places and a lie in others. Knowing which is which is the difference between "works locally" and "works". This lesson reads config.toml top to bottom, then follows a merged PR to production.
config.toml, section by section
| Section | Controls | Reaches prod? |
|---|---|---|
[api] config.toml:1-5 | Which schemas PostgREST exposes: public, notifications, snaptrade, audit, inventory, promotions, fx, news. A table in a schema not listed here is invisible to supabase-js even with a perfect RLS policy. max_rows = 1000 caps every REST response. | No; set in dashboard |
[auth], [auth.email], [auth.rate_limit] config.toml:7-28, 41-48 | Redirect URLs (Expo and wsbmobileapp:// schemes), 12-char passwords, 1-hour JWTs with refresh rotation, email confirmations on. The comment at line 25 records that email_sent must match prod's 2000 because the scaffold default of 2 blocked all magic links. | No; comment says a lower value pushed would re-break prod |
[auth.sms.test_otp] config.toml:68 | Fixed OTP codes for four test phone numbers, local only. | No |
[auth.external.apple], [auth.external.google] config.toml:79-106 | Native sign-in. client_id is a comma-separated audience list, one provider block shared by app and Admin Gate. skip_nonce_check = true is provider-wide, a documented downgrade. | No; "configured by hand in the dashboard" |
[functions.<name>] config.toml:108-198 | Per function: enabled, verify_jwt, entrypoint, optional import_map and static_files. Every function is verify_jwt = false except submit-feedback. news-admin and news-api bundle the resvg WASM and image assets via static_files. | Yes, via the GitHub integration's function deploy |
supabase config push" is a constraint, not an oversight: pushing the file would overwrite hand-set prod values with local ones. config.toml:77-78, docs/SOCIAL_SIGN_IN.md:48The three commands
npx supabase start from src/. Boots Docker containers for Postgres (54322), Kong (54321), Studio (54323), Mailpit (54324). Applies every migration in order, then seed.sql, which inserts mock companies with quartr_company_id >= 90000 so tests can tell seed rows from real ones. Prints the anon, service_role, publishable and secret keys. README.md "Start Local Development", seed.sql:1-18npx supabase functions serve --no-verify-jwt. One edge runtime hosting every function with hot reload. Reads functions/.env for secrets: Massive, Quartr, SnapTrade, Resend, Slack, Google keys. The flag disables the platform's pre-handler JWT gate, which locally would block the many callers that carry no user JWT. Handler-level auth still runs. CLAUDE.md "Local Development", functions/.envnpx supabase db reset. Drops the database and replays migrations plus seed. This is how a new migration is applied locally; there is no incremental apply. Slow but honest: it proves the whole chain still runs from zero, which is exactly what prod does on a fresh branch.The CLI is run from src/ because it looks for a supabase/ folder in the working directory. Running from the repo root finds the stray top-level supabase/ directory instead and confuses everything. CLAUDE.md "Command Execution Location"
Keys: two generations
The local stack emits both the legacy JWT-shaped anon/service_role keys and the modern sb_publishable_*/sb_secret_* keys. They are not interchangeable here. verifyPublicAuth in stocks accepts only the publishable key; validateServiceRole compares against the legacy service JWT the runtime injects as SUPABASE_SERVICE_ROLE_KEY. _testUtils.ts documents both traps. tests/functions/_testUtils.ts:16-33, stocks/utils/auth.ts:61-67
Scripts
scripts/ holds one-off operational Deno programs with their own deno.json import map (React Email, Resend) and lockfile. They run against prod by design, driven by SUPABASE_URL and SECRET_KEY env vars: backfill-daily.ts shards a two-year rebuild into 31-day calls because that is the edge function's wall-clock budget, retries a shard once, and supports --dry-run. Read docs/BACKFILL_RUNBOOK.md before running any of them. scripts/backfill-daily.ts:1-20, scripts/deno.json
How code reaches prod
| Artifact | Path to prod |
|---|---|
| Migrations | GitHub integration applies them on merge to main. DEPLOYMENT_GUIDE.md still documents the manual link → db push → functions deploy → unlink sequence and lists 7 functions; the repo has 17. Treat the guide as the fallback procedure and the integration as the normal path. CLAUDE.md forbids you running db push or functions deploy. |
| Edge functions | Same integration; [functions.*] in config.toml tells it which to deploy and with which verify_jwt. |
| Function secrets | Dashboard → Edge Functions → Secrets, by hand. A new Deno.env.get name in code is a deploy step you must remember. |
| Auth providers, rate limits, API schemas | Dashboard, by hand. The config.toml comments are the changelog. |
| Vault secrets, cron jobs | Cron jobs are migrations, so they ship automatically; the vault values they read (supabase_url, supabase_service_role_key) were inserted once by hand. See Lesson 2. |
db command at prod. DEPLOYMENT_GUIDE.md step 7 calls unlinking "security hygiene"; the README's warning against supabase db link without a staging project is the same fear from the other side. docs/DEPLOYMENT_GUIDE.md:60-66The docs viewer
.devcontainer/devcontainer.json is not a dev environment for the backend. It exists so a teammate can open a private Codespace that serves docs/api/ (OpenAPI specs, access matrix, data catalog) on port 4173 with Python's http.server, because GitHub Pages on the team plan would be public. .devcontainer/devcontainer.json, docs/api/README.md
Check yourself
Do this in the repo
- Run
npx supabase statusfromsrc/and match each printed key to the constants insupabase/tests/functions/_testUtils.ts. - Count function blocks:
grep -c '^\[functions\.' supabase/config.toml, then compare with the "7 functions" claim indocs/DEPLOYMENT_GUIDE.md. - Open Studio at
localhost:54323, runselect count(*) from "Companies" where quartr_company_id >= 90000, and confirm it matches the seed.
Primary source
Supabase: Local development and the config.toml reference, which lists every key with its default.
Next: Lesson 19, Deno fundamentals: the runtime underneath functions serve. Stuck on a key that 401s? Ask the teacher: "which of the four keys does this endpoint want?"