Migrations
Migrations are timestamped SQL files applied in order — the versioned, repeatable counterpart to typing SQL directly into SQL Editor. Each project records which versions it has already applied, so db push only runs new ones. Each migration runs in a transaction — it either fully applies and is recorded, or rolls back, so a project's schema never ends up half-migrated.
Your schema is part of your application, and it has to change in lockstep with the code that depends on it. The alternative — typing DDL into a SQL console whenever something needs adding — works exactly until the second environment or the second developer appears. Then nobody can say what shape production is in, staging has three columns that live doesn't, and rebuilding a database means remembering a year of ad hoc statements.
A migration makes each schema change an ordinary reviewable artifact: a file, in git, in the same commit as the code that needs it. Checking out an old revision gives you the schema that matched it, code review covers the database too, and a fresh project reaches a known state by replaying the files in order. Because each project records what it has already applied, db push is safe to run repeatedly — a CI deploy can just always call it.
The rule that keeps this honest: migrations are append-only. Once a file has been pushed anywhere but your own machine, don't edit it — the change won't re-run on environments that already recorded that version, and their schemas will silently diverge. Fix a mistake by writing the next migration.
- Shipping a feature that needs a column
- The migration adding
todos.due_daterides in the same pull request as the UI that uses it, so neither can be deployed without the other. - Rebuilding or cloning an environment
- A brand-new project reaches the exact production schema by replaying the folder — the basis for per-tenant provisioning and throwaway CI databases.
- Policies and buckets as code
- RLS policies and
storage.bucketsrows belong in migrations too, so security rules are reviewed and reproducible rather than clicked into a console once and forgotten. - Backfilling data alongside a schema change
- Add the column, populate it from existing rows, then add the
not nullconstraint — all in one file, one transaction, all-or-nothing.
| Surface | Availability |
|---|---|
| Portal UI | None directly — the portal has no migration-authoring UI; SQL Editor is its ad hoc equivalent |
| CLI | shovelbase migration create / shovelbase db push — the primary way to change schema |
| SDK / HTTP | Not applicable — migrations run once, at push time, not from app code |
Writing one
shovelbase migration create add_scores # shovelbase/migrations/20260706120000_add_scores.sqlcreate table public.scores ( id bigint generated always as identity primary key, player_id uuid not null references auth.users(id), points int not null default 0);alter table public.scores enable row level security;create policy "own scores" on public.scores for all to authenticated using (player_id = auth.uid()) with check (player_id = auth.uid());Applying it
shovelbase db push # applies anything not yet recordedshovelbase db push # → "Up to date"Migrations run as the project owner role, so they can manage public tables, RLS policies, storage.buckets rows and storage.objects policies, and reference auth.users in foreign keys. After a push the PostgREST schema cache reloads automatically, so new tables and columns are immediately queryable — no restart needed. Filenames must start with a sortable timestamp; the CLI names new ones for you.
See Permissions for how to write the RLS policies that go inside a migration like the one above.