Back to BlogArchitecture & Engineering

Lovable and Supabase Migrations: Stop Editing Production by Hand

Rupak Amin

Founder & Lead Engineer, RAITHub

13 min read

To stop editing production by hand, make the supabase/migrations folder the only route a schema change takes to production. Pull a baseline of the live schema with supabase db pull, make changes on a local or staging database, capture them with supabase db diff, test with supabase db reset and supabase test db, and deploy with supabase db push from CI.

This guide is for teams whose Lovable app runs on Supabase and whose schema has been changed in several places: approved Lovable migrations, the Table Editor, the SQL editor, perhaps another AI tool. It shows how to find out how far production has drifted, how to capture it, and how to keep Lovable useful once the rule is "no hand edits".

Why is editing the production database by hand a problem?

Because the repository stops describing the database, and every tool that trusts the repository starts to fail. Supabase's database migrations guide is direct about it: making changes on a remote database from the dashboard "bypasses the migration history and will cause db push to fail with sync errors". Its team rule is "Never change the remote database directly."

The cost shows up in four ways:

  • No rebuild. A new staging project, a preview branch or a disaster recovery cannot be built from the files, because the files are missing changes.
  • Missing security. A row-level security policy added by hand in the dashboard is not in the files. Any environment built from them has the table without the policy. That is the flaw class behind CVE-2025-48757, whose advisory describes Lovable projects "deployed with insufficient Row Level Security (RLS) policies".
  • No review. A change typed into a production SQL editor was seen by one person, once, and cannot be read by anyone else afterwards.
  • No undo. Nobody knows the state before the change, so nobody can reliably write the change that reverses it.

How does Lovable handle Supabase migrations?

Better than most people assume. Lovable's Supabase integration docs say that "Schema changes run as reviewed migrations: Lovable writes the SQL, shows it to you, and asks for your approval in the project chat before running it." After approval, "Lovable executes the migration on your Supabase project, saves the migration file in your project's code (under supabase/migrations/), and regenerates the TypeScript types your app uses." It also asks before inserting or changing data.

So changes made through Lovable are recorded. The gaps are elsewhere:

  • They run on whichever project is connected. If that is production, every approved prompt is a production schema change, reviewed in a chat window.
  • Everything else is not recorded. Table Editor clicks, SQL editor queries and changes by other tools never reach the folder.
  • No environments by default. The Supabase integration docs we checked do not describe separate test and live databases for a connected project, so staging is something you set up yourself.
Where the change came fromIn supabase/migrations?Risk
Lovable, after you approve the SQLYesLow for the record; runs on the connected project straight away
Supabase Table Editor on productionNoDrift; db push sync errors later
SQL editor on productionNoDrift, no review, often policies and functions nobody can find again
Another AI tool running SQL on the databaseOnly if it writes a fileSame as the SQL editor unless it does
A developer using the Supabase CLIYesLow, if one pipeline does the pushing

Step 1: how far has production drifted?

Link the CLI to the project and compare. Run these from the repository root, where Lovable has already created the supabase folder:

supabase login
supabase link --project-ref YOUR_PROJECT_REF
supabase migration list
supabase db diff --linked

migration list shows which migration files the remote database has recorded as applied and which it has not. db diff --linked compares the schema the files produce with the live one; any SQL it prints is drift, changes that exist in production but in no file. Save that output. It is your inventory of hand edits.

Step 2: how do I capture the live schema as a baseline?

Pull it into a migration file, so the repository matches production again:

supabase db pull
git add supabase/migrations
git commit -m "Baseline: capture production schema"

db pull writes the remote schema into a new file in supabase/migrations/. The CLI may offer to record it as already applied in the remote migration history; accept, because production already has those changes. If the history ever disagrees with reality, supabase migration repair --status applied TIMESTAMP corrects the record without running any SQL. Read the baseline before committing it: look for tables without enable row level security and for policies you did not know existed.

Step 3: can the files rebuild the database?

Prove it locally. With Docker running:

supabase start
supabase db reset

db reset drops the local database and replays every migration, then the seed file, from empty. If it fails, fix the files now, while the only thing at stake is your laptop. If it succeeds, you have what most hand-edited projects lack: a schema anyone can recreate from the repository.

