Cloud Firestore
Fireemu serves Cloud Firestore over gRPC, REST and the WebChannel transport the Web SDK uses, with Firestore Security Rules.
Connect
Section titled “Connect”Firestore listens on 127.0.0.1:8080 by default (--firestore-port or firebase.json changes it; see all default ports).
// Web SDKimport { getFirestore, connectFirestoreEmulator } from 'firebase/firestore';connectFirestoreEmulator(getFirestore(app), '127.0.0.1', 8080);The Admin SDK needs no code: under fireemu exec, FIRESTORE_EMULATOR_HOST is set to the Firestore host:port for your command (see Environment variables). With fireemu up, set it yourself, for example FIRESTORE_EMULATOR_HOST=127.0.0.1:8080. More SDKs and the project ID rules: Connect your app.
Editions and API modes
Section titled “Editions and API modes”Select the database edition and API mode in fireemu.toml. fireemu init writes the Standard edition with the Native API:
schemaVersion = 1
[firestore]# "standard" or "enterprise"edition = "standard"# "native" or "mongodb-compatible"apiMode = "native"{ "schemaVersion": 1, "firestore": { "edition": "standard", "apiMode": "native" }}edition accepts standard and enterprise. The Enterprise Pipeline API supports a limited subset of read operations; see Enterprise text search, Enterprise pipelines and Limitations.
Composite indexes
Section titled “Composite indexes”Production refuses a query whose composite index is not defined. The official emulator does not check composite indexes and runs every query, so a missing index first shows up when the query reaches production. Fireemu checks each query against the indexes declared in firestore.indexes.json (referenced from firebase.json), as production does.
firestore.missingIndexes |
Behavior |
|---|---|
refuse (default) |
The query is refused with FAILED_PRECONDITION and the console link, as production refuses it. |
report |
The query runs. Each missing index is reported once per database on standard error, with production’s refusal text and the firestore.indexes.json entry to add. |
The emulator profile always runs the query, as the official emulator does, while reporting missing-index suggestions on standard error.
An OR query can require a separate composite index for each branch. Supplementary diagnostics list all distinct missing branch indexes in one attempt. Under strict with refuse, the SDK still receives its existing FAILED_PRECONDITION response and first creation link; the additional branch definitions are on standard error. Under report, the control API report also collects the distinct missing definitions.
Fail a CI job on a missing index
Section titled “Fail a CI job on a missing index”The report on standard error is easy to miss in a CI log. Under strict with "report", the control API also lists every index that was missing, so a job can fail on a non-empty list. Turn the report on:
schemaVersion = 1# "strict" (the default) or "emulator"; "report" below needs strictprofile = "strict"
[firestore]# "refuse" (the default): refuse the query with FAILED_PRECONDITION# "report": run the query, and list each missing indexmissingIndexes = "report"{ "schemaVersion": 1, "profile": "strict", "firestore": { "missingIndexes": "report" }}fireemu exec gives its command FIREEMU_CONTROL_URL (ending in /v1/) and FIREEMU_CONTROL_TOKEN, so the script that runs your tests can check the list when they are done:
npx fireemu exec -- sh -c ' npm test status=$? curl -fsS -H "Authorization: Bearer $FIREEMU_CONTROL_TOKEN" \ "${FIREEMU_CONTROL_URL}firestore/missingIndexes" > missing-indexes.json || exit 1 [ "$(jq .count missing-indexes.json)" = 0 ] || { jq .indexesJson missing-indexes.json; exit 1; } exit $status'GET /v1/firestore/missingIndexes answers:
{ "count": 1, "truncated": false, "dropped": 0, "missingIndexes": [ { "project": "demo-app", "database": "(default)", "name": "projects/demo-app/databases/(default)", "index": { "collectionGroup": "notes", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "owner", "order": "ASCENDING" }, { "fieldPath": "created", "order": "ASCENDING" } ] }, "message": "The query requires an index. You can create it here: https://console.firebase.google.com/..." } ], "indexesJson": { "projects/demo-app/databases/(default)": { "indexes": [{ "collectionGroup": "notes", "queryScope": "COLLECTION", "fields": ["..."] }] } }}Response format
Section titled “Response format”- Each entry is one index per database, in the order it was first needed, with production’s refusal text.
indexesJsongroups the entries per database in the shapefirestore.indexes.jsonholds, so you can merge theindexesarray of a database into that file.
Sessions and clearing
Section titled “Sessions and clearing”GET /v1/sessions/{session}/firestore/missingIndexeslists only the session’s project;GET /v1/firestore/missingIndexeslists every project. A sessionresetdoes not clear the list, so a suite that resets between cases still reports every index it missed, and deleting a session clears its project’s entries.DELETEon either route clears the list and the dropped count and answers{"cleared": <count>}. The session route clears that session’s project only; the route without a session clears every session’s entries, so a test world that shares a daemon with others should use the session route. An index that is needed again afterwards is reported again.- With
fireemu/testing, callworld.control('GET', '/v1/sessions/default/firestore/missingIndexes')andworld.control('DELETE', ...)on the same path, so the read and the clear stay within your world’s project.
Limits and refusals
Section titled “Limits and refusals”- The list holds at most 1,000 entries. Past that,
truncatedistrueanddroppedcounts the queries served whose index is not listed. A project whose every index was dropped is answered with409and a message that the list is full, never with an empty list, so a check oncountcannot pass on an incomplete list. Clear the list and run again. - Where nothing is reported (the
emulatorprofile,strictwith"refuse", or Firestore not selected),GETanswers409withFAILED_PRECONDITION, so a check that depends on the list cannot pass on an empty answer.DELETEstill answers{"cleared": 0}. - A page in a browser needs
Authorization: Bearer <control token>for both methods, as for the resource report, because the entries name the fields your queries filter and order on.
Checks on firestore.indexes.json
Section titled “Checks on firestore.indexes.json”Under strict, Fireemu refuses at start-up (and keeps the previous indexes when the file changes while it runs) what firebase deploy refuses, and the message names the entry:
The message starts with the path of the file, as in ./firestore.indexes.json: indexes[0] (collection group notes): .... Each refusal below is shown with the entry that causes it and one that production accepts.
A composite index with a single field. Production answers “this index is not necessary, configure using single field index controls”. Declare it under fieldOverrides instead, which is also how a collection-group query on one field is enabled. (field, __name__ DESC), (__name__ DESC) and a single vector field are accepted, as production accepts them.
{ "indexes": [ { "collectionGroup": "notes", "queryScope": "COLLECTION", "fields": [{ "fieldPath": "owner", "order": "ASCENDING" }] }] }indexes[0] (collection group notes): this index is not necessary, configure using single field index controls{ "indexes": [], "fieldOverrides": [ { "collectionGroup": "notes", "fieldPath": "owner", "indexes": [{ "queryScope": "COLLECTION", "order": "ASCENDING" }] }] }An entry without a fields array, which is what a misspelled key such as feilds looks like.
{ "indexes": [ { "collectionGroup": "notes", "queryScope": "COLLECTION", "feilds": [{ "fieldPath": "owner", "order": "ASCENDING" }, { "fieldPath": "created", "order": "ASCENDING" }] }] }indexes[0] (collection group notes): Must contain "fields" (an array of index fields){ "indexes": [ { "collectionGroup": "notes", "queryScope": "COLLECTION", "fields": [{ "fieldPath": "owner", "order": "ASCENDING" }, { "fieldPath": "created", "order": "ASCENDING" }] }] }A fieldOverrides entry without an indexes array. A TTL-only override needs "indexes": [].
{ "fieldOverrides": [ { "collectionGroup": "notes", "fieldPath": "expireAt", "ttl": true }] }fieldOverrides[0] (notes.expireAt): Must contain "indexes" (an array, empty for a TTL-only override){ "fieldOverrides": [ { "collectionGroup": "notes", "fieldPath": "expireAt", "ttl": true, "indexes": [] }] }A fieldOverrides index that has only a vectorConfig. Each override index needs exactly one of order and arrayConfig. Declare a single vector field as a composite index instead, as production does.
{ "fieldOverrides": [ { "collectionGroup": "notes", "fieldPath": "emb", "indexes": [{ "queryScope": "COLLECTION", "vectorConfig": { "dimension": 3, "flat": {} } }] }] }fieldOverrides[0] (notes.emb): Must contain exactly one of "arrayConfig,order" in each of its indexes{ "indexes": [ { "collectionGroup": "notes", "queryScope": "COLLECTION", "fields": [{ "fieldPath": "emb", "vectorConfig": { "dimension": 3, "flat": {} } }] }] }A missing or empty queryScope is read as COLLECTION, and the older collectionId and field mode keys are read as collectionGroup and order / arrayConfig, as firebase deploy reads them. The limits of at most 200 composite indexes, 100 fields per index and one array field per index apply under both profiles. The Admin API’s indexes.create applies the same checks, and counts the indexes of the file and the ones created at run time together against the 200.
The emulator profile does not refuse these entries, because the official emulator never reads the file. It reads the composite field modes of an entry with the precedence and truthiness firebase deploy uses, and ignores the search configuration of a field override that also sets an order or array mode.
Query limits
Section titled “Query limits”Under the strict profile, the Standard edition query limits are enforced as production enforces them. Setting firestore.enforceLimits to false lets queries pass that production refuses, so leave it on unless you have a specific reason. Under strict the key switches only these query limits (at most 30 disjunctions, 10 not-in values, and so on): the limits on writes, transactions and indexes, and REST and WebChannel behavior, are unaffected by this switch. Under the emulator profile, true also asks for those production limits.
Listen streams
Section titled “Listen streams”Production ends an end user’s native gRPC Listen stream with INTERNAL an hour after it opened. By default Fireemu keeps the stream open however long it is held (firestore.listenStreamLifetime: "unlimited"). To test a client’s reconnect, either set it to "oneHour" (the default with productionExact), or call POST /v1/firestore/listen:expire on the control API to end every held end-user stream at once with production’s status.
A WebChannel Listen or Write opening refused for credentials or App Check completes HTTP transport negotiation with 200 and then delivers a terminal UNAUTHENTICATED or PERMISSION_DENIED protocol error that the Web SDK can read. An invalid or different-app App Check replacement receives only its terminal error, without replaying retained documents. Refused openings do not consume admitted-session capacity. This transport behavior follows the SDK protocol; it is not a recorded production-parity claim.
Transactions and history
Section titled “Transactions and history”Like production, Fireemu keeps every version of a document written in the last hour, so a read_time read, a read-only transaction or a resumed listener can see it. Transactions lock what they read, as production does. Contended gRPC and WebChannel writes wait without occupying runtime workers. Canceling a waiting Commit ends that request’s deadlock wait; its transaction keeps read locks until it commits, rolls back or expires.
A test that updates the same documents many times therefore holds many versions in memory. These local limits, which production does not have, bound it:
- Each database retains at most 1 GiB of logical history bytes and 1,000,000 versions, each session 1 GiB and 1,000,000 versions, and the daemon 4 GiB and 4,000,000 versions. Logical bytes follow Firestore’s storage size model and are not the process’s memory, which can be several times larger.
- On a pinned clock (
daemon.clockStart), time does not pass on its own, so a database keeps at most 1,024 versions of any one document. This limit works likemaxVersionsPerPathbelow: once a document goes past it, older snapshots are released for the whole database. - Strict Listen retains the latest 4,096 boundary records per live database. A resumed stream echoes the original boundary’s
read_time, then sends the new snapshot time withCURRENT. Token bytes are opaque and may change without a write. An expired record or expired document history triggersRESETand a full replay; reset, restore and daemon restart discard the records. This bound is local to Fireemu and is not a production limit. - A commit that would cross a limit is refused whole with
RESOURCE_EXHAUSTEDand the messageFirestore retained history capacity is exhausted.
Versions older than an hour are released by the next commit after the clock passes them. To use less memory, lower the limits in firestore.history:
schemaVersion = 1
[firestore]edition = "standard"apiMode = "native"
[firestore.history]maxVersionsPerPath = 16maxBytes = 268435456{ "schemaVersion": 1, "firestore": { "edition": "standard", "apiMode": "native", "history": { "maxVersionsPerPath": 16, "maxBytes": 268435456 } }}maxVersionsPerPath(1 to 1,000,000) sets the most versions of one document a database keeps, on either clock, in place of the defaults above.maxBytes(1 MiB to 1 GiB) lowers the logical history bytes one database retains. An import (--import) is not checked against it: a larger import starts with awarning:line naming the database, its bytes and the limit (a snapshot restore above it logs the same line), and every later write that would grow the history is refused withRESOURCE_EXHAUSTED.
Without these keys the defaults apply.
The version cap applies to each document separately, but its effect reaches the whole database. Snapshots stay exact for every document, so once any document has more versions than the cap, the database keeps no snapshot older than that document’s oldest kept version. After that:
- A
read_timeread older than that point is refused withFAILED_PRECONDITION(The requested 'read_time' is no longer retained by this database.). So is a new read-only transaction at such aread_time(read_time is no longer retained by this database). - A transaction already open on an older snapshot fails its next read or commit with
ABORTED(The referenced transaction has expired or is no longer valid.), even if it only touches other documents. - A listener resuming from an older point is reset and sent the full result again.
Client SDKs retry ABORTED transactions, so avoid very small caps: choose a cap above the number of times any document is updated while your longest transaction is open.
fireemu doctor --connect <control URL> reports the retained versions and bytes of a running daemon: its history.* lines give the retained versions and logical bytes against the session and daemon limits, and how many bytes the next compaction would release.
Security Rules
Section titled “Security Rules”Rules are read from the firestore.rules file named in firebase.json, or from rules.source in fireemu.toml. Under strict, a project without rules refuses every client request, as a production project without a rules release does. See Security Rules.
Named databases
Section titled “Named databases”Declare each named database in the firestore array of firebase.json, with the rules and indexes files it uses, or create it through the Admin API. Under strict a request to a database that is neither declared nor created gets NOT_FOUND, as in production, and a declared database id must be one production could create (4 to 63 characters of [a-z0-9-], starting with a letter), or Fireemu does not start.
Production governs a database only by its own rules release. So a database created at run time has no Security Rules, and refuses every client request, until you set some for it; the (default) database’s rules do not apply to it. Declare the database in firebase.json with its rules file, or load them at run time:
PUT /emulator/v1/projects/{project}/databases/{database}:securityRules{"rules": {"files": [{"name": "firestore.rules", "content": "..."}]}}This is the route PUT /emulator/v1/projects/{project}:securityRules uses for the (default) database, with the same body and answers. The rules set this way last until the database is deleted; they are not part of a snapshot. The emulator profile keeps the official emulator’s behavior: a database that has no rules of its own is served by the (default) database’s rules (rules loaded for it with the route above apply to it).
Enterprise text search
Section titled “Enterprise text search”Supported text definitions in firestore.indexes.json (searchConfig.textSpec) enter the local text catalog on Enterprise databases and serve collection(...).search({ query: documentMatches(...) }). Search indexes remain separate from ordinary query planning. Standard edition still accepts valid search-index configuration without serving search, and geo search is unsupported.
The local fireemu-lexical-v1 engine uses deterministic Unicode normalization and word segmentation, AND terms, positional phrases, exclusions and indexed-field restrictions. Creation-time ordering is supported, and score ordering follows the recorded production result orders; numeric score() values are approximations. There is no stemming, synonym expansion, typo correction or language autodetection. productionExact refuses search until corresponding production behavior is recorded. Explicit Fireemu control actions advance, complete, fail and repair a generation-scoped build. See the local text search guide for setup, controls and exact limits.
Enterprise pipelines
Section titled “Enterprise pipelines”Enterprise pipelines are verified against production for the recorded corpus: 1,951 recorded cases, of which 1,906 match, 17 match within production’s own variation (response framing and random sample positions), 26 differ only in a way the project owner accepted (the order of aggregate groups, the legal selection of groups under a limit or offset after unsorted groups, the order of rows tied on every sort key, and a value that depends on the clock) and 2 gRPC requests the recorder could not send are covered by a later observation. Shapes that were not recorded are not claimed, and the strict profile refuses many of them as unobserved; the Production verification table lists the not-claimed items.
On an Enterprise database, fireemu runs pipelines from the Admin SDK (gRPC) and the Web SDK’s firebase/firestore/lite/pipelines (REST :executePipeline). The supported subset starts with a collection, collection_group, documents or finite database input, followed by where, select, sort, add_fields, remove_fields, distinct, aggregate, offset, limit and unnest stages in any order and number. Aggregates include count, count_distinct, sum, average, minimum and maximum, with or without groups.
| Expression | Supported |
|---|---|
| Fields | Nested paths (a.b) and __name__ (the document reference) |
| Literals | Values of every type, including arrays and maps of literals and vectors of doubles |
| Comparisons | equal, not_equal, less_than, less_than_or_equal, greater_than and greater_than_or_equal, anywhere an expression stands |
| Construction | array(...) and map(...) |
| Array access | array_length, array_first, array_last, array_get, array_slice, array_first_n, array_last_n, array_index_of, array_index_of_all, join, maximum_n and minimum_n |
| Array helpers | array_concat and array_reverse |
| Timestamp functions | current_timestamp, timestamp_add, timestamp_subtract, timestamp_trunc, timestamp_diff, timestamp_extract, unix_micros_to_timestamp, unix_millis_to_timestamp, unix_seconds_to_timestamp, timestamp_to_unix_micros, timestamp_to_unix_millis and timestamp_to_unix_seconds |
| Map functions | map_get, map_set, map_remove, map_merge, map_keys, map_values and map_entries |
| Regular expressions | regex_contains, regex_match, regex_find and regex_find_all |
| Document functions | documentId, collectionId and parent |
| Logic, existence, type and membership | and, or, not, xor, nor, exists, is_absent, is_error, type, is_type, array_contains, array_contains_all, array_contains_any, equal_any and not_equal_any |
| String | byte_length, string_index_of, string_repeat, string_replace_all, string_replace_one, string_reverse, concat, to_lower, to_upper, starts_with, ends_with, string_contains, char_length, substring, trim, ltrim, rtrim and split |
| Vector scalars | vector_length, dot_product, euclidean_distance and cosine_distance |
| Lazy control | if_absent, if_null, if_error, conditional and switch_on |
| Math | add, subtract, multiply, divide, mod, abs, round, floor, ceil and pow |
More recorded forms
Section titled “More recorded forms”The recorded empty array() and map() constructors return empty values. Other constructor shapes remain outside these observations.
Additional recorded forms include like, rand, array_filter and array_transform. Input types, lambda placement and combinations remain bounded. Random values do not reproduce production’s random sequence.
Recorded scalar forms of minimum, maximum, sum and negate are also supported, including captured integer, finite-double, mixed numeric and null/absent cases. The recorded sum([]) returns null. Other types, non-finite inputs and numerical extremes remain outside this finite compatibility claim.
Additional recorded generic forms include cmp, storage_size, coalesce, get_field, offset, reverse, length, string_concat, namespace, has_ancestor, reference_slice, replace_document_id and autogen_document_id. Each observation covers its declared input types: for example, storage_size currently supports strings, while recorded reverse forms cover strings, bytes and arrays. Generated document IDs follow the finite 20-character ASCII-alphanumeric contract; these observations do not establish entropy or global uniqueness.
The array, map, regular-expression and timestamp functions above, unnest and the zero-count behavior are available in both profiles. The production comparison covers the recorded cases, not every input type or combination. map_set retains an explicit null value and removes a key whose value is absent; a null or absent input map yields an absent result, while the other listed map functions return null for that input; that removal follows the documented API and has not been observed in production. Map keys, values and entries use Fireemu’s deterministic map order, and corresponding keys and values stay paired. The recorded production map agrees for its two keys; production ordering for other maps is unverified.
The emulator profile also evaluates sqrt, exp, ln, log, log10 and trunc with omitted or zero precision. Nonzero truncation precision and finite exponential subnormal outputs are unobserved. Strict admits finite recorded numeric and error shapes in their supported contexts; unrecorded numeric, domain, null, type and precedence cases remain gated. These functions do not establish general production parity.
The string functions operate on their supported shapes; strict refuses unrecorded types and non-ASCII search offsets, and repeat and replacement apply a local output-size bound. Strict array_index_of_all accepts the observed integer forms. Vector scalar functions refuse unrecorded, empty, mismatched or non-finite binary vectors and unsupported numerical extremes; these bounds do not establish general production compatibility.
The additional array functions and unnest cover the recorded input shapes; unrecorded combinations and ordering remain unverified. Strict Firestore REST accepts the recorded standard and URL-safe base64 forms and the protobuf bytes_value alias, and rejects recorded CR/LF and mixed-alphabet forms. The ordinary emulator profile retains permissive decoding for development; its extra accepted forms are not a production compatibility claim.
Behavior details
Section titled “Behavior details”Pipeline timestamps use microsecond precision. current_timestamp samples the configured clock once per request and stays fixed throughout that pipeline evaluation. Zone-aware truncation and extraction use named IANA zones; the production recordings cover only specific zone cases. Ambiguous or nonexistent local daylight-saving boundaries are refused as unobserved, and custom GMT offset semantics are not implemented. Other time zones, boundaries and input combinations are not established as production-compatible by those recordings.
The regex functions follow the production recordings of 2026-11-10 in both profiles. \d, \D, \w, \W, \b and \B are ASCII only; \s is the ASCII whitespace characters plus the vertical tab and does not match a no-break space; \pL and \p{Nd} are Unicode; (?i) folds ASCII letters only; (?u) and (?-u) are accepted and change nothing; (?U) does not make a quantifier lazy. POSIX classes such as [[:digit:]] and the (?m) flag are refused with Unsupported regex syntax., as in production. regex_find and regex_find_all accept at most one capture group and refuse more with At most one capture group is supported.; an unmatched group gives null. regex_contains and regex_match accept any number of groups. regex_match requires a full-string match. The inputs under strict that no recording covers (brace repetitions, other flags and flag combinations, \A and \z, escapes such as \n, other property names, nested classes and class set operators, (?i) or (?U) with a class) are refused as unobserved; the emulator profile evaluates them with the local engine, whose result for them is not established as production parity. The members of \s other than space, tab and the vertical tab (line feed, form feed, carriage return) are assumed, not recorded. Production recorded no pattern-length limit; locally a pattern longer than 128 KiB, or one whose ASCII rewrite is longer than 1 MiB, is refused as unobserved in both profiles, as is a pattern that exceeds the engine’s compiled-size limit.
A limit of zero passes no rows to the next stage and an offset of zero passes all rows through, in both profiles. The strict profile follows the recorded production cases for these inputs.
The strict profile reproduces production’s recorded INTERNAL response for array_reverse() with no arguments and empty options, and for a duplicate select alias when one spelling is quoted and the other is unquoted. Fireemu writes a fixed explanation to standard error without request values. A zero-argument array_reverse() with nonempty options keeps its prior refusal, and the emulator profile retains its prior validation behavior. These errors cover the recorded forms only; adjust the call or alias to proceed.
Strict native gRPC also reproduces the recorded INTERNAL errors for finite integer forms of bit_not, bit_left_shift and bit_right_shift. The console explains this behavior without including request values. These observations establish error compatibility for the declared native request shapes; successful bit-operation semantics and REST error parity remain unverified. The emulator profile retains its prior refusal.
Additional recorded validation-priority cases match, and structured ErrorInfo is preserved for the captured ordered REST collection_id type error. Other precedence combinations and ambiguous native map order remain gated. The captured native gRPC geo_distance select call with two valid geo points returns its search-only INVALID_ARGUMENT diagnostic; geo search remains outside this pipeline compatibility claim. In both profiles, the recorded nested aggregate scalar sum call on an integer returns INVALID_ARGUMENT with the production array_sum diagnostic: expected Array, got LONG. Supply an array to that scalar call; other argument shapes retain their existing gates.
A where keeps a row only when its condition (a comparison, a field or another supported function) is true. A select sets at most 64 fields, as in production. Results without a select carry the document’s name, create time and update time, and answers normally carry the execution time; a missing-only documents input retains its selected execution time. A small answer comes as production frames it: all results in one response message over gRPC, or in one array element over REST. Fireemu cuts a larger answer at about 1 MiB of results; production recordings show variable boundaries; large-answer framing is not verified. select({__name__: field("__name__")}) answers the document name, including the recorded forms beside other aliases or with quoted aliases and values; unrecorded forms remain UNIMPLEMENTED under strict. Without sort, recorded collection reads follow ascending CRC-32 of document IDs; collection groups use parent path then that order. Non-ASCII IDs and CRC-32 ties are unverified, as are aggregate group order and ordering of ties under a limit. A stored -0.0 reads back as -0.0, as in production; the official emulator stores it as 0.0.
Differences between profiles
Section titled “Differences between profiles”The recorded matching answers cover specific request shapes from the FS-ENT-ORACLE observations, including comparisons, literal values, select aliases, document functions, stage ordering and the supported string and math functions. Other recorded and unrecorded shapes remain unverified. Under strict, a refusal is generally answered only when the request has the shape of a request production refused, with that request’s text, and is UNIMPLEMENTED otherwise; the gRPC depth exceptions described below also apply. A shape keeps the stages, function names, value kinds and every map’s entries in order, and replaces only plain field names and values of the same kind, so select({total: documentId(field)}) is answered like the recorded select({v: documentId(field)}). A REST body keeps its key order; over gRPC a map of two or more entries has no order, so strict generally answers no refusal of such a request there, except for the recorded gRPC select-width case described below. Where production has not been observed, the profiles differ:
| Input | strict |
emulator |
|---|---|---|
| A document function of a collection reference with a non-ASCII name | UNIMPLEMENTED |
INVALID_ARGUMENT, the index counted in UTF-16 units |
| Recorded reserved map-key refusals | INVALID_ARGUMENT for the recorded shapes; unrecorded shapes remain gated |
INVALID_ARGUMENT, naming a reserved key first |
| Recorded document-function wrong-type requests | INVALID_ARGUMENT for the recorded REST shapes; unordered gRPC vector maps remain UNIMPLEMENTED |
INVALID_ARGUMENT, naming the type |
A map(...) key that is a double, null or another type than an integer or boolean |
UNIMPLEMENTED |
INVALID_ARGUMENT, naming the type |
A vector literal with integer or NaN elements, no element, without value or with another key |
UNIMPLEMENTED |
INVALID_ARGUMENT, the official text |
| A vector literal with an infinite element | UNIMPLEMENTED |
The vector |
A __type__ key that is not a string, or a reference literal that names no document |
UNIMPLEMENTED |
INVALID_ARGUMENT, the official text |
| Two refusals in one pipeline | UNIMPLEMENTED |
INVALID_ARGUMENT, one of them |
A refusal production recorded only in another stage, such as a literal holding a field inside a where condition |
UNIMPLEMENTED |
INVALID_ARGUMENT, the official text |
| A vector literal with a negative-zero element | UNIMPLEMENTED |
The vector |
Recorded empty or reserved literal-key refusals, including __name__ and multi-key cases |
INVALID_ARGUMENT over REST and for unambiguous gRPC; three unordered multi-key gRPC cases remain UNIMPLEMENTED |
INVALID_ARGUMENT, naming one key |
Recorded wrong-type limit and offset arguments and a negative offset |
INVALID_ARGUMENT for the recorded shapes; other shapes remain gated |
INVALID_ARGUMENT, the official text |
A limit of 0 or an offset of 0 |
The recorded zero-count behavior | The same zero-count behavior |
A select alias inside another in another order than production’s {m, m.r}, such as {a.b, a} |
UNIMPLEMENTED |
INVALID_ARGUMENT, naming the later alias over REST (as the official emulator does) and the child over gRPC |
An unrecorded select with two fields that both fail when evaluated, such as {a: documentId("s"), b: parent(true)} |
UNIMPLEMENTED |
INVALID_ARGUMENT, the first in key order |
A vector inside an array, or a __type__ key beside other keys in a literal |
UNIMPLEMENTED |
The official answer |
| An unrecorded wrong argument count for a supported function | UNIMPLEMENTED |
INVALID_ARGUMENT, fireemu’s text |
| Function options | The recorded single-integer x option on document_id(__name__) is accepted in its two-stage projection form; other options remain gated |
Ignored, as the official emulator ignores them |
An empty select map or a later stage’s check after a function or stage value fireemu does not check (production names an unknown function first) |
UNIMPLEMENTED |
INVALID_ARGUMENT, the empty map or the later check |
A regex construct no recording covers, such as a{2}, \n, (?x) or (?i)[a-c] |
UNIMPLEMENTED |
The local engine’s result |
An empty map or another refusal beside a function call that fails on its constants, such as map(1.5, i), or beside any other part strict does not answer |
UNIMPLEMENTED |
INVALID_ARGUMENT, the empty map or the other refusal |
| An unrecorded stage argument count or options | UNIMPLEMENTED |
Depends on the stage and argument shape |
An empty array() or map() |
The recorded empty array or map | An empty array or map |
An aggregate without its groups argument |
INVALID_ARGUMENT with production’s arity text for the recorded shape (directly after the collection, a non-empty map of plain aliases to count), else UNIMPLEMENTED |
INVALID_ARGUMENT with the same text |
Unions, sampling, subqueries and other stages
Section titled “Unions, sampling, subqueries and other stages”Finite union branches can use collection inputs with row-local where and select; unsupported child stages and inputs are refused. Source reads share a snapshot, and nested branches have local depth and size limits. sample(size, mode) supports documents sampling without replacement and percent sampling in input order: for example, sample(0.5, "percent") uses a 0.5 probability per row, while sample(2, "documents") selects up to two rows. Random results vary and do not reproduce production’s random sequence.
Finite replace_with supports full replacement and the implemented overwrite and keep merge modes; full replacement drops source document metadata. Finite find_nearest supports Euclidean, cosine and dot-product measures with the locally required limit and distance-field options. Other shapes remain unverified or refused. Both the gRPC and REST routes serve finite documents, collection and collection_group inputs with a read time, an existing transaction or a new transaction, and auto-commit of an existing or new transaction, in the recorded shapes. A read time with auto-commit, and database, union or search inputs with a selector remain refused. Unsupported execution shapes are explicitly refused. Recorded validation failures can return INVALID_ARGUMENT without making a suggested function executable. Recorded matches establish compatibility only for the recorded request shapes; other shapes remain unverified.
Finite recorded forms of array_agg, array_agg_distinct, first, last and count_if are supported. Bounded subcollection and correlated scalar subquery forms share a source snapshot; unsupported inputs, stages and bindings are refused, and local resource limits apply. Aggregate ordering and unrecorded forms are not claimed as production parity.
Ordinary REST reports the recorded depth diagnostic for deeply nested pipeline JSON, and strict gRPC admits the recorded 65-field select refusal. gRPC function expressions over 32 levels are refused in both profiles; strict gRPC also maps protobuf recursion failures to the recorded invalid-argument diagnostic. The gRPC limits and diagnostics generalize beyond the observed chains; error precedence and other unrecorded cases remain unverified.
Selecting a missing nested alias preserves its empty parent maps. Production recorded three dotted-alias cases; other missing-expression shapes are generalized from those observations.
A supported correlated scalar subquery may apply an additional string-equality filter before checking cardinality. The recorded single-result request returns the selected value, and the recorded multiple-result request receives the production INVALID_ARGUMENT diagnostic. These observations cover only those recorded request shapes; zero-result behavior and other scalar forms remain unverified.
Production verification
Section titled “Production verification”Writes and limits, queries and indexes, transactions, real-time listeners, Security Rules, database administration, Enterprise full-text search and Enterprise pipelines are verified against recordings from real Firestore, for the recorded cases of each area. See the compatibility tables.