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, no schema versioning, and the AI agent treats destructive operations as routine refactors. The default state is "every schema change is a footgun."
This guide 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.
What Base44 does and does not give you
The platform exposes entity definitions through its IDE. You can add fields, remove fields, change field types, and add or remove indexes. 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.
- Built-in rollback. Code has version history. Entity schemas do not. 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 staging environment by default. Pro tier supports a staging app, but most users develop directly against production data.
- Native export/restore. You build this 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:
- Snapshot the entity (always — even for additive changes).
- Add the field via the IDE.
- Update writes to populate the new field.
- Optional: backfill historical records from existing data via a backend function.
- 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):
- Snapshot the entity.
- Add the new field as a sibling. Do not delete the old.
- In a backend function, backfill the new field from the old field for every record. Verify the count matches.
- Update writes to populate both fields.
- Update reads to prefer the new field, falling back to the old if
null. - Run in this dual-write state for at least one week. Watch error rates and credit usage.
- Snapshot again.
- Update reads to use only the new field.
- Run for a few more days.
- 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:
- Snapshot the entity. Verify the snapshot is restorable.
- Add a new field with the target type as a sibling.
- 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.
- Update writes to populate only the new field.
- Update reads to use only the new field.
- Run in this state for at least two weeks. Check that the old field has stopped receiving writes and is no longer read anywhere.
- 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:
- Snapshot the entity. Verify restorability.
- Stop using the field in code. Wait two weeks.
- Audit logs and third-party integrations. Confirm no external system reads the field.
- Snapshot again, immediately before the drop.
- Apply the drop during a low-traffic window.
- 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:
// backend/functions/backfillTodoState.ts
export default async function handler(req: Request) {
const PAGE_SIZE = 500;
let cursor: string | null = null;
let processed = 0;
let errors = 0;
while (true) {
const filter: Record<string, unknown> = {
state: { $exists: false },
};
if (cursor) filter.created_date = { $gt: cursor };
const batch = await base44.entities.Todo.list(filter, "created_date", PAGE_SIZE);
if (batch.length === 0) break;
for (const todo of batch) {
try {
await base44.entities.Todo.update(todo.id, {
state: mapStatusToState(todo.status),
});
processed++;
} catch (err) {
console.error("backfill failed", { id: todo.id, err });
errors++;
}
}
cursor = batch[batch.length - 1].created_date;
}
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:
// backend/functions/snapshotEntity.ts
export default async function handler(req: Request) {
const { entityName, bucket } = await req.json();
const PAGE_SIZE = 1000;
let cursor: string | null = null;
let total = 0;
const timestamp = new Date().toISOString();
const key = `${entityName}/${timestamp}.jsonl`;
while (true) {
const filter: Record<string, unknown> = {};
if (cursor) filter.created_date = { $gt: cursor };
const batch = await base44.entities[entityName].list(filter, "created_date", PAGE_SIZE);
if (batch.length === 0) break;
const ndjson = batch.map(r => JSON.stringify(r)).join("\n") + "\n";
await fetch(`https://${bucket}.r2.cloudflarestorage.com/${key}`, {
method: "PUT",
headers: {
"content-type": "application/x-ndjson",
authorization: `Bearer ${Deno.env.get("R2_TOKEN")}`,
},
body: ndjson,
});
cursor = batch[batch.length - 1].created_date;
total += batch.length;
}
return new Response(JSON.stringify({ key, 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 file, 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.
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.
Related reading
- Base44 Database Best Practices — schema design that minimizes future migration pain.
- Base44 Production Readiness Guide — the broader operational picture.
- Base44 Deployment Checklist — pre-deploy verification including schema-touching changes.