BASE44DEVS

ARTICLE · 12 MIN READ

Base44 Schema Migration Best Practices: Don't Lose Data

Schema migration is where Base44 apps lose data. The AI agent will rename a field, drop a column, or change a type without warning the user that the change is destructive. Safe migration is mechanical: snapshot first, version the schema, dual-write during transitions, never trust the agent for migrations, and always test rollback before applying. This guide is the playbook we run on every migration audit.

Last verified
2026-10-20
Published
2026-05-01
Read time
12 min
Words
2,309
  • MIGRATION
  • SCHEMA
  • BEST-PRACTICES
  • DATA-INTEGRITY

Base44 schema changes are destructive and cannot be transactionally rolled back; Version History covers code, and Backup & Restore (Elite, Business and Enterprise plans) restores table data but not fields that no longer exist. The AI agent treats schema operations as cosmetic refactors and will silently rename fields, change types, or drop columns without warning that records are losing data. Safe migrations require five mechanical steps: snapshot all affected entity data to external storage before any change, version fields by adding new ones rather than mutating in place, dual-write during transitions for at least one week, apply changes off-peak and never on a Friday, and keep a tested rollback playbook that has been exercised in the last 90 days. Never let the AI agent execute schema operations — have it propose, you snapshot, you apply manually, you verify.

Why this matters

Schema migrations are the highest-stakes change in any database-backed app. Get it right, the change is invisible. Get it wrong, you lose data. On Base44, the platform actively fights you on this: there is no migration tool, no transactional rollback and no migration log (entity schemas are JSON files that version with your code, but nothing records what a change did to the data), and the AI agent treats destructive operations as routine refactors. The default state is "every schema change is a footgun."

This guide, written by the lead engineer at Base44Devs, is the playbook we run when migrating client schemas. It is conservative on purpose. The cost of an extra dual-write week is a minor inconvenience. The cost of losing customer data is the company. For the broader database design context, start with the entity-layer best practices first.

What Base44 does and does not give you

The platform exposes entity definitions in the editor, and in a CLI project as JSON files in base44/entities/. You can add fields, remove fields and change field types. What it does not give you:

  • Transactional schema changes. A change is applied directly; if it fails partway, you may be left in an inconsistent state.
  • Rollback of a dropped field. Version History covers code and design, not table data. Backup & Restore (Elite, Business and Enterprise plans) puts records back to a point in time, but "Fields that no longer exist in your table are not restored." Removing a field is permanent.
  • Migration scripts. There is no concept of a numbered migration file or a forward/back script. The schema is whatever it is right now.
  • A separate staging database. Test data (Builder plan and higher) lets you preview against test records, but Base44's branches use "your real, live data."
  • Point-in-time restore on every plan. Any table exports to CSV from the dashboard, but restore needs the Elite plan or Business/Enterprise; below that, you build snapshots yourself with backend functions.

Knowing the gap matters because every safe-migration practice in this guide is compensating for one of those missing pieces.

The four migration archetypes

Every schema change falls into one of four buckets. Each has a different safe procedure.

1. Additive (low risk)

Adding a new field. Existing records get a null (or default) value. Existing reads continue to work because they ignore unknown fields. Writes can opt into the new field.

Safe procedure:

  1. Snapshot the entity (always — even for additive changes).
  2. Add the field via the IDE.
  3. Update writes to populate the new field.
  4. Optional: backfill historical records from existing data via a backend function.
  5. Update reads to use the new field where appropriate.

Risk: low. The only failure mode is forgetting to populate the field on creates, which surfaces as null in the UI.

2. Renaming (medium risk)

Changing the name of a field. The platform does not have a true rename; what looks like rename is "drop old + add new" with no data preservation.

Safe procedure (dual-write pattern):

  1. Snapshot the entity.
  2. Add the new field as a sibling. Do not delete the old.
  3. In a backend function, backfill the new field from the old field for every record. Verify the count matches.
  4. Update writes to populate both fields.
  5. Update reads to prefer the new field, falling back to the old if null.
  6. Run in this dual-write state for at least one week. Watch error rates and credit usage.
  7. Snapshot again.
  8. Update reads to use only the new field.
  9. Run for a few more days.
  10. Snapshot a third time, then drop the old field.

