Chapter 19 · Firestore with MongoDB Compatibility: Drivers, MQL/BSON, Tools, and Serverless Differences

Port a Small MongoDB Application and Build Compatibility Tests Instead of Assuming Drop-In Equivalence

Port a small AtlasMart MongoDB repository layer behind a compatibility test suite that records supported behavior, intentional differences, performance evidence, rollback assumptions, and blockers for Chapter 20 migration planning.

Advanced · 180–240 minutesMongoDB compatibility · MQL/BSON · tools · serverless differencesNode 22.23.2 · mongodb 6.21.0 · Mongoose 8.24.4 · mongosh 2.10.0gcloud 585.0.0 · managed MongoDB-compatible database optional · local contract harness mandatoryLast reviewed: 17 September 2026

1. AtlasMart capstone: port the repository layer, not the assumptions

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.

The team now takes a small source-MongoDB repository used by AtlasMart products and orders and points it at Firestore with MongoDB compatibility. The goal is not to change one URI and celebrate. The goal is to preserve a versioned contract: data types, query results, updates, transactions, indexes, errors, tools, authorization, observability, performance, and rollback. Every failed case becomes either a refactor task or a migration blocker.

Chapter 19 reproducibility baseline · reviewed 17 September 2026

AtlasMart keeps the course-wide project identity demo-atlasmart-firestore. The mandatory compatibility harness is local/no-cost and targets Node.js 22.23.2 LTS with mongodb@6.21.0, a current Firestore-supported 6.x Node driver. Tool examples pin mongosh 2.10.0, MongoDB Compass 1.50.0, Mongoose 8.24.4, Google Cloud CLI 585.0.0, and retain Firebase CLI 15.30.0 for course continuity. The optional managed database is atlasmart-mongo-lab, Enterprise edition with MongoDB-compatible data access in an explicitly chosen supported location such as us-central1. Re-check location availability before creation.

Managed-service boundary

Unlike the Native-mode labs, current official documentation does not provide a Local Emulator Suite endpoint that emulates the MongoDB wire/API compatibility surface. Therefore the mandatory lab uses deterministic BSON/query/index/compatibility fixtures and executable contract tests without credentials. A real Firestore with MongoDB compatibility database is an optional verification layer: database creation requires an actual Google Cloud/Firebase project and billing enabled, and connection/authentication uses SCRAM or Google Cloud identity. No lesson fabricates a successful handshake, Query Explain result, billed unit count, p95/p99 latency, or tool connection that was not actually executed.

Learning outcomes

01

Wrap MongoDB access behind a repository boundary so compatibility differences do not leak across the application.

02

Build a source-versus-destination contract suite with exact fixtures and expected result IDs/state.

03

Separate functional parity, semantic parity, performance, security, operations, and cost as independent gates.

04

Generate a compatibility/blocker report that can drive Chapter 20 migration planning.

05

Design a rollback/exit path instead of making the first successful query irreversible.

2. Repository boundary: the port should be reversible

AtlasMart's HTTP/controller layer should not know whether a repository uses source MongoDB or Firestore's MongoDB-compatible endpoint. It asks for business operations such as listSellerProducts, reserveStock, or appendOrderEvent. The adapter encapsulates driver/session/query details. That structure makes dual-run testing and rollback possible.

product-repository.mjs
export class ProductRepository {  constructor(db) { this.c = db.collection("products"); }  listSellerProducts({tenantId, sellerId, limit=20}) {    return this.c.find({tenantId, sellerId, published:true})      .sort({price:1, _id:1}).limit(limit).toArray();  }  async decrementStock({_id, qty}) {    const r = await this.c.findOneAndUpdate(      {_id, stock:{$gte:qty}},      {$inc:{stock:-qty}},      {returnDocument:"after"}    );    return r;  }}

3. Compatibility suite: compare business evidence, not raw implementation

