Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

How to Use Supabase Migrations

Supabase migrations are versioned SQL files stored in supabase/migrations/ that track every schema change. Use the Supabase CLI to create migrations with supabase migration new, apply them locally with supabase migration up, pull remote changes with supabase db pull, and deploy to production with supabase db push. This gives you version-controlled, reproducible database schemas across environments.

What you'll learn

  • How to create and organize migration files with the Supabase CLI
  • How to apply migrations locally and deploy them to production
  • How to generate migrations from existing schema changes using db diff
  • How to handle seeding, resetting, and pulling remote schemas
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced10 min read20-25 minSupabase CLI v1.100+, PostgreSQL 14+, all Supabase plansMarch 2026RapidDev Engineering Team
TL;DR

Supabase migrations are versioned SQL files stored in supabase/migrations/ that track every schema change. Use the Supabase CLI to create migrations with supabase migration new, apply them locally with supabase migration up, pull remote changes with supabase db pull, and deploy to production with supabase db push. This gives you version-controlled, reproducible database schemas across environments.

Managing Database Schema Changes with Supabase Migrations

Migrations are the standard way to version-control your database schema. Instead of making ad-hoc changes in the Dashboard SQL Editor, you write SQL files that can be committed to Git, reviewed in pull requests, and applied consistently across local, staging, and production environments. This tutorial covers the full migration workflow from initialization to production deployment.

Prerequisites

  • Supabase CLI installed (brew install supabase/tap/supabase or npm install supabase --save-dev)
  • A Supabase project (local or remote)
  • Docker Desktop installed and running (required for supabase start)
  • Basic SQL knowledge for writing CREATE TABLE and ALTER TABLE statements

Step-by-step guide

1

Initialize the Supabase project and start the local stack

Run supabase init to create the supabase/ directory structure in your project. This creates config.toml (project configuration), a migrations/ folder (for SQL migration files), and a seed.sql file (for test data). Then run supabase start to launch the local development stack, which includes PostgreSQL, Auth, Storage, Realtime, and Studio. The local stack gives you the same environment as production without affecting your remote database.

typescript
1# Initialize the project structure
2supabase init
3
4# Start the local development stack (requires Docker)
5supabase start
6
7# Output shows local URLs and keys:
8# API URL: http://localhost:54321
9# Studio URL: http://localhost:54323
10# anon key: eyJ...
11# service_role key: eyJ...

Expected result: The supabase/ directory is created with config.toml and migrations/ folder. The local stack is running with PostgreSQL accessible at localhost.

2

Create your first migration file

Use supabase migration new to create a timestamped SQL file in the migrations/ folder. The filename format is YYYYMMDDHHMMSS_description.sql. Write your CREATE TABLE, ALTER TABLE, or other DDL statements in this file. Migrations run in order based on the timestamp prefix, so always use the CLI to generate the filename. Never rename migration files after they have been applied.

typescript
1# Create a new migration
2supabase migration new create_posts_table
3
4# This creates: supabase/migrations/20260327120000_create_posts_table.sql
5# Open the file and write your SQL:
6
7-- supabase/migrations/20260327120000_create_posts_table.sql
8create table public.posts (
9 id bigint generated always as identity primary key,
10 title text not null,
11 content text,
12 author_id uuid not null references auth.users on delete cascade,
13 published boolean default false,
14 created_at timestamptz default now(),
15 updated_at timestamptz default now()
16);
17
18-- Always enable RLS on new tables
19alter table public.posts enable row level security;
20
21-- Add RLS policies
22create policy "Public can read published posts"
23 on public.posts for select
24 to anon, authenticated
25 using (published = true);
26
27create policy "Authors can manage own posts"
28 on public.posts for all
29 to authenticated
30 using ((select auth.uid()) = author_id)
31 with check ((select auth.uid()) = author_id);
32
33-- Add index for RLS policy performance
34create index idx_posts_author_id on public.posts using btree (author_id);

Expected result: A new migration file is created in supabase/migrations/ with your table schema, RLS policies, and indexes.

3

Apply migrations to the local database

Run supabase migration up to apply all pending migrations to the local database. You can also use supabase db reset to drop the entire local database and reapply all migrations from scratch, plus the seed.sql file. Use db reset when you want a clean slate or when testing that migrations run correctly in order. The local migration state is tracked in the supabase_migrations.schema_migrations table.

