Generate Supabase Schema Migrations
Generate migrations from SQL schemas with RLS policies and grants.
Introduction
30 Second Summary
A small database change can become a risky guessing game when the intended design drifts away from its change history. Security rules are especially easy to miss.
In this project, you will use the Supabase CLI with declarative schemas to describe a local database in plain SQL. You will generate local migrations from that description before verifying the result in Supabase Studio.
What You'll Build
You will update one SQL schema file, generate a focused migration, and see Supabase Studio display the matching table with its security policy attached.
By the end of this project, you'll have:
- A declared table that becomes a readable migration file you can review before applying.
- A security migration that includes both a row level security policy and its matching grant.
- A verified local database where Supabase Studio shows the policy attached to the table, plus a focused migration containing only a later policy change.
- Secret Mission: An optional challenge to push your declarative migration skills further.
Do I need any experience before starting?
You need Docker running on your computer plus basic SQL knowledge. Step 1 installs the Supabase CLI for you.
Before We Start
Generated migrations need a local database where you can test each change safely. The Supabase CLI runs that database through Docker on your computer.
This step prepares the tools for your declarative schema workflow. You will finish with a local project running in Supabase Studio without signing in to a cloud service.
In this step, get ready to:
- Install Docker Desktop with its local engine running.
- Install the Supabase CLI as a global command.
- Initialize the local project before starting Supabase Studio.
Install and start Docker Desktop
The local Supabase services run inside Docker containers. A version check that shows both client information and server information proves the Docker engine is available.
- Press Cmd+Space on macOS or the Windows key on Windows to open system search.
- Type Terminal on macOS or PowerShell on Windows.
- Press Enter to open the terminal.
- Check whether the Docker engine is already running by running this command:
docker version
What Does This Command Check?
- The client section describes the Docker command installed on your computer.
- The server section confirms that the Docker engine is running.
✔️ I see Client and Server sections
Good progress. Docker now has a live engine ready to host the local Supabase services.
ⓧ I only see client information
Docker is installed but its engine has not finished starting. Start Docker Desktop before checking the engine again.
- Press Cmd+Space on macOS or the Windows key on Windows to open system search.
- Type Docker Desktop.
- Press Enter to start Docker Desktop.
- Select Accept if the subscription agreement appears.
- Wait for Docker Desktop to finish starting.
- Confirm that the engine now responds by running the check again:
docker version
What Does the Second Check Prove?
The repeated check contacts the Docker engine after Docker Desktop starts. A server section confirms that containers can now run locally.
You should now see both client information and server information.
ⓧ Command not found
Docker Desktop needs to be installed before the terminal can reach its command-line tools. Use the instructions for your operating system.
macOS
- Visit the official Docker Desktop installation guide for macOS.
- Download the installer that matches your Mac processor.
- Double-click Docker.dmg to open the installer.
- Drag the Docker icon into the Applications folder.
- Press Cmd+Space to open Spotlight.
- Type Docker Desktop.
- Press Enter to start Docker Desktop.
- Select Accept if the subscription agreement appears.
The first launch can take a moment while Docker prepares its local engine.
- Confirm the installation after Docker Desktop finishes starting by running:
docker version
What Does This Confirmation Show?
The client section confirms that the command is installed. The server section confirms that Docker Desktop is ready to run containers.
You should see both client information and server information.
Windows
- Visit the official Docker Desktop installation guide for Windows.
- Download Docker Desktop Installer.exe.
- Double-click Docker Desktop Installer.exe to start the installer.
- Choose the recommended per-user installation mode.
- Complete the installation wizard.
What Happens After Installation?
Docker Desktop does not start automatically after installation. The next actions start its local engine.
- Press the Windows key to open Windows search.
- Type Docker Desktop.
- Press Enter to start Docker Desktop.
- Select Accept if the subscription agreement appears.
- Confirm the installation after Docker Desktop finishes starting by running:
docker version
What Does This Confirmation Show?
The client section confirms that the command is installed. The server section confirms that Docker Desktop is ready to run containers.
You should see both client information and server information.
Docker Still Is Not Responding?
Confirm that Docker Desktop has finished starting before repeating the version check. Restart Docker Desktop if the command shows client information without server information.
Ask for targeted help with diagnosing the local Docker engine.
Install the Supabase CLI
The Supabase CLI initializes the project files before asking Docker to start the local services. A global installation lets you use the same supabase command throughout this project.
- Check whether the Supabase CLI is already installed by running:
supabase --version
What Does This Command Check?
The command asks the installed CLI to report its version. A version number confirms that the global command is available in this terminal.
✔️ I see a version number
That is the Supabase CLI ready. The terminal can now initialize and manage the local project.
ⓧ Command not found
Install the global command with the package manager supported by your operating system. The macOS path uses Homebrew. The Windows path uses Scoop.
macOS
- Visit the official Homebrew page if Homebrew is missing from your Mac.
- Complete the Homebrew installation instructions on that page.
- Install the Supabase CLI by running this command:
brew install supabase/tap/supabase
What Does This Command Install?
Homebrew installs the Supabase CLI as a global command. This installation supports the direct commands used throughout the project.
- Confirm the global command by running:
supabase --version
What Does This Confirmation Prove?
A version number confirms that the Supabase CLI is installed. It also confirms that this terminal can find the command.
You should see a Supabase CLI version number.
Homebrew Cannot Install the CLI?
Finish every instruction shown by the Homebrew installer before retrying the CLI installation. Start a fresh terminal if the installer updated your command path.
Ask for help with installing the Supabase CLI through Homebrew.
Windows
- Visit the official Scoop page if Scoop is missing from your computer.
- Complete the Scoop installation instructions in a non-administrator PowerShell window.
- Add the official Supabase bucket before installing the CLI by running:
scoop bucket add supabase https://github.com/supabase/scoop-bucket.git
scoop install supabase
What Do These Commands Install?
- The first command adds the package source maintained for the Supabase CLI.
- The second command installs the CLI as a global command.
- Confirm the global command by running:
supabase --version
What Does This Confirmation Prove?
A version number confirms that the Supabase CLI is installed. It also confirms that this PowerShell session can find the command.
You should see a Supabase CLI version number.
Scoop Cannot Install the CLI?
Confirm that PowerShell is running without administrator access. Start a fresh PowerShell session if Scoop installed successfully but the command is still missing.
Ask for help with installing the Supabase CLI through Scoop.
Initialize and start the local project
The CLI expects every local stack to have its own project directory. Initializing that directory creates the configuration Docker uses when the services start.
- Move the terminal to your Desktop by running this command:
cd ~/Desktop
What Does This Command Do?
The command moves your terminal session to the Desktop. Your project folder will be easy to locate from Finder or File Explorer.
- Create the supabase-schema-migrations folder by running these commands:
mkdir supabase-schema-migrations
cd supabase-schema-migrations
What Do These Commands Do?
- The first command creates the project folder on your Desktop.
- The second command moves the terminal into that folder.
Your terminal prompt should now end with supabase-schema-migrations.
- Initialize the local Supabase project inside supabase-schema-migrations by running:
supabase init
What Does Initialization Create?
- The command creates the supabase directory inside your project folder.
- The generated supabase/config.toml stores the configuration for this local project.
- Initialization creates local files only. It does not sign you in or connect the folder to a hosted project.
The initialization should finish successfully with supabase/config.toml available inside the project folder.
✔️ Awesome, I've got everything!
Your generated local configuration is ready. Keep the terminal inside supabase-schema-migrations for the remaining commands.
ⓧ I'd like to double check the generated setup
Your supabase/config.toml is the generated local Supabase project configuration created by supabase init.
The terminal should still be inside the supabase-schema-migrations project folder.
The first start can take a few minutes while Docker downloads the service images. A quiet stretch during this download is expected.
Before you run the command, which parts of the project do you expect Docker to start locally?
- Start the Docker-based local Supabase services by running:
supabase start
What Does This Command Start?
The command reads supabase/config.toml before starting the local services in Docker containers. The service summary includes the address for the local Studio interface.
Everything stays on your computer. No cloud sign-in is required.
The terminal should finish with a summary of the running local services. That summary confirms that the Docker-based Supabase stack started successfully.
- Enter http://localhost:54323 in your browser address bar.
- Press Enter to load the local Studio interface.
You should see Supabase Studio for the local project. That visible dashboard proves the stack is running from your generated configuration.
- Keep the terminal open at the supabase-schema-migrations project root.
Studio Does Not Load?
Check that Docker Desktop is still running. Review the terminal output for a service that did not become healthy.
Confirm that you ran supabase start from the folder containing supabase/config.toml.
Ask for targeted help with starting the local Supabase stack.
Your local database stack is ready for schema work. Next, you will declare a table in SQL before generating its first migration.
Declare Your Tasks Table
Your local Supabase stack is running. You can now give the project its first database structure.
The repository still needs a source of truth for the tasks table. You will describe that table in a declarative schema before turning the declaration into a reviewable migration.
In this step, get ready to:
- Create the supabase/schemas/tasks.sql schema file.
- Declare the columns for public.tasks.
- Generate a timestamped migration for the table.
Create the schema file
A declarative schema describes the database state you want using plain SQL. The generated migration records the change needed to reach that state.
The terminal syntax for writing a multiline file differs by platform. Choose the tab that matches the terminal from earlier.
macOS
- Create supabase/schemas/tasks.sql from the repository root by running these commands:
mkdir -p supabase/schemas
cat > supabase/schemas/tasks.sql <<'SQL'
-- Describe the desired tasks table for generated migrations
create table public.tasks (
id bigint generated by default as identity primary key,
title text not null,
user_id uuid not null
);
SQL
cat supabase/schemas/tasks.sql
What Do These Commands Do?
- The mkdir command creates the supabase/schemas directory.
- The first cat command writes the schema into tasks.sql.
- The public.tasks declaration places the table in the public schema.
- The id column generates identity values for its primary key.
- The title and user_id columns reject null values.
- The final cat command prints the saved file for immediate verification.
You should see the complete table declaration printed in the terminal. The output should end with the closing parenthesis and its SQL terminator.
Schema File Was Not Created?
Check that your terminal is still at the repository root containing supabase/config.toml. Make sure the closing SQL delimiter starts at the beginning of its line.
Ask for help with creating the schema file from a macOS terminal.
Windows
- Create supabase/schemas/tasks.sql from the repository root by running these commands in PowerShell:
New-Item -Path "supabase/schemas" -ItemType Directory -Force
$content = @'
-- Describe the desired tasks table for generated migrations
create table public.tasks (
id bigint generated by default as identity primary key,
title text not null,
user_id uuid not null
);
'@
Set-Content -Path "supabase/schemas/tasks.sql" -Value $content
Get-Content -Path "supabase/schemas/tasks.sql"
What Do These Commands Do?
- The New-Item command creates the supabase/schemas directory.
- The PowerShell here-string holds the complete schema declaration.
- The Set-Content command writes that declaration into tasks.sql.
- The public.tasks declaration places the table in the public schema.
- The id column generates identity values for its primary key.
- The title and user_id columns reject null values.
- The Get-Content command prints the saved file for immediate verification.
You should see the complete table declaration printed in PowerShell. The output should end with the closing parenthesis and its SQL terminator.
Schema File Was Not Created?
Check that PowerShell is still at the repository root containing supabase/config.toml. Make sure the closing '@ marker starts at the beginning of its line.
Ask for help with creating the schema file from PowerShell.
Good. Your repository now contains the desired shape of the tasks table.
✔️ Awesome, I've got everything!
Great. Your saved schema contains the complete table declaration.
ⓧ I'd like to double check the full code
-- Describe the desired tasks table for generated migrations
create table public.tasks (
id bigint generated by default as identity primary key,
title text not null,
user_id uuid not null
);
Generate the migration
The schema file captures the desired state. The diff converts that state into a timestamped migration without applying it to the local database.
Before you run the diff, what database change do you expect the generated migration to describe?
- Generate the pending create_tasks migration by running this command:
supabase db diff -f create_tasks
What Does This Command Do?
- The supabase db diff command calculates the database change represented by your schema declaration.
- The -f create_tasks argument gives the generated migration its descriptive suffix.
- The command writes the result under supabase/migrations without applying it.
You should see the diff finish successfully. The supabase/migrations directory now contains a timestamp-prefixed file ending in _create_tasks.sql.
Migration File Is Missing?
Confirm that supabase/schemas/tasks.sql contains the saved table declaration. Check that the terminal is still at the repository root.
Ask for help with diagnosing the missing migration file.
Review the pending migration
A generated migration deserves the same review as handwritten SQL. This check proves that the file preserves every required column before a later step applies it.
Before you inspect the file, which column should generate its own values?
macOS
- List the migrations and print the generated tasks migration by running these commands:
ls supabase/migrations
cat supabase/migrations/*_create_tasks.sql
What Do These Commands Do?
The first command lists the migration files. The second command prints the generated tasks migration so you can review its SQL.
- Find the statement that creates public.tasks.
- Confirm that id is a bigint identity primary key.
- Confirm that title is text not null.
- Confirm that user_id is uuid not null.
The migration should contain the SQL needed to create the declared table. The migration remains pending because you have only generated and reviewed it.
Migration Does Not Match the Schema?
Compare the generated column types with supabase/schemas/tasks.sql. A missing column usually points to an unsaved or mistyped schema declaration.
Ask for help with comparing the schema and generated migration.
Windows
- List the migrations and print the generated tasks migration by running these commands in PowerShell:
Get-ChildItem -Path "supabase/migrations"
Get-Content -Path "supabase/migrations/*_create_tasks.sql"
What Do These Commands Do?
The first command lists the migration files. The second command prints the generated tasks migration so you can review its SQL.
- Find the statement that creates public.tasks.
- Confirm that id is a bigint identity primary key.
- Confirm that title is text not null.
- Confirm that user_id is uuid not null.
The migration should contain the SQL needed to create the declared table. The migration remains pending because you have only generated and reviewed it.
Migration Does Not Match the Schema?
Compare the generated column types with supabase/schemas/tasks.sql. A missing column usually points to an unsaved or mistyped schema declaration.
Ask for help with comparing the schema and generated migration.
That is the full declaration-to-migration loop working. Your table design is now captured in a reviewable file without changing the running database.
Your tasks table now has a declared shape and a pending migration. Next, you will extend that declaration with database security rules.
Add Row Level Security
Your table declaration from the previous step gives the local Supabase project its first structure. The table still lacks a boundary between each user's rows.
In this step, you will declare Row Level Security in the same schema file. A generated migration will capture the grant plus the policy as database changes.
In this step, get ready to:
- Declare access rules for public.tasks.
- Generate a second migration from the updated schema.
- Verify that the migration contains every security change.
Declare the access rules
Row Level Security makes PostgreSQL evaluate policies when a request accesses protected rows. The authenticated role represents signed-in requests.
- Return to supabase/schemas/tasks.sql in the file editor you used earlier.
- Move to the end of supabase/schemas/tasks.sql.
- Add the access rules by pasting this code:
-- Enforce policy checks for every row in the tasks table.
alter table public.tasks enable row level security;
-- Give signed-in sessions permission to request task rows.
grant select on table public.tasks to authenticated;
-- Limit each signed-in user to rows carrying their user ID.
create policy "Authenticated users can view their own tasks"
on public.tasks
for select
to authenticated
using (auth.uid() = user_id);
What Does This Code Do?
- The alter table statement activates policy checks for the tasks table.
- The grant select statement lets the authenticated role request task rows.
- The policy applies that access to select operations.
- The auth.uid() = user_id check limits visible rows to the signed-in user's identifier.
- Save supabase/schemas/tasks.sql.
- Read the final section to confirm that it includes the RLS statement.
- Read the final section to confirm that it includes the authenticated grant.
- Read the final section to confirm that it includes the own-task policy.
Schema Rules Look Incomplete?
- Check that all three security declarations appear after the table declaration.
- Confirm that each SQL statement ends with a semicolon.
- Ask for help with checking the tasks schema.
✔️ Awesome, I've got everything!
The saved schema now describes the tasks table plus its complete read boundary.
ⓧ I'd like to double check the full code
- Compare your saved supabase/schemas/tasks.sql with this complete file:
create table public.tasks (
id bigint generated by default as identity primary key,
title text not null,
user_id uuid not null
);
-- Enforce policy checks for every row in the tasks table.
alter table public.tasks enable row level security;
-- Give signed-in sessions permission to request task rows.
grant select on table public.tasks to authenticated;
-- Limit each signed-in user to rows carrying their user ID.
create policy "Authenticated users can view their own tasks"
on public.tasks
for select
to authenticated
using (auth.uid() = user_id);
Generate the security migration
The diff command converts the new declarations into a second migration. The existing table migration remains unchanged.
Before you run the diff, picture the three security changes you expect the new migration to capture.
- Generate the security migration from the project root by running this command:
supabase db diff -f add_tasks_rls
What Does This Command Do?
The command compares the declared schema with the migration history. The -f value gives the new migration its add_tasks_rls suffix.
The resulting file records the security delta separately from the table creation.
- Wait for the diff command to finish.
- Confirm that the terminal reports a second migration whose name ends with add_tasks_rls.sql.
Migration Was Not Generated?
- Confirm that the terminal is still at the project root.
- Check that supabase/schemas/tasks.sql was saved before you ran the diff.
- Ask for help with diagnosing the migration diff.
Inspect the generated migration
Timestamped migration names can be fiddly to scan. The suffix identifies the file you need.
- Return to the project file browser in your editor.
- Expand supabase/migrations.
- Open the newest migration whose name ends with add_tasks_rls.sql.
- Confirm that the migration enables row level security on public.tasks.
- Confirm that the migration grants SELECT access to authenticated.
- Confirm that the migration creates the Authenticated users can view their own tasks policy.
- Confirm that the policy checks auth.uid() = user_id.
You should see only the RLS change plus the authenticated grant plus the own-task policy. The original table creation remains in the first migration.
Security Changes Are Missing?
- Compare your saved schema with the full-file reference above.
- Confirm that you opened the migration ending with add_tasks_rls.sql.
- Ask for help with checking the generated migration.
That closes the access gap. Your second migration now carries the table's read boundary as reviewable SQL.
Your schema history now includes the table plus its access rules. Next, you will apply both migrations to the local database.
Apply and Verify the Migrations
The previous step turned your declared schema into two database migrations. Those files are still instructions stored in your repository.
This step applies those instructions to your local database. Supabase Studio then shows the table created from that migration history.
You will also confirm that the security migration applied its authenticated SELECT grant. Studio will show the Row Level Security policy attached to public.tasks.
In this step, get ready to:
- Apply both generated migration files to the local database.
- Confirm the columns created in public.tasks.
- Verify the attached security policy in Studio.
Apply the pending migrations
The Supabase CLI tracks which migration files your local database has applied. Its migration command runs the pending files in timestamp order.
Before you run this, predict which database objects the two migrations should create.
- Apply the pending migrations to your local database by running this command:
supabase migration up
What Does This Command Do?
- The command reads pending migration files from supabase/migrations.
- The local migration history records each file after it applies successfully.
- The first migration creates public.tasks.
- The second migration applies its grant plus its security policy.
You should see one applied filename ending in _create_tasks.sql. You should also see one applied filename ending in _add_tasks_rls.sql.
The command should finish without a database error. That is the key handoff complete: your generated SQL now controls the local database.
Migration Did Not Apply?
- Confirm that your terminal remains at the project root.
- Check that the local Supabase stack remains running.
- Read the first migration filename reported with the database error.
Ask for targeted help with diagnosing the failed local migration.
Verify the table structure
Studio reads the database state produced by the migrations. The table view lets you compare that state with the declaration in supabase/schemas/tasks.sql.
- Switch back to Supabase Studio from earlier.
- Select Table Editor in the left sidebar.
- Select the public schema.
- Select the tasks table.
You should see the id, title, and user_id columns.
The id column should be a bigint identity primary key.
The title column should be required text. The user_id column should be a required uuid.
Table Missing in Studio?
- Refresh the Supabase Studio browser tab.
- Confirm that public is the selected schema.
- Review the migration command output for the filename ending in _create_tasks.sql.
Ask for help with finding the migrated table.
Confirm the security policy
The authenticated grant permits the role to request rows with SELECT. The policy limits those rows through its ownership condition.
Before you check the policy, predict which role plus operation Studio should show.
- Select Database in the Studio sidebar.
- Select Policies.
- Select tasks under the public schema.
You should see that Row Level Security is enabled for tasks.
Studio should show the Authenticated users can view their own tasks policy. It should target SELECT for the authenticated role.
The policy condition should compare auth.uid() with user_id. The successful security migration also confirms that its authenticated SELECT grant reached the local database.
That closes the loop. Your local database now matches the table structure plus the access rules declared in supabase/schemas/tasks.sql.
Policy Missing in Studio?
- Refresh the Policies page.
- Confirm that tasks belongs to the public schema.
- Review the migration command output for the filename ending in _add_tasks_rls.sql.
Ask for targeted help with finding the applied RLS policy.
Your generated migrations now rebuild the declared table plus its security controls. Next, you will change the policy declaration and generate a migration containing only that change.
Generate a Policy Change Migration
Your first two migrations now reproduce the tasks table with its original read policy. That applied history gives this final schema change a stable baseline.
Editing an applied migration would rewrite that history. You will update the declared state before asking the Supabase CLI to generate a focused third migration.
In this step, get ready to:
- Extend the existing tasks policy to cover updates.
- Generate a third migration from the updated schema file.
- Confirm the migration contains only the access-control change.
Expand the policy to cover updates
PostgreSQL separates table privileges from Row Level Security checks. The authenticated role needs update permission before the policy can decide which rows it may change.
- Open supabase/schemas/tasks.sql in your code editor.
- Find the access-control section below the row level security statement.
- Replace the existing grant and policy with this updated section:
-- Give authenticated users read and update privileges
grant select, update on table public.tasks to authenticated;
-- Restrict visible rows and validate updated rows against ownership
create policy "Authenticated users can view their own tasks"
on public.tasks
for all
to authenticated
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
What Does This Schema Change Do?
- The grant statement gives authenticated users permission to select rows.
- The same statement gives authenticated users permission to update rows.
- The using expression limits visible rows to tasks owned by the current user.
- The with check expression prevents an update from assigning a task to another user.
Why Use One Policy?
PostgreSQL accepts one command target in each policy definition. The for all target lets the existing policy govern reads and writes.
The table grant restricts authenticated users to select and update operations. Insert and delete access remain unavailable.
- Save supabase/schemas/tasks.sql.
- Confirm the grant includes select, update.
- Confirm the policy ends with the with check expression.
Policy Section Looks Different?
Confirm that you edited supabase/schemas/tasks.sql. Keep the policy name exactly as it appears in the existing file.
Ask for help with checking the tasks policy update.
✔️ Awesome, I've got everything!
Your schema file now declares read access plus owner-only updates.
ⓧ I'd like to double check the full code
- Compare supabase/schemas/tasks.sql with this complete file.
-- Define the tasks stored by the application
create table public.tasks (
id bigint generated by default as identity primary key,
title text not null,
user_id uuid not null
);
-- Apply row-level checks before client roles access tasks
alter table public.tasks enable row level security;
-- Give authenticated users read and update privileges
grant select, update on table public.tasks to authenticated;
-- Restrict visible rows and validate updated rows against ownership
create policy "Authenticated users can view their own tasks"
on public.tasks
for all
to authenticated
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
Generate the focused migration
The applied migrations remain your database baseline. The updated declarative schema represents the new desired state.
Before you run the diff, do you expect the migration to rebuild public.tasks or focus on its access controls?
- Generate the third migration from the terminal from earlier by running this command:
supabase db diff -f update_tasks_policy
What Does This Command Do?
The command compares the declared schema with the migration history. The file flag saves the resulting SQL as a new timestamp-prefixed migration.
The suffix update_tasks_policy makes the migration's purpose clear during review.
- Confirm the command created a timestamp-prefixed supabase/migrations/<timestamp>_update_tasks_policy.sql file.
The new file proves that the schema change was detected. Your first two migrations remain untouched.
No Migration File Created?
Confirm that the terminal is still at the project root. Check that you saved the updated schema file before running the diff.
Ask for help with diagnosing the missing policy migration.
Review the migration without applying it
A generated migration is executable database history. Reviewing its scope now catches accidental table changes before they reach the local database.
- Expand supabase/migrations in your code editor.
- Open the newest timestamp-prefixed migration ending in _update_tasks_policy.sql.
- Confirm the migration grants update access to the authenticated role.
- Confirm the migration changes the existing tasks policy.
- Confirm the migration does not recreate public.tasks.
You should see a focused access-control diff. It adds the update grant without repeating the table definition.
The policy change should preserve the ownership check for visible rows. It should also add the ownership check for updated rows.
Why Is the Database Unchanged?
Generating a migration writes a proposed change to the repository. The local database still uses the original applied select policy because this third migration remains unapplied.
That is the payoff of declarative migrations. A small security change becomes a focused reviewable file without rewriting earlier history.
Secret mission
Audit the Pending Policy Migration
Compare the declared policy with the pending migration. Prove that the live database still holds the previous applied state.
Clean Up Your Resources
Clean Up Your Resources
Your local project uses the Supabase CLI to manage services that run through Docker. No hosted project is required, so the service cost is $0.
Resources you used:
- The running local Supabase stack with Supabase Studio.
- The supabase/ folder with its local project artifacts.
Keep everything running
No cleanup is required. Choose this option if you want to continue reviewing the declarative schema or testing the generated policy migration.
- Leave the local Supabase stack running.
- Preserve supabase/config.toml for the local project configuration.
- Preserve supabase/schemas/tasks.sql as the declared database state.
- Keep all three timestamp-prefixed files in supabase/migrations.
Supabase Studio remains available locally. Docker continues using memory until you stop the stack.
The unapplied third migration remains ready for your next session.
Pause - I'll come back to this later
Stop the local services to free their Docker resources. Your schema file and migration history remain in the project root.
- Return to the terminal at the project root from earlier.
- Stop the local Supabase stack through the Supabase CLI.
- Watch the terminal for confirmation that the local services have stopped.
- Return to the Supabase Studio browser tab.
- Refresh the browser tab.
You should no longer reach the local project through Supabase Studio. This confirms the local services have stopped.
- Close the Supabase Studio browser tab.
- Keep the supabase/ folder in the project root.
That's the pause complete. Your project files remain ready for another session.
Delete - I don't want to use this again
Deleting the supabase/ folder is permanent. Your Supabase CLI and Docker installations stay available for other projects.
- Return to the terminal at the project root from earlier.
- Stop the local Supabase stack through the Supabase CLI.
- Wait for the terminal to confirm that the local services have stopped.
- Return to the Supabase Studio browser tab.
- Refresh the browser tab.
You should no longer reach the local project through Supabase Studio. The running local services are now stopped.
- Close the Supabase Studio browser tab.
macOS
- Press Cmd+Space to open the search bar.
- Type Finder into the search bar.
- Select Finder from the search results.
Finder is now open. The terminal's current path identifies the project root you need.
- Browse to the project root shown in your terminal.
- Select the supabase/ folder.
- Delete the selected folder with the file manager's deletion control.
- Open the file manager's deleted-items location.
- Permanently delete supabase/ from that location.
- Return to the project root.
- Confirm that supabase/ no longer appears.
Windows
- Press the Windows key to open the search bar.
- Type File Explorer into the search bar.
- Select File Explorer from the search results.
File Explorer is now open. The terminal's current path identifies the project root you need.
- Browse to the project root shown in your terminal.
- Select the supabase/ folder.
- Delete the selected folder with the file manager's deletion control.
- Open the file manager's deleted-items location.
- Permanently delete supabase/ from that location.
- Return to the project root.
- Confirm that supabase/ no longer appears.
Cleanup is complete. Your local Supabase services are stopped.
The supabase/ folder no longer appears in the project root.
Nice Work!
Nice Work!
You made it. You built a local Supabase workflow that turns declared SQL schemas into focused migration files.
You've learned how to:
- Turn a declarative schema in supabase/schemas/tasks.sql into a migration that creates public.tasks.
- Protect public.tasks with a generated Row Level Security policy for authenticated users.
- Inspect generated migration diffs that isolate grants or policy updates from the original table creation.
- Secret Mission: Complete the optional challenge to push your declarative migration skills further.
Ready to quiz yourself?