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

SectionControlsReaches prod?
[api] config.toml:1-5Which 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-48Redirect 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:68Fixed OTP codes for four test phone numbers, local only.No
[auth.external.apple], [auth.external.google] config.toml:79-106Native 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-198Per 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
Why does a config block exist if prod ignores it? Two reasons in the file itself. First, the local stack needs it to behave like prod during tests. Second, the comments are the only written record of what prod's dashboard is set to. The line "nothing in CI runs 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:48

The three commands

1npx 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-18
2npx 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/.env
3npx 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

ArtifactPath to prod
MigrationsGitHub 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 functionsSame integration; [functions.*] in config.toml tells it which to deploy and with which verify_jwt.
Function secretsDashboard → 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 schemasDashboard, by hand. The config.toml comments are the changelog.
Vault secrets, cron jobsCron 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.
Why unlink after a manual deploy? A linked CLI aims every subsequent 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-66

The 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

  1. Run npx supabase status from src/ and match each printed key to the constants in supabase/tests/functions/_testUtils.ts.
  2. Count function blocks: grep -c '^\[functions\.' supabase/config.toml, then compare with the "7 functions" claim in docs/DEPLOYMENT_GUIDE.md.
  3. Open Studio at localhost:54323, run select 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.