typescript
1# Apply pending migrations to the local database
2supabase migration up
3
4# Or reset the entire local database (drops + reapplies all migrations + seed)
5supabase db reset
6
7# List applied migrations
8supabase migration list

Expected result: All migration files are applied to the local database in timestamp order. Tables, policies, and indexes exist as defined in the migrations.

4

Generate migrations from Dashboard changes using db diff

If you made schema changes in the local Studio Dashboard instead of writing SQL manually, use supabase db diff to generate a migration file from the difference. This compares the current database state against the last applied migration and outputs the SQL needed to reproduce the change. Review the generated SQL carefully — it may include unnecessary statements or miss subtleties like RLS policies.

typescript
1# Generate a migration from Dashboard changes
2supabase db diff -f add_categories_table
3
4# This creates a migration file with the detected changes
5# Review the generated file before committing
6cat supabase/migrations/20260327130000_add_categories_table.sql
7
8# If you need to diff against the remote database
9supabase db diff --linked

Expected result: A new migration file is created containing the SQL diff between the current schema and the last migration state.

5

Pull the remote schema as a local migration

If you started building in the Dashboard and want to switch to a migration-based workflow, use supabase db pull to download the remote database schema as a migration file. First link your local project to the remote one with supabase link, then run db pull. This creates a baseline migration that captures the current state of your production database. All future changes should be made through new migration files.

typescript
1# Link to your remote Supabase project
2supabase link --project-ref your-project-ref
3
4# Pull the remote schema as a local migration
5supabase db pull
6
7# This creates a migration file with the complete remote schema
8# Now you can start making changes via new migrations
9supabase migration new add_comments_table

Expected result: A migration file is created containing the complete schema of your remote database. Your local project now has a baseline for future migrations.

6

Add seed data for local development

Create or edit supabase/seed.sql to insert test data that populates your local database during supabase db reset. Seed data is only for development — it never runs on production. Write idempotent INSERT statements or use ON CONFLICT DO NOTHING to prevent errors when seeds run multiple times. Seed data makes it easy for team members to start with a working local database.

typescript
1-- supabase/seed.sql
2-- Insert test data for local development
3
4insert into public.posts (title, content, author_id, published)
5values
6 ('Getting Started with Supabase', 'A beginner guide...', '00000000-0000-0000-0000-000000000000', true),
7 ('Advanced RLS Patterns', 'Deep dive into policies...', '00000000-0000-0000-0000-000000000000', true),
8 ('Draft Post', 'Work in progress...', '00000000-0000-0000-0000-000000000000', false)
9on conflict do nothing;

Expected result: Running supabase db reset applies all migrations and then inserts seed data, giving you a fully populated local database for development.

7

Deploy migrations to production

When your migrations are tested locally and committed to Git, deploy them to your remote Supabase project with supabase db push. This applies all migrations that have not yet been applied on the remote database. The command shows a preview of the SQL that will run and asks for confirmation. Never run db push without testing locally first — there is no automatic rollback for production migrations.

typescript
1# Link to remote project if not already linked
2supabase link --project-ref your-project-ref
3
4# Push all pending migrations to production
5supabase db push
6
7# The command shows which migrations will be applied
8# and asks for confirmation before executing

Expected result: All pending migrations are applied to the remote production database. The schema matches your local development environment.

Complete working example

20260327120000_create_posts_table.sql
1-- Migration: Create posts table with RLS and indexes
2-- Created with: supabase migration new create_posts_table
3-- Apply with: supabase migration up
4-- Deploy with: supabase db push
5
6-- Create the posts table
7create table public.posts (
8 id bigint generated always as identity primary key,
9 title text not null,
10 content text,
11 author_id uuid not null references auth.users on delete cascade,
12 published boolean default false,
13 metadata jsonb default '{}',
14 created_at timestamptz default now(),
15 updated_at timestamptz default now()
16);
17
18-- Enable Row Level Security
19alter table public.posts enable row level security;
20
21-- RLS: Anyone can read published posts
22create policy "Published posts are publicly readable"
23 on public.posts for select
24 to anon, authenticated
25 using (published = true);
26
27-- RLS: Authors can read all their own posts (including drafts)
28create policy "Authors can read own posts"
29 on public.posts for select
30 to authenticated
31 using ((select auth.uid()) = author_id);
32
33-- RLS: Authors can insert their own posts
34create policy "Authors can create posts"
35 on public.posts for insert
36 to authenticated
37 with check ((select auth.uid()) = author_id);
38
39-- RLS: Authors can update their own posts
40create policy "Authors can update own posts"
41 on public.posts for update
42 to authenticated
43 using ((select auth.uid()) = author_id)
44 with check ((select auth.uid()) = author_id);
45
46-- RLS: Authors can delete their own posts
47create policy "Authors can delete own posts"
48 on public.posts for delete
49 to authenticated
50 using ((select auth.uid()) = author_id);
51
52-- Performance indexes
53create index idx_posts_author_id on public.posts using btree (author_id);
54create index idx_posts_published on public.posts using btree (published);
55create index idx_posts_created_at on public.posts using btree (created_at desc);

