Supabase Local Dev: The Codebase That Never Runs supabase start
Eight migrations, six RLS policies and 357 tests in 3.3 seconds — with no Docker and no local stack. What that buys, and exactly where it stops working.
Supabase local development means running the whole platform — Postgres, Auth, Storage, the REST gateway and Studio — as Docker containers on your machine, driven by the Supabase CLI. supabase init writes a supabase/config.toml, supabase start brings the stack up, and supabase db reset replays every file in supabase/migrations/ into a clean database.
This storefront does not do that. It has eight migration files, six RLS policies, two SECURITY DEFINER RPCs and a private storage bucket — and no config.toml, no Docker, and no supabase entry in package.json. That is a deliberate position, not an oversight, and working out why it holds is a faster way to understand the local stack than a setup walkthrough.
What the local stack gives you
| Capability | Local stack (supabase start) | Remote-only (what this repo does) |
|---|---|---|
| Replay migrations from zero | supabase db reset, seconds | Only by re-provisioning a project |
| Break the schema safely | Yes — it is a throwaway container | No — the project is shared |
| Work offline | Yes | No |
| Test RLS policies as a real user | Yes, against local Auth | Only against the remote project |
| Costs project quota | No | Yes |
| Requires Docker | Yes | No |
| Studio UI | localhost:54323 | The hosted dashboard |
| Typed schema generation | supabase gen types typescript --local | --project-id, needs network |
The honest summary: the local stack is unambiguously better for schema work. Every row above that matters is a row about changing the database. What it buys you for application work depends entirely on how much of your application needs a database at all.
The measurement that decides it here
$ npm test
Test Files 31 passed (31)
Tests 357 passed (357)
Duration 3.33s
Three hundred and fifty-seven tests, no container, no network, 3.3 seconds. That is possible because the database-shaped logic in this codebase is not written against a database client. src/lib/entitlements.ts — the module that decides whether a buyer may download a file — is a pure function over plain objects:
export function authorizeDownload(
slots: Slot[],
target: DownloadTarget,
): boolean {
return slots.some((s) => slotCovers(s, target));
}
No Supabase import, no await, nothing to mock. The payment webhook takes the same shape: processWebhook receives a narrow WebhookDb interface with two methods, and the real Supabase-backed version of it lives in a separate 43-line file, src/lib/lemonsqueezy/webhookDb.ts. Signature verification, payload parsing, store-id and test_mode checks, tier mapping and the idempotency decision are all tested against an in-memory fake.
So the question "do I need supabase start?" resolves to a different question: how much of your logic currently needs a live database to be exercised at all? If the answer is "most of it", the local stack stops being optional, because the alternative is testing against a shared remote project — which is a worse version of the same thing.
Where remote-only stops working
Three categories of work here genuinely cannot be checked without a database, and they are exactly where a local stack earns its Docker requirement.
RLS policies. The whole policy set is six for select policies, all read-own:
$ grep -c "create policy" supabase/migrations/*.sql | grep -v ":0"
supabase/migrations/0001_commerce_init.sql:4
supabase/migrations/0008_refund_requests.sql:2
A policy is only correct relative to a real auth.uid() in a real session. You can read the SQL and believe it; you cannot prove it without signing in as two different users and watching one of them get zero rows. Locally that is a supabase db reset plus two test users. Remotely it is the same test performed on the database your teammates are using.
RPC behaviour under concurrency. 0007_atomic_download_rate_limit.sql exists because the download endpoint originally read a count and then inserted an audit row as two separate calls, so parallel requests could all pass the same check. The fix moved the count and the conditional insert into one RPC holding a transaction-scoped advisory lock. Verifying a race fix means firing concurrent calls at a database you are allowed to hammer — a local container, not production.
Anything not expressible as DDL. The private releases bucket every paid download is served from is not in the migration history; it was created out of band. supabase db reset does not recreate it, which is a limitation worth knowing before you assume a local reset reproduces your environment. Eight migrations, four of them fixes goes through the whole directory and the habits that keep each file re-runnable.
The workflow, either way
The migration commands are the same whether the target is a container or a hosted project:
supabase migration new add_refund_requests # writes an empty timestamped file
# ... fill it with DDL ...
supabase db reset # local: replay everything from zero
supabase db push # apply pending migrations to the linked project
Two rules survive both workflows and are worth stating plainly:
- Never edit a migration that has been applied anywhere but your own machine. The runner records versions, not checksums, so an edit changes nothing on databases that already ran the file and silently produces two schemas claiming the same history.
- Never make a schema change in the dashboard. A policy or constraint created in the SQL editor exists on exactly one database and is invisible to every other environment, which is the drift that makes
db pushfail later for reasons nobody can reconstruct.
The second rule is the one local development quietly enforces: if your day-to-day database is a container you reset constantly, a dashboard-only change cannot survive to become a habit.
The middle option nobody mentions
Between "everything local" and "one shared remote project" sits a third arrangement: keep the remote project, and make the application not need it. Concretely, in this repo:
src/lib/supabase/{client,server,admin}.ts— three clients, each constructed per call site, so nothing is a module-level singleton that a test has to defeat.src/lib/entitlements.ts— authorization as pure functions.WebhookDbinsrc/lib/lemonsqueezy/webhook.ts— a two-method interface, with the Supabase implementation isolated behind it.
That split is worth doing regardless of your local-stack decision, because it is what makes 357 tests run in 3.3 seconds instead of needing a database to start. The local stack then covers what it is uniquely good at — schema, policies, and concurrency — rather than being load-bearing for the whole test suite.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
supabase start hangs or fails immediately | Docker is not running, or ports are taken | Start Docker; supabase stop any previous stack before restarting |
db reset produces a schema unlike production | Something was created in the dashboard and never written as a migration | Move the change into a migration file; treat the dashboard as read-only for schema |
| Downloads 404 after a local reset | Storage buckets are not part of the migration history | Recreate the bucket as a provisioning step; note it in the migration that depends on it |
db push reports a migration already applied but the change is missing | An applied file was edited afterwards | Add a new migration; use migration repair only to fix the tracking table |
| Auth works locally, fails on the remote project | Redirect URLs and providers are dashboard settings, not schema | Configure them per project; they do not travel in migrations |
| Local types drift from production types | gen types was run against the wrong target | Regenerate with --local or --project-id deliberately, and commit the result |
Frequently asked questions
Do I need Docker to develop with Supabase?
Only for the local stack. A Next.js app talking to a hosted Supabase project needs nothing but the project URL and the anon key. This repository is that case: no Docker, no config.toml, eight migrations applied to a hosted project.
Can I share one remote project across a team instead? You can, and it works until two people change the schema in the same afternoon. The cost is that nobody can reset it, break it, or test a destructive migration. If your team is one person and your schema is settled, the cost is close to zero — which is why this repo pays it.
How do I test RLS policies properly? Sign in as two different users and assert that each sees only their own rows. That needs real sessions, so it needs a database — local for preference, since the test wants to create and delete users freely.
What is the fastest way to make a Supabase app testable without a database?
Put the rules in pure functions and the data access behind a narrow interface. authorizeDownload(slots, target) takes plain objects and returns a boolean; the interface it sits behind has two methods. Everything else in the suite follows from that one decision.
Templates in this post
ASoc Sentinel is a security-software marketing site. ASoc Signal is an AI voice-and-image product landing page. ASoc Sterling is a wealth-management site with service and advisor sections. Each is a front end you could put in front of exactly the schema described above.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
