Lovable Cloud → Supabase migration (claude.ai chat)
This skill migrates a Lovable Cloud project to a standalone Supabase project from claude.ai chat (the consumer Claude product). It automates the hardest 95% of the work: schema, data, auth, storage, and edge functions. The last step is official since July 2026: once verification passes, the user removes Lovable Cloud and connects the new Supabase to the same Lovable project from the dashboard — no code push, no new project. The old new-project-plus-code-push route survives only as a fallback for users who must keep the Cloud project untouched.
When to use
Trigger this skill when the user is in claude.ai chat and asks to migrate a Lovable Cloud project to their own Supabase. Common phrasings: "migrate my Lovable project to Supabase", "export from Lovable Cloud", "leave Lovable Cloud", "move my data out of Lovable".
If the user is in Claude Code CLI, Claude Code on the web, or Claude Desktop, point them to the original lovable-cloud-to-supabase-migration skill instead, which handles the frontend code automatically via gh and git.
Required MCPs
This skill requires both MCPs to be connected in claude.ai chat (Settings → Connectors):
- Lovable MCP (
https://mcp.lovable.dev) — for reading the source project - Supabase MCP (
https://mcp.supabase.com/mcp) — for creating and writing the destination
If either is missing, stop and ask the user to connect it before proceeding.
What this skill automates
| Phase | What happens | Automation level |
|---|---|---|
| 1. Scan source | Audit the Lovable Cloud project structure | Fully automatic |
| 2. Create destination | Provision new Supabase project | Asks user for ~2 approvals |
| 3. Schema | Recreate tables, enums, RLS, functions, triggers, sequences, indexes | Asks for ~3 approvals |
| 4. Auth users | Migrate auth.users AND auth.identities with bcrypt hashes intact |
Asks for ~2 approvals |
| 5. Insert data | Copy all rows table by table using string_agg SQL pattern |
Asks for ~N approvals (1 per table) |
| 6. Storage | Deploy temporary edge function + invoke via pg_net |
Asks for ~3 approvals |
| 7. Edge functions | Read source code, deploy to destination with correct verify_jwt |
Asks for ~M approvals (1 per function) |
| 8. Verify | Run a comprehensive audit query BEFORE anything is removed | Fully automatic |
| 9. The switch | Remove Lovable Cloud + connect own Supabase on the SAME project | Manual user step (dashboard) |
Important: token consumption and compaction
Migrating a typical project consumes approximately 200K tokens — close to the full context window of claude.ai chat.
Recommended compaction checkpoints:
- After Phase 4 (Auth) — discharge source scan details
- After Phase 6 (Storage) — before loading edge function code
Use the /compact command in the chat menu when prompted, or the model will auto-compact at ~95% capacity.
For projects larger than 200 storage files OR 20+ edge functions, split the migration into two sessions:
- Session 1: Phases 1–6 (schema + data + storage)
- Session 2: Phases 7–8 (edge functions + verification), then the switch
The endgame: same project (default) or new project (fallback)
Default since July 2026: the frontend never moves. Lovable added official Export, Pause and Remove buttons to Cloud. After this skill migrates the backend and verification passes, the user removes Lovable Cloud from the source project and connects their own Supabase to the same Lovable project via the dashboard. Same code, same repo, same URL — only the backend changes. Phase 9 walks through it.
Sacred order — never violate it: migrate → verify (Phase 8, all green) → download the official Export backup → Remove Cloud → Connect. Removing Cloud deletes its database AND storage permanently, including any un-downloaded exports. Do not let the user remove anything while verification has a single red row.
Fallback: new project + code push. Only needed when the user must keep the original Cloud project untouched (e.g., staging stays live while they test the copy). The frontend codebase (typically 200+ files) cannot be faithfully reproduced from claude.ai chat:
- No GitHub MCP exists for the consumer Claude product
Lovable MCP:remix_projectinherits its own fresh Cloud state — two Cloud instances mid-migration, unclear where data lives. Do not use remix for migrationLovable MCP:create_projectwith aninitial_messagemakes the agent rewrite from scratch (not faithful to the original)- Reading and rewriting 200+ files individually is prohibitively token-expensive
For the fallback, delegate the code push:
Option 1: Claude Code on the web (fastest) Direct the user to claude.ai/code, where they can:
- Connect their GitHub account (one-time setup)
- Run the original
lovable-cloud-to-supabase-migrationskill (fresh-project path) - The skill handles
gh repo clone,rsync,git pushautomatically in the cloud VM
Option 2: Local terminal
If the user has gh and git installed, generate the exact commands for them to run locally. See references/trampas.md for the command template.
In both fallback options, after the code lands in the new Lovable repo, send a follow-up Lovable MCP:send_message to trigger a rebuild against the new Supabase.
Critical traps
This skill incorporates 12 traps discovered through end-to-end testing on real projects. Read references/trampas.md before starting Phase 1 — these traps cause silent failures that the original skill (Code version) didn't catch.
The most critical:
- Trap 1: Lovable's default tech_stack changed to TanStack Start on May 6, 2026. Use
tech_stack: "classic"for projects created before that date (most migrations). - Trap 8: Custom functions like
handle_new_userare not in the table schema and must be migrated separately or new user signups break. - Trap 11:
auth.identitiesmust be migrated alongsideauth.usersor session recovery breaks. - Trap 5:
verify_jwtper edge function must be read fromsupabase/config.tomlor webhooks (Stripe etc.) reject calls.
Migration workflow
The migration follows phases 1–9 in order. Each phase below describes the expected tool calls and SQL. Detailed templates and trap fixes are in references/trampas.md.
Phase 1: Scan source (~6K tokens)
Inputs needed from user:
- Lovable project ID (or workspace and project name to look up)
- Lovable workspace ID
Tool calls:
Lovable MCP:list_workspaces— confirm workspaceLovable MCP:get_project— get latest commit SHA and project metadataLovable MCP:read_fileforpackage.jsonat the latest SHA — detect tech stack:- If
name === "vite_react_shadcn_ts"→ usetech_stack: "classic"if the Phase 9 fallback is taken - If
namecontainstanstack_start→tech_stack: "modern" - Default fallback:
"classic"(most migrations are pre-May-2026 projects)
- If
Lovable MCP:read_fileforsupabase/config.toml— readverify_jwtper function for Phase 7Lovable MCP:query_database— run the audit queries:-- Tables, enums, RLS, functions, triggers, sequences, indexes, auth, storage -- Full query in references/trampas.md, section "Phase 1 audit query"
Output a structured summary to the user before proceeding:
Source project: <name>
Tech stack: classic | modern
Tables: N | Enums: N | RLS policies: N
Custom functions: N (handle_new_user: yes/no)
Custom triggers: N
Custom sequences: N
auth.users: N | auth.identities: N
Storage buckets: N (public: N, private: N)
Storage files: N (total: X MB)
Edge functions: N (config.toml verify_jwt mapping: ...)
Phase 2: Create destination Supabase project (~2K tokens)
Supabase:list_organizations— confirm orgSupabase:get_cost— show user the cost of new project ($10/mo for Free → Pro upgrade)- Tell the user: "This will create a new Supabase project at $10/mo. Confirm to proceed."
Supabase:confirm_cost— get confirmation tokenSupabase:create_projectwith parameters:name: derive from user's preferenceorganization_id: from step 1region: default tous-west-1(us-east-1 has had multiple capacity outages — see Trap 2)confirm_cost_id: from step 4
- Wait for project to be
ACTIVE_HEALTHY. Save the new project ref.
If us-east-1 was specifically requested and create_project fails with capacity errors, retry with us-west-1 and inform the user.
Phase 3: Migrate schema (~10K tokens)
This phase replicates the source schema into the destination. Critical: do not just copy CREATE TABLE — replicate functions, triggers, sequences, and indexes too.
- Enable pg_net first (Trap 3):
CREATE EXTENSION IF NOT EXISTS pg_net WITH SCHEMA extensions; Supabase:apply_migration— create all enums:CREATE TYPE public.<enum_name> AS ENUM (...);Supabase:apply_migration— create all tables with their columns, FKs, and constraints (read schema from source viaquery_databaseintrospection oninformation_schema.columns)Supabase:apply_migration— create custom sequences with the correctlast_value:CREATE SEQUENCE public.order_number_seq; SELECT setval('public.order_number_seq', <last_value_from_source>);Supabase:apply_migration— create custom functions (usepg_get_functiondeffrom source). This includes critical functions likehandle_new_user.Supabase:apply_migration— create triggers that wire functions to tables.Supabase:apply_migration— create custom indexes (Trap 10):CREATE INDEX idx_brands_slug ON public.brands USING btree (slug);Supabase:apply_migration— create RLS policies:ALTER TABLE public.<table> ENABLE ROW LEVEL SECURITY; CREATE POLICY "<name>" ON public.<table> FOR <action> USING (...);
After Phase 3, the destination has the complete schema structure. Verify with:
SELECT
(SELECT count(*) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname='public') AS functions,
(SELECT count(*) FROM information_schema.triggers WHERE trigger_schema IN ('public','auth')) AS triggers;
Phase 4: Migrate auth users (~3K tokens)
Critical: migrate BOTH auth.users AND auth.identities (Trap 11). Without identities, login partially works but session recovery breaks.
Lovable MCP:query_database— read both tables:-- Get users SELECT id, email, encrypted_password, email_confirmed_at, raw_user_meta_data, ... FROM auth.users;Then separately:
SELECT id, user_id, provider, provider_id, identity_data, created_at, last_sign_in_at FROM auth.identities;Supabase:apply_migration— insert users with the bcrypt hashes intact ($2a$10$...format, 60 chars):INSERT INTO auth.users ( instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, raw_user_meta_data, raw_app_meta_data, created_at, updated_at, ... ) VALUES ('00000000-0000-0000-0000-000000000000', '<uuid>', 'authenticated', 'authenticated', '<email>', '<hash>', ...);Supabase:apply_migration— insert identities:INSERT INTO auth.identities ( id, user_id, provider, provider_id, identity_data, created_at, last_sign_in_at ) VALUES (...);Optional: preserve JWT secret (Trap 11b). If the user wants existing sessions to remain valid (no forced re-login), they need to manually copy the JWT secret from source to destination via the dashboard. Mention this only if relevant to their use case.
Phase 5: Migrate row data (~65K tokens — the heaviest phase)
Key technique (sole web advantage over Code): use PostgreSQL's string_agg + quote_literal to make Postgres generate its own INSERT statements. This eliminates the need for Python intermediates and is more token-efficient.
For each table (in dependency order — tables with no FKs first, then dependent ones):
Lovable MCP:query_databaseon the source — generate the INSERT SQL:SELECT string_agg( 'INSERT INTO public.<table> (<col1>, <col2>, ...) VALUES (' || quote_literal(<col1>::text) || '::<type>, ' || COALESCE(quote_literal(<col2>::text), 'NULL') || '::<type>, ' || '...' || ');', E'\n' ) AS insert_sql FROM public.<table>;Type casts matter: include
::uuid,::jsonb,::timestamptz, etc. for non-text columns. NULLs need special handling with COALESCE.Take the
insert_sqlresult and pass it directly toSupabase:apply_migrationon the destination.Batching for large tables: if a table has more than ~500 rows, the resulting SQL may exceed message size limits. Split with
LIMIT/OFFSET:... FROM public.<table> ORDER BY <pk> LIMIT 500 OFFSET 0;URL rewriting (Trap 7): if any column type
text,text[], orjsonbcontains references to the old Supabase ref (e.g.,https://<old_ref>.supabase.co/storage/...), rewrite them in the destination:UPDATE public.<table> SET <column> = REPLACE(<column>, '<old_ref>', '<new_ref>') WHERE <column> LIKE '%<old_ref>%';Don't only check
*_urlcolumns — check all text/jsonb columns. URLs hide in JSONB metadata fields.
After Phase 5, suggest compaction to the user before proceeding to Phase 6:
"We've migrated the schema, auth, and data. Context is at ~85K tokens. Run
/compactin the chat menu to reduce context before storage migration, then continue with Phase 6."
Phase 6: Migrate storage (~27K tokens)
Key technique (web is BETTER than Code here): use pg_net to invoke an edge function from Postgres itself. This is cleaner than Code's curl approach.
Lovable MCP:query_database— list buckets and detect public/private flag (Trap 4):SELECT id, name, public FROM storage.buckets;Supabase:apply_migration— create buckets in the destination with their RLS policies:INSERT INTO storage.buckets (id, name, public) VALUES ('<bucket_id>', '<name>', <public_bool>); -- Recreate the matching storage.objects RLS policies from sourceSupabase:deploy_edge_function— deploy the temporarymigrate-storagefunction. Full code template inreferences/trampas.md("migrate-storage edge function template"). Passverify_jwt: false.Generate the migration payload via SQL:
SELECT jsonb_build_object( 'files', jsonb_agg( jsonb_build_object( 'source_url', 'https://<source_ref>.supabase.co/storage/v1/object/public/' || bucket_id || '/' || name, 'bucket', bucket_id, 'path', name, 'content_type', metadata->>'mimetype' ) ) ) AS payload FROM storage.objects WHERE bucket_id = '<bucket_id>';For private buckets (Trap 4): use signed URLs instead:
-- Generate signed URL for each object via storage.create_signed_url(bucket_id, name, 3600)Supabase:execute_sqlon destination — invoke the edge function via pg_net:SELECT net.http_post( url := 'https://<dest_ref>.supabase.co/functions/v1/migrate-storage', headers := '{"Content-Type": "application/json"}'::jsonb, body := '<payload from step 4>'::jsonb ) AS request_id;Wait 5-30 seconds depending on file count and check the response:
SELECT id, status_code, content::jsonb, error_msg, created FROM net._http_response WHERE id = <request_id>;For large buckets (>50 files): split into chunks of 25 files each to stay under edge function timeout limits. Loop the pg_net calls with different request IDs.
After migration completes, delete the migrate-storage edge function from the destination (it's a temporary helper).
After Phase 6, suggest compaction again before Phase 7:
"Storage migrated. Context is now ~113K tokens. Run
/compactbefore Phase 7 — we're about to load 12+ edge functions."
Phase 7: Migrate edge functions (~90K tokens — the second-heaviest)
For each edge function in the source supabase/functions/<name>/index.ts:
Lovable MCP:read_fileat pathsupabase/functions/<name>/index.tsGrep for secrets the function uses (Trap 6):
grep -E 'Deno\.env\.get\(["\047](\w+)["\047]\)' index.tsTrack which secrets are needed (excluding the Supabase auto-provided ones:
SUPABASE_URL,SUPABASE_ANON_KEY,SUPABASE_SERVICE_ROLE_KEY).Supabase:deploy_edge_function:name: same as sourcefiles:[{ name: "index.ts", content: <read_file output> }]verify_jwt: read fromsupabase/config.toml(Trap 5) — typicallytruefor user-facing functions,falsefor webhooks.
Repeat for each function. For projects with 20+ functions, this phase alone can use 100K+ tokens — strongly consider splitting into a separate session.
After all functions are deployed, give the user the list of secrets they need to set manually in the destination Supabase dashboard:
"Deployed N edge functions. You need to set these secrets in your Supabase dashboard (Settings → Edge Functions → Secrets):
- LOVABLE_API_KEY (used by extract-colors, generate-concept)
- OPENAI_API_KEY (used by ...)
- STRIPE_SECRET_KEY (used by ...)
The functions will fail until these are set."
Phase 8: Verify (~5K tokens)
Run this BEFORE anything is removed. The source Cloud project must still be alive and untouched when this phase runs — if verification fails, nothing has been lost.
Run a comprehensive audit query on the destination and compare with the source scan from Phase 1. This catches the things the original Code skill's verification missed (Trap 12).
-- Comprehensive audit query — full version in references/trampas.md
SELECT 'tables' AS category, COUNT(*) AS count FROM information_schema.tables WHERE table_schema='public'
UNION ALL SELECT 'enums', COUNT(*) FROM pg_type t JOIN pg_namespace n ON t.typnamespace=n.oid WHERE n.nspname='public' AND t.typtype='e'
UNION ALL SELECT 'rls_policies', COUNT(*) FROM pg_policies WHERE schemaname='public'
UNION ALL SELECT 'functions_custom', COUNT(*) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname='public' AND p.proname NOT LIKE 'pg_%'
UNION ALL SELECT 'triggers', COUNT(*) FROM information_schema.triggers WHERE trigger_schema IN ('public','auth')
UNION ALL SELECT 'sequences', COUNT(*) FROM pg_sequences WHERE schemaname='public'
UNION ALL SELECT 'auth_users', COUNT(*) FROM auth.users
UNION ALL SELECT 'auth_identities', COUNT(*) FROM auth.identities
UNION ALL SELECT 'storage_buckets', COUNT(*) FROM storage.buckets
UNION ALL SELECT 'storage_objects', COUNT(*) FROM storage.objects
UNION ALL SELECT 'pg_net_enabled', CASE WHEN EXISTS(SELECT 1 FROM pg_extension WHERE extname='pg_net') THEN 1 ELSE 0 END;
Then scan for old Supabase ref leaks — any URL still pointing at the source ref means the data wasn't fully rewritten:
-- For each text/jsonb column in public schema, check for old ref
-- Full discovery query in references/trampas.md
SELECT count(*) FROM <table> WHERE <col> LIKE '%<old_ref>%';
Present the audit results to the user as a comparison table:
Category Source Destination Status
───────────────── ────── ─────────── ────────
tables N N OK
enums N N OK
functions_custom N N OK <-- if MISSING, Phase 3 step 5 was skipped
auth_identities N N OK <-- if MISSING, Phase 4 step 3 was skipped
...
If any row shows MISSING or PARTIAL, point the user to the specific phase that needs to be re-run.
GATE DECISION: all green → proceed to Phase 9. Any red → STOP. Fix and re-verify. Cloud is still alive and untouched; nothing has been lost. Do NOT let the user remove Cloud with a red gate. There is no rush — some users sit at the green gate for days. That's fine.
Also add functional checks beyond the counts:
- Log in with a REAL migrated user and their ORIGINAL password
- Open a real storage file in the browser
- Invoke one edge function
Phase 9: The switch — Remove Cloud + Connect (dashboard, HUMAN steps)
These are dashboard clicks the user does themselves. Walk them through, one step at a time:
Download the official Export backup first. Lovable dashboard → Cloud tab → Export project data. Download the
.backupfile locally. This is the insurance policy — the export is saved INTO the Cloud storage that's about to be deleted, so an un-downloaded export dies with Cloud.Remove Lovable Cloud. Cloud tab > Overview > Advanced settings > Remove Lovable Cloud. Two checkboxes + type the project name. This permanently deletes the Cloud instance INCLUDING its storage.
Connect the own Supabase. The Cloud tab transforms. At the bottom: "Already have a Supabase project? Connect it here." > authorize the Supabase org > pick the new project > Connect. Lovable auto-rewrites
.envwith the new URL + key.Fix the integration overwrite. The connect step may refresh
src/integrations/supabase/client.ts(or equivalent) with a template — an error page right after connecting is THIS, not data loss. Send the Lovable agent (viaLovable MCP:send_message, in English):Review my Supabase connection: confirm the app points to my new Supabase project, check that auth, database queries, storage and edge functions all work against it, and list anything still referencing the old backend.
Important context: the database, users, storage files and edge functions ALREADY EXIST in the new project, they were migrated. Do NOT create tables, run migrations, re-seed data, or rewrite files beyond the connection wiring. Fix wiring only.
The Do NOTs matter: the agent is eager and a bare "fix my connection" can trigger re-migrations or file rewrites.
Phase 9-fallback: new project + code push (only if Cloud must stay untouched)
Generate this guidance for the user:
Frontend migration (manual)
Your destination Supabase project is ready:
<dest_ref>Source Lovable project:<source_id>at commit<latest_sha>Recommended: Claude Code on the web
- Open https://claude.ai/code
- Connect your GitHub account (Settings → GitHub App)
- Run the
lovable-cloud-to-supabase-migrationskill there (fresh-project path)- Provide it the same source and destination IDs above
- The Claude Code skill handles
gh repo clone,rsync, andgit pushautomaticallyAlternative: your local terminal If you have
ghandgitinstalled, run these commands:# 1. Clone the original Lovable repo gh repo clone <github_username>/<lovable_repo_name> ~/lovable-old # 2. Create new empty Lovable project (we'll do this via MCP for you) # New project ID: <new_lovable_id> # New repo: <github_username>/<new_lovable_repo> # 3. Clone the new (empty) repo gh repo clone <github_username>/<new_lovable_repo> ~/lovable-new # 4. Copy code over (excluding Lovable Cloud-specific files) rsync -av --exclude='.git' --exclude='.lovable' --exclude='supabase/.temp' \ ~/lovable-old/ ~/lovable-new/ # 5. Update .env or src/integrations/supabase/client.ts to point to <dest_ref> # 6. Commit and push cd ~/lovable-new && git add -A && git commit -m "Migrate from Lovable Cloud to Supabase" && git push
This skill CAN automate two fallback steps (creating the new empty Lovable project and triggering rebuild). Offer them to the user:
Lovable MCP:create_project:description: "Migration target for "workspace_id: same workspacetech_stack: from Phase 1 detection — usually"classic"(Trap 1)visibility:private- No
initial_message— we want it as empty as possible, the user will push code via git
After user reports they pushed the code,
Lovable MCP:send_message:project_id: new project IDmessage: "The codebase has been updated via GitHub. Please rebuild against the connected Supabase database."
Closing the migration
When verification passed and the switch is done:
- Tell the user the same Lovable project now runs on their own Supabase
- Remind them to set edge function secrets in the dashboard (from Phase 7's list) — functions fail until these are set
- Recreate OAuth providers if used (Google etc.): client ID/secret + redirect URI
https://<new_ref>.supabase.co/auth/v1/callback+ Site URL - Offer to delete the temporary
migrate-storageedge function from Phase 6 - Tell them to delete the local
.backuponce everything is verified (it holds password hashes)
Errors and recovery
apply_migrationrejected: most often a missing dependency (e.g., trying to create a trigger before its function). Re-order following Phase 3's exact sequence: enums → tables → sequences → functions → triggers → indexes → RLS.pg_netreturns null content: the edge function may have crashed. Checkerror_msginnet._http_response. Common cause: source URL is private but you used public URL format.create_projectfails with capacity error in us-east-1: retry withus-west-1(Trap 2).- Edge function 401 Unauthorized:
verify_jwtdoesn't match the calling pattern. Webhooks must haveverify_jwt: false(Trap 5). - New users sign up but no profile is created: the
handle_new_usertrigger wasn't migrated (Trap 8). Re-run Phase 3 step 5 + step 6. - Error page right after connecting the own Supabase: the connect step refreshed the integration client file with a template (Phase 9 step 4). This is wiring, not data loss — send the Lovable agent the fix-wiring-only message from Phase 9.