ALTER TABLE statements by hand, tracking which migrations ran where - doesn’t scale. Declarative schema management flips this: you define the desired state, and the tool figures out how to get there. It’s the Terraform approach for databases.
Both Supabase and pgschema offer this declarative workflow for PostgreSQL. Recently, we’ve seen users migrating from Supabase’s declarative schemas to pgschema. This article compares both tools to help you understand when each makes sense.
Workflow Comparison
Supabase (migration history in DB)
pgschema (migration history in git)
Supabase
Supabase maintains two directories and a database table:supabase/schemas/- Your desired state (what you edit)supabase/migrations/- Generated migration files (timestamped SQL)supabase_migrations.schema_migrations- Table tracking which migrations have been applied
supabase db diff, it compares your schema files against the cumulative result of existing migrations and generates a new timestamped migration file.
Adding a column:
pgschema
pgschema has a single source of truth:- Your schema SQL file(s) - that’s it
- No migration history table
- No migrations directory
plan command does a fresh comparison: desired state (your SQL files) vs current state (live database).
Adding a column:
Concurrent Change Detection in CI/CD
Both tools decouple migration generation from execution. Both produce migration SQL. But pgschema’s JSON plan format encodes more than just the SQL statements - it includes a fingerprint of the database schema at plan time:- PR is opened, CI runs
pgschema planand savesplan.jsonas an artifact - Team reviews the plan in the PR
- PR is merged, CD runs
pgschema apply --plan plan.json
Database Object Support
Supabase uses migra for schema diffing, which has documented limitations. pgschema also has limitations, but they’re fundamentally different. Supabase/migra cannot track these schema-level objects:- Row-level security (RLS) policies
- Materialized views
- Comments
- Partitions
- Column privileges
- View ownership and grants
- Domain statements
- Cluster level:
CREATE DATABASE,CREATE ROLE,CREATE TABLESPACE - Database level:
CREATE EXTENSION,CREATE SCHEMA,CREATE PUBLICATION RENAMEoperations (use DROP + CREATE instead)
| Object Type | Supabase/migra | pgschema |
|---|---|---|
| Tables, views, functions | ✅ | ✅ |
| RLS policies | ❌ | ✅ |
| Materialized views | ❌ | ✅ |
| Triggers | ❌ | ✅ |
| Procedures | ❌ | ✅ |
| Partitions | ❌ | ✅ |
| Comments | ❌ | ✅ |
| Column privileges | ❌ | ✅ |
| Domain types | ❌ | ✅ |
| CREATE INDEX CONCURRENTLY | ❌ | ✅ |
| ADD CONSTRAINT … NOT VALID | ❌ | ✅ |
CREATE INDEX, pgschema generates CREATE INDEX CONCURRENTLY. You add a foreign key constraint, pgschema generates ADD CONSTRAINT ... NOT VALID followed by VALIDATE CONSTRAINT. No manual intervention needed.
Organizing Schema Files
Both tools support splitting schemas into multiple files, but with different approaches.Multi-file Structure
Supabase processes files insupabase/schemas/ in lexicographic order. For custom ordering, you configure config.toml:
\i (include) directive. You can dump an existing database into a modular structure:
\i directive, so your schema files work directly with psql - no tool-specific configuration needed.
Ignoring Objects
Supabase has a--schema flag to include/exclude entire schemas. By default, auth and storage schemas are excluded from diffs since they’re managed by Supabase. You can diff specific schemas with:
.pgschemaignore file with pattern matching:
- Gradual adoption - onboard tables incrementally without managing everything at once
- Temporary objects - exclude debug views, temp tables, development-only objects
- Legacy objects - ignore deprecated tables while managing new ones
Summary
| Aspect | Supabase | pgschema |
|---|---|---|
| License | Apache 2.0 | Apache 2.0 |
| Workflow | Schema files → migrations → history table | Schema files → plan → apply (git history, fingerprinting) |
| Database object support | Limited by migra (no RLS, triggers, partitions) | Comprehensive schema-level support |
| Online DDL | Manual | Auto-generates CONCURRENTLY, NOT VALID |
| Multi-file schemas | config.toml with glob patterns | PostgreSQL-native \i directive |
| Ignore patterns | Schema-level only | Granular .pgschemaignore with wildcards |
| Database compatibility | Supabase-hosted PostgreSQL | Any PostgreSQL database |

