Chapter 20 · Migrating MongoDB Workloads to Firestore MongoDB Compatibility
Compatibility Matrix Testing, Unsupported Features, Semantic Differences, and Application Refactoring
Turn MongoDB compatibility into executable AtlasMart contracts with golden queries, canonical hashes, explicit transformations, index translation, and application refactoring.
1. AtlasMart problem: “supported” is not the same as “same result”
The inventory from Lesson 1 shows AtlasMart uses ordinary CRUD plus an aggregation, a transaction, an idempotency key, and a Mongoose model. The migration team now needs executable evidence that the target produces acceptable results. A compatibility matrix turns vague statements such as “MongoDB API compatible” into per-operation contracts.
- Build a per-feature compatibility matrix with pass/refactor/block states.
- Run golden query/result regression using canonicalized results.
- Translate one unsupported BSON/identity behavior without hiding semantic change.
- Separate application refactors from data transforms and index changes.
- Detect target drift by rerunning the matrix after driver/product releases.
Use the Emulator Suite, a Firebase demo project, or an isolated test project for destructive, security-sensitive, billing-sensitive, migration, backup/restore, or write-heavy exercises unless the lesson explicitly marks managed verification as required. Treat shown output as expected evidence unless it is explicitly identified as captured output, and re-check current Firebase/Google Cloud edition, mode, quota, pricing, and security documentation before production execution.
AtlasMart keeps project identity
demo-atlasmart-firestore. The mandatory lab is
local/no-cost and uses Node.js 22.23.2, the
MongoDB Node driver 6.21.0 from Chapter 19, and
deterministic Extended-JSON-like fixtures plus an append-only
change log. No real Firestore Enterprise MongoDB-compatible
database, Datastream stream, Dataflow job, Cloud Storage
bucket, service-account key, or billable migration resource is
required. The optional managed path uses an isolated
Enterprise MongoDB-compatible database, an explicitly selected
region, Google Cloud CLI 585.0.0, IAM/ADC or
SCRAM as appropriate, and a hard operation/budget/cleanup
plan. The local harness is a semantic rehearsal: it does not
prove managed service throughput, billing, network latency,
Datastream/Dataflow behavior, IAM, or production cutover
timing.
2. Edition, mode, client, security, and billing boundary
This chapter targets Firestore Enterprise edition with MongoDB compatibility. That is not Firestore Standard Native mode and it is not Enterprise Native mode. Enterprise Native exposes Core and Pipeline operations; MongoDB compatibility exposes a MongoDB-compatible endpoint and MQL/BSON surface. Do not move a Standard/Core/Pipeline assumption into this migration unless the current target documentation explicitly supports it.
| Boundary | Chapter 20 assumption |
|---|---|
| Application client | MongoDB-compatible server driver/tool path; Chapter 19 pins Node driver 6.21.0. |
| End-user auth | Firebase Authentication/App Check are not treated as the MongoDB driver authorization layer; backend application authorization remains explicit. |
| Database identity | Use current MongoDB-compatible authentication/IAM/SCRAM/OIDC guidance; never ship service-account credentials to browsers/mobile clients. |
| Security Rules | Do not assume mobile/web Firestore Security Rules authorize MongoDB-compatible driver operations. |
| Local execution | Deterministic migration harness only; it simulates contracts and CDC evidence, not a managed MongoDB-compatible service. |
| Managed execution | Enterprise target plus Datastream/Dataflow/Cloud Storage are optional and potentially billable; record region, IAM principal, budget and cleanup. |
| Performance/cost | Local operation counts and latency are not production Read/Write Unit, network, Datastream or Dataflow evidence. |
| Rollback | Source remains authoritative until the cutover state machine says otherwise; source retirement is a separate approved step. |
3. Compatibility matrix states
| State | Meaning | Release rule |
|---|---|---|
| PASS | Observed behavior matches the application contract | retain automated test |
| PASS_WITH_DIFFERENCE | Difference is understood and accepted | document + regression test |
| REFACTOR | Application/data/index/auth change required | block cutover until shipped |
| PRODUCTION_ONLY | Cannot be faithfully proven locally | run bounded managed test |
| BLOCK | Required capability has no accepted design | stop migration |
4. Build tests from business behavior, not operator names
A feature matrix should say “recent paid tenant orders are
returned in descending time order, with stable pagination”
rather than merely “$match and
$sort exist.” This keeps the contract meaningful if
the implementation changes.
[
{"id":"Q1","contract":"tenant paid orders sorted newest-first","source":"PASS","target":"TEST"},
{"id":"Q2","contract":"idempotent externalOrderId insertion","source":"PASS","target":"TEST"},
{"id":"A1","contract":"revenue by tenant and status","source":"PASS","target":"TEST"},
{"id":"T1","contract":"reserve inventory and create order atomically","source":"PASS","target":"TEST"},
{"id":"C1","contract":"consume order changes without duplicate business effect","source":"PASS","target":"REFACTOR"},
{"id":"D1","contract":"legacy BSON Code field survives meaningfully","source":"PASS","target":"REFACTOR"}
]
5. Golden data and canonical comparison
Result comparison must normalize ordering only where the business contract says order is irrelevant, preserve BSON distinctions that matter, and exclude intentionally target-generated metadata. Never sort away an ordering bug.
import { createHash } from "node:crypto";
function canonical(value) {
if (Array.isArray(value)) return value.map(canonical);
if (value && typeof value === "object") {
return Object.fromEntries(
Object.keys(value).sort().map(k => [k, canonical(value[k])])
);
}
return value;
}
export function canonicalHash(doc) {
const normalized = JSON.stringify(canonical(doc));
return createHash("sha256").update(normalized).digest("hex");
}
import assert from "node:assert/strict";
import { canonicalHash } from "./canonical-hash.mjs";
export function assertSameDocuments(source, target, {ordered=true}={}) {
const s = source.map(canonicalHash);
const t = target.map(canonicalHash);
if (!ordered) { s.sort(); t.sort(); }
assert.deepEqual(t, s);
}
6. Query regression suite
AtlasMart executes a small but representative set: equality/range filters, sort+limit, update operators, aggregation pipeline, transaction boundary, duplicate insert/idempotency case, and one deliberately unsupported command from the inventory. Record result IDs, canonical result hashes, errors by stable category, and explain/index evidence where supported.
export const golden = [
{
id: "Q1",
description: "tenant-a paid orders",
run: db => db.collection("orders")
.find({tenantId:"tenant-a", status:"PAID"})
.sort({_id:1}).toArray()
},
{
id: "A1",
description: "revenue by tenant",
run: db => db.collection("orders").aggregate([
{$match:{status:"PAID"}},
{$group:{_id:"$tenantId", revenue:{$sum:"$total"}, count:{$sum:1}}},
{$sort:{_id:1}}
]).toArray()
}
];
7. Translate one incompatibility explicitly
Assume a legacy source document stores a BSON JavaScript
Code field used only as historical metadata. The
target does not support that BSON type. The wrong migration
casts every unsupported value to a string inside the copier. The
repaired design adds a versioned transform with a named policy
and provenance.
import { Code } from "mongodb";
export function transformLegacy(doc) {
const out = structuredClone(doc);
if (out.legacyScript instanceof Code) {
out.legacyScriptText = String(out.legacyScript.code);
out.migration = {
...(out.migration ?? {}),
sourceType: "BSON_CODE",
transformVersion: "code-to-audit-text/v1"
};
delete out.legacyScript;
}
return out;
}
This transform is only valid if product/security owners confirm that the field is non-executable historical text. If the application executes it, converting it to text breaks behavior and the migration is blocked until the application is redesigned.
8. Identity translation needs a reference plan
If an unsupported source _id type exists, use a
mapping table and update every known foreign reference or
embedded identity. Preserve legacyIdCanonical for
verification. The migration report must prove that mapping
cardinality is one-to-one and collision-free.
{
"sourceCollection":"orders",
"sourceIdCanonical":"date:2026-09-17T00:00:00.000Z",
"targetId":"legacy-order-20260917T000000000Z",
"mappingVersion":"id-date-to-string/v1",
"referencesRewritten":["payments.orderId","audit.subjectId"]
}
9. Index translation is query-driven
Do not import index definitions as inert configuration. For each golden query, run it against the target, inspect the target index requirements and explain evidence, then create only the structures justified by the workload. Record build state before performance testing. MongoDB tuning advice about shards, working sets, or node-local cache does not transfer mechanically to a serverless Firestore backend.
10. Authentication and authorization regression
Connection success proves identity authentication, not tenant authorization. The compatibility suite includes a tenant-bound backend request that must reject cross-tenant IDs before the database call, then verifies least-privilege IAM or SCRAM/IAM configuration on the optional managed target. Do not migrate MongoDB users/roles as if their semantics were portable objects.
11. Controlled failure: post-filter a broad query
Run a target query that fetches all orders and filters tenant-a in application code. It may return the same visible rows in a tiny test, but it expands data exposure, cost, and performance risk. Mark the test failed even if final UI output matches.
The compatibility contract includes query shape and authorization boundary, not only final rendered rows. Push the tenant predicate into the supported target query and enforce backend authorization before execution.
12. Local compatibility runner and expected evidence
const results = [];
for (const test of matrix) {
try {
const source = await test.run(sourceAdapter);
const target = await test.run(targetAdapter);
test.assert(source, target);
results.push({id:test.id,status:"PASS"});
} catch (error) {
results.push({id:test.id,status:"FAIL",category:classify(error)});
}
}
console.table(results);
if (results.some(r => r.status === "FAIL")) process.exitCode = 1;
Expected state: the intentionally incompatible fixture fails before transformation; after the approved transform, its migration test passes while the report still records the semantic difference. No “green” status is granted to a production-only property that the local adapter cannot prove.
Production judgment
The compatibility matrix is a release artifact. Re-run it against the exact driver/ODM versions and target API surface intended for cutover, especially after product releases. Treat undocumented behavior as unstable. A migration is viable when required contracts either pass or have explicitly accepted refactors—not when most operators happen to work.
Verification checklist and cleanup
- Every inventory blocker maps to a matrix row.
- Golden queries preserve intentional order semantics.
- Hashes exclude only documented non-semantic fields.
- Identity transforms are collision-tested and reversible.
- Auth/tenant tests are included.
- Index decisions link to query contracts.
- Production-only checks remain clearly marked.
Bridge to Lesson 3
With application semantics tested, Lesson 3 moves data. It contrasts Firestore managed export/import with the documented MongoDB-source migration pipeline and builds a local snapshot+CDC replay that exposes duplicates, ordering, checkpoints, and dead-letter handling.
Knowledge check
- What makes a compatibility test stronger than an operator checklist?
- Why is a silent BSON conversion dangerous?
- Should source indexes be copied mechanically?
-
What does a local matrix label
PRODUCTION_ONLYmean? - Why include auth in query regression?
Review the answers
1. It asserts application-level input/output/ordering/error/authorization semantics.
2. It hides semantic change and can corrupt behavior or identity without an auditable decision.
3. No; translate them from target query contracts and target explain/index behavior.
4. The contract still needs a bounded managed-target test before cutover.
5. Equal query results do not prove the same trust boundary or tenant isolation.
Summary and next step
This lesson established the working contract for Compatibility Matrix Testing, Unsupported Features, Semantic Differences, and Application Refactoring. Keep its edition/mode assumptions, trust boundary, verification evidence, and operational constraints explicit when reusing the pattern.
Next, continue to Bulk Migration with Export/Import or Datastream→Cloud Storage→Dataflow Patterns and Change Capture.
Authoritative references
- Firebase · Migrate to Firestore with MongoDB compatibility
- Google Cloud · MongoDB-compatible migration process
- Google Cloud · Configure migration resources and IAM
- Google Cloud · Datastream import from MongoDB source
- Google Cloud · Dataflow write to Firestore with MongoDB compatibility
- Google Cloud · Traffic migration, freeze and cutover
- Google Cloud · Migration troubleshooting
- Google Cloud · Supported BSON types, drivers and tools
- Google Cloud · Behavior differences from MongoDB
- Google Cloud · Managed export/import for Firestore with MongoDB compatibility
- Google Cloud · Firestore with MongoDB compatibility release notes