How to move a Lovable app from Lovable's managed hosting to your own Supabase project and Cloudflare Workers — while keeping Lovable as an editor.
Lovable is great for building. But its managed Supabase instance and auto-deploy pipeline mean you don't control the database, auth config, email templates, or hosting. Ejecting gives you:
- Your own Supabase project — full control over schema, RLS policies, auth settings, edge functions, and email templates, all managed as code
- Cloudflare Workers hosting — static asset serving with custom domains, no build pipeline to maintain
- Lovable still works — keep
lovable-taggerin your dev dependencies and Lovable can still push code changes to your repo
| Concern | Before (Lovable-managed) | After (ejected) |
|---|---|---|
| Database | Lovable's shared Supabase project | Your own Supabase project |
| Auth config | Dashboard clicks | config.toml (as code) |
| Email templates | React TSX via auth-email-hook edge function |
Simple HTML templates |
| Edge functions | Deployed by Lovable | supabase functions deploy |
| Hosting | Lovable auto-deploy | Cloudflare Workers via wrangler |
| Code editing | Lovable UI | Lovable UI + local IDE (both work) |
- Node.js (18+)
- Supabase CLI (
npm i -g supabase) - Wrangler CLI (
npm i -g wrangler) - A Cloudflare account (free tier works)
- A Supabase account (free tier works)
Go to supabase.com and create a new project. Grab these values:
- Project ref — the subdomain in the API URL (e.g.
abcdefghijklmnop) - Publishable key — Dashboard → API Keys
- Service role key — Dashboard → API Keys (keep this secret)
- Database password — the one you set during project creation
You need to get your current schema out of Lovable's Supabase and into a migration file.
Connect to Lovable's Supabase project (the old one) and dump the schema:
supabase link --project-ref <lovable-project-ref>
supabase db dump -f supabase/migrations/00000000000000_initial_schema.sqlReview the dump. It will include Lovable's internal tables and extensions you may not need. Clean it up:
- Keep your
publicschema tables, RLS policies, functions, and triggers - Keep any
authschema seed data you want (e.g. a first admin user) - Remove Lovable-specific artifacts
- Remove
CREATE EXTENSIONstatements for extensions your app doesn't use
The goal is a single, clean SQL file that creates your entire schema from scratch.
Structure your supabase/ directory:
supabase/
├── config.toml
├── .env # gitignored — DB password
├── .env.example
├── migrations/
│ └── 00000000000000_initial_schema.sql
├── templates/
│ ├── invite.html
│ └── magic_link.html
└── functions/
└── (your edge functions)
Replace Lovable's config with your own. This is the key file — it controls auth settings, email templates, and edge function config:
project_id = "<your-project-ref>"
[auth]
site_url = "https://yourdomain.com"
additional_redirect_urls = ["https://yourdomain.com", "http://localhost:3000"]
enable_signup = false # or true, depending on your app
[auth.email]
enable_signup = false
enable_confirmations = true
[auth.email.template.magic_link]
subject = "Your sign-in link"
content_path = "./supabase/templates/magic_link.html"
[auth.email.template.invite]
subject = "You've been invited"
content_path = "./supabase/templates/invite.html"
[functions.your-function-name]
verify_jwt = false # or true — set per functionLovable uses a auth-email-hook edge function with React (TSX) email templates. You don't need any of that. Supabase supports simple HTML templates with Go template variables natively.
Create supabase/templates/magic_link.html:
<html>
<body style="font-family: system-ui, sans-serif; padding: 32px;">
<h2>Sign in</h2>
<p>Click the button below to sign in.</p>
<p><a href="{{ .ConfirmationURL }}" style="display:inline-block; background:#1a1a2e; color:#fff; padding:12px 24px; border-radius:8px; text-decoration:none;">Sign In</a></p>
<p style="color:#888; font-size:12px;">If you didn't request this, ignore this email.</p>
</body>
</html>Create supabase/templates/invite.html:
<html>
<body style="font-family: system-ui, sans-serif; padding: 32px;">
<h2>You've been invited</h2>
<p>Click the button below to accept your invitation.</p>
<p><a href="{{ .ConfirmationURL }}" style="display:inline-block; background:#1a1a2e; color:#fff; padding:12px 24px; border-radius:8px; text-decoration:none;">Accept Invitation</a></p>
</body>
</html>You can now delete the auth-email-hook edge function and its _shared/email-templates/ directory entirely.
supabase/.env (gitignored):
SUPABASE_DB_PASSWORD=<your-db-password>
.env (app root, gitignored):
VITE_SUPABASE_PROJECT_ID="<project-ref>"
VITE_SUPABASE_PUBLISHABLE_KEY="<publishable-key>"
VITE_SUPABASE_URL="https://<project-ref>.supabase.co"
.env.example (committed, no secrets):
VITE_SUPABASE_PROJECT_ID=
VITE_SUPABASE_PUBLISHABLE_KEY=
VITE_SUPABASE_URL=
Add these if not already present:
supabase/.env
supabase/.branches
Lovable's Supabase project may inject env vars with non-standard names. The self-hosted Supabase runtime provides these automatically:
SUPABASE_URLSUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEY
If your edge functions reference SUPABASE_PUBLISHABLE_KEY or any other Lovable-specific name, rename them to SUPABASE_ANON_KEY:
// Before (Lovable)
const anonKey = Deno.env.get("SUPABASE_PUBLISHABLE_KEY")!;
// After (self-hosted)
const anonKey = Deno.env.get("SUPABASE_ANON_KEY")!;Link the CLI to your new project and push:
supabase link --project-ref <project-ref>
supabase db push # apply migrations
supabase config push # auth settings + email templates
supabase functions deploy <fn-name> # repeat for each edge functionOr add npm scripts to make this repeatable:
{
"scripts": {
"db:push": "supabase db push && npm run db:types",
"db:types": "supabase gen types typescript --linked > src/integrations/supabase/types.ts",
"functions:deploy": "supabase functions deploy fn-one && supabase functions deploy fn-two",
"supabase:push": "supabase db push && supabase config push && npm run functions:deploy && npm run db:types"
}
}This is required after every supabase config push — the CLI resets this setting and there's no config.toml key for it.
Go to Supabase Dashboard → Authentication → Providers → Email and make sure Enable Email Provider is toggled ON. Without this, magic link sign-in fails with "Email logins are disabled."
Cloudflare Workers custom domains require your domain's DNS to be managed by Cloudflare. If it isn't already:
- In the Cloudflare dashboard, click Add a site and enter your domain
- Select the Free plan
- Cloudflare will scan your existing DNS records and import them. Review the list — make sure nothing is missing (especially MX records for email)
- Cloudflare gives you two nameservers (e.g.
anna.ns.cloudflare.com,bob.ns.cloudflare.com). Go to your domain registrar and replace the existing nameservers with these - Wait for propagation — usually a few minutes, can take up to 24 hours. Cloudflare will email you when it's active
If you're using a subdomain (e.g. app.yourdomain.com), the root domain must be on Cloudflare. You don't need to move other services off your old DNS provider — just make sure you import all existing records during setup.
npm i -D wrangler
npx wrangler loginKey settings:
not_found_handling: "single-page-application"— servesindex.htmlfor all unknown paths, which is what a Vite/React SPA needscustom_domain: true— Wrangler creates the DNS record pointing your domain to the Worker and provisions TLS automatically (requires your domain on Cloudflare DNS — see above)workers_dev: false— disables the*.workers.devsubdomain
{
"scripts": {
"deploy": "vite build && npx wrangler deploy"
}
}npm run deployThat's it. Wrangler builds nothing — it just uploads the dist/ directory that Vite already built. On first deploy it creates the Worker and configures the custom domain.
The trick is: don't remove lovable-tagger. Keep it as a dev dependency and in your Vite config:
import { componentTagger } from "lovable-tagger";
export default defineConfig(({ mode }) => ({
plugins: [
react(),
mode === "development" && componentTagger(),
].filter(Boolean),
// ...
}));Lovable reads from and pushes to your Git repo. As long as the tagger is present and the repo is connected, you can still use Lovable's visual editor to make changes. Those changes land in your repo as commits — you then deploy to Cloudflare yourself with npm run deploy.
Lovable just can't deploy for you anymore, and that's the point.
Supabase's built-in SMTP is rate-limited to a few emails per hour — fine for testing, not for real users. Resend is the easiest upgrade: generous free tier (3,000 emails/month), takes about 5 minutes to set up.
- Sign up at resend.com
- Go to API Keys → Create API Key
- Give it a name (e.g. "Supabase"), set permission to Sending access only, and copy the key
In the Resend dashboard → Domains → Add Domain, enter the domain you want to send from (e.g. yourdomain.com). Resend will give you DNS records to add — since your domain is now on Cloudflare (step 7), add them in the Cloudflare dashboard → your domain → DNS → Records:
| Type | Name | Value |
|---|---|---|
| MX | send.yourdomain.com |
feedback-smtp.us-east-1.amazonses.com |
| TXT | send.yourdomain.com |
v=spf1 include:amazonses.com ~all |
| TXT | resend._domainkey.yourdomain.com |
(DKIM key — long string from Resend) |
Make sure the Cloudflare proxy toggle is off (grey cloud, DNS only) for the MX record — proxying mail records breaks email delivery. TXT records are always DNS-only. Wait for verification in the Resend dashboard — usually a few minutes.
In the Supabase Dashboard → Project Settings → Authentication → SMTP Settings, toggle Enable Custom SMTP and fill in:
| Field | Value |
|---|---|
| Host | smtp.resend.com |
| Port | 465 |
| Username | resend |
| Password | your Resend API key |
| Sender email | noreply@yourdomain.com (must be on your verified domain) |
| Sender name | Your App Name |
The sender email must use the domain you verified in Resend. The username is literally the string resend, not your email.
Trigger a magic link sign-in. The email should arrive within seconds, sent from your domain instead of Supabase's default noreply@mail.app.supabase.io.
SMTP settings are configured in the dashboard only — there's no config.toml support for custom SMTP. This means supabase config push won't overwrite your SMTP settings, but it also means SMTP config isn't in your repo. Document the settings in your project README so they can be recreated for a new deployment.
You can safely delete:
supabase/functions/auth-email-hook/— replaced by HTML templatessupabase/functions/_shared/email-templates/— the React TSX templatessupabase/functions/_shared/siteConfig.ts— if it only existed for email rendering- Any Lovable-specific migrations — you've consolidated into one clean migration
After ejecting you have:
- Supabase as code — schema, auth, templates, and edge functions all in your repo, deployable with one command
- Cloudflare hosting — static assets served from the edge, custom domain with auto-TLS
- Resend for email — magic links and invites sent from your own domain, no rate limit headaches
- Lovable still works — for visual editing and code generation, just not for deployment
- No CI pipeline needed —
npm run deployfrom your laptop
The whole setup is simple enough to replicate for multiple deployments (e.g. one per client or region) by swapping out the Supabase project ref, Cloudflare worker name, and domain.
{ "name": "your-app-name", "compatibility_date": "2026-02-16", "workers_dev": false, "routes": [{ "pattern": "yourdomain.com", "custom_domain": true }], "assets": { "directory": "./dist", "not_found_handling": "single-page-application", "html_handling": "auto-trailing-slash" } }