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.

Advanced · 180–240 minutesMongoDB migration · compatibility · CDC · cutover · rollbackNode 22.23.2 · mongodb 6.21.0 · deterministic local harnessGoogle Cloud CLI 585.0.0 · Enterprise MongoDB-compatible target optional/billableLast reviewed: 17 September 2026

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.

Learning outcomes
  • 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.
Execution and safety note

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.

Chapter 20 reproducibility baseline · reviewed 17 September 2026

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.

compatibility-matrix.json•••
[
  {"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.

canonical-hash.mjs•••
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");
}
compare-results.mjs•••
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.

golden-queries.mjs•••
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.

transform.mjs•••
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;
}
Semantic change

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.

ID map contract•••
{
  "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.

Repair

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

run-matrix.mjs•••
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

  1. What makes a compatibility test stronger than an operator checklist?
  2. Why is a silent BSON conversion dangerous?
  3. Should source indexes be copied mechanically?
  4. What does a local matrix label PRODUCTION_ONLY mean?
  5. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.