Free Base44 Exporter
Base44 is great for building. When you want more control over your data and backend, move your entity documents into MongoDB.
This free tool copies selected entities into an empty MongoDB database, or takes a plain ZIP backup. It uses the same export flow as our Lovable Cloud exporter and Replit exporter.
Your Base44 data belongs to you. This tool gives you a clear way to copy it into a MongoDB database you control.
- Nothing shuts down. The exporter reads your selected entity documents while your Base44 app keeps running.
- You can verify the copy first. A direct transfer compares document counts in Base44 and MongoDB before you cut over.
- Just want a backup? Pick Download as ZIP in Step 2. You do not need a MongoDB account for that path.
What you'll need
- A Base44 app with backend functions enabled
- A MongoDB connection URI (skip it for a ZIP backup)
- About 15 minutes
Want additional help? Book an expert migration review ($299, 90 min) or have us move the whole app for you (custom quote).
This page does not save your connection details.
Keep this tab open while you work. Refreshing or leaving clears the helper secret, MongoDB URI, and run status.
Step 1: Connect your Base44 app
Add a temporary export helper, then run preflight — a safe, read-only check that lists your entities and document counts so you can see what will move before anything happens.
Find your app ID
Base44 editor URL
Create the temporary export helper
Copy the prompt and paste it into your app's AI builder. It creates a backend function named dreamlit-export-helper from the code below.
The prompt tells Base44 to save this as EXPORT_SECRET. Keep it only until you verify the export.
import { createClientFromRequest } from "npm:@base44/sdk";
const CONTRACT_VERSION = "dreamlit.base44-export.v1";
const HELPER_BUILD_ID = "dreamlit-base44-mongo-export-0.1.0";
const PAGE_SIZE = 500;
const PREFLIGHT_DOCUMENT_CAP_PER_ENTITY = 100_000;
// Base44 function name: dreamlit-export-helper
// IMPORTANT: Include EVERY Base44 entity name, including User.
const ENTITIES: string[] = ["REPLACE_WITH_EVERY_ENTITY_IN_THIS_APP","User"]; // the Base44 builder/user must fill this with EVERY entity name incl "User"
interface RequestBody {
action?: unknown;
app_id?: unknown;
entities?: unknown;
entity?: unknown;
skip?: unknown;
limit?: unknown;
file_uri?: unknown;
}
interface EntityCount {
count: number;
capped: boolean;
}
class RequestError extends Error {}
function json(body: Record<string, unknown>, status = 200): Response {
return Response.json(
{ contract_version: CONTRACT_VERSION, ...body },
{
status,
headers: { "cache-control": "no-store, max-age=0" },
},
);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function requireString(value: unknown, field: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new RequestError(field + " is required.");
}
return value.trim();
}
function normalizeEntities(value: unknown): string[] {
const source = value === undefined ? ENTITIES : value;
if (!Array.isArray(source)) {
throw new RequestError("entities must be an array of entity names.");
}
const entities: string[] = [];
const seen = new Set<string>();
for (const candidate of source) {
if (typeof candidate !== "string") {
throw new RequestError("Every entity name must be a string.");
}
const entity = candidate.trim();
if (
!entity ||
entity.length > 200 ||
entity.includes("\0") ||
entity === "__proto__" ||
entity === "constructor" ||
entity === "prototype"
) {
throw new RequestError("An entity name is invalid.");
}
if (!seen.has(entity)) {
seen.add(entity);
entities.push(entity);
}
}
if (entities.length === 0) {
throw new RequestError("Add at least one entity name to ENTITIES.");
}
return entities;
}
async function listEntityPage(
base44: ReturnType<typeof createClientFromRequest>,
entity: string,
limit: number,
skip: number,
): Promise<Record<string, unknown>[]> {
const handler = base44.asServiceRole.entities[entity];
if (!handler || typeof handler.list !== "function") {
throw new RequestError("The requested entity is not available.");
}
const documents = await base44.asServiceRole.entities[entity].list(
"created_date",
limit,
skip,
);
if (!Array.isArray(documents)) {
throw new Error("The entity list response was invalid.");
}
return documents as Record<string, unknown>[];
}
async function countEntityDocuments(
base44: ReturnType<typeof createClientFromRequest>,
entity: string,
): Promise<EntityCount> {
let count = 0;
while (count < PREFLIGHT_DOCUMENT_CAP_PER_ENTITY) {
const limit= Math.min(
PAGE_SIZE,
PREFLIGHT_DOCUMENT_CAP_PER_ENTITY - count,
);
const documents= await listEntityPage(base44, entity, limit, count);
count = documents.length;
if (documents.length < limit) return { count, capped: false };
}
return { count, capped: true };
}
function readStatus(error: unknown): number | null {
if (!isRecord(error)) return null;
return typeof error.status= "number"
? error.status
: typeof error.statusCode= "number"
? error.statusCode
: null;
}
Deno.serve(async (req): Promise<Response> => {
if (req.method !== "POST") {
return json({ ok: false, error: "Only POST requests are supported." }, 405);
}
const configuredSecret = Deno.env.get("EXPORT_SECRET");
const suppliedSecret = req.headers.get("x-export-secret");
if (
!configuredSecret ||
!suppliedSecret ||
suppliedSecret !== configuredSecret
) {
return json({ ok: false, error: "The export secret was rejected." }, 403);
}
let body: RequestBody;
try {
const parsed: unknown = await req.json();
if (!isRecord(parsed)) throw new RequestError("A JSON object is required.");
body = parsed;
} catch (error) {
const message =
error instanceof RequestError
? error.message
: "A valid JSON body is required.";
return json({ ok: false, error: message }, 400);
}
let base44: ReturnType<typeof createClientFromRequest> | null = null;
try {
base44 = createClientFromRequest(req);
if (body.action === "ping") {
return json({ ok: true, helper_build_id: HELPER_BUILD_ID });
}
if (body.action === "preflight") {
const appId = requireString(body.app_id, "app_id");
const entityNames = normalizeEntities(body.entities);
const entities: Array<{ name: string; document_count: number }> = [];
const warnings: string[] = [];
for (const name of entityNames) {
const result = await countEntityDocuments(base44, name);
entities.push({ name, document_count: result.count });
if (result.capped) {
warnings.push(
name +
" reached the preflight count cap of " +
PREFLIGHT_DOCUMENT_CAP_PER_ENTITY +
"; its true document count may be higher.",
);
}
}
const user = entities.find((entity) => entity.name === "User");
if (!user) {
warnings.push(
"User is not listed. Add User to ENTITIES to include user profiles.",
);
}
return json({
ok: true,
app_id: appId,
app_name: null,
helper_build_id: HELPER_BUILD_ID,
entities,
total_documents: entities.reduce(
(total, entity) => total + entity.document_count,
0,
),
user_profiles: {
present: Boolean(user),
document_count: user?.document_count ?? 0,
},
resource_inventory: null,
warnings,
});
}
if (body.action === "export_page") {
const appId = requireString(body.app_id, "app_id");
const entityNames = normalizeEntities(body.entities);
const entity = requireString(body.entity, "entity");
if (!entityNames.includes(entity)) {
throw new RequestError(
"The requested entity is not listed in ENTITIES.",
);
}
if (
!Number.isInteger(body.skip) ||
typeof body.skip !== "number" ||
body.skip < 0
) {
throw new RequestError("skip must be a non-negative integer.");
}
if (
!Number.isInteger(body.limit) ||
typeof body.limit = "number" ||
body.limit < 1 ||
body.limit > PAGE_SIZE
) {
throw new RequestError("limit must be an integer from 1 to 500.");
}
const documents = await listEntityPage(
base44,
entity,
body.limit,
body.skip,
);
return json({
ok: true,
app_id: appId,
entity,
skip: body.skip,
limit: body.limit,
documents,
done: documents.length < body.limit,
});
}
if (body.action= "sign_file") {
const fileUri= requireString(body.file_uri, "file_uri");
const result=
await base44.asServiceRole.integrations.Core.CreateFileSignedUrl({
file_uri: fileUri,
});
if (
!result ||
typeof result.signed_url = "string" ||
!result.signed_url
) {
throw new Error("The signed file response was invalid.");
}
return json({ ok: true, signed_url: result.signed_url });
}
throw new RequestError("The requested action is not supported.");
} catch (error) {
if (error instanceof RequestError) {
return json({ ok: false, error: error.message }, 400);
}
if (readStatus(error)= 429) {
return new Response(
JSON.stringify({
contract_version: CONTRACT_VERSION,
ok: false,
error: "Base44 rate limit reached.",
}),
{
status: 429,
headers: {
"content-type": "application/json",
"cache-control": "no-store, max-age=0",
"retry-after": "1",
},
},
);
}
return json(
{ ok: false, error: "The helper could not complete the request." },
500,
);
} finally {
base44?.cleanup();
}
});This code adds a secure, read-only export endpoint to your app. Generate a secret above to unlock Copy prompt.
Paste the prompt into the AI builder
Base44 AI builder
Publish your app so the helper is live, then paste its Base44 URL. We add /functions/dreamlit-export-helper automatically.
Preflight is a quick, read-only check of your app — it lists your entities and how many documents each has, so you can see what will move before anything happens. It never changes your Base44 app.
Ensure your app is published
Base44 Publish dialog
Step 2: Choose how to export
Everything moves — every entity plus User profile records. Pick a direct MongoDB transfer or a ZIP backup.
How do you want to export it?
Move the documents into MongoDB, or download a ZIP you can keep and inspect later.
You'll connect an empty MongoDB database in Step 3.
Step 3: Connect MongoDB
Connect your Base44 app in Step 1 before connecting MongoDB.
Step 4: Run the transfer
This copies your selected documents into MongoDB. Your Base44 app is only read from, never changed.
Need help? Book an expert migration review ($299, 90 min), or have us do it for you.
Step 5: Transfer your files to Supabase
Copy Base44's uploaded files into a Supabase bucket you own and update every file link in MongoDB. Or skip the file transfer and handle it later with your finishing guide.
Connect Supabase
Approve access so the exporter can copy your uploaded files into storage you own.
Connect Supabase to transfer your uploaded files.
Choose a project and bucket
Pick the Supabase project that should hold your files. We'll copy them into the bucket below.
We'll create this public bucket if it doesn't exist.
Storage destination
Choose a Supabase project
base44-files
Transfer
Copy every uploaded file into your bucket and update each file link in MongoDB to point at the copy.
Step 6: Verify and finish
Review the Base44 and MongoDB document counts, then follow the finishing guide to update your app.
Your finishing guide
# Base44 migration port prompt You are helping finish a migration off Base44. The app's data has already been exported to MongoDB — your job is the app changes an export cannot make. Before you change any code, inspect the app to learn how it reads and writes data. This is a point-in-time export: keep the Base44 app as a rollback, but don't let both Base44 and MongoDB take writes — once MongoDB is live, returning to Base44 loses anything written since the export. Work through the checklist below one item at a time. ## Migration context - **Entities / collections:** the selected Base44 entities - **Destination:** your MongoDB database ## What moved The exporter copied the selected Base44 entities into your MongoDB database. Base44 IDs and document field values were retained, so relationships between documents stay intact. Each Base44 id is stored as the document's string `_id` (not an ObjectId), and built-in fields like `created_date`, `updated_date`, and `created_by_id` are kept as-is. ## What did not move automatically - **Base44 Auth:** the `User` documents carry profile data, but passwords, active sessions, and OAuth links did not move — they are not sign-in identities in a new provider. Set up Supabase Auth (or another provider), create identities through its supported flow, map each new auth user to its Base44 user id, and have users reauthenticate. - **Backend functions:** port each function to a Deno-compatible runtime (e.g. Supabase Edge Functions) — a rewrite, not a copy: replace Base44 SDK calls with a server-side MongoDB repository or API, and re-check limits, timeouts, and networking. Never connect to MongoDB from the browser. - **Automations and schedules:** recreate triggers, schedules, retry policies, and monitoring in your new stack. - **Integrations, connectors, and secrets:** reconnect every external service and create fresh secrets. Connector credentials were not exported. - **RLS and security rules:** Base44 row- and field-level access rules did not move. Reimplement authorization in your API and database layer, default to deny, filter protected fields from responses, and test every user role and ownership boundary. - **Uploaded file bytes:** file URI and URL fields remain in documents, but the files themselves did not move. Copy them to Supabase Storage or another object store and update references after validating the copy. - **Realtime behavior:** Base44 subscriptions and events do not transfer automatically. Rethink which features need realtime updates and implement them against the new API. ## Cutover checklist - [ ] Add MongoDB indexes based on entity schemas and real query patterns. - [ ] Replace Base44 SDK entity calls with a server-side MongoDB repository or API. Never connect to MongoDB or expose the connection string from browser code. - [ ] Choose and configure Supabase Auth or another new auth provider. - [ ] Reconnect integrations and recreate secrets without copying old connector tokens. - [ ] Port backend functions to a Deno-compatible runtime (e.g. Supabase Edge Functions) — rewriting Base44 SDK calls, not copying — and port automations or schedules. - [ ] Reimplement Base44 RLS and security rules in the new API. - [ ] Migrate uploaded file bytes to Supabase Storage and verify every rewritten reference. - [ ] Update application environment variables and deployment configuration. - [ ] Run smoke tests for sign-in, permissions, writes, integrations, files, and critical user journeys. - [ ] Compare source and MongoDB documents — per-collection counts, plus spot-checks of ids, fields, types, and cross-document references — and investigate every mismatch. - [ ] Switch production traffic only after backups and rollback steps are ready. - [ ] Remove `dreamlit-export-helper` and rotate or delete `EXPORT_SECRET` after verification.
A prompt for your coding agent. The placeholders fill in with your entities, destination, and counts after the run.
Your finishing checklist
Delete the backend function named dreamlit-export-helper and remove the EXPORT_SECRET environment variable. Confirm that both the function and the secret are gone.
Want additional help? Book an expert migration review ($299, 90 min) or have us move the whole app for you (custom quote).