Local Development¶
App-name is developed locally against a full Supabase stack — Postgres, Auth, Storage, and the Studio dashboard — running in Docker on your machine. Code moves through GitHub (push/pull); the database does not. This page is the end-to-end loop: stand up the local database, load realistic data, develop against the Studio dashboard, and bring your changes back upstream.
For first-time machine setup see Developer Onboarding; for the Supabase CLI specifics see Supabase (Local).
The local-first model¶
flowchart LR
subgraph local[Your machine]
direction TB
code[Working copy] --- db[(Local Supabase<br/>Postgres · Auth · Storage · Studio)]
end
gh[(GitHub)]
test[(Hosted test)]
prod[(Hosted production)]
code -->|git push / pull| gh
gh -->|deploy applies migrations| test
test -->|promote| prod
test -.->|clone data down| db
db -.->|schema up via Alembic migration| gh
Two things cross the boundary, in opposite directions:
- Schema goes up as code — you change a SQLModel model, generate an Alembic migration, and it flows through GitHub and is applied on deploy. You never hand-edit a shared database.
- Data comes down — you clone the hosted test database into your local stack to develop against realistic records.
Who owns which schema
The local Supabase stack owns the auth and storage schemas (GoTrue, file metadata). Alembic owns the public app tables — they are built from your migrations, never by hand. Keep that split clear: app schema = migrations, platform schema = Supabase.
1. Stand up the local database¶
Install the Supabase CLI and Docker, then from the repo root:
supabase start # boots Postgres, Auth, Storage, Studio in Docker
cd backend && alembic upgrade head # build the public app schema from migrations
supabase start prints the local URLs and keys. The dashboard is Supabase Studio:
| Service | URL |
|---|---|
| Studio (dashboard) | http://localhost:54323 |
| API gateway | http://localhost:54321 |
| Postgres (direct) | postgresql://postgres:postgres@localhost:54322/postgres |
Point your local .env at this stack (full instructions in Supabase (Local)). The local anon/service keys are deterministic dev keys — not secret, never used beyond your machine.
2. Populate with data¶
The standard way to fill a fresh local database is to clone the hosted test environment down, then seed known login users. Schema comes from your local migrations; you load data only so the migration history stays authoritative.
flowchart LR
A[alembic upgrade head<br/>schema from migrations] --> B[clone data-only<br/>from hosted test]
B --> C[seed local test users]
C --> D[run the app]
Clone app data from hosted test¶
First confirm your local migrations are at the same head as test (otherwise a data-only load can fail on columns that don't exist yet):
cd backend && alembic upgrade head
Then dump data only from the hosted test project's direct (5432) connection and load it into local Postgres. The full dump/load procedure — including the session_replication_role = replica trick for out-of-order foreign keys — is on Dump & Load:
# Dump data-only from hosted TEST (direct connection), then load into local:
supabase db dump --db-url "$TEST_DB_URL" -f data.sql --data-only --schema public
psql --variable ON_ERROR_STOP=1 \
--command "SET session_replication_role = replica;" \
--file data.sql \
--dbname "postgresql://postgres:postgres@localhost:54322/postgres"
Mind what's in the test data
Clone from test, not production. If the test database ever contains real personal data, scrub PII before loading it locally and never load an auth dump from production. See the warnings on Dump & Load.
Seed local login users¶
Cloned data is public-only, so it gives you no way to log in. The project ships a small seed-users script that uses the local service_role key to create known accounts directly in the local auth schema (via the Supabase admin API). A minimal version:
# backend/app/seed_users.py — run after `supabase start`: python -m app.seed_users
import os
from supabase import create_client, Client
# Local stack values from `supabase status` (deterministic dev keys, not secret).
supabase: Client = create_client(
os.environ["VITE_SUPABASE_URL"], # http://localhost:54321
os.environ["SUPABASE_SERVICE_KEY"], # local service_role key
)
# Known local accounts — identical on every developer's machine.
SEED_USERS = [
{"email": "admin@app-name.test", "password": "local-dev-password", "role": "admin"},
{"email": "user@app-name.test", "password": "local-dev-password", "role": "member"},
]
def main() -> None:
for u in SEED_USERS:
supabase.auth.admin.create_user(
{
"email": u["email"],
"password": u["password"],
"email_confirm": True, # skip email confirmation locally
"user_metadata": {"role": u["role"]},
}
)
print(f"seeded {u['email']}")
if __name__ == "__main__":
main()
Run it against the running local stack:
cd backend && python -m app.seed_users
You can now log in locally as admin@app-name.test / local-dev-password — and every developer has the same predictable credentials on a fresh database. Keep the seeded accounts to email/password; OAuth is awkward against localhost, so test the real Google/Microsoft flows on the hosted test project instead.
Local credentials only
These accounts exist only in your local auth schema and are wiped by supabase db reset. Never reuse a local seed password for a real test or production user.
Reset to a clean slate¶
supabase db reset drops the database, re-applies all Alembic migrations, and re-runs supabase/seed.sql. Re-clone and re-seed afterward if you need test data back:
supabase db reset
cd backend && alembic upgrade head && python -m app.seed_users
3. Develop against the Studio dashboard¶
Studio at http://localhost:54323 is your full local database GUI:
- Table editor — browse and hand-edit rows while developing.
- SQL editor — run ad-hoc queries against local Postgres.
- Authentication — see and manage the seeded local users.
- Logs — inspect API, Postgres, and Auth logs.
Tip
Use Studio to inspect and query freely. For schema changes, prefer the model-first path below — it keeps your migrations the source of truth instead of letting the dashboard drift ahead of them.
4. Bring your changes back upstream¶
Schema changes — model-first (the rule)¶
This is how schema changes reach test and production:
- Edit the SQLModel model in
backend/app/models/. cd backend && alembic revision --autogenerate -m "describe the change".- Review the generated migration, then test it both ways:
alembic upgrade head && alembic downgrade -1 && alembic upgrade head. - Commit the migration with your code, push, open a PR.
- On merge, the deploy pipeline runs
alembic upgrade headagainst test (and production on release).
Full detail and safety rules are on Migrations. The /db-migration skill walks this exact sequence.
Schema changed in Studio — capture it back (the exception)¶
If you ALTER something directly in the Studio dashboard, the dashboard is not the source of truth — capture the change into code or it will be lost on the next reset and never reach test/prod:
- Make the same change in the SQLModel model in
backend/app/models/. alembic revision --autogenerateand confirm the generated migration matches what you did in Studio.supabase db reset(oralembic upgrade headon a clean DB) to rebuild local from migrations and confirm local now matches the captured state.- Discard any Studio-only tweak that you did not capture into a model.
Reference / lookup data for test & production¶
Lookup tables, enums-as-rows, and other data that every environment must have do not travel in a clone. Put them in an Alembic data migration so they version with the schema and run on deploy:
def upgrade() -> None:
op.bulk_insert(country_table, [{"code": "NL", "name": "Netherlands"}, ...])
That way the same reference rows land in local, test, and production through the normal migration path. See Migrations.
Local-only and throwaway data¶
Records you create while developing stay on your machine. They are wiped by supabase db reset and supabase stop --no-backup, and they are never pushed upstream. Don't rely on them surviving — if data matters beyond your session, it belongs in a seed script or a data migration.
Daily commands¶
| Command | Purpose |
|---|---|
supabase start / supabase stop |
Start / stop the local stack (data preserved on stop) |
supabase status |
Reprint local URLs and keys |
cd backend && alembic upgrade head |
Apply pending migrations to local Postgres |
cd backend && alembic revision --autogenerate -m "…" |
Generate a migration after a model change |
python -m app.seed_users |
Seed known local login users |
supabase db reset |
Drop, re-migrate, and re-seed local from scratch |
supabase stop --no-backup |
Stop and wipe all local data |
Where things live¶
| Topic | Page |
|---|---|
| Supabase CLI stack, ports, env wiring | Supabase (Local) |
| Migrations, autogenerate, rollback | Migrations |
| Dump / clone procedures | Dump & Load |
| Hosted test & production projects, OAuth | Supabase (Hosted) |
| First-time machine setup | Developer Onboarding |