Risk: medium. The dual-write window is your safety net. If something is reading the old field via an external integration or a forgotten code path, you'll see it.

3. Type change (high risk)

Changing a field's type — string to number, single-value to array, integer to float. Base44 may attempt coercion, may wipe the field, or may corrupt the data depending on the change.

Safe procedure:

  1. Snapshot the entity. Verify the snapshot is restorable.
  2. Add a new field with the target type as a sibling.
  3. In a backend function, transform every record's old field into the new field's type. Validate every conversion. Reject (and log) any record that doesn't convert cleanly.
  4. Update writes to populate only the new field.
  5. Update reads to use only the new field.
  6. Run in this state for at least two weeks. Check that the old field has stopped receiving writes and is no longer read anywhere.
  7. Snapshot, then drop the old field.

Risk: high. The transformation logic must be tested exhaustively. Edge cases (empty strings, malformed numbers, invalid dates) will exist in real data and must be handled.

4. Destructive (highest risk)

Dropping a field, dropping an entity, or removing records. There is no recovery from this once applied.

Safe procedure:

  1. Snapshot the entity. Verify restorability.
  2. Stop using the field in code. Wait two weeks.
  3. Audit logs and third-party integrations. Confirm no external system reads the field.
  4. Snapshot again, immediately before the drop.
  5. Apply the drop during a low-traffic window.
  6. Monitor error rates for 48 hours. Be ready to restore from snapshot.

Risk: highest. Test the restore procedure before you apply the drop, not after. Restore should be a documented, executable process.

The AI agent migration trap

Base44's agent is excellent at code refactors and dangerous at schema work. The reason: a code refactor is reversible (revert the commit). A schema change that drops data is not.

We have seen the agent:

  • Rename a field by dropping the old and adding the new, wiping every record's data in that field.
  • Change a string field to a JSON field, corrupting every existing string value.
  • Drop a "test" entity that turned out to hold legitimate user data.
  • Re-create an entity with a new schema, deleting all records in the original.

The agent does each of these without warning that the operation is destructive. It treats schema as code, code as reversible, and applies surgical refactors that work for code and destroy data when applied to schema.

Rule: never let the agent execute schema operations. Have it propose. You snapshot. You apply. You verify.

The dual-write pattern in detail

Dual-write is the highest-leverage safe-migration pattern. It enables zero-downtime renames and additive type changes. The mechanism:

// Phase 1: dual-write, prefer-new on read.
async function saveTodo(todo: TodoInput) {
  return base44.entities.Todo.create({
    ...todo,
    state: todo.state, // new field
    status: todo.state, // old field, kept in sync
  });
}

function getTodoLabel(todo: Todo): string {
  return todo.state ?? todo.status ?? "unknown";
}

Phase 2 (after backfill confirmed): drop the fallback on read.

function getTodoLabel(todo: Todo): string {
  return todo.state;
}

Phase 3 (after observation): drop the old field write.

async function saveTodo(todo: TodoInput) {
  return base44.entities.Todo.create({
    ...todo,
    state: todo.state,
  });
}

Phase 4: snapshot, then drop the old field from the schema.

The key insight: each phase is independently revertable. If phase 2 reveals that some reader was relying on status, you can roll back to phase 1 by re-adding the fallback. If phase 3 reveals an external integration was writing to status, you can re-enable the dual-write. Only phase 4 is destructive, and you only reach it after weeks of confirmation.

Backfill in a backend function

Backfilling is the work of populating a new field from existing data. A simple backfill:

// base44/functions/backfillTodoState/entry.ts
import { createClientFromRequest } from "npm:@base44/sdk";