The same test vectors run against two adapters: a known source MongoDB environment and the isolated Firestore MongoDB-compatible destination. The suite compares canonicalized results and invariant outcomes. It intentionally does not require identical explain plans, error text, topology metadata, or operation latency—those belong to separate gates.

contract-cases.json
[  {"id":"Q01","op":"seller-products","input":{"tenantId":"tenant-a","sellerId":"seller-a"},"expectedIds":["p-1002","p-1006","p-1001"]},  {"id":"Q02","op":"low-stock","input":{"tenantId":"tenant-a","threshold":5},"expectedIds":["p-1002","p-1004"]},  {"id":"W01","op":"decrement-stock","input":{"_id":"p-1001","qty":2},"expectedStock":6},  {"id":"W02","op":"decrement-stock","input":{"_id":"p-1004","qty":2},"expected":"REJECT_NO_STOCK"},  {"id":"N01","op":"mapReduce","expected":"DESTINATION_UNSUPPORTED"}]

4. Canonicalization prevents false mismatches

Drivers can represent BSON types differently in JavaScript objects. Before comparing source and destination, normalize ObjectId/Long/Decimal/Date/Binary into a stable extended representation, sort arrays only when application semantics define them as sets, and ignore server metadata that is not part of the business contract. Do not normalize away real semantic differences.

canonicalize sketch
function canonical(v) {  if (v?.constructor?.name === "ObjectId") return {$oid:v.toHexString()};  if (v instanceof Date) return {$date:v.toISOString()};  if (Array.isArray(v)) return v.map(canonical);  if (v && typeof v === "object") return Object.fromEntries(    Object.keys(v).sort().map(k => [k, canonical(v[k])])  );  return v;}

5. Six independent migration gates

Gate What passes it Example failure
Functional Required CRUD/query/aggregation returns correct state Unsupported operator or wrong result set
Semantic Transactions, ordering, numeric/date behavior, retries match business requirements Natural-order dependency or snapshot write-skew risk
Security IAM/database identity + application tenant authorization enforce least privilege Broad backend principal leaks cross-tenant rows
Performance Bounded p50/p95/p99, scans, bytes/read units meet SLO in target region Unindexed scan passes functionally but costs/latency fail
Operational Monitoring, explain, backup/recovery, on-call procedures exist Runbook depends on serverStatus/replica-set commands
Cost/exit Unit/storage/network/recovery cost understood; rollback path tested No source rollback or unexpected scan/index cost

6. Mandatory no-cost rehearsal: local oracle + compatibility blocker report

