.sql files run through psql, Prisma Migrate, and Drizzle Kit. Pick whichever fits your stack — they all produce the same kind of result.
For the underlying connection setup, see Direct Postgres patterns. For the per-driver configuration each migration tool needs to coexist with the pooler, see Connection pooling.
The platform / user schema boundary
Powabase projects ship with five schemas. Two are yours, three are the platform’s:| Schema | Owner | Safe to migrate? |
|---|---|---|
public | You | Yes — your application tables live here |
extensions | You | Yes — add custom extensions here |
ai | Platform | No — managed by the AI surface |
auth | GoTrue | No — managed by GoTrue migrations |
storage | Storage API | No — managed by the Storage API |
ai.*, auth.*, and storage.* because you connect as supabase_admin (the project owner) and the platform doesn’t REVOKE those grants. Don’t. The platform’s services assume their schemas’ invariants; modifying them risks data corruption or platform-side breakage.
Always scope your migrations to public and extensions. If you need to add an index to an ai.* table for a performance reason, file a platform issue rather than touching it directly.
Pattern 1 — Hand-written SQL through psql
The simplest approach: a numbered directory of.sql files, each one a migration step. Apply them in order with psql. Track which have been applied in a small metadata table.
Directory layout:
.down.sql file) and you don’t get auto-generation from a model.
Pooler note. psql opens a persistent connection that PgBouncer treats nicely, and your migration wraps everything in a transaction, so the transaction-mode constraints don’t apply. If you have a CREATE INDEX CONCURRENTLY that needs to run outside a transaction, run it as its own migration without the BEGIN/COMMIT wrapper.
Pattern 2 — Prisma Migrate
If your stack is TypeScript / Next.js, Prisma is the path of least resistance. Schema lives inprisma/schema.prisma; prisma migrate dev generates SQL migrations from schema changes.
prisma/schema.prisma — minimal setup:
.env — point at the pooler URL with Prisma’s required flags:
pgbouncer=true flag disables prepared statements (which transaction-mode pooling breaks); connection_limit=1 keeps Prisma from opening more connections than the pool can serve. See Connection pooling for the full driver-flags table.
Generate and apply a migration:
_prisma_migrations table it manages itself. Don’t touch it.
Caveat. Prisma’s migrate dev assumes it can open many connections (it spins up a shadow database to validate the migration). Through the pooler with connection_limit=1, this can hang. The workaround: run migrate dev against a local Postgres during development, copy the generated migration file to your repo, and only run migrate deploy (not migrate dev) against Powabase.
Pattern 3 — Drizzle Kit
For TypeScript users who want a lighter-touch ORM (closer to raw SQL, no model decorators), Drizzle is increasingly the default. Drizzle Kit generates and applies migrations from schema definitions written in TypeScript.src/schema.ts — schema as TypeScript:
drizzle.config.ts:
.env — same URL with the prepare=false query param for the postgres.js driver Drizzle uses:
.sql files in the ./drizzle directory plus a _journal.json metadata file. Inspecting them is straightforward — same flavor as Pattern 1 except autogenerated.
SQLAlchemy + Alembic
The Python equivalent. Covered on its own page: SQLAlchemy + Alembic.Patterns that work across all three
A few practices that apply regardless of which tool you use. Always wrap migrations in transactions. All three patterns do this by default for individual statements, but if you’re writing multi-statement migrations, make sure the whole change is atomic. Half-applied migrations are the worst kind of mess. Avoid renames; prefer “add new column → migrate readers → drop old column”. A directALTER TABLE ... RENAME COLUMN is fine in development but invalidates every cached query in PostgREST, breaks every running client, and leaves no rollback path. The 3-step pattern is slower to develop but doesn’t break running services.
Test against a real Postgres, not a mock. Migrations interact with the entire schema — types, constraints, triggers, indexes. A mock that just records calls won’t catch a NOT NULL violation on existing data. The fastest setup: a local Postgres container, run your migrations against it, then run your test suite.
Backfill data carefully. Schema changes are usually fast; data backfills can take hours. For backfills that need to run online (the app keeps serving traffic while the backfill runs), use a separate background job that pages through the table in chunks, with LIMIT and WHERE id > <last_seen> — not one massive UPDATE.
For CREATE INDEX CONCURRENTLY, run it outside a transaction. It can’t run inside one, and the migration tools all support running specific statements outside the transactional wrapper (Prisma: edit the generated SQL file by hand; Drizzle: same; psql: just don’t wrap in BEGIN/COMMIT). Concurrent indexes don’t block writes during creation, which is what you want on production tables.
When migrations fail
Each pattern handles failure differently: Hand-written: if the migration’s transaction rolls back, nothing changed. Re-run after fixing the SQL. If the migration committed partially (only possible withCREATE INDEX CONCURRENTLY outside a transaction), you have a half-done state — re-running may be safe (CREATE INDEX IF NOT EXISTS) or may need manual cleanup.
Prisma: migrate deploy is idempotent — re-running picks up where it left off. If the migration failed mid-application, Prisma marks it as applied_steps_count < N in _prisma_migrations. Resolve with prisma migrate resolve after manually fixing the state.
Drizzle: similar to Prisma — migrations are tracked in a metadata table; a failure leaves them marked partial. Re-running with drizzle-kit migrate picks up.
In all three cases, the right move after a failure is: stop, inspect the partial state, fix it by hand (or revert if possible), then continue. Re-running blindly after a failure can compound damage.
Next steps
Direct Postgres patterns
The connection-level basics every migration tool builds on.
Connection pooling
The PgBouncer constraints that shape per-driver migration tool config.
Prisma
The full Prisma setup — schema, queries, migrations.
Drizzle
Drizzle setup including schema, queries, and Drizzle Kit migrations.