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 startCreate a migration
supabase migration new add_profile_fieldsAdd 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 --localThis applies pending migrations to your local database.
Verify
supabase migration list --localConfirm 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 loginLink your project
supabase linkSelect the project you want to update.
Preview the pending migrations
supabase db push --linked --dry-runReview the migration list and SQL before applying it.
Apply the migrations
supabase db push --linkedVerify
supabase migration list --linkedConfirm 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 --localThe 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.