This is the discipline RAITHub runs on its own platforms. On Sundor Skin, a B2B wholesale platform RAITHub built with a 146-table PostgreSQL core, CI replays all 76 migrations on an empty database on every change and fails the build if any buyer-scoped table lacks a row-level-security policy. Sundor Skin uses PostgreSQL directly rather than Supabase, but the principle is identical: if the files cannot rebuild it, it is not under control.

Step 4: how should every new schema change be made?

As a file, on a database that is not production. There are two ways to create one:

  • Write the SQL: supabase migration new add_invoices creates an empty timestamped file to fill in.
  • Click, then capture: change the local database in the local Studio, then run supabase db diff -f add_invoices to write the difference to a file. Supabase's guide says to "Only use the Dashboard to make schema changes on your local database".

Either way, put the table and its security in the same file. A migration that creates a table and leaves the policy "for later" is how exposed tables are made:

-- supabase/migrations/20261022093000_add_invoices.sql
create table public.invoices (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null default auth.uid() references auth.users (id) on delete cascade,
  amount_cents integer not null check (amount_cents >= 0),
  status text not null default 'draft',
  created_at timestamptz not null default now()
);

alter table public.invoices enable row level security;

create policy "Owners can read their invoices"
on public.invoices for select
to authenticated
using ( (select auth.uid()) = user_id );

create policy "Owners can create draft invoices"
on public.invoices for insert
to authenticated
with check ( (select auth.uid()) = user_id and status = 'draft' );

create index invoices_user_id_idx on public.invoices (user_id);

The policy pattern, with the role named and auth.uid() wrapped in a select, follows Supabase's row-level security guide. There is no update or delete policy, so users cannot edit or remove invoices from the browser; that is a product decision written down in SQL.

Step 5: how do I test a migration before it ships?

Replay it and run database tests against the result. Supabase runs pgTAP tests from supabase/tests with supabase test db, as its testing guide describes. Start with one test that protects every future migration, the Supabase equivalent of Sundor Skin's policy gate:

-- supabase/tests/rls_everywhere.test.sql
begin;
create extension if not exists pgtap with schema extensions;
select plan(1);

select is_empty(
  $$ select c.relname
     from pg_class c
     join pg_namespace n on n.oid = c.relnamespace
     where n.nspname = 'public'
       and c.relkind in ('r', 'p')
       and not c.relrowsecurity $$,
  'every public table has row-level security enabled'
);

select * from finish();
rollback;

Any migration, from Lovable or a developer, that adds a table without RLS now fails the check. Add behaviour tests next: as one user, try to read another user's invoice and expect nothing back.

Step 6: how do I deploy migrations from CI?

Let one pipeline, and only that pipeline, run supabase db push. Supabase's migrations guide warns that "concurrent pushes from different machines can cause conflicts", and its managing environments guide describes the shape: feature branches on a local database, a develop branch deploying to a staging project, and main deploying to production, with the access token, database password and project ID stored as encrypted GitHub secrets.

# .github/workflows/database.yml
name: database
on:
  pull_request:
    paths: ['supabase/**']
  push:
    branches: [main]
    paths: ['supabase/**']
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: supabase/setup-cli@v1
        with:
          version: latest
      - run: supabase db start
      - run: supabase test db
  deploy:
    if: github.event_name == 'push'
    needs: test
    runs-on: ubuntu-latest
    env:
      SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
      SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}
      SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }}
    steps:
      - uses: actions/checkout@v4
      - uses: supabase/setup-cli@v1
        with:
          version: latest
      - run: supabase link --project-ref $SUPABASE_PROJECT_ID
      - run: supabase db push

Pull requests replay the migrations and run the tests; merging to main deploys them. Supabase recommends the access token be "a scoped personal access token limited to the projects this workflow deploys to". Add a staging job on a develop branch the same way once you have a staging project; the environments guide says staging should be a new project rather than a reused one.

Step 7: where does Lovable fit once CI owns deploys?

Pick one of two set-ups, and write the choice down.