Deno.serve(async (req) => {
  const base44 = createClientFromRequest(req); // invoke as an admin; acts with the caller's permissions
  const PAGE_SIZE = 500;
  const query = { state: null }; // records not backfilled yet
  let processed = 0;
  let errors = 0;

  let page = await base44.entities.Todo.filter(query, { sort: "created_date", limit: PAGE_SIZE });
  while (true) {
    for (const todo of page.items) {
      try {
        await base44.entities.Todo.update(todo.id, {
          state: mapStatusToState(todo.status),
        });
        processed++;
      } catch (err) {
        console.error("backfill failed", { id: todo.id, err });
        errors++;
      }
    }

    if (!page.has_more) break;
    page = await base44.entities.Todo.filter(query, { cursor: page.next_cursor, limit: PAGE_SIZE });
  }

  return new Response(JSON.stringify({ processed, errors }), {
    status: 200,
  });
});

function mapStatusToState(status: string): string {
  const map: Record<string, string> = {
    open: "pending",
    closed: "done",
    pending: "pending",
  };
  return map[status] ?? "unknown";
}

Notes on this pattern:

  • Process in batches with a cursor so the function does not timeout on large entities.
  • Skip records that already have the new field (idempotent).
  • Catch and log per-record errors; don't fail the whole job on one bad record.
  • Return counts so the operator can verify completion.

For very large entities, run the backfill in chunks scheduled across multiple invocations. The function timeout on Deno is short.

Snapshot procedure

A working snapshot procedure for a Base44 entity:

// base44/functions/snapshotEntity/entry.ts
import { createClientFromRequest } from "npm:@base44/sdk";

Deno.serve(async (req) => {
  const base44 = createClientFromRequest(req);
  const { entityName, bucket } = await req.json();
  const PAGE_SIZE = 1000;
  let total = 0;
  let part = 0;

  const timestamp = new Date().toISOString();
  const prefix = `${entityName}/${timestamp}`;

  let page = await base44.entities[entityName].list({ sort: "created_date", limit: PAGE_SIZE });
  while (true) {
    const ndjson = page.items.map(r => JSON.stringify(r)).join("\n") + "\n";
    // One object per page, so a later page never overwrites an earlier one
    await fetch(`https://${bucket}.r2.cloudflarestorage.com/${prefix}/part-${part++}.jsonl`, {
      method: "PUT",
      headers: {
        "content-type": "application/x-ndjson",
        authorization: `Bearer ${Deno.env.get("R2_TOKEN")}`,
      },
      body: ndjson,
    });
    total += page.items.length;

    if (!page.has_more) break;
    page = await base44.entities[entityName].list({ cursor: page.next_cursor, limit: PAGE_SIZE });
  }

  return new Response(JSON.stringify({ prefix, parts: part, total }), { status: 200 });
});

Trigger this manually before every schema change. Verify the snapshot exists by reading it back and counting records.

Restore procedure: the inverse — read the JSONL parts in order, batch into Entity.bulkCreate calls of 100 at a time. Test the restore on a non-production entity at least once a quarter so you know it works.

Migration timing

When to apply schema changes:

  • Off-peak. Lowest traffic window for your user base. For a US B2B app, typically 4–6am ET on a weekday.
  • Not on a Friday. Anything you break on Friday eats your weekend.
  • Not before a public release. Schema migrations should land at least 48 hours before any major announcement so you have time to catch issues.
  • One change at a time. Don't bundle a rename with a type change with a drop. If something breaks, you want a single suspect.

Common migration mistakes

Letting the AI agent apply the change. Already discussed. Always propose-then-apply.

Skipping the snapshot because it's "just a small change." Every snapshot ever skipped is the one you wished you had.

Doing reads-before-writes during dual-write. Update writes first, run for a few hours, then start reading the new field. The opposite order leaves you reading a field that hasn't been populated yet. For the broader pre-deploy context that wraps this work, see the deployment checklist.

Backfilling synchronously inside the deployment. A long-running backfill blocks the deploy and risks a partial state. Always run the backfill as a separate, idempotent backend function.

Not testing rollback. A snapshot you can't restore is not a snapshot. Test the restore on a copy of the entity before you need it for real.

Forgetting external integrations. If a Zapier flow, a third-party reporting tool, or a customer-facing webhook reads from the old field, your migration breaks them silently. Audit external consumers before any drop.

