Base44 Exporter

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.
Base44
Base44
MongoDB
MongoDB

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.

1

Find your app ID

Base44 editor URL

2

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.

ts
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

3

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.

1

Allow temporary network access

Make sure your MongoDB deployment accepts connections from the exporter — without this, the exporter can't reach your database and the transfer can't start. If you use MongoDB Atlas:

  1. Open Security → Network Access.
  2. Add 0.0.0.0/0 to the IP Access List.
  3. Turn on temporary access, choose 6 hours, then confirm.

This temporarily allows connections from any IP address. Atlas still requires your database username and password and will remove the entry after 6 hours. You can remove it sooner after you verify the transfer.

Allow temporary access in MongoDB Atlas

Add 0.0.0.0/0 to the IP Access List for 6 hours

2

MongoDB destination

Use a database user that can create collections and write documents. Make sure your MongoDB deployment allows network access from the exporter.

Paste the MongoDB connection URI and replace any password placeholder with the database user's real password.

Pick any name — MongoDB creates the database on the first write, so it doesn't need to exist yet.

MongoDB Atlas example

If you use Atlas, copy its connection URI into the field on the left

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.

1

Connect Supabase

Approve access so the exporter can copy your uploaded files into storage you own.

Connect Supabase to transfer your uploaded files.

2

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

3

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

Finishing guide (preview)
# 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).

FAQs

Frequently asked questions

Add the temporary Dreamlit export helper to your Base44 app, then connect it to this free tool. The exporter lists your entities and document counts before you choose a direct MongoDB transfer or a ZIP backup.
Connect the temporary helper, choose the entities you want, and enter an empty MongoDB database. The tool copies each Base44 entity into its own MongoDB collection and compares document counts when the transfer finishes.
Yes. Choose Download as ZIP backup. You will get one data file per entity, a manifest with source counts, and a finishing guide. The archive does not contain MongoDB credentials.
The tool walks you through adding a temporary function named dreamlit-export-helper and a private secret that only you know. Base44's chat builder can create the function for you, so this works even on the free plan.
No. The helper reads the entity documents you select. A transfer writes copies to your empty MongoDB database, and a backup only creates a ZIP. Your Base44 app keeps running until you decide to change it.
No. These details stay in the open tab while you use the exporter and are cleared when you refresh or leave. They are sent only when needed to run your checks and export.
You can copy User profile documents, but they do not include passwords, sessions, OAuth connections, or working sign-in accounts. Choose a new auth provider and connect those profiles as part of your app cutover.
No. The exporter moves entity documents. You will need to port backend functions and automations, reconnect integrations, and recreate their secrets. The finishing guide turns these into a clear checklist.
File URLs and references stay in your documents, but the uploaded file bytes are not copied by this version. Keep the original files available until you move them separately and update those references.
The first version creates a clean copy and does not merge with existing collections. The exporter checks that the database is reachable, empty, and writable before it starts.
You can use MongoDB Atlas, another hosted MongoDB service, or a self-hosted MongoDB deployment. Enter a mongodb:// or mongodb+srv:// URI with a username, password, and host, and make sure the exporter can reach it. The destination database must be empty and writable.
Keep it until you verify the transfer or backup. Then remove dreamlit-export-helper from Base44 and delete or rotate DREAMLIT_EXPORT_SECRET so the temporary access is closed.