Moving a Lovable app on Supabase and Clerk
This guide moves a React app built in Lovable, with its database, files and sign-in on Supabase (or Lovable Cloud), some of its users on Clerk and its site on Vercel, onto one Flatstack project. Your users keep their passwords, and anyone who has accounts on both Supabase and Clerk ends up as one user.
You do it twice. First a rehearsal in a project of its own, which your users never see: you import, change the code, deploy and test on a flatstack.app address. Then the move, in a quiet hour: you import again into a fresh project, deploy the code you already tested, and point your domain at it. Every step says how to undo it.
Your agent can do most of it. Each command below works as it is in Cursor, Claude Code or your terminal.
Before you start
You need:
- A Flatstack plan. Pro (£19 a month) holds three projects, which is enough for a rehearsal and the real one.
- Your Supabase database URL: Supabase, then Connect, then Session pooler. On Lovable Cloud: Cloud, then Database. For files, also your project URL and service role key (Project settings → API).
- Your Clerk secret key: Clerk dashboard, Configure → API keys. It starts
sk_live_. - Your Clerk user export: Clerk dashboard, Users, then Export. It’s a CSV with each user’s password hash. Save it in your repo folder as
clerk-users.csvand don’t commit it.
Nothing in this section changes anything. Supabase, Clerk and Vercel keep running as they are until the last step, and the importers only read from them.
Part 1: the rehearsal
1. Sign in and create the rehearsal project
In your app’s repo folder:
npx flatstack login
npx flatstack projects create "Yoga Booker rehearsal" --slug yoga-booker-rehearsal --link
login opens the dashboard to approve this computer. --link makes this folder use the new project, so later commands don’t need its name.
To undo: delete the project in the dashboard (Project → Settings). Nothing else has changed.
2. Look before you import
A dry run shows what each import would do and writes nothing:
SUPABASE_DB_URL='postgresql://…' SUPABASE_URL=https://abcd.supabase.co SUPABASE_SERVICE_ROLE_KEY=… npx flatstack import lovable --dry-run
CLERK_SECRET_KEY=sk_live_… npx flatstack import clerk --export clerk-users.csv --dry-run
Use import supabase instead of import lovable if your project is on Supabase itself; the two are the same importer. Read both reports. The parts under Needs you are what the rest of this guide is about: tables with access rules to check, edge functions to port, passkeys, and users with sign-ins Flatstack doesn’t offer.
To undo: nothing to undo.
3. Import Supabase, then Clerk
Supabase first. Your tables point at Supabase user IDs, and the Clerk import merges into those users rather than the other way round:
SUPABASE_DB_URL='postgresql://…' SUPABASE_URL=https://abcd.supabase.co SUPABASE_SERVICE_ROLE_KEY=… npx flatstack import lovable
CLERK_SECRET_KEY=sk_live_… npx flatstack import clerk --export clerk-users.csv
Each import runs to the end and writes its files into your repo: the migrations in flatstack/migrations/, your tables and their access rules in flatstack/schema.ts, flatstack/storage.ts, flatstack/cron.ts, your edge functions in functions/legacy/, and the two reports, MIGRATION_REPORT.md and MIGRATION_REPORT_CLERK.md. If one stops, npx flatstack import resume <importId> carries on where it stopped and copies nothing twice.
A Clerk user whose verified email matches a Supabase user becomes that user, keeping both ways of signing in. If your app saved their Clerk ID (user_…) in a table, say in bookings.booked_by for bookings made after you added Clerk, the import changes it to their Supabase ID, so those bookings keep their owner. MIGRATION_REPORT_CLERK.md lists every column it changed under Merged users. A Clerk-only user keeps their Clerk ID, so rows that store it still point at them.
A Clerk user whose email matches a Supabase user, but isn’t verified in both, isn’t imported: an unverified address could belong to someone else, so Flatstack won’t join the two. The import counts them as skipped when it finishes. Their Supabase sign-in still works. To bring their Clerk sign-in across too, ask them to verify their email (in your app before the move, or in Clerk), then run npx flatstack import clerk --export clerk-users.csv again: users already imported are left as they are, and the newly verified ones are merged.
To undo: delete the rehearsal project and the generated files (git checkout . and git clean -fd flatstack functions if you hadn’t committed them). Supabase and Clerk weren’t touched.
4. Change the app’s code
Your app talks to Supabase through supabase-js and to Clerk through @clerk/clerk-react. Both are replaced by Flatstack’s client:
npm install @flatstackdev/client @flatstackdev/react
The changes, which you can hand to your agent as they are:
- Replace the Supabase client (
src/integrations/supabase/client.tsin a Lovable app) withexport const client = createClient();from@flatstackdev/client. It needs no URL or key: it talks to your app’s own address. - Replace
supabase.from("bookings").select()withclient.data.from("bookings").list(),.insert()with.insert(),.update().eq("id", id)with.update(id, changes)and.delete().eq("id", id)with.delete(id). Filters becomewhere:.eq("studio_id", id)is{ where: { studio_id: id } },.gte("starts_at", t)is{ where: { starts_at: { gte: t } } }. Each table you read from the browser needsexpose: trueinflatstack/schema.ts(see Data API). - Replace
supabase.storage.from(bucket).upload()andgetPublicUrl()withclient.storage.from(bucket).upload()andgetUrl(). - Replace
<ClerkProvider>with<FlatstackProvider client={client}>, and Clerk’s<SignIn/>,<SignUp/>and<UserButton/>with the ones from@flatstackdev/react.useUser()becomesuseSession(). Remove any Supabase sign-in screens: Flatstack’s<SignIn/>covers everyone. - Port each edge function in
functions/legacy/to a function inflatstack/functions/(see Functions); the note at the top of each file lists what changes. A call to Resend becomesctx.email.send. Call them from the browser withclient.fn.<name>(input).
The client SDK page has every method, and a table of Clerk names and their Flatstack equivalents.
Then tell Flatstack how to serve the site. A Vite app routes in the browser, so unknown pages get index.html:
// flatstack.config.ts
export default {
hosting: { spa: true },
};
To undo: it’s all in git: git checkout . puts the Supabase and Clerk code back. Your live site on Vercel hasn’t changed.
5. Turn on Google sign-in
If your users sign in with Google, give Flatstack your own Google OAuth client. In Google Cloud Console, open the OAuth client Supabase or Clerk used (or make one), and add these two redirect URIs. Keep the old ones: removing them would break sign-in on your live site.
https://yoga-booker-rehearsal.flatstack.app/auth/callback/googlehttps://book.example.com/auth/callback/google, your own domain, for after the move
Then set the client in your project’s sign-in settings:
npx flatstack auth set --app-name "Yoga Booker"
npx flatstack auth providers set google --client-id 123.apps.googleusercontent.com
The second command asks for the client secret (GOCSPX-…) and doesn’t show it as you type, so it stays out of your shell history. In a script, pipe it in instead. npx flatstack auth settings shows what’s set; the secret only ever shows as set. Settings reach the app on its next deploy. The same goes for GitHub, Apple and Microsoft.
To undo: remove the two redirect URIs you added. Nothing on your live site used them.
6. Deploy and try it
npm run build
npx flatstack deploy --assets-dir dist
The deploy applies the import’s migrations, runs the security scan and prints your app’s address, https://yoga-booker-rehearsal.flatstack.app. If the scan blocks the deploy, it says which file and why; fix that and deploy again.
Now try it as your users will:
- Sign in with a Supabase user’s email and their usual password.
- Sign in with a Clerk user’s email and password, and with Google.
- Check the bookings, studios and files are there, and that one user can’t see another’s.
- Users who had a passkey on Clerk sign in once by email code or link, then add a new one.
<UserProfile/>has Add a passkey.
Everyone signs in once more after the move, because sessions don’t move. That’s the only thing your users notice.
To undo: your live site still hasn’t changed. Later, if a deploy goes wrong, npx flatstack rollback puts the one before it back.
7. Send email from your domain
Booking confirmations and sign-in emails come from yoga-booker-rehearsal@mail.flatstack.app until you add your domain. Adding it needs one DNS record, alongside the records Resend already uses, so both keep working while you switch. There’s no CLI command for sender domains yet, so use the API with a short-lived token:
npx flatstack tokens create --name "Email domain" --scope projects:write --expires 1
curl -X POST https://api.flatstack.dev/v1/projects/$PROJECT/email-domains -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{"domain":"example.com"}'
$TOKEN is the token tokens create printed, and $PROJECT is your project’s ID from npx flatstack projects get. Create the CNAME record the API returns, flatstack._domainkey.example.com, at your DNS host. See Sending from your own domain.
To undo: delete the record. Resend’s records were never touched.
Commit everything except clerk-users.csv. The rehearsal is done; your live app hasn’t changed.
Part 2: the move
Anything your users do on the old app after you import, such as a new booking, doesn’t come across on its own. So the move imports again, in a fresh project, as close to the switch as you can. Pick a quiet hour and allow about half an hour.
8. Import again into the real project
npx flatstack projects create "Yoga Booker" --slug yoga-booker --link
SUPABASE_DB_URL='postgresql://…' SUPABASE_URL=https://abcd.supabase.co SUPABASE_SERVICE_ROLE_KEY=… npx flatstack import lovable --overwrite
CLERK_SECRET_KEY=sk_live_… npx flatstack import clerk --export clerk-users.csv --overwrite
npx flatstack auth set --app-name "Yoga Booker"
npx flatstack auth providers set google --client-id 123.apps.googleusercontent.com
Export the Clerk users again first, so the CSV has anyone who signed up since. --overwrite replaces the rehearsal’s generated files with this import’s. Check git diff: flatstack/schema.ts comes back as the importer wrote it, so put back any expose: true you added in step 4.
The two auth commands set Google sign-in again, as in step 5: settings belong to a project, and this is a new one.
To undo: delete the project. Nothing your users see has changed.
9. Deploy
npm run build
npx flatstack deploy --assets-dir dist
Try it again on https://yoga-booker.flatstack.app, as in step 6.
To undo: npx flatstack rollback, or leave it: nobody’s using this address yet.
10. Point your domain
npx flatstack domains add book.example.com
It prints the DNS records to create. To switch with no downtime, add the TXT record first, while the domain still points at Vercel: it proves the domain is yours, so the certificate is ready before any traffic moves. When npx flatstack domains list shows the domain active, replace the record that points at Vercel (usually a CNAME to cname.vercel-dns.com) with the CNAME Flatstack gave you. Write down Vercel’s old value before you change it.
New visitors reach Flatstack within minutes; some networks take up to an hour.
To undo: put Vercel’s record back. Your Vercel site, Supabase and Clerk are still running. Anyone who signed up or booked on Flatstack in the meantime is only on Flatstack, so undo soon if you’re going to.
After the move
Keep Supabase, Clerk and Vercel for a week or two, in case you want to undo. Then:
- Remove your Vercel project and Clerk application, and pause or delete the Supabase project.
- Revoke the Clerk secret key and rotate the Supabase service role key and database password you gave the importers. Delete
clerk-users.csv. - Revoke the Email domain token:
npx flatstack tokens list, thennpx flatstack tokens revoke <tokenId> --yes. - Delete the rehearsal project in the dashboard.
- Once Resend isn’t sending any more, remove its DNS records.
Your bill is now one flat price, and bandwidth is unmetered. If your app reaches a plan limit, new requests get a 429 until the month resets, while sign-in and your site’s files keep working; the bill doesn’t move.
As Markdown: apps/guide-lovable-supabase-clerk.md