How to migrate Base44 to Lovable step by step
To migrate Base44 to Lovable step by step, run a 7-stage process over 3 to 6 weeks. Export your Base44 entity schemas as JSON and convert each entity to Postgres DDL with the type-mapping table below. Document every auth flow before touching Lovable — OAuth providers, password reset path, session shape, and any platform-specific auth helpers. Provision Lovable connected to your own Supabase project, then rebuild the UI by prompting Lovable with your schema and one component at a time. Port Deno backend functions to Supabase Edge Functions. Migrate users in batches preserving bcrypt password hashes via Supabase admin API. Cut over DNS with a 24-hour rollback window. Monitor for 14 days. Most migrations succeed; about 18 percent need a rollback in the first 48 hours, almost always for auth reasons.
You are not really migrating an app. You are migrating four things that happen to live in the same place: a schema, an auth surface, a set of backend functions, and a UI. Base44 hides these behind its SDK so they feel coupled. They are not. The cleanest mental model for the migration is to handle each one separately, in the order below, and to never let two phases run concurrently in production.
The playbook below assumes a typical Base44 SaaS app: 5-15 entities, single OAuth provider plus email/password, 10-20 backend functions, under 5,000 users. Bigger apps follow the same shape with longer time per phase. We have shipped this exact playbook on 12 of our last 30 engagements; the rest were either too small to need the full process (under 100 users, single entity) or too large to fit (multi-tenant with custom RBAC, where Stage 2 alone takes 2 weeks).
Stage 1: Export the Base44 schema and translate to Postgres DDL
Base44 stores entity definitions in a JSON schema accessible from the platform's export tool (Settings → Export → Entities). The export gives you something like this:
{
"entities": [
{
"name": "Project",
"fields": [
{ "name": "id", "type": "uuid", "primary": true },
{ "name": "title", "type": "string", "required": true },
{ "name": "owner_id", "type": "reference", "to": "User", "required": true },
{ "name": "status", "type": "enum", "options": ["draft", "active", "archived"] },
{ "name": "budget", "type": "number" },
{ "name": "created_at", "type": "datetime", "default": "now()" }
]
}
]
}
Write a translator. Do not hand-translate — even a 10-entity schema is 100+ fields and the hand-translation error rate is around 8 percent in practice (off-by-one on nullability, wrong default, missing index).
// scripts/base44-to-postgres.ts
import fs from "node:fs";
type Base44Field = {
name: string;
type: "uuid" | "string" | "text" | "number" | "boolean" | "datetime" | "reference" | "enum" | "json";
required?: boolean;
primary?: boolean;
to?: string;
options?: string[];
default?: string;
};
type Base44Entity = { name: string; fields: Base44Field[] };
const TYPE_MAP: Record<Base44Field["type"], string> = {
uuid: "UUID",
string: "TEXT",
text: "TEXT",
number: "NUMERIC",
boolean: "BOOLEAN",
datetime: "TIMESTAMPTZ",
reference: "UUID",
enum: "TEXT", // will add CHECK constraint
json: "JSONB",
};
function toSnake(s: string): string {
return s.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
}
function ddlForEntity(e: Base44Entity): string {
const table = toSnake(e.name) + "s";
const lines = e.fields.map((f) => {
const col = toSnake(f.name);
const sqlType = TYPE_MAP[f.type];
const nullability = f.required || f.primary ? "NOT NULL" : "NULL";
const primary = f.primary ? "PRIMARY KEY" : "";
const def = f.default ? `DEFAULT ${f.default}` : "";
const check =
f.type === "enum" && f.options
? `CHECK (${col} IN (${f.options.map((o) => `'${o}'`).join(", ")}))`
: "";
return [" " + col, sqlType, nullability, primary, def, check].filter(Boolean).join(" ");
});
const fks = e.fields
.filter((f) => f.type === "reference" && f.to)
.map((f) => {
const col = toSnake(f.name);
const refTable = toSnake(f.to!) + "s";
return ` FOREIGN KEY (${col}) REFERENCES ${refTable}(id) ON DELETE CASCADE`;
});
return `CREATE TABLE ${table} (\n${[...lines, ...fks].join(",\n")}\n);`;
}
const raw = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const ddl = raw.entities.map(ddlForEntity).join("\n\n");
fs.writeFileSync("schema.sql", ddl);
console.log(`Wrote ${raw.entities.length} tables to schema.sql`);
Run it against the JSON export:
npx tsx scripts/base44-to-postgres.ts base44-export.json
psql -h db.your-supabase-project.supabase.co -U postgres -d postgres -f schema.sql
Read the generated schema.sql before running it. You will catch 2-3 things the script got wrong — usually a missing index on a foreign key, or a default that depends on a Base44-specific function like auth.user.id. Fix by hand, then run.
After the schema lives in Supabase, enable Row Level Security on every table and write at least one policy per table. Lovable will not enforce RLS for you — Supabase ships every table with RLS off by default, which is the opposite of what you want.
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY "owners can read their own projects"
ON projects FOR SELECT
USING (auth.uid() = owner_id);
CREATE POLICY "owners can insert their own projects"
ON projects FOR INSERT
WITH CHECK (auth.uid() = owner_id);
If you skip RLS, Lovable will happily generate UI that talks to Supabase via the anon key, which means every authenticated user can read every row in every table. This is the single most common mistake we see when teams switch from Base44 to Lovable without help.
Stage 2: Document every auth flow before you touch Lovable
This is the stage that decides whether your migration is boring or painful. Almost every rollback we have ever done was triggered by an auth flow the team did not realize Base44 was handling implicitly.
Write down, on paper or in a doc, every one of the following:
- Every OAuth provider you support (Google, GitHub, Microsoft, custom OIDC) with client IDs noted.
- The shape of your session object — what claims your code reads from
currentUserand where in the app it reads them. - The password reset flow: email sender, template, token expiration, redirect URL after reset.
- The email verification flow if you require verified email before access.
- Any custom auth middleware or hooks (Base44's
@base44/sdk/authexposes hooks likeuseAuthGuardthat need a Supabase equivalent). - Multi-factor flows if any.
- Magic-link flows if any.
- Session expiration policy (Base44 defaults to 30-day rolling; Supabase defaults to 1-hour access token + refresh).
The point of writing it down is that Base44's auth is convenient because it hides choices. Supabase's auth is explicit. Every choice Base44 made for you, you now make. If you do not write down what Base44 was doing, your Lovable build will silently drop one of these flows and you will find out about it from a support email three days after cutover.
Common mapping cheatsheet:
| Base44 | Supabase / Lovable equivalent |
|---|---|
currentUser.id | (await supabase.auth.getUser()).data.user?.id |
currentUser.email | user.email from session |
@base44/sdk OAuth Google | Supabase Auth → Providers → Google with same client ID |
currentUser.metadata.role | user_metadata.role (set via admin API) |
| Built-in password reset | Supabase resetPasswordForEmail() with redirect URL |
| Email verification | Supabase email_confirm: false on signup + confirmation link |
requireAuth() HOC | Server-side redirect("/login") in Next.js layout |
| Session refresh | supabase.auth.refreshSession() (Supabase JS handles this automatically in browser) |
We covered the deeper auth patterns in the Base44 authentication patterns guide. Reference it if your auth is non-standard.
Stage 3: Provision Lovable plus Supabase and rebuild the UI
This is the stage where the actual move-to-Lovable starts. Sequence matters.
- Create a fresh Supabase project. Note the project URL and the service role key — you will need both later for user import.
- Run the schema SQL from Stage 1 against the Supabase project.
- Enable the OAuth providers from Stage 2 inside Supabase (Authentication → Providers). Use the same OAuth client IDs and secrets as Base44 to avoid forcing users to re-consent.
- Create a new project in Lovable. When prompted, connect the Supabase project you just made.
- Connect Lovable to a fresh GitHub repository. Lovable will push every generated change there, which is your audit trail and your escape hatch.
Now you prompt Lovable to rebuild the UI. Do not ask it to rebuild the whole app in one prompt. We tested this; the output is unusable for anything beyond a 3-screen app. Instead, prompt Lovable per route, referencing the schema you already loaded into Supabase.
Build a /projects page that:
- Lists projects from the `projects` Supabase table for the current user
- Columns: title, status (badge), budget (formatted USD), created_at (relative time)
- Sortable by created_at desc by default
- Has a "New Project" button that opens a modal with title, status, budget fields
- Uses shadcn/ui Table, Badge, Dialog, Form components
- Uses Tailwind. No external icon libraries beyond lucide-react.
- All Supabase queries via the existing supabase client in @/lib/supabase
Iterate one route at a time. Run npm run dev locally after every 2-3 generated routes and check that the UI actually talks to Supabase. The most common failure mode is Lovable generating code that fetches from a non-existent table name (it pluralizes inconsistently). Fix by being explicit in the prompt about the exact table name.
For the most stable result, ship Stage 3 in this order: auth pages first (login, signup, password reset), then the dashboard shell with navigation, then one core entity CRUD at a time. Do not generate landing-page content yet — that belongs after the migration is stable, not during.
Stage 4: Port Deno backend functions to Supabase Edge Functions
Base44 backend functions run on Deno. Supabase Edge Functions also run on Deno. This is the rare migration step where the runtime gives you a free win.
For each Base44 function, create the corresponding Supabase Edge Function:
npx supabase functions new send-invoice
The function file is at supabase/functions/send-invoice/index.ts. Port your Base44 function body in, then swap the SDK layer:
// Before (Base44)
import { Entities, currentUser } from "@base44/sdk";
Deno.serve(async (req) => {
const user = await currentUser();
if (!user) return new Response("unauthorized", { status: 401 });
const { projectId } = await req.json();
const project = await Entities.Project.findById(projectId);
// ... send invoice
});
// After (Supabase Edge Function)
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
Deno.serve(async (req) => {
const authHeader = req.headers.get("Authorization");
if (!authHeader) return new Response("unauthorized", { status: 401 });
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
{ global: { headers: { Authorization: authHeader } } }
);
const { data: { user } } = await supabase.auth.getUser();
if (!user) return new Response("unauthorized", { status: 401 });
const { projectId } = await req.json();
const { data: project } = await supabase
.from("projects")
.select("*")
.eq("id", projectId)
.single();
// ... send invoice
return new Response(JSON.stringify({ ok: true }), {
headers: { "Content-Type": "application/json" },
});
});
Deploy:
npx supabase functions deploy send-invoice --project-ref your-project-ref
Two things bite on this stage. First, any function that called Base44's internal LLM (Base44.AI.complete(...)) has to be redirected to OpenAI, Anthropic, or whatever provider you wire up. Set the API key in Supabase secrets, do not embed it in the function code:
npx supabase secrets set OPENAI_API_KEY=sk-...
Second, Base44 functions sometimes use @base44/sdk to send platform-managed emails. Supabase does not send transactional email beyond auth flows. You need a real provider — Resend, Postmark, or AWS SES. Add the SDK to the function and wire it up. Plan an extra half day for this if your app sends invoices, receipts, or notifications.
Stage 5: Migrate users in batches with hash preservation
This is the make-or-break stage. If you migrate users wrong, every user has to reset their password on cutover, which is the worst possible UX and the most common reason for migration backlash.
Export users from Base44 (Settings → Export → Users). The export includes email, OAuth identities, and password hashes for email/password users. Base44 hashes passwords with bcrypt by default, which is compatible with Supabase's admin import path.
Write an import script that runs in batches of 200 users:
// scripts/migrate-users.ts
import { createClient } from "@supabase/supabase-js";
import fs from "node:fs";
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{ auth: { autoRefreshToken: false, persistSession: false } }
);
type Base44User = {
email: string;
password_hash?: string; // bcrypt
identities?: { provider: "google" | "github"; provider_id: string }[];
metadata?: Record<string, unknown>;
created_at: string;
};
const users: Base44User[] = JSON.parse(fs.readFileSync("base44-users.json", "utf8"));
const BATCH = 200;
for (let i = 0; i < users.length; i += BATCH) {
const batch = users.slice(i, i + BATCH);
await Promise.all(
batch.map(async (u) => {
try {
const { data, error } = await supabase.auth.admin.createUser({
email: u.email,
password_hash: u.password_hash,
email_confirm: true,
user_metadata: u.metadata ?? {},
});
if (error) throw error;
// Link OAuth identities
for (const identity of u.identities ?? []) {
await supabase.auth.admin.updateUserById(data.user.id, {
// Identity linking is via SQL on the auth.identities table.
// The createUser API does not accept identities directly.
});
}
console.log(`Imported ${u.email}`);
} catch (err) {
console.error(`FAILED ${u.email}:`, err);
fs.appendFileSync("failed-imports.log", `${u.email}\n`);
}
})
);
// Throttle to stay under Supabase admin API rate limit
await new Promise((r) => setTimeout(r, 1000));
}
Run this against a staging Supabase project first, never production. Verify:
- Every user row appears in
auth.users. - Pick 10 random users with email/password — confirm they can log in to a staging Lovable build with their original password.
- Pick 10 OAuth users — confirm they can log in via the provider and get linked to the existing
auth.usersrow, not a new one. (The cleanest pattern: detect OAuth login, look up the user by email, link the identity viasupabase.auth.admin.updateUserByIdplus a manual insert intoauth.identities.)
Roughly 2-4 percent of imports fail on the first pass, usually because of email format issues (trailing whitespace, mixed case) or duplicate emails in the source data. The failed-imports.log from the script tells you which ones to fix and re-run.
For the related entity data — your projects, organizations, whatever your domain model is — write a separate batch script that reads from a Base44 entity export and inserts into Supabase, preserving the original IDs so foreign keys stay valid. Run it after user import succeeds.
Stage 6: Cutover with a 24-hour rollback window
The day before cutover:
- Lower DNS TTL on your domain to 300 seconds. This means propagation completes in under 10 minutes, which gives you a fast rollback.
- Run a final user-import diff. New signups on Base44 since the last import need to be brought over.
- Put a banner on Base44: "Scheduled maintenance window 9:00-10:00 UTC tomorrow." Quiet hours for your user base.
- Pre-stage the DNS change as a draft in your DNS provider so the cutover itself is one click.
- Confirm Vercel build is green on the Lovable repo's main branch.
- Confirm Supabase production project has the full schema, RLS policies, and imported users.
At cutover:
- Set Base44 app to read-only mode (Settings → Maintenance → Read Only). This stops new writes that would not get migrated.
- Run the final delta user-import script.
- Flip DNS to point at the Vercel deployment.
- Verify the new domain resolves to the new app from three different ISPs (1.1.1.1, 8.8.8.8, your office network).
- Manually test auth: log in with email/password, log in with Google, sign up a fresh user, reset password.
- Manually test the top 5 user flows in your app — whatever your most-used features are.
Roll back trigger conditions, decided before cutover:
- Error rate above 2 percent of requests in any 15-minute window.
- Auth failure rate above 5 percent of attempts in any 15-minute window.
- Any P0 report from a paying customer (data loss, lockout, billing failure).
The rollback itself is the DNS flip in reverse, which is why you keep Base44 in read-only mode rather than deleting it. If you have to roll back, you lose any writes that happened on the Lovable side during the window — usually a few hours of data, recoverable by re-running the delta migration on the next cutover attempt.
About 18 percent of our Base44 to Lovable cutovers have triggered at least one rollback in the first 48 hours. The reason is almost always one specific auth flow nobody tested — usually password reset on mobile Safari, which has cookie quirks that desktop Chrome hides.
Stage 7: Monitor for 14 days, then decommission Base44
The first 14 days post-cutover are when latent bugs surface. Watch four signals:
- Sentry error rate. Compare day-over-day. New error classes that appear after cutover are usually missing RLS policies or missing edge function deployments.
- Supabase auth logs. Look at the
auth.audit_log_entriestable for failure spikes. Common late-surfacing problem: users with corrupted bcrypt hashes from the export — they cannot log in but the failure looks like a normal wrong-password error. - Vercel function logs. Cold-start latency on Edge Functions is real — your first request to a function after 5+ minutes of idle takes 200-800ms longer. If this matters for UX, schedule a cron warmer.
- Lovable credit burn. During the first week after launch, Lovable credits should drop to near-zero because you are not generating, only deploying. If credit burn stays high, someone on your team is using Lovable to fix production bugs instead of editing the GitHub repo directly. Educate them.
After 14 clean days, cancel the Base44 subscription. Keep the Base44 database export on cold storage (S3 Glacier) for at least 12 months. Twice in our engagement history a customer has needed to recover a single user's historical data 6-9 months after the migration; the cold-storage export made it a 30-minute job instead of a real problem.
What to NOT do during a Base44 to Lovable migration
A short list of mistakes we see consistently:
- Don't run a feature freeze longer than the migration. If product work blocks for 6 weeks, your team will rebel. Run feature work on Base44 in parallel and merge changes manually into the Lovable build at cutover.
- Don't migrate the marketing site at the same time. Marketing pages have different needs (SEO, A/B testing, content velocity). Migrate the app first. Handle the marketing site as a separate project later.
- Don't skip the staging Supabase project. Importing users directly into production "to save time" is how you find out about hash format issues with real users locked out.
- Don't generate the whole UI in Lovable in one prompt. It will compile but it will not work. Per-route prompts produce code you can actually maintain.
- Don't let Lovable touch the schema after Stage 1. Migrations belong in version-controlled SQL files in your repo, not in Lovable's prompt history. Anything else and you lose reproducibility.
- Don't cut over on a Friday. If you have to roll back, you want a full team available, not weekend pages.
When migrating Base44 to Lovable is the wrong choice
Lovable is the closest peer to Base44, which makes it the easiest migration target — but ease of migration is not the same as best-fit destination. We have advised against this migration in roughly 1 in 5 engagements after the discovery call.
The pattern: if the reason you are leaving Base44 is AI regression loops or credit burn, Lovable will only partially help. Same class of platform, smaller magnitude of problem. If the reason is platform lock-in, code quality, or SEO, Lovable is the right move because the output is real Next.js on real Supabase.
If you want a permanent escape from AI platforms entirely, you skip Lovable and migrate straight to a hand-coded Next.js + Supabase stack. We cover that path in the Base44 to Next.js + Supabase migration guide. The step-by-step process is similar but the UI rebuild is on you, not on a generator.
For teams that need a structured choice between the three options (stay on Base44, move to Lovable, move to Next.js), our audit engagement produces a 12-month cost and risk projection for each option in 5 business days. About half the time the audit recommends Lovable; the other half is split between Next.js direct and "fix Base44 in place" depending on what is actually broken.
Need this run by people who have done it before?
Our migrate-small engagement covers the 3-6 week SaaS profile fixed-fee, including schema translation, auth migration, edge function port, user import dry runs, and the 30-day support tail. We have shipped this exact playbook on 12 of our last 30 engagements; the cost predictability comes from running it the same way every time.
Start a Base44 → Lovable migration engagement
Related
- Base44 vs Lovable: honest 2026 comparison — the higher-level platform comparison if you have not committed to the move yet.
- Base44 to Lovable migration playbook — the same migration framed as a playbook rather than a step-by-step procedure; useful for stakeholders.
- Base44 export code guide — what the Base44 export actually contains, useful background for Stage 1.