This guide walks through moving a Lovable app onto a stack you own and control — using an AI coding agent (Claude Code, Cursor, or similar) to do the heavy lifting safely. It's the clean, repeatable path distilled from a real migration, including the steps where the defaults will trip you up.
---
What You're Setting Up
Lovable gets you to a working app fast, but it bundles your backend. When you want production-grade control — your own database, your own AI provider, your own storage and cron — you move to a stack you own.
Before: Lovable app → Lovable-bundled Supabase (+ Lovable AI calls)
After: Your front end
→ Supabase (database, auth, cron, row-level security)
→ Cloudflare (R2 storage, optional Workers/Pages)
→ OpenRouter (one API for many AI models)In plain terms: Supabase holds your data and rules, Cloudflare holds your files and can serve your site, and OpenRouter is a single doorway to many AI models so you're not locked to one vendor.
---
Before You Start
1. A Supabase account and organisation (pick your region — keep it close to users) 2. A Cloudflare account (for R2 storage and/or hosting) 3. An OpenRouter account and API key (for AI generation) 4. Your AI agent connected to Supabase via MCP, plus the Supabase CLI installed 5. CSV exports of your reference tables from the old Lovable project 6. Your schema written down — tables, functions, policies — as SQL or a spec
The golden rule for working with an AI agent on production: the agent plans and prepares; *you* approve anything irreversible. Creating a project, applying a migration, inserting a secret — those wait for your explicit "go."
---
Important Safety Note
Treat every database mutation as one-way until proven otherwise. An API key or a CRON secret is a password — never paste it into a public repo, a screenshot, or a blog post. If one ever appears in your agent's log, rotate it before you publish anything.
---
Step 1: Point the Agent at the Right Place First
Before creating anything, have the agent list your existing Supabase projects.
"List my Supabase projects."
You should see: your current projects. Confirm the target project does not already exist, and that the agent isn't about to build into an unrelated one. This thirty-second check prevents the worst-case mistake.
---
Step 2: Make the Agent Read the Plan and Stop
Don't let the agent improvise your backend. Give it the schema, any module specs, and a build runbook, then ask it to produce a numbered plan and do nothing yet.
"Read the schema and the runbook. Tell me, step by step, exactly what you'll do, what you need from me, and where the first human gate is. Do not create anything yet."
You should see: a numbered plan and a clear statement of the first approval gate. If the agent starts building instead, stop it. Planning before mutating is the whole discipline.
---
Step 3: Run the Cost Check, Then Approve the Project (Human Gate)
Creating a project is the first irreversible action. Have the agent run the cost check and present it before anything runs.
get_cost → confirm_cost
You should see: project name, organisation, region, and a confirmed cost (often $0/month on a free tier). Review it. Only then say go, and let the agent run create_project.
This is the most important pause in the whole process. Everything before it is reversible. Everything after it touches a live database.
---
Step 4: Apply the Schema in Dependency Order
Have the agent apply migrations in sequence — never all at once, so each is reviewable:
1. Extensions + custom types (e.g. citext, pg_cron, pg_net, role enums) 2. Tables (in dependency order — parents before children) 3. Indexes 4. Grants (anon / authenticated / service_role) 5. Functions + triggers 6. Row-Level Security policies (the big one)
You should see: each migration applied and confirmed. After the tables step, ask the agent to count them and check the number matches your spec.
"List the tables and confirm the count is N."
---
Step 5: Store Secrets in the Vault (Human Gate)
Anything secret — a CRON secret, an API key the database needs — goes into Supabase Vault, not into a table or a policy.
"Generate a secure CRON secret and show me the Vault insert SQL. I'll approve before it runs."
You should see: the SQL, for your approval, before execution. Approve, then have the agent verify the secret is readable from decrypted_secrets (the path your cron jobs will use).
---
Step 6: Schedule Only the Cron Jobs This Phase Needs
When moving off Lovable, some bundled background jobs won't apply to your new architecture. Tell the agent which to keep, drop, or defer — don't let it schedule everything blindly.
Keep: jobs that call SQL functions or live edge functions Drop: jobs tied to the old app's behaviour you're removing Defer: jobs whose edge functions you'll rebuild in a later phase
You should see: only the relevant jobs scheduled, with any YOUR_SUPABASE_URL placeholders replaced by your real new project URL.
---
Step 7: Decide What's Worth Migrating
Not every row in your old database needs to come across. The cleanest migration is the one that brings what's reusable and leaves the rest. A useful triage:
ALWAYS MIGRATE → reference / lookup tables (countries, categories,
taxonomies, calendars — anything the front end
filters or groups on)
OFTEN MIGRATE → published content (articles, pages, completed records)
CAREFULLY → user accounts and PII (only if you're keeping the same
auth provider AND the user_ids stay stable)
USUALLY DROP → drafts, half-built test rows, cached/derived data
(it's faster to regenerate than to clean)If you're switching auth providers or starting fresh on users, leave the user table behind. Reference data and published content almost always travel. Drafts and cache rarely do — and trying to bring them across is how you import the mess you wanted to leave.
---
Step 8: Clean the Data as You Seed It
This is your one cheap chance to fix dirty data. Before importing, run a distinct-value check on every column the front end filters or groups by.
SELECT DISTINCT status_label FROM old_table; -- spot the three spellings of the same thing
You should see: the inconsistencies (the same field labelled three different ways, casing drift, typos that propagated). Fix them in your seed-generation script so the corrected values land on first insert, then verify with a count after loading.
"After seeding, count the corrected values and confirm zero of the old ones remain."
---
Step 9: Load Seed Data with the CLI, Not the High-Level Tool
Large seed files (hundreds of rows) often exceed what an MCP tool or agent can push in one call. Use the Supabase CLI's file flag instead.
supabase link supabase db query --linked -f path/to/seed.sql
You should see: the full file loaded in one go. After all seeds run, verify every table's row count matches what you exported.
---
Step 10: Run the Security Audit
The penultimate check. Have the agent run the security advisor.
get_advisors(security)
You should see: a list of warnings. Triage every one — many are intentional (public signup forms allow open INSERT; helper functions used by RLS must be callable). The dangerous ones are trigger-only or cron-only functions that are reachable over the public API. Step 11 handles those.
---
Step 11: Harden Function Access — Revoke from PUBLIC, Not Just the Role
This is the single most-missed step in any Postgres hardening pass, including on Supabase. If the advisor flags functions as RPC-callable when they shouldn't be, the natural instinct is to REVOKE EXECUTE from anon and authenticated. That often does nothing.
The reason: in Postgres, every function gets EXECUTE granted to the PUBLIC role by default. anon and authenticated inherit it. Revoking from those roles doesn't touch the inheritance.
The fix is to revoke from PUBLIC first, then grant back only where needed:
-- Strip the default REVOKE EXECUTE ON FUNCTION my_trigger_only_fn() FROM PUBLIC; -- For functions that legitimately need it (e.g. RLS helpers), grant back narrowly: GRANT EXECUTE ON FUNCTION has_role(user_id uuid, required_role text) TO anon, authenticated;
You should see: the advisor warnings clear on re-run. Functions intended to be trigger-only or cron-only show no EXECUTE for anon/authenticated. Helpers used by RLS policies still work.
---
Common Problems and Fixes
Problem: Anonymous users can see draft content
Check that your content-table anon SELECT policy is scoped to published rows only.
articles anon SELECT → WHERE status = 'published'
If you forget this, every draft article is one bad URL away from being public.
Problem: The seed file won't load through the agent
It's a size limit, not a data problem. Drop to the CLI with supabase db query --linked -f.
Problem: A secret showed up in the agent's log
Treat it as exposed. Rotate it before publishing or sharing anything.
Problem: The PUBLIC revoke "did nothing" twice in a row
If a function still shows as RPC-callable after a REVOKE, you almost certainly revoked from the wrong role. Re-read Step 11 — the access is inherited from PUBLIC, not granted directly to anon.
---
Quick Checklist
[ ] Confirmed the target project doesn't already exist [ ] Agent produced a plan and stopped before mutating [ ] Cost checked and project creation approved at the gate [ ] Schema applied in dependency order, table count verified [ ] Secrets stored in Vault (approved before insert) [ ] Only relevant cron jobs scheduled, URLs replaced [ ] Migration scope decided (reference + published, not drafts/cache) [ ] Data cleaned in the seed script, counts verified [ ] Seed loaded via CLI, row counts match exports [ ] Security advisor run and every warning triaged [ ] PUBLIC revoke applied where needed; drafts hidden from anon
---
What's Happening Under the Hood
Layer 1 → Supabase (Postgres) your data, auth, cron, and the RLS rules
that decide who can read or write what
Layer 2 → Cloudflare R2 / Pages your files (generated media, assets) and,
optionally, where your site is served
Layer 3 → OpenRouter one API key, many AI models — your
generation layer, vendor-flexible
Layer 4 → The AI agent + MCP executes the migration, but only mutates
production after you approve each gateThe migration itself is mostly the Supabase layer. Cloudflare and OpenRouter wire in around it — storage and AI — once your data and rules are solid.
---
*All views are my own. This guide is shared for learning and experimentation.*