Common mistakes when using Supabase Migrations

Why it's a problem: Making schema changes in the Dashboard SQL Editor without creating a corresponding migration file

How to avoid: Always use supabase migration new to create a migration file, or use supabase db diff to capture Dashboard changes as a migration. Untracked changes will be lost on db reset and will not be deployed to other environments.

Why it's a problem: Editing or renaming a migration file after it has been applied to any database

How to avoid: Never modify applied migrations. Create a new migration to make additional changes. If you need to fix a broken migration, use supabase migration repair to mark it and then create a corrective migration.

Why it's a problem: Running supabase db push to production without testing migrations locally first

How to avoid: Always run supabase db reset locally to verify all migrations apply cleanly from scratch before pushing to production. There is no automatic rollback for production migrations.

Why it's a problem: Forgetting to include RLS policies in migration files, leaving new tables exposed

How to avoid: Include ALTER TABLE ... ENABLE ROW LEVEL SECURITY and all CREATE POLICY statements in the same migration that creates the table. Treat RLS as part of the table definition.

Best practices

  • Write one migration per logical change — do not combine unrelated schema changes in a single migration file
  • Include RLS policies, indexes, and grants in the same migration as the table creation
  • Always test migrations with supabase db reset before committing to verify they apply cleanly from scratch
  • Use supabase db diff to capture Dashboard changes, but always review and clean up the generated SQL
  • Write idempotent seed data with ON CONFLICT DO NOTHING to prevent errors on repeated resets
  • Add supabase db push to your CI/CD pipeline to automate production deployments
  • Keep migration files small and focused — this makes them easier to review, debug, and roll back
  • Never use supabase db push with the --include-all flag in production without thorough review

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I have a Supabase project where I have been making changes in the Dashboard. Help me set up a migration-based workflow: pull the current schema, create new migrations for future changes, and deploy them to production using the Supabase CLI.

Supabase Prompt

Create a migration file that creates a posts table with id, title, content, author_id (references auth.users), published boolean, and timestamps. Include RLS policies for public read of published posts and author-only CRUD. Add btree indexes on author_id and published columns.

Frequently asked questions

What is the difference between supabase db push and supabase migration up?

supabase migration up applies migrations to your local database. supabase db push applies migrations to the remote production database. Always test locally with migration up or db reset before deploying to production with db push.

Can I roll back a migration after deploying to production?

Supabase does not have automatic rollback. You need to create a new migration that reverses the changes (e.g., DROP TABLE, ALTER TABLE DROP COLUMN). For critical failures, you can use supabase migration repair to mark a migration as reverted and then apply a corrective migration.

What happens if two developers create migrations with the same timestamp?

Migration timestamps are generated to the second, so conflicts are unlikely. If they do occur, rename the timestamp of the later migration before applying. Git merge conflicts in the migrations/ folder will alert you to this issue.

Should I commit migration files to Git?

Yes, always commit migration files to Git. They are the source of truth for your database schema. This lets team members sync their local databases and enables automated deployments via CI/CD.

How do I handle migrations when multiple team members are working on the same project?

Each developer creates migrations on their own branch. During code review, check for conflicting migrations. After merging, each developer runs supabase db reset to apply all migrations in order. Use a CI job to verify migrations apply cleanly on every pull request.

Can I use Prisma migrations with Supabase instead of Supabase migrations?

Yes, but be careful. Prisma migrations can conflict with Supabase's auth schema and RLS policies. If you use Prisma, manage only your application tables with Prisma and handle RLS and auth-related schemas with Supabase migrations.

Can RapidDev help set up a migration workflow for my Supabase project?

Yes, RapidDev can configure your migration workflow, set up CI/CD pipelines for automated deployments, and help establish best practices for team collaboration on database schemas.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.