# 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.csv` and 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:

```bash
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:

```bash
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:

```bash
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:

```bash
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.ts` in a Lovable app) with `export 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()` with `client.data.from("bookings").list()`, `.insert()` with `.insert()`, `.update().eq("id", id)` with `.update(id, changes)` and `.delete().eq("id", id)` with `.delete(id)`. Filters become `where`: `.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 needs `expose: true` in `flatstack/schema.ts` (see [Data API](data-api.md)).
- Replace `supabase.storage.from(bucket).upload()` and `getPublicUrl()` with `client.storage.from(bucket).upload()` and `getUrl()`.
- Replace `<ClerkProvider>` with `<FlatstackProvider client={client}>`, and Clerk's `<SignIn/>`, `<SignUp/>` and `<UserButton/>` with the ones from `@flatstackdev/react`. `useUser()` becomes `useSession()`. Remove any Supabase sign-in screens: Flatstack's `<SignIn/>` covers everyone.
- Port each edge function in `functions/legacy/` to a function in `flatstack/functions/` (see [Functions](functions.md)); the note at the top of each file lists what changes. A call to Resend becomes `ctx.email.send`. Call them from the browser with `client.fn.<name>(input)`.

The [client SDK page](client.md) 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`:

```ts
// 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/google`
- `https://book.example.com/auth/callback/google`, your own domain, for after the move

Then set the client in your project's sign-in settings:

```bash
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

```bash
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:

```bash
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](email-domains.md).

**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

```bash
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

```bash
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

```bash
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`, then `npx 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.