How to migrate from Base44 to your own hosting
Three migration depths: move only the frontend, eject to a new Base44 backend, or fully replace with Supabase. Why partial migration is usually the right starting point.
Base44 is the tightest-coupled of the major AI builders. Exporting a Base44 project to GitHub gets you a real Vite + React frontend codebase, but the application is still calling Base44's backend for data, auth, functions and integrations. The frontend is portable; the backend is not — at least not without rebuilding it.
Base44's own local-development documentation makes this explicit: the environment variables you set up for local development include VITE_BASE44_APP_ID and VITE_BASE44_APP_BASE_URL, where the second value points at a *.base44.app URL (source). The exported frontend talks to Base44 over that URL. Moving the frontend to Vercel does not change that.
Plan prerequisite. GitHub 2-way sync is gated behind the Builder plan ($40/mo or higher) (source). On the Starter ($16/mo) or free tier, you cannot connect the app to GitHub at all — meaning the first step of any migration requires upgrading. Worth knowing before you commit to building anything serious on Base44.
This guide covers three migration depths, ordered by realism:
- Move only the frontend. Backend stays on Base44.
- Eject to a new Base44 backend you own. Schemas copy across; data does not.
- Full migration off Base44. Replace the backend with Supabase or your own Postgres + functions.
Most teams should run path 1 first, treat it as the stable state for a while, and only commit to path 3 when there is a concrete reason to leave Base44 entirely.
What a Base44 project actually contains
The repository structure, based on Base44's own example apps (base44/apps-examples):
your-app/
├── base44/
│ ├── entities/ # JSONC entity schemas (board.jsonc, task.jsonc, ...)
│ └── agents/ # AI agent instructions
├── public/ # static assets
├── src/
│ ├── sdk-client/ # Base44 SDK client configuration
│ ├── components/ # UI components
│ └── pages/ # route pages (auth.tsx, dashboard.tsx, ...)
├── index.html
├── package.json
├── vite.config.ts
├── tsconfig.*.json
└── CLAUDE.md / agents.md # agent instructions for the editor
What syncs to GitHub, what stays in Base44
This is the asymmetry that surprises people when they start a migration:
| Component | In the GitHub-synced repo? | Where it actually runs |
|---|---|---|
src/ frontend code | ✅ yes | In the browser (after Vite build) |
src/sdk-client/ Base44 SDK setup | ✅ yes | n/a — config only |
base44/entities/ schemas | ⚠️ Not via GitHub 2-way sync (Base44 docs: "entities are managed in Base44 and are not included in your local repository"). Visible in the repo when ejected via CLI. | On Base44 |
| Backend functions | ⚠️ Code may or may not be synced — Base44's example apps do not include a functions/ folder in the GitHub repo. The runtime is always on Base44's Deno environment. | On Base44 |
| Actual database data (rows) | ❌ never | On Base44 |
| Auth users + password hashes | ❌ never | On Base44 |
| Secret values (Stripe keys, OpenAI keys) | ❌ never | On Base44, managed via the base44 secrets CLI |
package.json, vite.config.ts, agent instruction files | ✅ yes | n/a — config |
Bottom line: the repo is a readable copy of the frontend plus partial backend metadata, not a deployable backend. To get a complete codebase including entity schemas, you have to run base44 eject (a separate CLI operation that creates a new Base44 backend project — covered in Path 2).
How to spot the SDK call sites
The SDK call pattern is the give-away. Anywhere the code reads like this:
import { base44 } from "@/sdk-client/base44Client";
const tasks = await base44.entities.Task.list();
await base44.auth.me();
await base44.functions.invoke("syncOrders", payload);
…the frontend is depending on Base44's backend. Counting how many files contain base44.entities, base44.auth and base44.functions is the fastest way to gauge migration scope.
Path 1 — Move only the frontend (recommended starting point)
This path treats Base44 like a backend-as-a-service. You self-host the React app, keep using Base44 for data and auth, and remove the dependency only when there is a real reason to.
Step 1 — Connect Base44 to GitHub
GitHub 2-way sync requires the Builder plan or higher, and only the app owner can perform the initial connection (source). In the Base44 dashboard, click the GitHub icon, authorize Base44 Builder, choose the GitHub account or organization, and create a repository for the app. Once connected, edits in Base44 push commits to the repo, and commits to the active branch sync back into Base44.
Clone the repo locally:
git clone <your-github-repo-url>
cd <repo-name>
npm install
git tag pre-migration && git push --tags
Create .env.local with the two Base44-specific variables:
VITE_BASE44_APP_ID=your_app_id
VITE_BASE44_APP_BASE_URL=https://your-app.base44.app
Both values come from the Base44 dashboard. The base URL is the same one Base44 uses to serve your app.
Where Base44 stores secrets
The two VITE_BASE44_* variables above are the only frontend env vars Path 1 needs. Anything else — Stripe keys, OpenAI keys, custom integration secrets — was set inside Base44 and is consumed by Base44's backend functions via Deno.env.get(). Since Path 1 keeps the Base44 backend, those secrets stay where they are; you do not migrate them.
The Base44 CLI manages secrets:
base44 secrets list # enumerate every secret on the app
base44 secrets set KEY=VALUE # add or overwrite one
base44 secrets set --env-file .env # bulk import from a file
base44 secrets delete KEY # remove one
Setting a secret auto-redeploys any backend functions that reference it (source). Run base44 secrets list before you migrate so you have a complete inventory — useful if you ever go to Path 2 or 3, where the backend leaves Base44 and the secrets need to land somewhere else (Supabase Edge Functions, Vercel env vars, Railway variables). See the hub guide's secrets-moving principles for what to rotate vs migrate.
Step 2 — Run locally
npm run dev
The frontend should boot on localhost:5173 and load data exactly as it did inside Base44 — because it is still calling Base44's backend over the network. Then:
npm run build
npm run preview
If npm run build fails, fix it before deploying. Vite outputs to dist/, which is what every static host expects.
Step 3 — Deploy to Vercel
- Import the GitHub repo at vercel.com/new.
- Framework preset: Vite (auto-detected).
- Build command:
npm run build. Output directory:dist. - Add the same two environment variables under "Environment Variables":
VITE_BASE44_APP_IDVITE_BASE44_APP_BASE_URL
- Deploy.
Base44 apps use React Router, so direct navigation to /settings will 404 unless the host falls back to index.html. Add vercel.json at the repo root:
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
Railway, Netlify and Cloudflare Pages all work too. The Vite build settings are identical; the SPA-fallback config is host-specific (_redirects on Netlify and Cloudflare Pages).
Step 4 — Update CORS on Base44
Base44 needs to accept requests from the new frontend domain. The app's CORS / allowed-origins configuration lives in the Base44 dashboard. Add the production domain (and any preview domains you care about) before users hit the deployed app.
Path 2 — Eject to a new Base44 backend you own
Base44's CLI includes an eject command that creates a new Base44 backend project with a local codebase (source). The original app stays untouched. The eject produces a new backend with its own app ID, frontend code locally, and entity schemas / functions / config in a base44/ folder.
npm install -g base44@latest
base44 eject
The CLI requires Node.js 20.19.0 or newer. It prompts for login, the source app and the destination folder.
The critical limitation: entity schemas copy across, but the actual data does not. The new backend's database starts empty. If the app has production users and records, this path requires a separate data migration plan — typically a script using the Base44 SDK that reads from the old app and writes to the new one.
When path 2 is the right choice: you want the app under your own Base44 account (separate billing, separate access), but you are not ready to rebuild the backend in Supabase. It is a halfway step, not a final destination.
Path 3 — Full migration off Base44
Working with a coding agent. Path 3 is mostly mechanical translation work — SDK calls rewritten, schemas defined, RLS policies added, data scripted from one backend to another. Claude Code, Codex, or another agent with repo access can do the bulk of this. Open the cloned repo in the agent, point it at this section, and let it inventory the
base44.entities.*/base44.auth.*/base44.functions.*call sites, propose the Supabase schema, and rewrite the SDK calls. CLI work (base44 secrets list,supabase link, running migration scripts) happens in your local terminal inside the agent session with your approval.
This is the path most articles describe and the fewest teams actually need. Base44's backend gives you NoSQL entities, auth, access rules, Deno functions, realtime and integrations as a bundled offering. Replacing all of it means rebuilding against Supabase or your own stack — that is not a migration, it is a re-implementation of the backend.
What to replace
| Base44 feature | Supabase equivalent |
|---|---|
| Entities (NoSQL collections) | Postgres tables |
| Auth | Supabase Auth |
| Access rules | Row Level Security (RLS) policies |
| Deno functions | Supabase Edge Functions |
| Realtime | Supabase Realtime |
| Integrations / connectors | Direct API calls in your backend |
Migration approach
There are two realistic paths:
- Rewrite SDK calls manually. Replace every
base44.entities.X.list()with the corresponding Supabase query (supabase.from("x").select("*")). Slow, but every developer on the team ends up understanding the data model.
// Before
const tasks = await base44.entities.Task.list();
// After
const { data: tasks, error } = await supabase
.from("tasks")
.select("*");
if (error) throw error;
- Use a community drop-in SDK. A project called
base44-to-supabase-sdkexists on GitHub as a community-maintained drop-in replacement that mimics the Base44 SDK shape against Supabase. Useful as a starting point. Not a substitute for owning the data model — the schema, RLS policies and edge functions still need design decisions.
Schema design
Base44 entities are loose by design — the data model can change without explicit migrations. Postgres is strict, which means the migration is also a forcing function for data modeling. For each Base44 entity:
create table public.tasks (
id uuid primary key default gen_random_uuid(),
title text not null,
status text,
due_date date,
user_id uuid references auth.users(id),
created_at timestamptz default now(),
updated_at timestamptz default now()
);
Then RLS policies for access control:
alter table public.tasks enable row level security;
create policy "Users can read their own tasks"
on public.tasks for select
using (auth.uid() = user_id);
create policy "Users can write their own tasks"
on public.tasks for insert
with check (auth.uid() = user_id);
The Base44 access rules need to translate to these policies one by one. Get this wrong and the new backend either leaks data or denies legitimate requests.
Auth migration
Base44 auth users do not export with a pg_dump. The two realistic options:
- Force re-authentication. Users sign up again on Supabase Auth. Acceptable if the user base is small or the friction is acceptable.
- Programmatic migration. Use the Base44 SDK to fetch all users, then call Supabase Auth's admin API to create matching accounts. Requires generating new passwords or sending password-reset emails.
Data migration
For each entity:
// Fetch from Base44
const users = await base44.entities.User.list();
// Insert into Supabase
const { error } = await supabase.from("users").insert(
users.map(u => ({
email: u.email,
name: u.name,
created_at: u.createdAt,
}))
);
Run this as a one-off script. Add idempotency (skip if the row already exists by email or by ID) so the script can be re-run safely if something fails halfway.
Common gotchas
404 on every API call after deploy. VITE_BASE44_APP_BASE_URL is missing or wrong on the host. The browser is hitting undefined/... and getting nothing.
CORS blocks the production app. Base44 needs the new domain whitelisted in the app's allowed origins. Local development worked because localhost was already in the list.
Direct navigation 404s on /dashboard. SPA fallback is not configured. Add vercel.json rewrites or the platform equivalent.
Eject worked, but the new app has no data. Expected — eject copies schemas, not data. Write a migration script using the Base44 SDK against both apps.
Functions migrated to Supabase Edge Functions return 500. Base44 Deno functions and Supabase Edge Functions are both Deno-based but expose different APIs (auth context, environment, request object). Each function needs the same logic but a different entry point.
Realistic recommendation
For most teams: run path 1, leave the backend on Base44, ship the frontend on Vercel. Revisit path 3 only when one of the original five graduation signals applies — the cost cliff hits, the customization wall blocks a real feature, lock-in becomes a board-level concern. Until then, Base44's bundled backend is a feature, not a problem.
Where to go next
The next guide in this pillar, "Your first real deploy: Supabase + Vercel from scratch," is the cleanest reference for path 3 — it walks through provisioning a fresh Supabase project, setting up a schema, and deploying a Vite frontend that talks to it. If the project might end up on Postgres anyway, that guide is the destination shape.
Next in this pillar
Claude Code & CLI workflows: shipping from your terminalGet the next guide when it lands
One email on Sunday with new /learn guides, tool updates, and a couple of links worth reading.