Firestore

Lightweight, dependency-free fake of Google Cloud Firestore (Native mode) that speaks the real Firestore v1 REST API (https://firestore.googleapis.com/v1), so application code using @google-cloud/firestore can run against it with zero cost and zero side effects.

KeyValue
Port4581
ProtocolFirestore v1 REST API (HTTP/1.1 + proto3 JSON)
Compatible client@google-cloud/firestore (v7) with preferRest: true
Size~90 KB
Startup< 100ms
StateIn-memory, ephemeral, resettable

Quick Start

Start the server:

import { FirestoreServer } from "./services/firestore/src/server.js";

const server = new FirestoreServer(4581);
await server.start();
// ... use it ...
await server.stop();

Connect with the real Firestore client. The client talks gRPC by default, so two things are required to route it to the parlel fake over plain HTTP:

  1. Set FIRESTORE_EMULATOR_HOST before constructing the client.
  2. Pass preferRest: true so the client uses its HTTP/1.1 REST transport.
process.env.FIRESTORE_EMULATOR_HOST = "127.0.0.1:4581";

import { Firestore, FieldValue } from "@google-cloud/firestore";

const db = new Firestore({
  projectId: "parlel",
  preferRest: true,
  // Any *valid* service-account key works — the fake never verifies the token,
  // but the client signs a JWT locally, so the private_key must be a real PEM.
  credentials: {
    client_email: "parlel@parlel.iam.gserviceaccount.com",
    private_key: PRIVATE_KEY_PEM, // e.g. from crypto.generateKeyPairSync("rsa", ...)
  },
});

// Write
await db.collection("users").doc("alice").set({ name: "Alice", age: 30 });

// Read
const snap = await db.collection("users").doc("alice").get();
console.log(snap.data()); // { name: "Alice", age: 30 }

// Query
const adults = await db.collection("users").where("age", ">=", 18).get();
console.log(adults.size);

// Atomic field update
await db.collection("users").doc("alice").update({ age: FieldValue.increment(1) });

The manifest sets FIRESTORE_EMULATOR_HOST=127.0.0.1:4581, GOOGLE_CLOUD_PROJECT=parlel and GCLOUD_PROJECT=parlel for you when the service is launched by the pool.

Why preferRest?

The real @google-cloud/firestore client speaks gRPC over HTTP/2 by default. The parlel fake is a pure-Node, zero-dependency HTTP/1.1 server, so it implements the Firestore v1 REST surface that the client's preferRest fallback uses. The two streaming-only RPCs (Write, Listen) require gRPC and are intentionally not supported.

Implemented operations / endpoints

All Firestore v1 RPCs are transcoded by the client to these REST endpoints:

Documents (CRUD)

RPCHTTPPath
GetDocumentGET/v1/{name=projects/*/databases/*/documents/*/**}
ListDocumentsGET/v1/{parent=.../documents}/{collectionId}
CreateDocumentPOST/v1/{parent=.../documents/**}/{collectionId}?documentId=
UpdateDocumentPATCH/v1/{document.name=.../documents/*/**}
DeleteDocumentDELETE/v1/{name=.../documents/*/**}

Reads & queries

RPCHTTPPath
BatchGetDocumentsPOST/v1/{database}/documents:batchGet
RunQueryPOST/v1/{parent}/documents:runQuery
RunAggregationQueryPOST/v1/{parent}/documents:runAggregationQuery
PartitionQueryPOST/v1/{parent}/documents:partitionQuery
ListCollectionIdsPOST/v1/{parent}/documents:listCollectionIds

Writes & transactions

RPCHTTPPath
BeginTransactionPOST/v1/{database}/documents:beginTransaction
CommitPOST/v1/{database}/documents:commit
RollbackPOST/v1/{database}/documents:rollback
BatchWritePOST/v1/{database}/documents:batchWrite

Internal (parlel-only, not part of Firestore)

HTTPPathPurpose
GET/_parlel/healthLiveness + document count
POST/_parlel/resetWipe all in-memory state
GET/_parlel/dumpDump raw stored documents (debugging)

High-level client features exercised

These map onto the RPCs above and are all covered by tests/firestore.test.ts:

Supported value types

All Firestore value types round-trip through proto3 JSON:

nullValue, booleanValue, integerValue, doubleValue, stringValue, bytesValue, timestampValue, geoPointValue, referenceValue, arrayValue, and mapValue (nested maps).

Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.

FeatureStatusNotes
Document CRUD (get/set/create/update/delete)✅ Supported
set merge / mergeFields✅ Supported
Field transforms (serverTimestamp, increment, min/max, arrayUnion/Remove, delete)✅ Supported
Structured queries (filters, order, limit, offset, cursors, projection)✅ Supported
Composite OR / AND filters✅ Supported
Aggregations (count, sum, avg)✅ Supported
Transactions (begin/commit/rollback)✅ SupportedBest-effort: no read-isolation or optimistic-retry enforcement
BatchGet / WriteBatch / BulkWriter✅ Supported
Subcollections & collection-group queries✅ SupportedallDescendants supported
PartitionQuery✅ SupportedAlways returns a single partition (no cursors)
ListDocuments / ListCollectionIds✅ SupportedName-ordered, pageSize/pageToken paging
Preconditions (exists, updateTime)✅ Supported
Write (streaming)⛔ UnsupportedgRPC-streaming only → 501 UNIMPLEMENTED
Listen (real-time snapshots)⛔ UnsupportedgRPC-streaming only → 501 UNIMPLEMENTED; onSnapshot is not available
FindNearest / vector search⛔ UnsupportedParsed but not executed
Security rules / auth enforcement⛔ UnsupportedAll requests are treated as admin
Indexes / index-error simulation⛔ UnsupportedAll queries run without index requirements
Persistence across restarts⛔ UnsupportedState is in-memory and ephemeral

Error codes / shapes

Errors use the standard Google error envelope:

{
  "error": {
    "code": 404,
    "message": "Document not found: projects/parlel/databases/(default)/documents/users/ghost",
    "status": "NOT_FOUND"
  }
}

The @google-cloud/firestore REST transport (google-gax) maps the body code to a canonical gRPC status, which surfaces as error.code on the thrown error:

ConditiongRPC codegRPC status
Document not found (get/update missing)5NOT_FOUND
Create on existing doc / failed precondition9FAILED_PRECONDITION
Invalid argument / malformed JSON3INVALID_ARGUMENT
Write / Listen streaming RPCs12UNIMPLEMENTED
Internal error13INTERNAL

Note: because the REST transport maps errors strictly by HTTP status, a create-conflict is surfaced as FAILED_PRECONDITION (non-retryable) rather than ALREADY_EXISTS — there is no HTTP status that the client decodes to ALREADY_EXISTS, and the retryable ABORTED mapping (HTTP 409) would cause the client's write-batch layer to retry. The operation still rejects, which is the behavior callers depend on.

Resetting state

// Programmatically
server.reset();

// Over HTTP
await fetch("http://127.0.0.1:4581/_parlel/reset", { method: "POST" });
<!-- parlel:testenv:start -->

Configuration — test.env

Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.

FIRESTORE_EMULATOR_HOST=parlel-bridge:4581
GOOGLE_CLOUD_PROJECT=parlel
GCLOUD_PROJECT=parlel
<!-- parlel:testenv:end -->