Skip to Content
DatabaseSchema & Migration

Schemas and migrations

NativeExpress stores database changes as SQL files in supabase/migrations/. Use the Supabase CLI to test them locally and apply them to your hosted project.

Prerequisites

Install the Supabase CLI and run the commands below from your app’s root directory. Local development also needs a running Docker-compatible container runtime .

Files

      • 20260904000000_initial_schema.sql
    • config.toml - Local configuration

The initial migration creates the boilerplate schema for a new project. Add subsequent changes in new migration files.

Test a schema change locally

Start the local stack

supabase start

Create a migration

supabase migration new add_profile_fields

Add your SQL to the new file in supabase/migrations/. Use a name that describes your change. Supabase’s migration guide  includes examples of creating tables and adding columns. A new table also needs its own grants.

Apply the migration

supabase migration up --local

This applies pending migrations to your local database.

Verify

supabase migration list --local

Confirm that your migration is applied, then check the changed table in local Supabase Studio. supabase start prints the Studio URL.

Apply migrations to a hosted project

Sign in to the CLI

supabase login
supabase link

Select the project you want to update.

Preview the pending migrations

supabase db push --linked --dry-run

Review the migration list and SQL before applying it.

Apply the migrations

supabase db push --linked

Verify

supabase migration list --linked

Confirm that the local migration files and hosted migration history match, then check your changed table in the Supabase Dashboard. After a schema change, regenerate your database types.

Grant access to a new table

Every table you add to public needs explicit grants, row-level security and policies in the migration that creates it:

create table public.notes ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users (id) on delete cascade, body text not null ); alter table public.notes enable row level security; revoke all on public.notes from public, anon, authenticated; grant select, insert, update, delete on public.notes to authenticated; grant all on public.notes to service_role; create policy "notes owner" on public.notes for all to authenticated using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id);

Supabase does not grant a new public table to anon, authenticated or service_role when it is created. That is the default for projects created since 30 May 2026, and Supabase applies it to every existing project on 30 October 2026; see its announcement . Tables you already have keep their grants.

Without a grant, the table answers 42501 permission denied for table notes to the app and to the edge functions, whatever its policies say. That includes service_role: a function that writes to a table with no grant … to service_role fails the same way. A table with grants but no row-level security exposes every row.

The revoke comes first because a project still on the old default grants everything to all three roles. With it, the migration gives the same result on old and new projects.

Grant each role only what it uses. The boilerplate’s own tables give authenticated select and delete at most, because the edge functions do the writing with service_role. A serial column also needs grant usage on sequence … to every role that inserts. A gen_random_uuid() or identity key does not.

Reset a local database

Resetting deletes the database’s data before replaying its migrations. Keep --local for this check. --linked resets your hosted database instead.

To test all migrations from an empty database:

supabase db reset --local

The boilerplate ships no seed data and [db.seed] is disabled in supabase/config.toml. To add test data, enable it and create supabase/seed.sql.

Last updated on