Set-upHow changes reach productionGood forWatch out for
Lovable stays on productionLovable runs approved migrations; CI only replays and tests the files on each pull requestSmall apps still built mainly by promptingA bad migration is live before CI sees it; read every SQL diff before approving it
Lovable on a staging projectLovable's migrations run on staging; CI pushes the same files to production after tests passAny app with real users or paying customersStaging data is not production data; seed it with realistic test records

One check before you switch CI on: run supabase migration list against production and confirm that migrations Lovable already ran appear as applied. If a file shows as pending even though its change is live, db push will try to run it again. Mark it applied with migration repair first, after confirming the change really is in production.

How do I make destructive changes without downtime?

Split them into steps that each keep the running app working, often called expand and contract. To rename a column:

  1. Add the new column, nullable. Deploy.
  2. Change the app to write both columns. Deploy.
  3. Backfill the new column from the old one in a migration.
  4. Change the app to read the new column. Deploy.
  5. Drop the old column in a later migration, once nothing reads it.

Treat migrations as forward-only: if one is wrong, write a new migration that corrects it rather than editing a file that has already run somewhere. And have a restore path before anything destructive. Supabase's production checklist recommends Point in Time Recovery for databases expected to pass 4 GB.

For the other gaps that usually sit next to missing migrations, such as secrets, validation and cost controls, see making a vibe-coded app production-ready. For multi-tenant schemas specifically, the multi-tenant SaaS guide covers how tenancy and row-level security fit together.

Why RAITHub for this

  • Migration discipline in production. Sundor Skin's 76 migrations replay from empty in CI, with a policy gate and an IDOR suite among 530+ tests. The workflow above is that practice, translated to the Supabase CLI.
  • Drift reconciled, not guessed. The baseline, the history repair and the first CI run are the risky part; they are done against a copy first, with the diff reviewed line by line.
  • Fixed scope. A free 15-minute technical audit, then a written, fixed quote. Work arrives as pull requests you own.

RAITHub has not published a case study of migrating a Lovable project to this workflow, so this post is engineering guidance, not a claim of past results.

When you don't need us

  • db diff --linked prints nothing and every change has come through Lovable. You are close already: add the RLS test and the CI workflow.
  • The app has no real users yet. Keep prompting, but capture a baseline now; it takes minutes and makes everything later easier.
  • You have a developer comfortable with the Supabase CLI. Give them this post; it is a few days of careful work.

If the drift is large, or production holds data you cannot afford to lose while reconciling it, send it to fix one issue for a single fix, or see API and backend development for ongoing database work.

Last reviewed: 29 September 2026. Supabase and Lovable documentation checked on 29 September 2026.

To have the drift measured and the workflow set up against a copy first, pick Code Rescue on the contact form and paste the output of supabase migration list.

Frequently asked questions

Does Lovable create Supabase migration files?

Yes. Lovable's documentation says that after you approve a schema change, it runs the migration on your Supabase project and saves the file under supabase/migrations/ in your code. Changes made by hand in the Supabase dashboard are not captured.

Why does supabase db push fail with sync errors?

Usually because someone changed the remote database outside migrations, or because the remote migration history does not match the files. Run supabase migration list and supabase db diff --linked to see the difference, capture it with supabase db pull, and repair the history if needed.

Can I still use the Supabase dashboard after switching to migrations?

Yes, on your local database. Supabase's guide says to make dashboard schema changes locally, then capture them with supabase db diff. On production, use the dashboard to read data and logs, not to change the schema.

How do I undo a Supabase migration?

Write a new migration that reverses it, test it with supabase db reset locally, and deploy it the normal way. Avoid editing a migration file that has already run on another database, because the history will no longer match.

Should Lovable be connected to my production Supabase project?

It can be for a small app with no real users. Once customers depend on the app, connecting Lovable to a staging project and promoting tested migrations to production through CI is safer, because a bad migration then fails on staging first.

How do I make sure every new table has row-level security?

Add a pgTAP test that fails if any table in the public schema has RLS disabled, and run it with supabase test db in CI on every pull request. Write each table's policies in the same migration that creates it.

lovable supabase migrationsSupabase CLIdatabase migrationsschema driftrow level securityGitHub ActionsLovable

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.