# Local Supabase or a hosted branch - pick per environment, not per team

> The CLI stack and Supabase branching solve different problems. Use local for the inner loop, a branch for preview deploys, and never share one dev project.

Three people share one Supabase project called `myapp-dev`. On Tuesday one of them
renames a column. The other two get `column "full_name" does not exist` in the
middle of unrelated work. Someone runs a destructive migration to unblock
themselves and wipes the fixtures everybody was relying on. By Friday the team has
a rule: "tell the channel before you touch dev".

That rule is the smell. A shared development database is a mutex with no lock, and
it gets worse as the team grows. Supabase gives you two ways out, and they are not
alternatives - they cover different parts of the workflow.

## The wrong way: one hosted project everyone points at

```
.env.local  (identical on three laptops)
NEXT_PUBLIC_SUPABASE_URL=https://abcdefgh.supabase.co
DATABASE_URL=postgresql://postgres.abcdefgh:...@aws-0-eu-west-1.pooler.supabase.com:6543/postgres
```

What breaks:

- **Migrations are applied out of order.** Whoever pushes first wins; the second
  person's migration assumes a schema that already moved.
- **You cannot test a destructive change.** Dropping a column to see what breaks
  breaks it for everyone.
- **Seed data rots.** After a month, `dev` holds a mixture of fixtures, half-finished
  test rows and one person's manual debugging.
- **Nothing is reproducible.** A bug that only appears on one laptop is untraceable,
  because the database is not part of the repository.

## The right way, part one: local for the inner loop

The Supabase CLI runs the whole stack - Postgres, PostgREST, GoTrue, Realtime,
Storage, Studio - in Docker on your machine.

```sh
bun run db:start     # supabase start
bun run db:reset     # replay supabase/migrations, seed, then db:migrate
bun run db:types     # regenerate src/db/types.generated.ts
```

`.env.local` points at the containers:

```
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
NEXT_PUBLIC_SUPABASE_ANON_KEY=<printed by supabase start>
DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
```

The local anon and service-role keys are fixed demo JWTs signed with a public
secret. They are the same on every machine, they are safe to commit to
`.env.example`, and they are worthless outside localhost.

What you get is a database that is part of the repo. `bun run db:reset` takes a
few seconds and returns you to a known state: every migration replayed from empty,
then every `supabase/seed/*.sql`, then your ORM's own migrations. That single
command is also your migration test - a migration that works as a one-off
statement but fails on a clean replay is broken, and you find out in the loop
rather than during a deploy.

Costs to be honest about: Docker has to be running, the images are roughly a
gigabyte, and the first `supabase start` after a version bump is slow. Pin
`major_version` in `supabase/config.toml` to whatever the hosted project runs -
Project Settings, Infrastructure - or you will develop against Postgres 15 and
deploy to 17. `bun run verify` compares the pin against the version it actually
connected to and fails when they disagree, so this is one mistake you do not have
to remember.

## The right way, part two: branches for preview deploys

Local stops working the moment something outside your laptop needs the database. A
Vercel preview deployment cannot reach `127.0.0.1:54321`. Neither can a designer
clicking your PR link, nor a Playwright run in a container, nor a webhook from
Stripe.

That is what Supabase branching is for. A branch is a real, separate Supabase
project - its own URL, its own keys, its own Postgres - created from your
repository's migrations.

```sh
bun run db:link --project-ref <production-ref>
bunx supabase branches create preview-checkout --persistent
bunx supabase branches list
bunx supabase branches get preview-checkout   # prints the branch URL and keys
```

Wire those values into the preview environment of your host. On Vercel, set them as
Preview-scoped environment variables:

```sh
bunx vercel env add NEXT_PUBLIC_SUPABASE_URL preview
bunx vercel env add NEXT_PUBLIC_SUPABASE_ANON_KEY preview
bunx vercel env add DATABASE_URL preview
```

Branch creation runs `supabase/migrations` and `supabase/seed/*.sql` against a fresh
database, so a branch is reproducible in the same way a local reset is - with one
gap. Supabase knows nothing about your ORM's migration history, so a branch comes
up with the Supabase half of the schema and none of the ORM half; run the ORM's
deploy command against the branch's connection string before you point a preview
at it. Two other things to know before you rely on branches: they are a paid
feature and are billed like small projects, and creating one takes minutes rather
than seconds because a real Postgres instance is being provisioned. Delete branches you are not using -
`bunx supabase branches delete <name>` - or you will pay for a dozen abandoned
preview databases.

## What about staging?

A long-lived staging project sits between the two. It is worth having when you need
data that survives - a support team clicking through, a load test, an integration
partner with a fixed callback URL. Treat it exactly like production: migrations
arrive through `supabase db push`, nobody edits it in the dashboard, and it never
holds a copy of real user data unless it has been anonymised.

## The decision table

| Situation | Use |
|---|---|
| Writing a feature, iterating on schema | Local CLI |
| Testing a destructive migration | Local CLI, then `db:reset` |
| Vercel preview deployment on a PR | Branch, Preview-scoped env vars |
| External webhook needs to reach the app | Branch or staging |
| Demo for a stakeholder | Staging |
| Anything a customer touches | Production only |

## The rule that actually prevents the Tuesday incident

Nobody points a laptop at a shared hosted project. If a person needs a database, it
is either in Docker on their machine or it is a branch that belongs to their pull
request. `supabase/migrations` and `supabase/seed/` are the only mechanism by
which schema and fixtures travel between them, which means the database is
reviewable in a diff like everything else.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
