FS-GW-1 | Implemented | Notes (6)- under the emulator profile, queries whose composite index is not configured are served with an FS_EMULATOR_INDEX_ASSUMED warning, like the Firebase Emulator Suite; under strict (the default) a query production refuses for its missing composite index is refused with FAILED_PRECONDITION and the console link, as production does, unless firestore.missingIndexes is "report", which runs it with an FS_MISSING_INDEX_REPORTED warning and reports it once per database and index with production's refusal text and the firestore.indexes.json fragment; the index merges production performs (equality filters served by several composite indexes that share the order suffix, verified against a real project on 2026-09-08) are accepted
- the profile decides whether composite indexes are assumed (emulator) or checked with production's rules (strict); under strict, firestore.missingIndexes decides whether a missing one is refused (the default) or run and reported (an explicit opt-out)
- the indexes reported under firestore.missingIndexes "report" are listed by GET /v1/firestore/missingIndexes on the control API (GET /v1/sessions/{session}/firestore/missingIndexes for one project; DELETE on either clears them, and a session reset keeps them): each entry carries its project, database, the index in the firestore.indexes.json shape and production's refusal text, indexesJson groups them per database, truncated and dropped say when the 1000-entry list overflowed (a project that lost all its indexes is refused with 409, never answered as empty), deleting a session clears its entries, and the read answers 409 where nothing is reported (the emulator profile, strict with "refuse", or Firestore not selected)
- firestore.enforceLimits follows the profile with an explicit override: under strict a Standard query limit violation refuses the query with INVALID_ARGUMENT, under emulator it is reported as an FS_LIMIT_OBSERVED:<limit id> warning and the query runs
- conformance: firestore/missing-composite-index (conformance/README.md); this records agreement with the official Local Emulator Suite, never with production
- the query limits the pinned official emulator refuses as well -- more than 30 DNF disjunctions ('IN' over 30 values) and a second array-contains clause (FAILED_PRECONDITION) -- stay refused under the emulator profile (fireemu_adapter_grpc::gateway::OFFICIAL_EMULATOR_REFUSES)
|
FS-LIM-1 | Partial | Implemented (25)- FS-LIMIT-DOCUMENT-BYTES
- FS-LIMIT-NESTED-MAP-ARRAY-DEPTH
- FS-LIMIT-FIELD-TRANSFORMS-PER-DOCUMENT
- FS-LIMIT-COLLECTION-ID
- FS-LIMIT-DOCUMENT-ID
- FS-LIMIT-SUBCOLLECTION-DEPTH
- FS-LIMIT-DOCUMENT-NAME-BYTES
- FS-LIMIT-FIELD-NAME
- FS-LIMIT-FIELD-PATH-BYTES
- FS-LIMIT-FIELD-VALUE-BYTES
- FS-LIMIT-INDEXED-FIELD-VALUE-BYTES
- FS-LIMIT-API-REQUEST-BYTES
- FS-LIMIT-INDEX-ENTRIES-PER-DOCUMENT
- FS-LIMIT-INDEX-ENTRY-BYTES
- FS-LIMIT-INDEX-ENTRY-SUM-PER-DOCUMENT
- FS-LIMIT-FIELDS-PER-COMPOSITE-INDEX
- FS-LIMIT-TRANSACTION-TOTAL-TIME
- FS-LIMIT-TRANSACTION-IDLE-TIME
- FS-QUERY-LIMIT-DNF-DISJUNCTIONS
- FS-QUERY-LIMIT-ARRAY-CONTAINS-PER-DISJUNCTION
- FS-QUERY-LIMIT-ARRAY-CONTAINS-COMBINATION
- FS-QUERY-LIMIT-NOT-IN-VALUES
- FS-QUERY-LIMIT-NOT-IN-NEQ-COMBINATION
- FS-QUERY-LIMIT-INEQUALITY-FIELDS
- FS-QUERY-LIMIT-COMPONENTS
Notes (4)- this list is derived from the runtime, not maintained beside it: fireemu_core_firestore::limits names every enforced limit, tests/enforced_limits.rs fails when it and the catalogs' implemented sets differ in either direction, and the daemon test the_limit_metadata_agrees_with_the_enforcement holds this entry and /v1/limits to the same set
- the Standard document and transaction limits are refused under both profiles; the transforms-per-document budget and the query limits are production-only refusals that the emulator profile observes as warnings instead (the budget through the profile's LimitScope, the query limits through firestore.enforceLimits, which under strict switches only the query limits), except the two query limits the official emulator refuses as well (more than 30 disjunctions, a second array-contains), which stay refused under both
- the write-path byte limits: FS-LIMIT-API-REQUEST-BYTES is a transport bound applied before decode on all three protocols (REST and WebChannel bodies through a bounded reader, gRPC unary and streamed messages by tonic). Its boundary is identical under both profiles and only its shape differs: the strict profile answers one documented production shape everywhere, HTTP 400 INVALID_ARGUMENT with the message Request payload size exceeds the limit: 10485760 bytes., while the emulator profile keeps the HTTP 413 request-body-too-large answer and tonic OUT_OF_RANGE the local runtime has always answered. Neither is a production observation; FS-LIMIT-FIELD-PATH-BYTES bounds every canonical field path, client-named or implied by nesting, and is checked after the document charge so an oversized document still reports its size; FS-LIMIT-INDEXED-FIELD-VALUE-BYTES is a truncating maximum charged on index entries and refuses nothing. The request and indexed-value bounds are identical under both profiles, and so is the path bound except for a path implied through an array element, which automatic index accounting never reached and which is therefore refused in the strict profile only
- FS-LIMIT-FIELD-VALUE-BYTES is split: a string or bytes payload is measured on its raw bytes, the metric observed against production on 2026-09-07, and refused under both profiles; a map or array value is measured with the storage-size formula and refused only in the strict profile, because production has not been observed on an aggregate value and the contract forbids adding a refusal to the emulator profile. Documented, production observation pending
|
FS-LSN-1 | Implemented | Implemented (8)- query and document targets
- live diffs after every commit
- once
- resume from a resume token or read time: the state at that version is recomputed from the retained MVCC history and only the changes since are replayed, followed by an ExistenceFilter with the current count (an undecodable or future token RESETs)
- strict resume tokens retain their exact issued boundary read_time in the latest 4096 records per live database; missing issuance records or compacted versions RESET and fully replay, never resume against unrelated history; reset/restore/restart discard records; emulator tokens keep version-based retention
- back-pressure: bounded response channel, commits coalesced into one refresh while the client is slow, 1000 targets per stream (RESOURCE_EXHAUSTED beyond)
- a target refused on its own merits (missing composite index, malformed or out-of-database query, rules denial) is answered with TargetChange REMOVE carrying that cause and the stream keeps serving its other targets; stream-level errors are reserved for session-wide or database-wide failures
- an end user's native gRPC Listen stream ends with INTERNAL an hour after it opened, as production ends it, only with productionExact or firestore.listenStreamLifetime "oneHour" (strict profile); by default it stays open, and POST /v1/firestore/listen:expire on the control API ends every held end-user stream at once with that status, answering {"expired": n}, so a client's reconnect can be tested without waiting an hour
Notes (2)- existence filters carry the count only (no bloom filter): the replay is exact, so the client never has to guess
- conformance: firestore/listen-sequence-with-resume (conformance/README.md); this records agreement with the official Local Emulator Suite, never with production
|
FS-PIPE-RPC-1 | Partial | Implemented (18)- StructuredPipeline wire decoding
- database name, consistency selector (non-empty transaction; auto_commit_transaction only with new_transaction) and pipeline option (index_mode) checks
- stage identification against the documented registry (collection, collection_group, database, documents, select, add_fields, remove_fields, where, sort, limit, offset, distinct, aggregate, find_nearest, sample, union, unnest, replace_with)
- input stage placement, arity, argument shape (paths, integers, boolean expressions, ascending / descending orderings, alias maps, vectors and distance measures, modes) and per-stage option checks (find_nearest limit / distance_field, sample mode, unnest index_field)
- write stages (update, delete) refused as FS-PIPE-WRITE-0
- unknown stages refused (FS_PIPE_UNSUPPORTED_STAGE), never skipped
- local latest-read collection input (root or nested), optional single where(condition), optional select aliases of expressions, optional offset, then optional limit; no repeated or reordered stages. limit and offset take any non-negative integer, and a string argument and a negative limit are INVALID_ARGUMENT with the official texts ("The 'limit(...)' cannot have a negative value: -1", "Expected limit to be INTEGER_VALUE, but was STRING_VALUE."), as FS-ENT-ORACLE O-4 (production, Enterprise, 2026-10-20) answered under both profiles; another type and a negative offset get the official emulator's texts under the emulator profile and UNIMPLEMENTED under strict (unrecorded); the argument checks of add_fields, distinct and aggregate (the one-argument arity, aliases, empty maps) are answered before such a stage is refused as unsupported locally, a stage's own checks before an empty map; under strict only in the orders production recorded: an empty map or a later stage's check after a function or stage value fireemu does not check (production names a nonexistent function first, O-4 o4/stages#select-empty-then-unknown-function) is UNIMPLEMENTED, and an aggregate accumulator must be count of at most one field
- local execution runs on the pipeline executor (three-valued evaluation: value, absent, error) over rows read at one read version; functions come from a static table and every other function name is refused (FS_PIPE_UNSUPPORTED_STAGE), never ignored
- operands: field references including nested paths (a.b; a path through a missing field or a non-map is absent, and an absent select value leaves its alias out), __name__ (the document reference, as FS-ENT-ORACLE O-1 (production, Enterprise, 2026-10-19) answers it), literals (as a select value too: values of every kind, vectors of doubles in their map form, nested arrays and maps, and map literals keyed by any name but an empty one ("Property names cannot be empty.") or a reserved one (__type__ aside), as FS-ENT-ORACLE O-1 to O-4 (production, Enterprise, 2026-10-19 and 2026-10-20) returned them, under both profiles; a vector with integer or NaN elements, no element, without value or with another key, a literal with more than one empty or reserved key and the key __name__ are UNIMPLEMENTED under strict, unrecorded; the emulator profile refuses those vectors with the official emulator's texts before any other check of the literal; function options are ignored under the emulator profile, as the official emulator ignores them, and UNIMPLEMENTED under strict) and calls of the table's functions; document_id / collection_id / parent take a document reference (__name__, a reference field or a reference literal)
- select aliases are field paths under both profiles, as FS-ENT-ORACLE O-1, O-3 and O-4 (production, Enterprise, 2026-10-19 and 2026-10-20) and the official emulator answer them alike: a dotted alias nests, a quoted one unquotes, a reserved name below another field (a.__x__, a.__name__) is a nested key, and an empty alias, an invalid path (plain, quoted or dotted, of any length), a reserved first segment (__x__, __name__, quoted or not, with or without a child) or an alias inside another is INVALID_ARGUMENT FS_PIPE_INVALID with production's recorded text (an alias inside another with its ErrorInfo, reason OVERLAPPING_PROPERTY_PATHS); an alias whose segments, unquoted and joined with '.', reach 1500 UTF-8 bytes, whatever the characters, is INVALID_ARGUMENT with production's text and ErrorInfo (reason PROPERTY_PATH_TOO_LONG); a repeated path keeps fireemu's text; a select stage sets at most 64 fields, and 65 or more are INVALID_ARGUMENT FS_PIPE_INVALID with production's text ("The 'select(...)' stage is limited to 64 fields but this stage has 65.", FS-ENT-ORACLE O-2, production, Enterprise, 2026-10-20), and an empty select is INVALID_ARGUMENT ("The 'select(...)' stage requires at least one field.")
- a field equality is applied by the collection scan before documents are copied (the executor re-checks it), without index validation: Enterprise pipelines need no index, so an equality is answered without a single-field index or with an exempted one; this is Enterprise behavior pending production observation
- a where condition is a comparison, a field, another table function (document_id, collection_id, parent, array, map) or a literal: production keeps the row only when it is true and drops it for every other value, constant or missing one (FS-ENT-ORACLE O-1 to O-4 (production, Enterprise, 2026-10-19 and 2026-10-20), conditions and constants of every kind), under both profiles; filtering precedes projection, offset and limit
- comparisons equal, not_equal, less_than, less_than_or_equal, greater_than and greater_than_or_equal stand anywhere an expression stands (a where condition, a select value, a function argument), with fields and literals on either side, and follow the official emulator's order (firebase-tools 15.28.2) under both profiles: two absent operands are equal, one absent operand or operands of different kinds (NaN its own kind) are incomparable (not_equal true, the others false), and values of one kind follow the canonical order (vectors by length first, then element by element, as O-4 recorded with vectors of different lengths). FS-ENT-ORACLE O-1 to O-3 (production, Enterprise, 2026-10-19 and 2026-10-20) recorded production answering every probe the same way: every ordered pair of kinds, vectors included (each kind with itself, NaN and null included), and a missing field against a missing field and every kind, under all six comparisons, integers and doubles compared exactly as one kind (1 equals 1.0, -0.0 equals 0 and is not below 0.0, 2^53 + 1 is above the double 2^53, i64::MAX is below the double 2^63, -Infinity is below i64::MIN)
- array and map construction: array(...) writes null for a missing element and map(key, value, ...) leaves out a key whose value is missing, under both profiles (FS-ENT-ORACLE O-1 (production, Enterprise, 2026-10-19)); a key that is no literal is INVALID_ARGUMENT ("map keys must be constants/literals"), an integer or boolean key is INVALID_ARGUMENT naming its type (another type gets the same text under the emulator profile and UNIMPLEMENTED under strict, unrecorded), and a key repeats only with two present values (a missing value drops its entry first), under both profiles as O-3 and O-4 recorded; arrayValue / mapValue literals of literals are constants; a field or function anywhere inside an array or map literal, as a select value or an argument, is INVALID_ARGUMENT ("Value type is not supported: FIELD_REFERENCE_VALUE" or FUNCTION_VALUE, naming the first in document order) under both profiles, as production answered every probed composition (O-1, O-3, O-4); a reserved map key in such a literal is named first ("field name '__x__' is reserved.") under the emulator profile, and a key that is not a plain name there is UNIMPLEMENTED under strict, unrecorded
- document functions, as FS-ENT-ORACLE O-1, O-3 and O-4 (production, Enterprise, 2026-10-19 and 2026-10-20) answered under both profiles: a missing or null value is null; the parent of a top-level document is the documents root reference and the parent of the documents root is null; document_id and collection_id of the documents root are INVALID_ARGUMENT ("<function>(...) requires a non-empty path."); a collection at any depth is INVALID_ARGUMENT naming the reference and its length in UTF-16 units ("Document parent name \"<name>\" lacks \"/\" at index <length>."); a string, integer, map or boolean argument is INVALID_ARGUMENT with production's text and ErrorInfo (reason FUNCTION_WRONG_TYPE); another type gets the same text under the emulator profile and UNIMPLEMENTED under strict (unrecorded). A collection with a non-ASCII name is UNIMPLEMENTED under strict (production recorded ASCII names only); a malformed reference is UNIMPLEMENTED under both; results are streamed, so a run-time refusal may follow partial results, and rows after a used-up limit are not evaluated
- offset skips matching rows once before the existing limit and paged stream; zero, past-end and empty results preserve the existing finite response shape
- result documents: without a select each result is the source document (full name, create_time, update_time and fields) under both profiles, as the pipeline response message documents it and the official emulator answers; after a select a result is its fields only; every response carries execution_time, the read time of the pipeline's input snapshot
- a function call with the wrong number of arguments, an operand field path that does not parse and a missing operand value return INVALID_ARGUMENT FS_PIPE_INVALID with fireemu's own texts under the emulator profile and UNIMPLEMENTED FS_PIPE_UNSUPPORTED_STAGE under strict (production names the function's signature); function options are ignored under the emulator profile and UNIMPLEMENTED under strict; a stage's own argument count, argument shape and stage options (limit(), offset(), limit(1, 2), where(a, b), a three-argument aggregate) answer fireemu's stage-registry texts with INVALID_ARGUMENT under the emulator profile for collection, where, select, limit, offset, add_fields, distinct and aggregate, UNIMPLEMENTED for every other stage, and UNIMPLEMENTED under strict; under strict, an INVALID_ARGUMENT is answered only when the request has the shape of a request production refused in the FS-ENT-ORACLE recordings (conformance/fs-ent-pipeline-requests.json and fs-ent-pipeline-production.json), with that request's text, and is UNIMPLEMENTED FS_PIPE_UNSUPPORTED_STAGE otherwise (the shape keeps the stages in order, stage and function names, value kinds, array lengths and each map's entries in wire order, and replaces only field paths of plain names, map keys that are plain names and values of the same kind; a REST body keeps its key order, a decoded gRPC map keeps none, so over gRPC a refusal of a request with a map of two or more entries is UNIMPLEMENTED under strict; a call of constants that fails is refused before any row is read, the collection stage's reference is checked first (the rooted form /c(/d/c)*, the official texts otherwise; a string collection is refused), every non-string literal map key and a literal deeper than 20 levels are refused statically, every constant is checked against production's value limits and name rules before any row is read (a reference names a document, the database root or the documents root, checked in the official emulator's order: a missing projects, databases or documents part, an empty, . or .. id, a reserved __x__ id, a trailing /, more than 100 key path elements, an id over 1500 bytes; a string or bytes value of at most 1,048,487 bytes; a field path inside a literal (keys joined with .) of at most 1500 bytes; a REST bytesValue (or bytes_value) read as the official emulator reads it (trailing = dropped, one base64 alphabet, no other character, no lone character); a vector of at most 2048 dimensions), with the official texts under the emulator profile and UNIMPLEMENTED under strict, where a reference outside the request's project and database, the database root or the documents root, or a reference with an id such as ____ (reserved by production's documented pattern, accepted by the official emulator), is UNIMPLEMENTED as well; timestamp constants are cut to microseconds and a database-root reference is answered as its documents root; the collection stage refuses . and .. ids, reserved ids and ids over 1500 bytes with the official texts; a body that does not parse is UNIMPLEMENTED under strict and Payload isn't valid for request. under the emulator profile, and the REST transcoder's refusals of the body go through the same gate, while the emulator profile answers them with Payload isn't valid for request.); a limit of 0, an offset of 0 up to 2147483647, two select fields that fail when evaluated, a nested select alias of a missing value, a vector inside an array and a __type__ key beside other keys are UNIMPLEMENTED under strict; an empty array() or map(), a vector element that is infinite or a negative zero, a __type__ key that is no string, a reference literal that names no document, a document function of an argument type production named only for another function, a reserved, nested or shared alias of add_fields, distinct or aggregate, and fireemu's own nesting-limit text are UNIMPLEMENTED under strict; under the emulator profile those get the official emulator's texts (Field __type__ must be a string; founds <TYPE>., Document parent name ... lacks ..., Stage '<stage>': field name '<name>' is reserved., The nested field reference ..., The 'aggregate(...)' stage cannot have overlapping field names: [...], 'map' function must have an even number of arguments, map key must be of type STRING, given type is <TYPE> for a literal key before any row is read), with a function's argument count before its arguments and every select alias before its values, the aliases in a REST body's key order (an alias inside another names the later one) and otherwise in key order; the differential corpus conformance/fs-ent-pipeline-differential.json (official emulator answers) holds both profiles to these rules in tests; well-formed unsupported operand arrangements return UNIMPLEMENTED FS_PIPE_UNSUPPORTED_STAGE, with canonical pipeline context
Not implemented (4)- execution outside collection / optional single where(condition) / optional select / optional offset / optional limit latest reads; transaction and read_time execution
- all functions other than the comparisons, array, map, document_id, collection_id and parent (including eq and between), logical operators, and all other execution stages
- text search functions against the text index catalog
- proxy / record-only modes
Notes (6)- REST geo points (every REST response and write, not only pipelines): a zero coordinate is left out and a left-out coordinate reads as zero, as production answered a pipeline literal (FS-ENT-ORACLE O-3) and a document write and read (O-4), following the proto3 JSON mapping
- Standard edition answers FAILED_PRECONDITION FS_PIPE_EDITION
- owner-only while rules are enforced
- REST POST {database}/documents:executePipeline (the Web SDK's firestore/lite transport) on Enterprise under both profiles: the JSON ExecutePipelineRequest (structuredPipeline, transaction / newTransaction / readTime, autoCommitTransaction, requestOptions accepted and ignored; a body database must equal the URL's) runs through the same validation, compilation and executor as gRPC over one snapshot; the answer is a JSON array with one {results: [document], executionTime} element per result and one {executionTime} element when there is none; refusals answer the one-element error array with the gRPC status, and a refusal answers no partial results; the strict profile's transcoder refuses unknown body fields as production's does; on Standard the strict profile answers production's FAILED_PRECONDITION and the emulator profile keeps its plain 404
- The finite local read subset is tested through tonic, with the Admin SDK over gRPC (firebase-admin 14.3.0 with @google-cloud/firestore 8.7.1 pipelines, tools/sdk-smoke/pipeline-admin.mjs) and over REST with the Web SDK (firebase 12.18.0 firestore/lite pipelines, tools/sdk-smoke/pipeline-lite.mjs) under both profiles. Enterprise production execution and exact production error parity beyond the FS-ENT-ORACLE O-1 to O-4 rows remain unobserved. Unsupported consistency selectors, metadata expressions and execution stages are refused, never silently ignored.
- known differences from FS-ENT-ORACLE O-1 to O-4 (production, Enterprise, 2026-10-19 and 2026-10-20) within this subset: production returns results without a sort in an order that is not the document name order (fireemu returns name order), and puts all results of a small answer in one response message (gRPC) or array element (REST) where fireemu sends one result per message or element; a stored -0.0 keeps its sign in production and in fireemu under both profiles, while the official emulator stores it as 0.0
|
FS-QRY-1 | Implemented | Notes (4)- orderBy on missing fields excludes documents
- range filters are type-restricted
- conformance: firestore/values-and-ordering, firestore/missing-composite-index (conformance/README.md); this records agreement with the official Local Emulator Suite, never with production
- conformance: the Firestore semantics matrix (conformance/firestore-matrix.json; pnpm -C conformance run firestore:check) gates values and type order, filters with null and NaN edges, cursors, collection groups, aggregations, projections and listings row by row against the pinned official emulator
|
FS-REST-1 | Implemented | Notes (5)- served on the Firestore port next to gRPC
- readTime selectors are validated and served through REST, and local Standard/Native findNearest is served through REST :runQuery with projection, Euclidean/cosine/dot-product distance, finite thresholds and limits
- the pinned official Firestore emulator refuses findNearest on REST; fireemu's local REST implementation is therefore not an official-emulator or production-parity claim
- conformance: firestore/rest-error-shapes, firestore/lite-rest-transport (conformance/README.md); this records agreement with the official Local Emulator Suite, never with production
- REST wire shapes follow the measured production-compatible JSON: empty fields are left out, timestamps carry 0/3/6/9 fraction digits, the last RunQuery and RunAggregationQuery responses arrive without a synthetic done marker, the first carries skippedResults when an offset is applied, and an unknown route or method answers the plain-text 404 the official adapter answers
|
FS-ADMIN-INVENTORY-1 | Implemented | Implemented (3)- read-only Firestore Admin REST GET projects/{project}/databases and projects/{project}/databases/{database} for locally registered Standard/Native databases
- exact project/name validation, owner credential authorization before catalog lookup, deterministic name ordering, Discovery-compatible showDeleted validation, stable documented database projection and healthy unreachable: []
- unknown databases return NOT_FOUND and catalog reads never create, mutate or detach database state
Notes (4)- local-only bounded adapter surface; it does not claim production parity
- Enterprise databases, create/update/delete, indexes, backups, import/export and long-running operations are outside this capability; the time-to-live field configuration is FS-FIELD-CONFIG-1
- database metadata projection is the stable local field subset documented in docs/compatibility/firestore-admin-inventory.md; unsupported identity and billing fields are omitted
- conformance input: spec/compatibility/upstream/2026-09-09-retry/discovery.json and the historical database projection v2 reference
|
FS-FIELD-CONFIG-1 | Implemented | Implemented (11)- Firestore Admin REST GET, PATCH and LIST on projects/{project}/databases/{database}/collectionGroups/{group}/fields, and GET on the field-configuration operations the patches returned
- ttlConfig is persisted per project and database, reported as ACTIVE on readback, refused for __name__, for the wildcard field and for a second field of one collection group, and bounded to 64 policies per database
- a fieldOverrides entry of firestore.indexes.json with ttl: true declares a TTL policy on that field, as firebase deploy creates one from that file: it is in force for every project of the database under both profiles, survives a session reset, and is checked like fields.patch is (not __name__, not the wildcard, one field per collection group, at most 64); a ttl that is not a boolean refuses the file (the daemon stops at start, and a reload keeps the last good indexes and policies). The file is the desired state, which differs from firebase deploy in two ways: ttl: false or an absent ttl declares nothing, so it does not clear a policy a project patched or imported, and dropping ttl on a reload removes the declaration where deploy leaves an existing policy alone. A project's first successful fields.patch or clear starts from the declared policies and from then on owns its copy: it no longer follows later reloads, a snapshot restore keeps a policy it cleared cleared, and an export does not record that clear, so an import into a daemon that declares the policy brings it back
- ttlConfig is validated before any state change: an empty object enables the policy, a null or absent value disables it, and a boolean, a number, a string, an array or an unknown setting is refused with INVALID_ARGUMENT leaving the catalog, the readback and the operation record as they were
- ttlConfig.expirationOffset is read, stored and reported back by fields.get, fields.list and the operation response; the expiration time of a document is the stored timestamp plus the offset, and an offset that is negative, larger than 2147483647 seconds or more precise than a second is refused
- documents whose time-to-live expiration time is earlier than the virtual clock are deleted by a sweep that runs one configurable interval after expiry (firestore.ttlSweepIntervalSeconds, default 86400, maximum 259200); the deletion is an ordinary delete that publishes to listeners and delivers triggers and evaluates no Security Rules
- an expired document stays readable until the sweep reaches it; a field whose value is not a timestamp is ignored rather than deleted or refused
- the configuration survives export, import and a session snapshot restore, expirationOffset included; the export sidecar omits an unset offset and declares version 1 when no policy carries one, so an artifact written by a session that configured none is unchanged, declares version 2 only when at least one offset is present, and refuses an offset the fields.patch surface would refuse as well as a version-1 artifact that carries one, which an older reader would restore without it
- the field-configuration operations are recorded per project, bounded to 64 each, and a poll answers exactly what the patch that minted it answered
- a sweep is scoped to the session that asked for it and deletes at most 1024 documents per run, leaving the rest to the next run rather than holding the clock response open
- a sweep deletion is bound to the configuration and the version it deletes under: both the policy in force for the collection group and the document current inside the database critical section that commits the delete are read again there, so a document whose time-to-live was extended, cleared or replaced, or whose policy was disabled or given a longer expirationOffset, between the scan and the deletion is kept
Notes (10)- local-only bounded adapter surface; it does not claim production parity, and no production observation of these methods has been made
- production applies a field configuration through a long-running operation, so the intermediate CREATING and the NEEDS_REPAIR states exist in the shape but are never observable locally: the patch returns an operation that is already done
- patching indexConfig is refused with UNIMPLEMENTED; single-field exemptions are taken from the project's index configuration and have no runtime transition
- the official Firestore emulator serves no field-configuration surface and performs no expiry sweep at all, so this capability diverges from it in the direction of production
- the expiry sweep runs when the shared virtual clock moves and on the control route POST /v1/sessions/{session}/firestore/ttl:sweep, which a test or a campaign uses to observe the post-expiry state without advancing a day
- conformance input: the Firestore Admin v1 reference for collectionGroups.fields and the documented time-to-live semantics; the FS-CONFIG-LIFECYCLE campaign will compare them against production
- the ttlConfig.expirationOffset range and precision rule (0 to 2147483647 seconds inclusive, values more precise than a second refused) is taken from the Firestore v1 discovery document, not from a production observation; the refusal statuses and messages it implies are production-observation pending, in the local runtime and in the export sidecar alike
- listing with indexConfig.usesAncestorConfig:false reports only fields that carry an index configuration of their own; a field with a TTL policy and no index override is reported by the unfiltered listing and by ttlConfig:*
- the expiry sweep runs inside the control request that moved the clock, so a clock advance observes its own deletions; the per-run bound is what keeps that response short rather than making the sweep asynchronous
- the name of each automatic single-field index reported under indexConfig.indexes is a derived synthetic value, a digest over the project, database, collection group, field, query scope and mode; production assigns a server-side id that no local run can reproduce, and whether a caller may depend on the name at all is a production observation this lane has not made
|
FS-RPC-1 | Implemented | Implemented (20)- GetDocument
- ListDocuments
- CreateDocument
- UpdateDocument
- DeleteDocument
- BatchGetDocuments
- BeginTransaction
- Commit
- Rollback
- RunQuery
- RunAggregationQuery
- ListCollectionIds
- BatchWrite
- Write
- Write stream handshake, ordered atomic request batches, active-stream acknowledgement continuity and local refusal/reopen lifecycle (FS-WRITE-STREAM-LOCAL); closed-stream resumption is unsupported
- Listen
- verify writes
- read_time snapshots (microsecond precision, within the past hour)
- read-only transactions at a read_time
- ListDocuments inside transactions
Notes (3)- ExecutePipeline is owned by FS-PIPE-RPC-1: registry validation plus a finite Enterprise local latest-read subset; other execution semantics remain explicitly unsupported
- history retention: a version is kept while any retention root can still reach it, namely the one-hour read_time window (READ_TIME_RETENTION_SECONDS), the read version of every transaction that is still inside its budget, and the newest version or tombstone of every path; everything older is compacted away at the end of the commit that makes it unreachable, and a read_time below the retained history is FAILED_PRECONDITION instead of an answer from another version. Named snapshots are independent copies: restoring one reproduces exactly what it captured, whatever the live database compacted since. Local bounds: a database retains at most 1 GiB of logical history and 1,000,000 versions, and on a pinned clock at most 1,024 versions of one document; firestore.history.maxVersionsPerPath sets that cap on either clock and firestore.history.maxBytes lowers the byte limit, and a commit past a limit is RESOURCE_EXHAUSTED. A version cap reaches the whole database: once one document passes it, older snapshots are released for every document, so an older read_time is FAILED_PRECONDITION and a transaction open on an older snapshot is ABORTED.
- conformance: the Firestore semantics matrix also gates preconditions, update masks, field transforms, batched writes and the transaction lifecycle; conformance/divergences.json pins every deliberate difference from the official emulator, each with the production observation (conformance/firestore-production-matrix.json) that justifies following production instead: no `done` marker and a leading readTime/skippedResults element over REST, production's validation codes, and the redacted marker fields production serves
|
FS-TEXT-VAL-1 | Implemented | Implemented (3)- firestore.textIndexDefinitionFile (schemaVersion 1, Admin API Index subset + xFirebaseTestd) validated at start
- control API: POST firestore/text-indexes:load, POST / GET firestore/text-indexes, GET / DELETE firestore/text-indexes/{id} (definitions are kept per project and database; a session sees the projects it owns)
- duplicate IDs refused, duplicate shapes warned (FS_TEXT_DUPLICATE_INDEX_DEFINITION, field-order insensitive, apiScope aware), unresolved language override warned, unsupported, unknown (at every level) and output-only fields (index.state) refused
Not implemented (2)- backfill lifecycle (advanceBackfill, completeBackfill, failBuild, repair answer UNIMPLEMENTED without changing state; FS-TEXT-IDX-1)
- posting data and local search (Milestone G)
Notes (2)- Enterprise edition only
- a search index (searchConfig) in firestore.indexes.json is checked as firebase deploy checks it (strict also refuses unknown keys inside searchConfig, as the Admin API does) and loads under both profiles and either edition, but it does not reach the text index catalog and never takes part in query planning; one note each time the file loads says how many were left out
|
FS-TXN-1 | Implemented | Notes (3)- pessimistic locking, as production runs it (concurrencyMode PESSIMISTIC): every document a read-write transaction read, and the range of every query it ran or listed, is locked until the transaction finishes (the range is the set of documents the query's filter matches, the whole scope when it has no filter; ordering, cursors, offset and limit are ignored, so a document beyond the limit is locked too); a commit outside the transaction that touches a locked document, judged on what all the commit's writes to that document leave, waits for the release (strict profile: 20 s, counted on the wall clock and on the virtual clock; emulator profile: 15 s on the wall clock) and is then refused with ABORTED 'Too much contention on these documents. Please try again.' in production's words, and the transaction's own commit succeeds
- a transaction waits on colliding read locks; when its pending writes also collide with a requester's locks, the deadlock aborts the younger transaction with retryable ABORTED 'Aborted due to cross-transaction contention. This occurs when multiple transactions attempt to access the same data, requiring Firestore to abort at least one in order to enforce serializability.'; age is ordered by (begin instant, transaction id), with ids issued in begin order, and an older requester commits in the same call when the victim releases the last colliding locks (FS-TRANSACTION P17, four saved SDK cases). A retry attempt receives a fresh age; production's retry age was not recorded. Applying this victim choice in the emulator profile and retaining the deadlock message on a later read are inferred; the official Local Emulator Suite's recorded out-of-band refusal remains ABORTED 'Transaction lock timeout.' (conformance/firestore-production-matrix.json, transactions/lifecycle)
- conformance: firestore/transaction-read-set-abort (conformance/README.md)
|
FS-WEB-1 | Implemented | Notes (3)- WebChannel v8 transport of the browser web SDK (Listen / Write) on the Firestore port; resume as in FS-LSN-1
- a target refused on its own merits, including one that rides along with the opening handshake, is delivered as TargetChange REMOVE through the first back channel and the session stays open; only a stream-level error closes it
- unacknowledged arrays keep their AIDs for response-loss replay and are bounded by explicit fireemu-only memory limits: 2 MiB per array, 16 MiB retained per session, 4 MiB per framed response batch, 4 MiB of concurrently owned replay copies per session, 64 MiB retained per Hub and 16 MiB of replay copies per Hub; terminal expiry tasks are cancelled by acknowledgement, explicit termination, removal and Hub shutdown
|
RULES-1 | Implemented | Implemented (5)- get / create / update / delete against the returned snapshot or the staged commit
- list / aggregation / Listen queries proven from equality, in, !=, not-in, array-contains(-any) and inequality (range) constraints plus request.query.limit / offset / orderBy (RULES-QUERY-CONSTRAINTS)
- get() / exists() / getAfter() (getAfter over the state the whole commit leaves behind; fails closed on reads and query proofs)
- the string, list, map, set, path, timestamp, duration, latlng, math and hashing surfaces, map.diff() and its key sets, range indexing (x[i:j]), path.bind(), and the int / float / string / path conversions
- collection groups need a recursive wildcard rule
Notes (10)- the language is measured rather than assumed: conformance/src/rules-probe installs the same bounded Rules programs into the pinned official Firestore emulator and into fireemu and compares every verdict (680 claims in conformance/rules-matrix.json, 22 whole-program probes in conformance/rules-programs.json)
- query proofs are sound, not complete: a rule that reads an unconstrained field denies the query
- the principal request.auth is built from follows the compatibility profile on both the Firestore and the Storage Rules surfaces (see AUTH-RULES-1); the rules themselves are evaluated identically under both profiles
- bounded local Standard/Native gRPC regression coverage: tests/rules/auth_atomicity.rs checks anonymous/user switching, same-UID tenant and custom claims, before/final document access in atomic commits, and unchanged document contents/timestamps/database versions after denied single, multiwrite and transaction commits; strict local revoked/disabled-token admission is covered separately, with production observations and browser/SDK parity still required
- request.query.orderBy is a map from field path to "ASC" / "DESC", request.query.limit is null without a limit, and request.resource and request.query are absent rather than null on a request that has neither
- string.matches() / replace(): the RE2 surface the official runtime has -- capturing groups with $0 / $1 expansion, the inline flags (?i) (?s) (?m), POSIX and Unicode classes, hex escapes -- and, as in RE2, no lookarounds and no backreferences; a literal pattern is compiled while the ruleset is, so an unusable one fails the load
- capture-free alternatives whose branches each consume one character repeat iteratively at constant match depth; every attempted branch remains charged to the regex step budget, and patterns with captures or multi-character branches keep the general matcher
- a recursive call and a call chain of more than twenty calls are compile errors, with the official compiler's messages
- three language rows are documented divergences, all the same one: the official compiler's static type checker rejects at load what fireemu raises at evaluation; the request decision is the same
- conformance: firestore/rules-decisions (conformance/README.md); this records agreement with the official Local Emulator Suite, never with production
|
RULES-BUDGET-1 | Implemented | Implemented (5)- RULES-EXPRESSIONS-PER-REQUEST 1000
- RULES-FUNCTION-CALL-DEPTH 20 calls (a chain of 21 functions is the deepest that compiles), refused when the ruleset loads
- RULES-RECURSION 0: a recursive or cyclical call is refused when the ruleset loads
- RULES-DOC-ACCESS-SINGLE 10 per operation and per query proof (get, exists and getAfter of one path each count once)
- RULES-DOC-ACCESS-MULTI-TOTAL 20 across atomic commits and BatchGetDocuments
Notes (1)- each number is measured against the pinned official emulator, not read off the documentation: ten document accesses are served and eleven are denied, thirty accesses of one path are served, and the call-depth boundary is the one its compiler reports (conformance/rules-programs.json)
|
RULES-DIAG-1 | Implemented | Implemented (7)- GET /emulator/v1/projects/{project}:ruleCoverage on the Firestore port: the loaded rules files, then a report of one tree per allow condition and per function body, each node naming its sourcePosition (line, column, currentOffset, endOffset) and the values it took with a count each
- GET /emulator/v1/projects/{project}:ruleCoverage.html: the same document as a page that underlines every evaluated expression and shows its values on hover
- PUT /emulator/v1/projects/{project}:securityRules: the official load route, answering 200 with issues or 400 whose message names the rejected position as L<line>:<column>
- PUT /emulator/v1/projects/{project}/databases/{database}:securityRules: the same route for one named database; under strict a database nothing declared in firebase.json (one created through the Admin API) has no rules, and refuses every client request, until this loads some for it; deleting the database drops them
- GET /v1/sessions/{session}/rules/requests on the control API: the last 100 decided requests, newest first, each with its decision and what every expression evaluated to; with the UI running the trace is kept from the first request on, so the Rules page lists denials that happened before it was opened
- each denial of an end-user Firestore or Storage request is printed on stderr with the request, the reason and every allow condition it evaluated with its line, its source and what it took (rules.denialLog "summary", the default; "off" silences it); the client's answer stays production's
- the Emulator UI's Rules page lists the decided requests and expands one into its per-expression trace, reading the same control route
Notes (5)- an expression that raised is reported as undefined carrying the innermost expression that raised and its message; an expression nothing reached has children and no values, as the official report does
- the coverage and the traces live with the ruleset they describe, so loading another one replaces both and no stale position is ever served
- the two emulator routes take no credential, because the official ones take none and @firebase/rules-unit-testing calls them with no headers; both are loopback-only and both echo the values the ruleset computed, which is what a coverage report is
- the control route carries the control token like every privileged route, and never carries a token, a signature or any claim other than the subject the rule saw as request.auth.uid
- the denial message fireemu returns is its own: the official emulator answers PERMISSION_DENIED with a per-allow trace of its evaluation, which is a diagnostic rather than an API contract, and fireemu publishes the same information through these routes instead
|
RULES-LINT-1 | Implemented | |