atlasmart-products.json · deterministic fixture
[  {"_id":"p-1001","sellerId":"seller-a","tenantId":"tenant-a","name":"Trail Camera","category":"cameras","price":99.0,"stock":8,"rating":4.6,"tags":["outdoor","camera"],"published":true},  {"_id":"p-1002","sellerId":"seller-a","tenantId":"tenant-a","name":"USB-C Hub","category":"accessories","price":49.0,"stock":3,"rating":4.4,"tags":["usb-c","desk"],"published":true},  {"_id":"p-1003","sellerId":"seller-b","tenantId":"tenant-a","name":"Temp Sensor","category":"sensors","price":39.0,"stock":12,"rating":4.7,"tags":["iot","sensor"],"published":true},  {"_id":"p-1004","sellerId":"seller-b","tenantId":"tenant-a","name":"Edge Gateway","category":"gateways","price":149.0,"stock":1,"rating":4.2,"tags":["edge","iot"],"published":true},  {"_id":"p-1005","sellerId":"seller-c","tenantId":"tenant-b","name":"PoE Camera","category":"cameras","price":199.0,"stock":5,"rating":4.8,"tags":["poe","camera"],"published":true},  {"_id":"p-1006","sellerId":"seller-a","tenantId":"tenant-a","name":"Bench PSU","category":"power","price":89.0,"stock":9,"rating":4.5,"tags":["bench","power"],"published":true}]
compatibility-matrix.json · versioned contract
{  "target": "firestore-enterprise-mongodb-compat-2026-09-17",  "driver": "mongodb@6.21.0",  "checks": {    "find": "supported",    "insert": "supported",    "update": "supported",    "delete": "supported",    "aggregate": "supported",    "transactions": "supported-with-different-defaults",    "retryableWrites": "unsupported-disable-in-uri",    "mapReduce": "unsupported",    "gridFS": "unsupported",    "wildcardIndexes": "unsupported",    "vectorIndexMongoAPI": "unsupported",    "textSearch": "preview",    "geoNear": "preview",    "changeStreams": "preview"  }}
compatibility-local.mjs · mandatory no-credential harness
import assert from "node:assert/strict";import fs from "node:fs";const products = JSON.parse(fs.readFileSync("atlasmart-products.json", "utf8"));const matrix = JSON.parse(fs.readFileSync("compatibility-matrix.json", "utf8"));assert.equal(products.length, 6);assert.equal(new Set(products.map(p => p._id)).size, 6);assert.deepEqual(  products.filter(p => p.tenantId === "tenant-a" && p.stock < 5).map(p => p._id).sort(),  ["p-1002", "p-1004"]);assert.equal(products.filter(p => p.category === "cameras").length, 2);assert.equal(matrix.checks.retryableWrites, "unsupported-disable-in-uri");assert.equal(matrix.checks.changeStreams, "preview");console.log(JSON.stringify({fixture:"PASS", products:6, lowStock:["p-1002","p-1004"]}));
run the mandatory harness
node compatibility-local.mjs
build-report.mjs
import fs from "node:fs";const matrix = JSON.parse(fs.readFileSync("compatibility-matrix.json","utf8"));const blockers = [];for (const [feature,state] of Object.entries(matrix.checks)) {  if (state === "unsupported") blockers.push({feature, severity:"BLOCKER_UNTIL_REFACTORED"});  if (state === "preview") blockers.push({feature, severity:"REQUIRES_PRE_GA_RISK_DECISION"});}console.log(JSON.stringify({target:matrix.target, blockers}, null, 2));

The local report intentionally labels unsupported and Preview features. A migration plan that hides those rows is not ready.

7. Optional managed compatibility runner

managed-contract-runner.mjs
import { MongoClient } from "mongodb";import assert from "node:assert/strict";const client = new MongoClient(process.env.ATLASMART_MONGO_URI);await client.connect();const c = client.db("atlasmart-mongo-lab").collection("compat_products");const ids = (await c.find({tenantId:"tenant-a", sellerId:"seller-a", published:true})  .sort({price:1, _id:1}).limit(20).toArray()).map(x=>x._id);assert.deepEqual(ids,["p-1002","p-1006","p-1001"]);const low = (await c.find({tenantId:"tenant-a", stock:{$lt:5}})  .sort({_id:1}).toArray()).map(x=>x._id);assert.deepEqual(low,["p-1002","p-1004"]);console.log("managed functional contract PASS");await client.close();

Run this only after seeding the exact six-document fixture into an isolated database. Record database ID/UID, location, driver/tool versions, dataset hash, index list, and test timestamp. Never treat a result from a different fixture as comparable evidence.

8. Performance test: measure distributions, not averages

After functional parity, run a bounded test with declared concurrency, warmup, operation mix, region, index state, returned document sizes, and request count. Record p50/p95/p99, errors, Query Explain bytes/read units for representative queries, and network/client timing separately. Do not load-test a production project or infer production capacity from the local harness.

performance-evidence.json
{  "environment": "OPTIONAL_MANAGED_ISOLATED",  "databaseId": "atlasmart-mongo-lab",  "location": "RECORD_ACTUAL",  "driver": "mongodb@6.21.0",  "fixtureHash": "RECORD_ACTUAL",  "indexes": ["RECORD_ACTUAL"],  "concurrency": "RECORD_ACTUAL",  "warmupRequests": "RECORD_ACTUAL",  "measured": {"p50Ms":null,"p95Ms":null,"p99Ms":null,"errors":null,"readUnits":null}}