Migration checklist

  • Snapshot the entity, verified restorable.
  • Identify the migration archetype (additive / rename / type change / destructive).
  • Document the change in writing with before/after schema.
  • Audit external consumers for the field.
  • Implement the dual-write phase if applicable.
  • Backfill via idempotent backend function, verified counts.
  • Run in dual-write for the appropriate window (1 week minimum, 2+ for type changes).
  • Migrate reads to the new field with fallback.
  • Confirm the old field is no longer read or written from any code path.
  • Snapshot again immediately before the drop.
  • Apply the drop during off-peak.
  • Monitor for 48 hours.
  • Document the change in the schema log.

Want us to plan or run a migration for you?

Our $497 audit reviews your current schema, identifies migration risks, and produces a written migration plan with phases, snapshots, and rollback steps. For execution, we offer a flat-fee migration sprint that includes the snapshot infrastructure and dual-write rollout. Order an audit or book a free 15-minute call.

QUERIES

Frequently asked questions

Q.01Can Base44 roll back a schema change automatically?
A.01

Not automatically. Version History covers your app's code and design, not the records in your tables. On the Elite plan and on Business or Enterprise, Backup & Restore can put a table's records back to a point in time (7 days on Elite, 30 on Business and Enterprise), but Base44 says 'Fields that no longer exist in your table are not restored.' So once you delete a field from an entity, the data in that field is gone unless you snapshotted it yourself. Type changes (string to number, etc.) often coerce or wipe data. Treat every schema change as a one-way operation and never apply one without a fresh snapshot of the entity's data.

Q.02Why is the AI agent dangerous for schema work?
A.02

Because the agent treats schema changes as cosmetic. Asked to 'rename status to state', it will issue a destructive rename that wipes the old column's data, without warning the user that records are about to lose data. The agent has no model of data integrity; it pattern-matches against code-only refactors where rename is safe. For schema work, never let the agent execute the change. Have it propose the change, then apply it manually after backup and review.

Q.03What's the safe pattern for renaming a field in Base44?
A.03

Add the new field as a sibling, not a replacement. Backfill it from the old field with a backend function. Update reads to prefer the new field with a fallback to the old. Update writes to populate both. Run in dual-write mode for at least a week to catch any edge cases. Then, only after verifying the new field is fully populated and all reads succeed without the fallback, drop the old field — and only with a fresh snapshot in hand.

Q.04How do I back up a Base44 entity before a schema change?
A.04

Start with what Base44 gives you: any table exports to CSV from the dashboard, and on the Elite plan and Business or Enterprise, Backup & Restore lets you download a table as it was at an earlier point or restore it. Before dropping a field, which a restore will not bring back, or on a lower plan, also write a backend function that pages through Entity.list() with a cursor and ships the JSON to external storage (S3, R2, or even a Postgres database via fetch). Run it on demand before any migration. Verify the export is restorable by spot-checking record counts and a sample of fields.

Q.05Can I run a migration on a live app without downtime?
A.05

Yes, with the dual-write pattern, but only for additive changes. Adding a field, copying data into it, updating reads, then dropping the old field is zero-downtime. Destructive changes (dropping a field with no replacement, changing a type that requires reformatting) require either a brief maintenance window or a careful schema-versioned rollout. Plan for downtime if you cannot avoid it.

Q.06What does an agent-driven migration disaster look like on Base44?
A.06

For example, say a team asks the agent to 'clean up' an entity by removing unused fields. The agent flags several fields as unused and drops them, but a nightly scheduled job still writes to some of them to populate downstream invoices. Nobody notices until the invoices come out wrong, and the missing data has to be rebuilt by hand from Stripe webhooks and support tickets. The lesson: always snapshot before agent-suggested changes, and never let the agent execute schema work.

NEXT STEP

Order a $497 base44 audit.

Delivered one business day after access (two for apps with 15+ tables or several integrations): 12 components, 110 pass/fail checks and a fix, rebuild or migrate verdict. The $497 fee is credited in full against any fix, build, migration or written-recommendation engagement booked within 30 days. If you ordered the audit for a specific bug and we cannot reproduce it, we refund the fee; otherwise it is non-refundable.