Nulls are intentional until the test runs. A course should never invent “representative” percentile numbers.

9. Security regression: two tenants, one backend principal

Because the service principal may have broad database access, the repository must bind a trusted tenant ID derived from the authenticated application caller, not from an arbitrary request field. The test creates tenant-a and tenant-b documents, asks a tenant-a caller for tenant-b, and expects application authorization to reject before the query is sent.

tenant regression
const caller = {uid:"user-a", tenantId:"tenant-a", sellerIds:["seller-a"]};assert.throws(  () => authorizeSeller(caller, "seller-c"),  /FORBIDDEN/);// Also assert repository methods never accept raw client tenantId without authorization binding.

10. Rollback and exit path

A good compatibility test suite is also an exit tool. Keep source schemas and query contracts documented, avoid destination-only behavior in the first cutover unless intentionally accepted, preserve export/replay procedures, and define the point after which source writes stop. Chapter 20 will add Datastream/Dataflow migration, freeze/cutover, lag/error monitoring, and rollback orchestration.

migration-readiness.yaml
status: NOT_YET_CUTOVERfunctional_contract: PASS_OR_FAILsemantic_contract: PASS_OR_FAILsecurity_contract: PASS_OR_FAILperformance_contract: PASS_OR_FAILoperational_contract: PASS_OR_FAILcost_model_reviewed: falseunsupported_features: []preview_dependencies: []rollback_owner: "SET_OWNER"source_write_freeze_plan: "Chapter20"

11. Deliberately wrong port: URI swap with no compatibility suite

The anti-pattern changes MONGODB_URI, sees the homepage load, then declares migration complete. It misses rare BSON values, background jobs, admin scripts, transaction anomalies, unindexed scans, unsupported plugins, and recovery procedures. The repair is staged gates with exact evidence and a blocker ledger.

12. Final Chapter 19 verification checklist

  • Connection options and supported driver major are pinned.
  • BSON/_id/schema constraints are tested against real source data samples.
  • All required CRUD/query/aggregation/update operators have executable contracts.
  • Tools/ODMs have their own compatibility ledger.
  • Transactions/read concerns/write concerns and retry behavior match business invariants.
  • Indexes are derived from query inventory and Explain evidence.
  • Preview features have an explicit risk/fallback decision.
  • Security tests cover cross-tenant attempts.
  • Performance evidence reports distributions and target environment.
  • Migration blockers and rollback/exit path are documented.

13. Bridge to Chapter 20

Chapter 19 answered “Can AtlasMart's workload semantics run here?” Chapter 20 answers “How do we move the real data and traffic safely?” The compatibility suite becomes the acceptance harness for inventory, transformation, Datastream/Dataflow migration, cutover, validation, and rollback.

Knowledge check

  1. Why hide the driver behind a repository boundary?
  2. Should source and destination Explain plans be identical?
  3. Why are functional and semantic gates separate?
  4. What should happen to unsupported and Preview dependencies?
  5. What does Chapter 20 add?
Review the answers

1. It localizes destination-specific behavior and makes source/destination dual-run and rollback practical.

2. No. Compare business results and separately evaluate target performance/cost; the execution engines differ.

3. A query can return the expected row in a simple case while transaction, ordering, retry, or type semantics still violate business requirements.

4. They must appear explicitly in the blocker/risk report with an owner and mitigation/decision.

5. Actual migration planning/execution: source inventory, data movement, change capture, cutover, validation, lag/error monitoring, and rollback.

Summary and next step

This lesson established the working contract for Port a Small MongoDB Application and Build Compatibility Tests Instead of Assuming Drop-In Equivalence. Keep its edition/mode assumptions, trust boundary, verification evidence, and operational constraints explicit when reusing the pattern.

Next, continue to Inventory MongoDB Features, Drivers, Data Types, Indexes, Aggregations, Transactions, and Operational Dependencies.

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.