Skip to content

Compatibility tables

The tables on this page are generated from the records in the repository when the site is built: the compatibility contract, the production verification records and the capability manifest.

Check support for the specific APIs and conditions your application uses, rather than relying on product names alone.

Every product in the Local Emulator Suite of firebase-tools 15.28.2, the official emulator release Fireemu is compared with, and the surfaces Fireemu adds.

Product or surfaceFireemuDefault portIn the Local Emulator Suite
Cloud FirestoreSupported8080Yes
Firebase AuthenticationSupported9099Yes
Cloud Storage for FirebaseSupported9199Yes
Cloud Functions for FirebaseSupported5001Yes
Firebase App CheckSupported (Fireemu addition)—No (Fireemu addition)
fireemu control API (sessions, clock, snapshots, faults)Supported (Fireemu addition)—No (Fireemu addition)
Emulator Suite UISupported4000Yes
Firebase CLI configuration and command surface (firebase.json, --only, emulators:start / exec)Supported——
Emulator HubSupported4400Yes
Emulator loggingSupported4500Yes
Cloud Pub/SubSupported8085Yes
EventarcSupported9299Yes
Cloud TasksSupported9499Yes
Firebase Data ConnectNot yet supported9399Yes
Firebase Realtime DatabasePartially supported9000Yes
Firebase HostingNot yet supported5000Yes
Firebase App HostingNot yet supported5002Yes
Firebase ExtensionsNot planned5001Yes

Fireemu is compatible with the listed Local Emulator Suite products as shipped by firebase-tools 15.28.2 — Cloud Firestore, Firebase Authentication, Cloud Storage for Firebase, Cloud Functions, Cloud Pub/Sub and Eventarc, with Security Rules on the Firestore and Storage surfaces — under the emulator compatibility profile and the evidence recorded in spec/compatibility/contract.json; it makes no complete-suite and no unqualified superset claim while the Realtime Database is served without a parity claim (an open gap), Firebase Hosting, App Hosting and Data Connect are deferred and Firebase Extensions is not planned.

Fireemu, the official emulator and production

Section titled “Fireemu, the official emulator and production”

Differences that most often change a test result. Fireemu’s column is the default strict profile.

Behavior Production Official emulator Fireemu
Query without its composite index Refused Runs Refused; runs and reports the missing index with firestore.missingIndexes: "report"
Firestore client request to a project without Security Rules Refused Allowed Refused
TOTP multi-factor authentication Supported Not available Supported, through the project’s MFA configuration
Request naming a tenant that does not exist Refused Creates the tenant Refused
beforeEmailSent blocking function Runs Not run Runs
App Check Supported Not available Debug tokens; no attestation providers or replay protection
Scheduled functions Run on Cloud Scheduler’s schedule Not run on a schedule Run on a virtual clock you control
Unsigned mock ID tokens in Security Rules Refused Accepted Accepted by default; refused with productionExact
Declared Storage upload checksums Verified Not verified Verified

Fireemu compares its answers with recordings taken from real Firebase projects. An area is verified against production when every condition in its frozen inventory matches the recordings, or differs only in a reviewed and documented way (a known difference). The counts include each area’s final regression run and closure review.

Cloud Firestore

AreaStatusConditions verified
Writes, batches and limits
Batched writes, malformed and undecodable values, document, field and index-entry size limits, and request size limits over gRPC, REST and WebChannel.
38 conditions
  • FS-WRITE-LIMITS-03/batch-malformed-middle
  • FS-WRITE-LIMITS-03/batch-undecodable-value
  • map-value-key-validation
  • FS-WRITE-LIMITS-03/batch-duplicate-document
  • FS-LIMIT-COLLECTION-ID
  • FS-LIMIT-SUBCOLLECTION-DEPTH
  • FS-LIMIT-DOCUMENT-NAME-BYTES
  • FS-LIMIT-INDEX-ENTRIES-PER-DOCUMENT
  • FS-LIMIT-INDEX-ENTRY-BYTES
  • FS-LIMIT-INDEX-ENTRY-SUM-PER-DOCUMENT
  • near-limit-delete-refusal
  • FS-LIMIT-FIELD-PATH-BYTES
  • FS-LIMIT-FIELD-VALUE-BYTES/scalar-refusal
  • FS-LIMIT-FIELD-VALUE-BYTES/aggregate-string
  • FS-LIMIT-FIELD-VALUE-BYTES/aggregate-map
  • FS-LIMIT-INDEXED-FIELD-VALUE-BYTES
  • FS-WRITE-LIMITS-03/implied-map
  • FS-WRITE-LIMITS-03/implied-array
  • FS-LIMIT-API-REQUEST-BYTES/rest-commit-json-accepted-samples
  • FS-LIMIT-API-REQUEST-BYTES/raw-16mib-over
  • FS-LIMIT-API-REQUEST-BYTES/non-commit-rest/batch-get
  • FS-LIMIT-API-REQUEST-BYTES/non-commit-rest/batch-write
  • FS-LIMIT-API-REQUEST-BYTES/non-commit-rest/create
  • FS-LIMIT-API-REQUEST-BYTES/non-commit-rest/patch
  • FS-LIMIT-API-REQUEST-BYTES/non-commit-rest/run-query
  • FS-LIMIT-API-REQUEST-BYTES/webchannel — known difference
  • FS-LIMIT-API-REQUEST-BYTES/grpc-unary
  • FS-LIMIT-API-REQUEST-BYTES/grpc-stream — known difference
  • stream-transaction-precedence
  • write-stream-trailing-metadata
  • write-stream-half-close
  • write-stream-empty-write-response
  • final-artifact-regression
  • closure-review
  • FS-DATA-WRITE-LIST/list-documents-rest
  • FS-DATA-WRITE-LIST/list-collection-ids-rest
  • FS-DATA-WRITE-LIST/list-grpc
  • FS-DATA-WRITE-LIST/setup-writes
Known differences (2)
  • The request-size bound of a gRPC Write or Listen stream cannot be observed safely in production. Both profiles apply the 11,534,336-byte bound bracketed on unary GetDocument (FS-LIMIT-API-REQUEST-BYTES/grpc-unary) as an estimate (decision D, 2026-09-27); the emulator profile adds no refusal beyond that production bound. The recorded stream rows at 10,485,760 and 10,485,761 bytes match.
  • Production's WebChannel form-body bound lies in (12,582,912, 16,777,216]: it accepted 12,582,912 bytes on a valid session and refused 16,777,216 with its HTML 400 page, after which the session was gone (follow-up recording, 2026-09-27, both passes). The bytes inside the interval are unobserved. Strict adopts the upper end as an estimate: it accepts up to 16,777,215 bytes and answers production's page and ends the session above it. Every recorded row matches.
Verified against production36 / 38
Queries and indexes
Filters, ordering, cursors, collection groups, aggregations, vector search, partition queries, explain, index selection and read-time queries.
21 conditions
  • field-filters
  • unary-filters
  • composite-filters
  • filter-validation
  • query-limits
  • order-by
  • cursors
  • offset-limit
  • projection
  • collection-group
  • aggregation
  • vector
  • partition-query
  • explain
  • index-selection
  • read-time
  • request-shape
  • pipeline-standard-refusal
  • grpc-transport
  • final-artifact-regression
  • closure-review
Verified against production21 / 21
Transactions
Read-write and read-only transactions, conflicts and locks, expiry, retries in the Web and Admin SDKs, and atomic visibility of commits.
18 conditions
  • read-write-lifecycle
  • read-only-snapshot
  • read-time-snapshot
  • token-validation-and-ownership
  • read-set-conflict
  • write-set-atomicity
  • query-range-lock
  • failed-commit-and-rollback
  • retry-token-lifecycle
  • idle-expiry
  • total-lifetime-expiry
  • read-time-retention
  • web-sdk-optimistic-retry
  • paging-and-cancellation
  • admin-sdk-server-retry
  • commit-atomic-visibility
  • final-artifact-regression
  • closure-review
Verified against production18 / 18
Real-time listeners
Snapshot listener event order, pending writes, reconnect and resume, existence filters and browser tab lifecycle.
15 conditions
  • document-event-order
  • pending-writes
  • query-change-order
  • reconnect-resume
  • unsubscribe
  • initial-unauthenticated-refusal
  • default-subscription
  • cross-principal-rules
  • raw-resume-token
  • browser-tab-lifecycle
  • existence-filter-reconnect
  • backend-cache-transitions
  • native-target-protocol
  • final-artifact-regression
  • closure-review
Verified against production15 / 15
Firestore Security Rules
Principals and token fields, tenants, request and resource data, query proofs, access budgets, compile and runtime limits, and refusal shapes.
17 conditions
  • principals
  • auth-token-fields
  • tenant
  • request-resource
  • document-access
  • query-proofs
  • access-budgets
  • compile-limits
  • runtime-limits
  • atomic-writes
  • refusal-shape
  • token-states
  • token-expiry
  • publication
  • named-database
  • final-artifact-regression
  • closure-review
Verified against production17 / 17
Databases and administration
Named databases, index and field configuration, TTL, long-running operations, export, import and bulk delete.
20 conditions
  • database-projection
  • database-create
  • database-delete
  • database-patch
  • database-mode-gating
  • default-database-lifecycle
  • project-boundary
  • locations
  • index-lifecycle
  • index-query-effect
  • field-index-config
  • ttl-config
  • operations
  • export
  • import
  • export-format-interop
  • bulk-delete
  • grpc-admin-transport
  • final-artifact-regression
  • closure-review
Verified against production20 / 20

Authentication

AreaStatusConditions verified
Accounts
Sign-up and profile updates, anonymous accounts, email handling, Admin account management, import with password hashes, and provider linking.
32 conditions
  • client-password-lifecycle
  • client-profile-attributes
  • client-password-change
  • input-validation-basic
  • anonymous-basic
  • email-canonical-and-duplicate
  • disable-reenable
  • delete-effects
  • default-policy-client-update
  • tampered-token-privileged-update
  • anonymous-upgrade
  • client-email-change
  • admin-create
  • admin-lookup-selectors
  • admin-update-fields
  • custom-attributes
  • privilege-boundary
  • admin-delete-and-batch-delete
  • admin-list-export
  • admin-query
  • import-basic
  • import-hash-formats
  • phone-accounts
  • provider-link-unlink
  • uid-reuse-after-delete
  • duplicate-email-mode
  • email-privacy-off
  • password-policy-routes-and-custom
  • client-permissions
  • value-classes
  • final-artifact-regression
  • closure-review
Verified against production32 / 32
Tokens and sessions
ID and refresh tokens, custom tokens, claim precedence, revocation, session cookies and token expiry.
15 conditions
  • id-token-composition
  • legacy-token-without-return-secure-token
  • refresh-exchange
  • refresh-refusals
  • id-token-refusals
  • revocation-valid-since
  • custom-token-sign-in
  • custom-token-validation
  • claim-precedence
  • session-cookie-durations
  • session-cookie-sessions
  • token-expiry
  • account-state-token-refusals
  • final-artifact-regression
  • closure-review
Verified against production15 / 15
Email actions
Password reset, email verification, email change and email link sign-in, including action-code ownership and expiry.
12 conditions
  • admin-link-generation
  • client-send-boundary
  • password-reset
  • verify-email
  • verify-and-change-email
  • email-link-sign-in
  • email-link-session-claims
  • legacy-token-actions
  • code-ownership
  • code-expiry
  • final-artifact-regression
  • closure-review
Verified against production12 / 12
Project configuration and SDKs
Project configuration reads and updates, password policies, provider switches, email enumeration protection, and the Admin and Web SDK flows that use them.
21 conditions
  • config-read-shape
  • config-update-mask
  • config-validation
  • password-policy-config
  • password-policy-projection
  • password-policy-existing-accounts
  • provider-switches
  • duplicate-email-switch
  • email-privacy-projection
  • client-permissions-paths
  • recaptcha-config
  • quota-config
  • mobile-link-settings
  • other-config-fields
  • admin-sdk-project-config
  • admin-sdk-tokens
  • admin-sdk-custom-token
  • web-sdk-password-policy
  • web-sdk-flows
  • final-artifact-regression
  • closure-review
Verified against production21 / 21
OIDC and SAML providers
Provider configuration, OIDC and SAML assertion checks, account creation and collisions, linking and claims from identity providers.
24 conditions
  • provider-config-oidc
  • provider-config-saml
  • provider-config-default-supported
  • provider-enablement
  • oidc-token-verification
  • oidc-nonce
  • oidc-replay
  • saml-response-verification
  • saml-nameid-and-attributes
  • create-auth-uri-provider
  • request-uri-session
  • idp-new-account
  • idp-returning-account
  • collision-verified-email
  • collision-unverified-email
  • link-with-id-token
  • duplicate-email-mode
  • pending-token
  • idp-claims-sign-in
  • idp-claims-refresh
  • idp-claims-session-cookie
  • third-party-refusals
  • final-artifact-regression
  • closure-review
Verified against production24 / 24
Multi-factor authentication
Project MFA configuration, TOTP enrollment and sign-in, SMS factors, withdrawal and second-factor claims.
14 conditions
  • disabled-project
  • project-config
  • totp-enrollment
  • totp-sign-in
  • withdrawal-and-revocation
  • sms
  • interactions
  • admin-factors
  • other-first-factors
  • second-factor-claims
  • pending-credential-lifetime
  • enrollment-session-lifetime
  • final-artifact-regression
  • closure-review
Verified against production14 / 14
Multi-tenancy and blocking functions
Tenant management and isolation, tenant settings, and blocking functions before account creation, sign-in and email sending.
27 conditions
  • tenant-management
  • multi-tenant-switch
  • tenant-selection
  • credential-isolation
  • admin-accounts
  • tenant-sign-in-settings
  • inheritance
  • tenant-password-policy
  • tenant-mfa
  • tenant-deletion
  • tenant-actions
  • provider-isolation
  • event-selection
  • ordering-and-visibility
  • refusal
  • rollback
  • timeout
  • claims
  • response-validation
  • blocking-config
  • event-payload
  • tenant-events
  • send-email-sms
  • session-isolation
  • hook-concurrency
  • final-artifact-regression
  • closure-review
Verified against production27 / 27
Authentication with Firestore
How tokens, tenants, sign-out, token refresh and revocation affect Firestore transactions and listeners.
11 conditions
  • tenant-same-uid
  • read-only-transaction-binding
  • foreign-project-token
  • sdk-optimistic-transaction
  • listen-sign-out-and-switch
  • listen-token-refresh
  • listen-revocation-disable-delete
  • listen-tenant-deletion
  • pending-write-across-switch
  • final-artifact-regression
  • closure-review
Verified against production11 / 11

Cloud Storage

AreaStatusConditions verified
Objects
Uploads, downloads, download tokens, metadata, listing, copy and rewrite, preconditions, checksums and errors, in both the Firebase and Cloud Storage JSON APIs.
28 conditions
  • firebase-simple-upload
  • firebase-multipart-upload
  • firebase-resumable-upload
  • firebase-download
  • firebase-download-tokens
  • firebase-metadata
  • firebase-delete
  • firebase-list
  • firebase-overwrite
  • gcs-simple-multipart-upload
  • gcs-resumable-upload
  • gcs-download
  • gcs-metadata
  • gcs-delete
  • gcs-list
  • gcs-copy-rewrite
  • generation-preconditions
  • metageneration-preconditions
  • checksums
  • missing-object-errors
  • authorization-errors
  • invalid-object-name-errors
  • invalid-range-errors
  • admin-credential
  • firebase-id-token
  • cross-dialect-state
  • final-artifact-regression
  • closure-review
Verified against production28 / 28
Storage Security Rules
Method grants, request and stored resources, token claims, path matching, Firestore lookups from Storage rules, denial shapes and rules releases.
24 conditions
  • method-grants
  • list-v2
  • anonymous-and-id-token
  • token-claims
  • token-refusal
  • upload-request-resource
  • metadata-request-resource
  • stored-resource
  • create-update-delete-state
  • request-time
  • path-variables
  • recursive-wildcard
  • storage-service-compile
  • firestore-get
  • firestore-exists
  • firestore-access-budget
  • firebase-denial-shape
  • denial-precedence
  • gcs-admin-boundary
  • download-token-boundary
  • release-switch
  • no-release
  • final-artifact-regression
  • closure-review
Verified against production24 / 24

Cloud Functions

AreaStatusConditions verified
HTTP and callable functions
Request and response handling, streaming, CORS, timeouts, callable envelopes and errors, and callable authentication.
19 conditions
  • http-method-path-query
  • http-request-headers
  • http-request-bodies
  • http-response-status-headers
  • http-large-response
  • http-streaming
  • http-cors-preflight
  • http-throw
  • http-timeout
  • http-no-response
  • callable-success-envelope
  • callable-https-errors
  • callable-malformed-requests
  • callable-auth-context
  • callable-auth-refusal
  • callable-streaming
  • public-invocation
  • final-artifact-regression
  • closure-review
Verified against production19 / 19
Event triggers
Firestore, Storage, Authentication and Pub/Sub triggers: which events fire, their payloads and routing, and delivery retries.
22 conditions
  • firestore-created
  • firestore-updated
  • firestore-deleted
  • firestore-written
  • firestore-noop
  • firestore-routing-params
  • firestore-payload
  • firestore-auth-context
  • storage-finalized
  • storage-deleted
  • storage-metadata-updated
  • storage-archived
  • storage-payload
  • storage-bucket-routing
  • auth-created
  • auth-deleted
  • auth-payload
  • pubsub-published
  • pubsub-topic-routing
  • delivery-retry-identity
  • final-artifact-regression
  • closure-review
Verified against production22 / 22
Scheduled functions
Schedule grammars, time zones and daylight saving time, delivery to v1 and v2 functions, retries, deadlines and overlap.
25 conditions
  • declarations-v1-v2
  • cron-grammar
  • groc-grammar
  • timezone-validation-defaults
  • next-occurrence
  • dst-calendar
  • v2-http-delivery
  • v1-pubsub-delivery
  • forced-and-natural-invocation
  • retry-config-validation
  • v2-retry-limits
  • v2-backoff
  • v1-two-stage-retry
  • deadline-and-overlap
  • clock-config
  • clock-forward-boundaries
  • catch-up-policy
  • overlap-policy
  • retry-delay-fairness
  • capacity-cursor
  • rewind-and-token-policy
  • manual-schedule
  • state-lifecycle
  • final-artifact-regression
  • closure-review
Verified against production25 / 25

Cloud Pub/Sub

AreaStatusConditions verified
Pub/Sub
Topics and subscriptions, publish, pull and acknowledgement, redelivery, filters, ordering, dead-letter topics, StreamingPull, snapshots and seek.
18 conditions
  • topic-lifecycle
  • resource-names
  • list-pagination
  • subscription-lifecycle
  • subscription-configuration
  • push-configuration
  • publish-wire
  • publish-limits
  • pull-ack
  • nack-deadline-redelivery
  • filter-evaluation
  • ordering
  • retry-dead-letter
  • streaming-pull
  • snapshot-seek
  • errors-authentication
  • final-artifact-regression
  • closure-review
Known differences (6)
  • The production Pub/Sub REST negative cache for subscriptions and snapshots is NOT emulated. After a slow 404 for a subscription or snapshot, production answered GET (once DELETE) of the same resource with a fast cached 404 for about 9.5 s, even after the resource had been created; it reproduced only in some request orders, counterexamples exist and the rule is not established. gRPC and topics never showed it. fireemu returns the steady-state answer (200) right after create in both profiles.
  • Known limitation of ordered unary Pull in strict: on an ordering-enabled subscription a unary Pull response never mixes keyed and unkeyed messages and deliverable unkeyed messages are served first, as production did in every recorded finite burst (coordinator ruling 1386). Under continuous unkeyed traffic that arrives faster than unary Pulls drain it, keyed messages starve: unary Pull does not deliver them until no unkeyed message is deliverable. Production behaviour under sustained unkeyed traffic was not observed. StreamingPull, push and the emulator profile are unchanged.
  • Production comparison rules applied by the per-case adjudication beyond exact equality: 1390 as clarified by 1404 (the original 2026-10-05 REST captures stored only status and body, so their REST rows MATCH on equal status and equal body content; response headers were neither recorded nor compared, and no later comparison judges any response header either); 1392 (original ordering cells: the next ordering key production serves varies by run and transport, so key choice is not judged; per-key order, no keyed/unkeyed mixing and the delivered set must match); 1394 (differences explained only by the delivery order of messages without an ordering guarantee and by a Pull returning fewer messages than available, including 0, MATCH when the cell delivered set, each message content and per-key order with ordering enabled match; fireemu answers immediately and does not simulate empty Pulls; cases /conditions/6/cases/4, 6/8, 9/0, 9/4, 11/2, 14/3); 1402 (for a Pull limited by maxMessages, the 1394 set condition reads: each delivered message is one of the cell published messages with equal content, its id correlates with the same-side publish reply, and the delivered counts are equal; applied to /conditions/6/cases/8); 1399 as clarified by 1400 (second-recording original ordering cells: each side holds at most one ordering key by an unacknowledged delivery at cell end and the whole-cell difference is only the tails of those held keys; per-key prefix and order and no keyed/unkeyed mixing still required; any unsuccessful Pull or Acknowledge on the ordered subscription is DIVERGES); 1401 defines a held key: that side delivered fewer of the key messages than the other side and its last delivery of the key is unacknowledged at cell end; keys unacknowledged on both sides with equal counts are not held. 1391 and 1401 are coordinator readings recorded in the ledger. The original-row comparator sorts topics, subscriptions and snapshots list members before comparing, a base comparator rule. The full rule list and what each recording family compares are in the comparison file.
  • REST Content-Type: the bound binary a97b87c1 answers JSON with Content-Type application/json, while production sends application/json; charset=UTF-8. Found in header-bearing production captures outside the comparison set (two GET-404 cause probes); no comparison in this closure judges headers. Owner 1404 has the charset fixed as a separate change in v0.17; that fix is not part of the binary this closure is bound to.
  • REST response headers are not compared anywhere in this closure. In the header-bearing production captures production sends 10 response headers (alt-svc, content-encoding, content-type, date, server, transfer-encoding, vary, x-content-type-options, x-frame-options, x-xss-protection); the bound binary a97b87c1 sends 3 (content-type, content-length, date).
  • Registered divergence: strict accepts a connection without an Authorization header as the PUBSUB_EMULATOR_HOST exception; a configured credential that fails local verification answers the recorded 401/UNAUTHENTICATED shape. Production answers a headerless request with 403.
Verified against production18 / 18

Eventarc

AreaStatusConditions verified
Eventarc
Channels, publishing through REST and the Admin SDK, publish limits, custom event handlers, trigger filters and delivery retries.
11 conditions
  • channel-lifecycle
  • publish-envelope
  • publish-content
  • admin-sdk-publish
  • publish-limits
  • custom-handler-envelope
  • trigger-filters
  • delivery-retry-identity
  • errors-authentication
  • final-artifact-regression
  • closure-review
Verified against production11 / 11

The capability manifest records, for each feature, what is implemented and what is not. npx fireemu capabilities prints it for the profile your configuration selects.

Cloud Firestore

CapabilityStatusDetails
FS-GW-1Implemented
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-1Partial
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-1Implemented
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-1Partial
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-1Implemented
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-1Implemented
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-1Implemented
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-1Implemented
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-1Implemented
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-1Implemented
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-1Implemented
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-1Implemented
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-1Implemented
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-1Implemented
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-1Implemented
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-1Implemented

Authentication

CapabilityStatusDetails
AUTH-CORE-1Implemented
Implemented (18)
  • accounts:signUp (email, anonymous)
  • local password policy validation across accounts:signUp, client and Admin accounts:update, accounts:resetPassword, and Admin batchCreate rawPassword/fakeHash imports: minimum 6 scalar characters, maximum 4096 UTF-16 units, and control-character rejection happen before account or credential mutation; trusted artifact restoration preserves the recorded credential without applying new-password policy; saved production evidence is limited to the approved end-user client accounts:update corpus (including finite 4095/4096/4097 UTF-16-unit maximum cases and six/five-character ASCII controls), so the other routes and local malformed/control/minimum cases remain production-unobserved
  • accounts:signInWithPassword
  • accounts:signInWithCustomToken (unsigned JWT or strict JSON fake token, uid or user_id)
  • accounts:lookup
  • accounts:update (profile, email, password, phone, deleteAttribute DISPLAY_NAME / PHOTO_URL / EMAIL / PASSWORD, deleteProvider phone / password / federated, linkProviderUserInfo; a credential change re-issues tokens in the response)
  • accounts:delete (the session's own account)
  • accounts:sendOobCode / resetPassword / signInWithEmailLink (PASSWORD_RESET, VERIFY_EMAIL, EMAIL_SIGNIN, VERIFY_AND_CHANGE_EMAIL; applyActionCode through accounts:update); resetPassword without newPassword is checkActionCode and describes any code type with its requestType, email and newEmail without consuming it, while the reset itself stays restricted to PASSWORD_RESET
  • accounts:sendVerificationCode / signInWithPhoneNumber
  • the official console lines for every issued email action link and SMS code (To verify the email address ..., follow this link: ...; To enroll MFA with ..., use the code ...), printed to standard output and streamed to the Logging emulator; auth.logActionCodes = false or --log-verbosity quiet silences them
  • accounts:createAuthUri
  • GET /v1/projects and /v1/recaptchaParams (the fixed documents the official emulator serves)
  • securetoken token (JSON and form)
  • Admin: projects/{p}/accounts create/lookup/update/delete/batchGet/batchCreate/batchDelete/query/sendOobCode and projects/{p}:createSessionCookie; mfaInfo / mfa.enrollments (phone), linkProviderUserInfo / deleteProvider
  • local session-cookie handoff: inclusive 300..1209600-second duration boundaries, ID-token issuer/audience/project/tenant isolation, expired/revoked/deleted/disabled-user refusal, and signature refusal in session-rsa mode; tenant custom-token developer claims, persisted custom attributes and blocking session claims survive cookie issuance and Secure Token refresh without persisting session-only claims
  • Admin provider configuration: project and tenant OAuth IdP and inbound SAML create/get/list/update/delete under the public v2 namespaces, with project/tenant and OIDC/SAML isolation, bounded creation-order pagination, validation and atomic refusal; no external IdP traffic
  • GET /emulator/action: the printed email action link acts on its code like the official emulator (verifyEmail, verifyAndChangeEmail, resetPassword with newPassword, signIn forwarding to continueUrl) and answers authEmulator JSON or a 303 to continueUrl
  • Emulator routes: /emulator/v1/projects/{p}/oobCodes, verificationCodes, accounts (DELETE), their tenant-scoped forms under /tenants/{t}, config (GET and PATCH: signIn.allowDuplicateEmails changes password-account uniqueness and lookup/sign-in selection, emailPrivacyConfig.enableImprovedEmailPrivacy changes what signInWithPassword, sendOobCode PASSWORD_RESET and createAuthUri reveal)
Notes (12)
  • nothing is mailed or texted: codes are read from the emulator routes; normal daemon starts re-key generated credential handles from a CSPRNG lifecycle incarnation so restart or namespace recreation cannot replay them, while tests may inject a fixed incarnation for reproducibility
  • session-cookie/custom-token coverage is local and documentation-backed, not production parity: saved auth-basic and auth-session observations cover adjacent client/refresh paths only; tools/sdk-smoke/auth.mjs exercises real Admin SDK unsigned-emulator cookie verification after password change, revocation, disable and deletion; session-rsa uses a local trust root, not Google's production certificate trust root; strict Rules accepts ID tokens only, while unsigned EmulatorMock Rules decoding is deliberately more permissive and is not cookie verification
  • an email-link sign-in mints firebase.sign_in_provider = password, the Rules-documented provider value, while emailLink remains the createAuthUri sign-in method and the sign-in method a blocking function sees; the production claim value is unobserved
  • no route stores a text field carrying a NUL or another control character: signUp, client and Admin accounts:update and Admin accounts:create check the account profile, and every writer of a federated identity (linkProviderUserInfo, a signInWithIdp assertion and an artifact import row) goes through one store-level check of providerId, rawId, email, displayName and photoUrl, and a second factor's displayName is checked the same way at phone enrollment, the Admin enrollment list and an import row; documented behavior, production observation pending for the refusal shape, and no length bound is imposed because production's limits are unrecorded
  • an Admin password-reset link request for an unknown address keeps EMAIL_NOT_FOUND under improved email privacy, which hides the address from end-user routes only; production's answer for this pair is unobserved
  • reCAPTCHA tokens are accepted unchecked
  • error envelopes measured against firebase-tools 15.28.2: EMAIL_NOT_FOUND / INVALID_PASSWORD by default and INVALID_LOGIN_CREDENTIALS under improved email privacy, MISSING_EMAIL before MISSING_PASSWORD, the Not Found envelope with status NOT_FOUND for an unknown path, kind fields on every response, absent rather than null optional fields, 28-character generated local ids
  • one route table (crates/fireemu-adapter-http/src/identity_toolkit/routes.rs) drives dispatch, the App Check bypass classification and the bounded operation labels; a route cannot be dispatched without a label
  • transient credentials are bounded: email action codes expire after an hour and phone codes after ten minutes of virtual time, pending second-factor sign-ins after an hour, and each kind refuses with QUOTA_EXCEEDED past 1000 outstanding codes per project or 32 pending sessions per user; expired entries are swept before every request and never observable; refresh tokens are not swept
  • Admin batchCreate (importUsers) verifies a rawPassword or the emulator's fakeHash form; any other passwordHash is kept out and the account is created without a password credential, which is observably what the official emulator does with a hash it cannot compare
  • Admin accounts:query preserves the official emulator's unbounded, ignored-limit behavior under the emulator profile; strict applies the documented Identity Platform default and maximum page size of 500, offset ordering, count semantics and field validation as an explicit production-oriented boundary
  • conformance: auth/sign-up-and-sign-in-errors, auth/identity-toolkit-error-shapes, auth/oob-code-shapes, auth/admin-account-lifecycle, auth/client-account-flows (conformance/README.md); this records agreement with the official Local Emulator Suite, never with production
AUTH-ACCOUNT-LIFECYCLE-1Implemented
Implemented (6)
  • Admin account create, get, list, update and delete through the projects/{project}/accounts routes
  • client account create, get, update and delete through the Identity Toolkit accounts routes
  • duplicate email refusal without mutating either account, and UID reuse after delete
  • disable and re-enable: disabled accounts remain readable and retain their credentials while client sign-in is refused
  • anonymous upgrade to an email/password account while retaining the anonymous UID, and federated provider linking through linkProviderUserInfo
  • post-state checks from both Admin selectors and the authenticated client subject
Notes (3)
  • conformance: auth/admin-account-lifecycle and auth/client-account-flows (conformance/README.md); these are pinned official Local Emulator Suite references, never production evidence
  • production candidate: account lifecycle observations require a project/API-key run with matching inputs and observer identity before any production compatibility claim is promoted
  • session cookies, out-of-band actions, MFA and external federation protocol exchange are separate capabilities
AUTH-MFA-SMS-0Implemented
Notes (4)
  • phone second factors: mfaEnrollment:start/finalize/withdraw with phoneEnrollmentInfo / phoneVerificationInfo, mfaSignIn:start/finalize with phoneSignInInfo; codes come from /emulator/v1/projects/{p}/verificationCodes; up to five factors per user
  • measured against firebase-tools 15.28.2: enrollment is refused with UNSUPPORTED_FIRST_FACTOR for an anonymous, phone, custom-token or Game Center session, with UNVERIFIED_EMAIL for an unverified email and with SECOND_FACTOR_EXISTS for a number already enrolled, on both the start and the finalize step and without creating or consuming a verification code; a pending-credential response obfuscates the number to its last four digits
  • the spec declared SMS out of scope; the user asked for Identity Platform parity
  • conformance: auth/mfa-error-shapes, auth/mfa-enrollment-eligibility (conformance/README.md); this records agreement with the official Local Emulator Suite, never with production
AUTH-MFA-TOTP-1Implemented
Notes (4)
  • window and enrollment TTL are local policies; an expired enrollment session answers SESSION_EXPIRED for one further session lifetime and is then reaped
  • TOTP enrollment applies the verified-email requirement of phone enrollment: mfaEnrollment:start and mfaEnrollment:finalize both refuse an account whose email is unverified with UNVERIFIED_EMAIL, before any enrollment state is created or consumed. The official emulator has no TOTP route to measure this against, and production has not been observed for this condition; the AUTH-MFA-AGE-TOTP-01 campaign will settle it
  • default snapshots carry no shared secret (INV-AUTH-003, ADR-034 amendment): an enrolled factor is captured with a detached secret and rebound on restore to the secret the live store still holds; a factor withdrawn in between is dropped from the restored account and reported on stderr, never restored unusable
  • conformance: auth/mfa-error-shapes (conformance/README.md); this records agreement with the official Local Emulator Suite, never with production
AUTH-FEDERATION-ASSERTION-LOCALPartial
Implemented (3)
  • Opt-in Rust adapter handler configuration, handle_with_oidc_trust, preserves the standard accounts:signInWithIdp REST request and injects one caller-pinned public RS256 JWK; no new client HTTP API or network traffic
  • Enabled provider and project/tenant/provider/issuer/client trust binding; real compact RS256 signature verification; issuer, subject, audience/authorized-party, expiry, issued-at, not-before and SDK raw-nonce SHA-256 checks before account mutation; nonempty accompanying access_token or refresh_token is refused before response or blocking credential forwarding
  • Signed assertion account creation, repeated sign-in, explicit account linking, cross-account identity collision refusal, unverified-email confirmation without tokens, tenant routing/pin isolation and populated refusal-state preservation
Not implemented (2)
  • Daemon trust configuration, discovery/JWKS retrieval and rotation, hosted redirects, authorization-code exchange, external IdP traffic and SDK end-to-end execution
  • SAML XMLDSig, assertion replay consumption and production parity; a repeated valid bearer ID token may sign in again
Notes (3)
  • Local evidence: crates/fireemu-adapter-http/tests/auth_oidc_assertions.rs; public contract and remaining limits: docs/compatibility/auth-federation-assertion-local.md
  • The default daemon, handle and handle_with remain emulator fixture mode. This entry is not a claim that provider-config CRUD enables signed assertion verification in the shipped daemon
  • Finite local policy: single pinned key, RS256 only, required matching kid, 2048-8192-bit RSA, 64 KiB token bound, integer timestamps with no clock skew; exact production error precedence and boundary policies remain unmeasured
AUTH-OAUTH-0Implemented
Implemented (7)
  • accounts:signInWithIdp (verifyAssertion): the credential is read from requestUri and/or postBody (the official getNormalizedUri merges the request URI query, the post body and the URI fragment); providerId is lowercased; the id_token (a fake JWT or strict JSON) or a JSON access_token carries the claims; no OAuth round trip
  • per-provider assertion shaping (fakeFetchUserInfoFromIdp): google.com reports federatedId https://accounts.google.com/<sub>, firstName / lastName and a granted-scopes rawUserInfo; saml.<name> reads the email from the SAML assertion subject nameId, forces emailVerified true and uses the attributeStatements as rawUserInfo; oidc.<name> and every other provider keep the JSON claims as rawUserInfo
  • SAMLResponse (a JSON blob, not real XML) is validated for assertion / subject / nameId with the official INVALID_IDP_RESPONSE messages
  • create-or-link semantics: match by (providerId, rawId), then by a verified assertion email (linking, recycling an unverified-email account by clearing its password, phone and providers and bumping validSince, and reporting emailRecycled), else needConfirmation for an unverified assertion email owned by an existing account, else a new account; linking to a session idToken with FEDERATED_USER_ID_ALREADY_LINKED and returnIdpCredential errorMessage handling
  • the id_token vs access_token NotImplementedError reproduction (google.com / apple.com and any other provider), and accounts:createAuthUri answering NotImplementedError for a providerId
  • the identity-provider login widget pages /emulator/auth/handler (the sign-in-via-popup / redirect handoff, listing the accounts already linked at the provider) and /emulator/auth/iframe (the gapi relay page), reproduced from the pinned widget_ui.js
  • ID tokens carry firebase.identities[providerId] and sign_in_provider = providerId
Notes (2)
  • the official emulator stores no SAML / OIDC provider configuration (defaultSupportedIdpConfigs / inboundSamlConfigs are in its OpenAPI spec but not served), so a saml.* / oidc.* provider needs no registration and there is none to round-trip
  • conformance: measured by crates/fireemu-adapter-http/tests/auth_flows.rs and crates/fireemu-core-auth/tests/store.rs against the pinned firebase-tools operations.js / widget_ui.js; no gated corpus scenario was added in this pass
AUTH-PROVIDER-CONFIG-1Implemented
Implemented (4)
  • project and tenant OAuth IdP and inbound SAML provider configuration create/get/list/update/delete under the public v2 namespaces
  • project and tenant default supported IdP configuration (defaultSupportedIdpConfigs: enabled, clientId, clientSecret, appleSignInConfig) create/get/list/update/delete under the public v2 namespaces, from the REST reference; which IdP IDs production accepts and how it validates the members are unobserved
  • project, tenant, OIDC and SAML stores are isolated; validation completes before mutation and snapshots never export provider configuration or OIDC client secrets
  • bounded creation-order pagination uses a namespace-local provider cursor and rejects unknown cursors
Notes (2)
  • this is a production-oriented local API implementation; it has no production or external-IdP round trip yet
  • the pinned official emulator does not serve these configuration resources, so official-emulator fixture assertions cannot establish this capability
AUTH-RULES-1Implemented
Notes (4)
  • request.auth from verified ID tokens on every Firestore surface; Bearer owner bypasses
  • the compatibility profile decides how much verification a caller's token must survive: under firebase the unsigned mock tokens the official emulators admit are admitted too (an unknown sub, an exp nobody reads), so @firebase/rules-unit-testing works unchanged; under strict the issuer, audience, virtual-clock expiry, subject and revocation are all checked
  • two checks stay on in both profiles, and spec/compatibility/contract.json records them as deliberate divergences of the emulator profile: the audience must name the routed Firestore resource project or Storage bucket project, and a token that claims to be signed must verify against the session key
  • a valid bare Storage bucket routed through the default session binds emulator-profile mock tokens to that bucket's project namespace without changing object ownership, tenancy, App Check, fault injection or resumable-upload routing, and without registering a session
AUTH-TOKEN-1Implemented
Implemented (2)
  • unsigned-emulator (alg none, the Firebase Auth Emulator format)
  • session-rsa (RS256 with a 2048-bit key derived from the session seed; JWKS at /.well-known/jwks.json and the securetoken@system.gserviceaccount.com path)
Notes (3)
  • with session-rsa every surface (Identity Toolkit, Firestore rules, Storage rules) refuses unsigned or foreign-signed tokens
  • the Firebase Admin SDK accepts only alg none while FIREBASE_AUTH_EMULATOR_HOST is set, so verifyIdToken through the Admin SDK needs unsigned-emulator
  • conformance: auth/sign-up-and-sign-in-errors (conformance/README.md); this records agreement with the official Local Emulator Suite, never with production

Cloud Storage

CapabilityStatusDetails
ST-OBJ-1Implemented
Implemented (10)
  • Firebase Storage protocol (/v0/b, the X-Goog-Upload resumable command grammar with terminal denied sessions, download tokens incl. the ?create_token / ?delete_token routes and production's first-metadata-read token minting (a media read does not mint; the official emulator mints on both), the official plain-status and rules-denial bodies)
  • JSON API with /b short spelling for every object method, /storage/v1 long spelling including object PATCH and copy/rewrite (the official emulator answers 501 on these spellings; a published divergence), /download/storage/v1 reads only (GET), /upload/storage/v1 uploads, the resumable session cancelled by a DELETE on its URL (499, as production answers it, where the official emulator answers 501; a status query after it is 499 too) and started with no body and a session URL that repeats the start query, the XML-style GET /{bucket}/{object} fallback, GET /b bucket listing, multipart/form-data POST /{bucket} upload and the ACL stub
  • generation / metageneration preconditions (match / not-match / source) honoured as production honours them under strict, and declared-checksum verification under strict only (the official emulator verifies none, and the emulator profile verifies none) (a not-match guard naming the current value is a bodiless 304, a match guard that fails or has a negative value (recorded: -1) is production's 412 body, a not-match guard with a negative value is accepted, a value that is no long is production's 400 Invalid long value and the write is not done) and ignored by the emulator profile, as the official emulator ignores them; PUT updates metadata in both profiles and replaces the custom metadata and download tokens when it carries metadata; the official emulator verifies no declared checksum, yet the emulator profile still refuses an upload whose declared checksum does not match (a known over-refusal, to be removed), and every such step is pinned as a documented divergence in conformance/storage-matrix.json
  • generation selectors on the JSON API dialect: a live or a noncurrent generation of a versioned bucket, for reads and for deletes by number
  • object versioning on the JSON API dialect (a production-side extension; the official emulator has none): GET and PATCH /storage/v1/b/{bucket} (and, under strict only, the short spelling /b/{bucket} the Admin SDK uses against a local endpoint: the official emulator has no route for it, so the emulator profile keeps its answers, a missing-object 404 on GET and the 501 catch-all on PATCH) carry `versioning` only, as a tri-state recorded in FE v5 (functions-events-formal-20261004T182904Z): never configured has no `versioning` member, enabled answers {"enabled":true} and disabled keeps answering {"enabled":false} on the PATCH and every later read (the default buckets, a bucket holding an object, a configured bucket and a bucket named in firebase.json, declared by a registered session or, under strict, named by a deployed Storage trigger exist, even when empty (the strict profile follows production, where a trigger needs its bucket; the emulator profile keeps its earlier answer); any other is 404 with the recorded `The specified bucket does not exist.` body; `fields` takes a, a/b and a(b)); a versioned bucket keeps the generation an overwrite or a plain delete replaces as a noncurrent version (`timeDeleted`) and announces it with an Archived event whose payload and time are recorded; `versions=true` lists every generation (the item shape is recorded) with a key-based page token (a token that does not read is 400) and honours `startOffset`, `endOffset` and `matchGlob` on every generation under strict, as it does for a plain listing (a versions listing with a filter is UNRECORDED); the `generation` selector reads, patches and deletes one version and `sourceGeneration` copies or rewrites from one (a delete of a noncurrent generation announces Deleted with `timeDeleted` set to the archive instant, recorded; an unknown generation on a patch is 404 in the strict profile, while the emulator profile keeps patching the live object as the official emulator does); noncurrent data counts toward storage.maxStoredBytes (the only bound), an overwrite needs two event reservations (it can be refused when the outbox has room for one), and export/import carries live objects only (a local-only difference); UNRECORDED: whether a plain delete of a live object also announces Deleted (it does not here), whether a metadata change of a noncurrent version announces anything (it does not here), the full bucket resource beyond `versioning`, the 400 bodies of a bucket PATCH, and the page token of a versions listing and its 400
  • prefix / delimiter listing with a combined page budget for items and folded prefixes, a 1000-entry cap and no prefix repeated across pages; unlike the official emulator, which pages items alone and repeats every folded prefix; the Firebase dialect's page token is the standard base64 of the last entry returned (as production answers it) where the JSON API's names the first entry of the next page, and maxResults=0 is production's 400 under strict and an empty page under the emulator profile
  • MD5 / CRC32C (X-Goog-Hash verified before commit under strict; the emulator profile stores the bytes whatever hash was declared, as the official emulator does)
  • strict-profile production framing for the request shapes the STORAGE-OBJECT recordings cover: recorded header sets, two-space JSON layout and member order, microsecond-timestamp generations, the protobuf etag, newest-first minted download tokens, running-hash headers on a 308, list filters startOffset / endOffset / matchGlob; the emulator profile keeps the official emulator's headers and bodies (see docs/compatibility-contract.md)
  • Range: bytes=a-b / a- / -n; an unsatisfiable range serves the whole object in the emulator profile, while strict returns 416 with Content-Range: bytes */N
  • object resources follow the production recording of stage 3 v9 (2026-09-30) wherever no refusal is involved: timestamps with three fractional digits, timeFinalized on the JSON API resource, base64 crc32c and a default contentDisposition of `inline; filename*=utf-8''<file>` on the Firebase dialect, no `metadata` for an empty custom map and no `downloadTokens` for an object without a token, the `Not Found.` JSON for an absent object, the `Parser Error` and `Parse Error: ...` bodies of a malformed PATCH, an empty resumable start and the `Upload has already been finalized.` late cancel; each differs from the official emulator and is pinned as a documented divergence in conformance/storage-matrix.json
Not implemented (3)
  • ACLs (the official emulator's no-effect POST acl stub is served) and signed URLs
  • compose
  • notifications
Notes (8)
  • Storage object triggers are not part of this entry: FN-EVT-1 delivers object finalized / deleted / metadataUpdated / archived events, and this entry owns the object surface only
  • objects are at most 256 MiB and request bodies at most 260 MiB (413 beyond that)
  • under strict the Storage emulator refuses what production refuses and the official emulator accepts: an anonymous GET /{bucket}/{object} of an object that is not public (403 AccessDenied; makePublic, predefinedAcl=publicRead and an allUsers READER ACL entry grant it for one generation, storage.publicBuckets lists buckets whose IAM grants allUsers; the refusal names both), an expired or unreadable signed URL (400), a form upload without a signed policy or against its policy (403 / 400), a Firebase ID token as a JSON API bearer (401 Invalid Credentials), an unknown ACL entity or role (400) and a Firebase list with an unreadable maxResults or pageToken (400); the XML API texts and the last three are unrecorded; signatures are never verified and any other JSON API credential is served as the owner (documented development defaults); a write to a bucket nothing declared (not a default bucket, a firebase.json target, a trigger bucket, imported data or listed in storage.buckets / storage.publicBuckets; storage.defaultBucket makes the other default name undeclared) is served and creates the bucket, with one note: line on standard error per bucket, where production answers 404
  • storage.maxStoredBytes (unset by default) bounds the object data the store retains; a write past it answers 402 storage.maxStoredBytes limit exceeded (reason storageCapacityExceeded on the JSON API) and changes nothing, and the objects.bytes resource gauge reports the bound
  • upload bytes are moved into the object store, never copied: one payload-sized buffer per upload
  • request bodies buffered at the same time are admitted against a process-wide 1 GiB budget; an upload that does not fit is refused with 503 and Retry-After: 1
  • download tokens are UUID-shaped, ride in the firebaseStorageDownloadTokens custom-metadata key, die with their generation, and every token change is a metadata update with its event, as measured against the pinned suite
  • conformance: storage/admin-upload-download-metadata, storage/client-protocol-and-rules (conformance/README.md) and the 218-step raw-HTTP storage probe (conformance/storage-matrix.json, pnpm -C conformance run storage-probe); this records agreement with the official Local Emulator Suite, never with production
STORAGE-RULES-1Implemented
Implemented (8)
  • service firebase.storage: get / list / create / update / delete with request.resource and resource, measured against the official runtime: request.resource is the whole prospective object (generation, metageneration, timestamps and hashes included), request.resource is null on get / list / delete, crc32c is the decimal spelling the official runtime passes, and the denial bodies are the official Permission denied. No READ|WRITE|LIST permission. envelopes
  • resumable uploads are authorized at finalization with the received bytes; a refusal is terminal for the session and no object is published
  • list requires rules_version 2 (production answers 400 with `Listing objects in a bucket is disallowed for rules_version = "1".` and asks for version 2, before any rule is read; both profiles do)
  • the Firebase protocol goes through the rules; the JSON API is a fully privileged surface on which rules never run, whatever credential is presented, exactly as the official emulator serves it
  • object-form firebase.json applies one ruleset to every bucket; array form resolves every target through .firebaserc.targets[project].storage and selects an isolated, hot-reloadable ruleset by request bucket; an unmapped bucket is denied for end-user requests before mutation or event publication, while the explicit owner credential keeps its documented Rules bypass
  • the official /internal/setRules endpoint and fireemu's /v1/storage/rules extension replace target mode with one global ruleset, as firebase-tools rebuilds its rules manager; configured target-file watchers retire after that explicit runtime override, while a global configured file remains watched across global updates
  • firestore.get() / firestore.exists() over the latest Firestore state of the project (STORAGE-RULES-FIRESTORE-ACCESS: two per request)
  • a run with no loaded ruleset (no storage.rules configured, or the rules dropped through the control API) denies every end-user request on the Firebase dialect, as the rules a production project is created with do (strict answers production's 400; the emulator profile keeps a 403 except for a demo-* project, which gets the official emulator's default open rules); the startup banner says so
Notes (7)
  • with no loaded ruleset the official emulator denies the Bearer owner / Firebase owner credential too, because it checks the ruleset before the admin credential; fireemu keeps the owner bypass ahead of the ruleset check, so the emulator's own privileged surfaces (the JSON API, import and the control API) stay reachable on a run that has no rules file
  • an upload over an existing object evaluates the create method, as production (stage 3 v9 recording) and the official emulator both do; a Firebase v0 PATCH of an absent object is refused with 403 as a write, as production answers every caller, where the official emulator answers 404
  • resource.cacheControl and resource.contentLanguage are production rules fields fireemu keeps; the official emulator's rules resource omits them, so a rule reading them raises there (pinned in conformance/storage-matrix.json)
  • end-user credentials on the Firebase dialect: under strict only `Firebase` followed by a value without a dot is an anonymous caller (the recorded shape); every other value that fails to decode, a `Bearer` value and a three-segment value with a bad signature, kid or algorithm included, is a 401; the emulator profile maps every decode failure to an anonymous caller and never admits a token that fails verification as its claimed user, where the official emulator admits a decodable three-segment value as its user (the rules.idTokenVerification divergence of the compatibility contract); an ID token whose refresh tokens were revoked is honoured on Storage in both profiles, as production honours it; under the emulator profile a token is admitted whatever audience it names, as the official emulator admits it (firebase-tools 15.28.2 never reads aud), and the strict profile answers a token of another project with production's 403 `Permission denied.`
  • strict profile answers as production was recorded answering: every rules refusal with production's bytes (two-space pretty JSON, `application/json; charset=UTF-8`, the message `Permission denied.`), a malformed Bearer credential on the JSON API object PATCH with its 401 Invalid Credentials, JSON framing (uppercase charset, the JSON API's error bodies in the Google pretty layout, a bare application/json on its 204), and request.resource.metadata as null for an upload with no custom metadata; the emulator profile keeps the official emulator's wording, framing and empty map, and ignores the credential on the JSON API
  • with no ruleset loaded: strict answers a bucket without a release with production's 400 (bucket not set up properly); the emulator profile admits every end-user request for a demo-* project, as the official emulator does with its default open rules, and keeps its own fail-closed 403 for any other project (the official emulator refuses to start there without a rules file)
  • conformance: storage/client-protocol-and-rules (conformance/README.md) and the rules-request-model program of the storage probe (conformance/storage-matrix.json); this records agreement with the official Local Emulator Suite, never with production

Cloud Functions

CapabilityStatusDetails
FN-EVT-1Implemented
Implemented (15)
  • Firestore document created / updated / deleted / written triggers (v2, JSON CloudEvents; v1 document handlers)
  • Firestore *.withAuthContext variants: authtype / authid of the committing principal. Strict names it as production does (recorded in FE v5 and v7): a write with a Firebase ID token is `api_key` with the user's uid, a write by the owner (the local stand-in for the credential) is `unknown` with the credential's own id (`owner` here), and any other principal (unauthenticated, system) keeps its own name; UNRECORDED: a write by a real service account (production documents `service_account` with the account's email; the owner still maps to the one recorded `unknown`) and ID-token kinds other than email-and-password users (anonymous, custom token, other providers all map to `api_key`). The emulator profile sends the official emulator's constants, `unknown` and `fake-auth-id@gmail.com`, for every writer (the Firestore emulator jar's FunctionsEmulatorEventPublisher)
  • Storage object finalized / deleted / metadataUpdated triggers (v2 and v1; an overwrite also announces deleted for the replaced generation in a bucket that is not versioned, stamped with the replacement's creation instant, as production does (recorded, FE v5, functions-events-formal-20261004T182904Z) and unlike the official emulator, which announces finalized only), and archived triggers (onArchive / onObjectArchived) for versioned buckets, whose payload and time are recorded (FE v5) and whose order is not fixed by production (observed: finalized before archived 4 of 4, deleted before finalized 3 of 4); whether a plain delete of a live object also announces Deleted is unrecorded
  • Pub/Sub topic triggers (v2 onMessagePublished, v1 topic().onPublish) fed by POST /v1/sessions/{s}/pubsub/topics/{t}:publish or /v1/projects/{p}/topics/{t}:publish
  • the Pub/Sub wire bridge preserves the canonical source topic resource and invokes a topic trigger only when that topic belongs to the Functions runtime project; a same-named topic in another project cannot cross the session boundary
  • Auth user created / deleted events to v1 auth.user() handlers (Admin deleteUsers delivers no deleted event, as the 1st-gen Auth documentation says and FE v5 recorded in two passes (functions-events-formal-20261004T182904Z: 4 users, 0 frames), unlike the official emulator, which fires one per user)
  • Identity Platform Blocking Functions: v2 beforeUserCreated / beforeUserSignedIn and v1 auth.user().beforeCreate / beforeSignIn run synchronously before an Auth commit in the Functions runtime's project; tenant stores forward tenantId and a tenant resource, other registered projects are isolated, rejection rolls the Auth operation back, and displayName, photoUrl, disabled, emailVerified, customClaims and before-sign-in sessionClaims are applied before tokens are issued
  • Eventarc custom events (onCustomEventPublished) fed by POST /projects/{p}/locations/{l}/channels/{c}:publishEvents on the dedicated Eventarc port, which CLOUD_EVENTARC_EMULATOR_HOST names, so firebase-admin getEventarc().channel().publish() reaches them unconfigured; the proto CloudEvent is converted the way cloudEventFromProtoToJson converts it and eventFilters are matched as matchesAll matches them
  • Firebase alerts (the whole onAlertPublished family) fed by POST /google/publishEvents, which is the only door the official emulator has for them too: an alert trigger is an Eventarc trigger on the sentinel google channel, and the CloudEvent is forwarded verbatim
  • the Eventarc listener maintains the live Functions trigger table through POST /emulator/v1/projects/{project}/triggers/{trigger}, its exact removal counterpart and GET /google/getTriggers; duplicate definitions remain visible and dispatch independently, removal stops delivery immediately, ${PROJECT_ID} is substituted, and count and retained-byte admission are atomic and bounded
  • Cloud Tasks queue functions (onTaskDispatched) enqueued with the Admin SDK's getFunctions().taskQueue(name).enqueue(payload) against CLOUD_TASKS_EMULATOR_HOST, which names the dedicated Tasks port; the queue is the function, its defaultUri is that function's own URL, and a dispatch carries the official X-CloudTasks-* header set, starts from the official empty token bucket, enforces maxConcurrentDispatches and production's maxDispatchesPerSecond contract across initial and retry attempts, and retries on the official backoff
  • GET /queueStats on the Tasks listener reports the pinned queue statistics shape (pending count, five-minute added/failed rates, completed-last-minute count, running slots and configured limits), uses each event's exact timestamp with the upstream strict-greater-than window boundary, and does not mutate dispatch or rate-refill state
  • Cloud Tasks scheduleTime: the strict profile holds a task until its scheduleTime on the native clock (the virtual task clock gates on the virtual clock as before), refuses a scheduleTime that is not RFC 3339, or more than 30 days ahead of the queue's clock (UNRECORDED text), with 400, and releases waiting tasks with POST /v1/sessions/default/functions/{queue}:run (all, or one with {"task": id}) and the Functions page of the Emulator UI; the emulator profile dispatches at once, as the official emulator does
  • retries with virtual-time backoff, dead letters, await-idle
  • bounded diagnostic retention: the 1000 most recent invocation records, 500 dead letters and 1000 terminal event records are kept (older ones are dropped) while the succeeded / deadLettered / overlapRejected counters stay cumulative; every retained record carries a monotonic sequence inside a generation the reset bumps, and the UI log stream consumes cursor deltas and resyncs when its cursor is expired or from an older generation
Not implemented (2)
  • Realtime Database triggers (the daemon serves the database but delivers no events from it yet) and Remote Config triggers (no such service in the daemon)
  • protobuf CloudEvents payloads (JSON is what firebase-functions consumes)
Notes (9)
  • Blocking Functions use a bounded synchronous loopback bridge. The strict profile waits production's seven-second response deadline and the emulator profile the official emulator's sixty seconds; an explicit Functions SDK error below 500 other than 429 answers 400 with the function's own error body in the message, a 429 or 5xx answer, an unhandled handler or transport failure, and a function's own timeout within the deadline answer production's opaque 503 Error code: 47, and an elapsed deadline answers 400 Cloud function deadline exceeded, without publishing speculative user state. A blocking configuration that changes while a request runs is a retryable 409 in the strict profile only
  • firebase-tools 15.28.2 configures only beforeCreate and beforeSignIn in the Auth emulator, and the emulator profile does the same; the strict profile also runs beforeEmailSent before a password reset or email sign-in mail, as production does, and lists beforeSmsSent without running it
  • Eventarc and Cloud Tasks support listeners start with a configured Functions runtime on the official default ports 9299 and 9499; selecting either support service without a Functions codebase is an accepted no-op, matching firebase-tools 15.28.2
  • no fireemu-only alert-injection route exists: firebase-tools has none either, and its Emulator UI fires an alert by POSTing to /google/publishEvents like any other client
  • Eventarc trigger definitions are bounded to 256 KiB after project substitution, the registry to 4096 entries and 16 MiB including owned lookup and display indexes, and all active Functions events to 4096 records and 64 MiB; Eventarc is capped at 3072 records and 48 MiB so other sources keep a reservation, and that queue bound is the only limit on a publication (the emulator profile has no per-publication cap on events or deliveries, as the official emulator has none; a publication that would overflow the queue is refused whole with 429 and nothing enqueued), full custom channel names retain the project boundary, successful official google routes expose CORS only to a loopback origin, and OPTIONS follows the pinned emulator route miss
  • Eventarc removal treats JSON object member order as semantically irrelevant, and a source reload rebuilds the manifest-owned table at the next exact region/name/generation key rather than retaining manually duplicated stale-generation registrations
  • a task may not name a URL of its own (TaskOptions.uri): only this emulator's function URL is dispatched to, because a local emulator making an arbitrary outbound request on a caller's say-so is a request-forgery surface the rest of fireemu does not offer
  • the pinned emulator bounds each pending queue at 10000 tasks but retains completed task names forever; fireemu additionally bounds all outstanding task data to 10000 tasks and 64 MiB, with completed duplicate-history queue and task names retained inside that byte budget and capped at 65536 entries and 8 MiB of identifier text across all queues, returning 429 when the process-wide safety budget is exhausted
  • task retry backoff runs on real time, not the virtual clock, as it does upstream and in production; the default unit is 100 ms, so three attempts cost 300 ms rather than a clock advance, and await-idle waits for a task across its attempts
FN-HTTP-1Implemented
Implemented (7)
  • onRequest / onCall (v2) and v1 HTTPS functions proxied to the runner at /{project}/{region}/{function}[/path], with the path and query below the mount point reaching the handler
  • v2 callable streaming for the exact Accept: text/event-stream protocol: response.sendChunk values and onCallGenkit stream values arrive progressively, the callable final result remains pending until completion, and a client disconnect aborts the runner response signal and releases runtime admission
  • streamed v2 responses retain one bounded backpressure slot, obey the function deadline, and enforce production's 10 MiB streaming response limit cumulatively without buffering the whole response
  • the official emulator's two 404s: `Not Found` (text/plain) for a path that is not a function route, and `Function <region>-<name> does not exist, valid functions are: <trigger keys>` (text/html) for a route whose function is unknown, with the keys in the official order (grouped by region, export order within a region)
  • the strict profile refuses a call without credentials to a function whose invoker is not public (`private` or a list of service accounts) and to every task-queue function on the public function URL with the front end's 403 page (UNRECORDED: the page text is the documented one, not a measured shape); a bearer shaped like a Google-issued identity token (a JWT whose iss is https://accounts.google.com or accounts.google.com) is let in because Google's verification of its signature cannot run locally, and any other bearer, a Firebase Auth ID token included, gets 401; task dispatch, the Emulator UI and the control API call as the project owner (the control token in X-Fireemu-Owner-Token, never forwarded to the function); ingressSettings is recorded and not enforced
  • timeoutSeconds: a function that overruns it gets the official answer, HTTP 500 with the body {"code":"ECONNRESET"} and no content-type, and the emulator logs `Your function timed out after ~Ns.`
  • in the emulator profile, a default onRequest function uses the official enableCors wrapper: a preflight answers 204 with the origin reflected, GET,HEAD,PUT,PATCH,POST,DELETE, the requested headers and Vary, and an ordinary answer reflects the origin; explicit cors: false disables this wrapper, while the strict profile leaves a default preflight to the handler
Notes (6)
  • needs functions.source (or --functions <dir>) and the bundled Node runner; the codebase must have express installed (a dependency of firebase-functions)
  • the real Firebase Web SDK .stream() acceptance test covers ordinary onCall and onCallGenkit chunks, final results, HttpsError after a chunk, non-streaming sendChunk, downstream cancellation, and admission release
  • streaming accepts only an absent or identity Content-Encoding because the production 10 MiB quota is measured on uncompressed response data; refusing an encoded local runner response prevents compressed data from bypassing the limit
  • conformance: functions/callable-error-envelope, functions/callable-auth-context, functions/http-routing-cors-and-timeouts (conformance/README.md); this records agreement with the official Local Emulator Suite, never with production
  • both profiles admit callable requests from loopback, external and LAN origins; callable OPTIONS reaches the SDK CORS wrapper without Auth or App Check admission, respecting cors:false and profile-specific Origin lists, with the SDK response and Vary forwarded unchanged
  • the timeout does not kill the runner; the official emulator kills the runtime process of the one trigger it runs per process, and one fireemu runner serves a whole codebase
FN-SCH-1Implemented
Implemented (4)
  • onSchedule with cron and App Engine schedules driven by the virtual clock (scheduler.catchUp = all | latest | none; all is capped per advance with the remainder kept due; latest and none compute the run they keep without walking the missed ones, and record one skipped summary per job and clock change whose count is exact up to scheduler.maxCatchUpRuns and "at least N" beyond it)
  • manual runs via POST /v1/sessions/{s}/functions/{name}:run
  • IANA time zones with daylight-saving rules (chrono-tz; gaps skipped, ambiguous times run once)
  • scheduler.overlap = allow | skip | queue | reject | skip-in-flight (the strict profile's default: an occurrence is dropped while a handler of the function that started before the clock move is still executing; the emulator profile's default is allow)
Notes (5)
  • strict profile: a scheduled run reaches a handler as production's Cloud Scheduler delivers it (second delivery recording, 2026-10-05): a Gen2 function is called through its HTTP wrapper with POST /, x-cloudscheduler headers carrying the job id and the schedule time written in America/Los_Angeles with its offset, and an empty body, so the SDK builds the event, its context getter and its 500 on a throw; a Gen1 handler gets the Pub/Sub context (17-digit message id, the topic as resource.name, the PubsubMessage resource type); a Gen1 schedule that names no time zone runs in America/Los_Angeles; the emulator profile keeps the direct handler call, the job's resource name and a UTC time
  • strict profile: a schedule whose job production refuses is refused at startup with Cloud Scheduler's message: retryCount 6 or more, and a fractional maxRetrySeconds; the emulator profile accepts them, as the official emulator does (it creates no Scheduler job)
  • strict profile: an occurrence of a function is not run while a handler of that function, started before the clock move, is executing (recorded: an every 1 minutes job with a 100 second handler started every 2 to 3 minutes and never while one was in flight); a manual run is not refused; whether in flight ends at the function timeout or at the handler's end is not determined by the recording
  • both profiles: a Gen2 schedule with a retry window and no retry count (maxRetrySeconds > 0, retryCount 0) retries until the window ends, as production did for an HTTP target; a Gen1 schedule's handler is never retried, whatever it declares (its job targets Pub/Sub, so Cloud Scheduler's retry covers the publish; the recorded Gen1 handler that threw ran once per occurrence)
  • not claimed: the phase of an interval schedule (production's first occurrence was on the minute and later ones kept a constant per-job fraction of a second; the anchor is not determined by the recording, and it does not follow the job's creation instant), the alignment of an every 5 minutes job after forced runs, Cloud Scheduler's run-now (manual runs are Fireemu control), the OIDC and trace headers, a Gen1 schedule's topic and published message, the fourth gap of a retry chain with fewer than five doublings (about 18 seconds, seen twice with one parameter set, where the documented formula gives 32); see docs/compatibility/scheduled-functions-delivery.md
FN-DISCOVERY-1Implemented
Implemented (9)
  • every exported function the runner cannot serve is reported, never dropped: the manifest carries an `ignored` array with the name, region, trigger family and product scope, GET .../functions publishes it, and the daemon prints one `functions[<region>-<name>]: function ignored (<family>): <reason>` line per entry
  • an export whose endpoint cannot be described -- a v1 provider whose lazily-computed __endpoint throws -- becomes one ignored record naming it rather than a dead runner
  • export order is the codebase's, not the ES module namespace's alphabetical order, so the emulator lists functions the way the official one does
  • every selected codebase declared in a firebase.json `functions` array is loaded, one runner process per codebase, behind one runtime and one functions port; --only functions:<codebase> selects one; at most 32 codebases may be selected in one fireemu run
  • a function name two codebases both export is refused, naming both
  • `source`, `codebase`, `runtime` and `ignore` are read; a codebase whose runtime is not nodejs* is refused with the language named
  • the bundled runner selects Node before loading user code: functions.runner and FIREEMU_NODE are exact overrides; on Unix automatic candidates are the first Node on PATH plus installed runtimes under VOLTA_HOME, a Node 20+ request prefers the capability to synchronously load ES modules from CommonJS before the firebase runtime major, package.json engines.node and stable discovery order, and every effective mismatch is reported without evaluating user code twice; on Windows FIREEMU_NODE or the first PATH Node preserves the previous runner behavior
  • source, compiled output, dotenv, local-secret and runtime-config changes start a fresh discovery runner and atomically replace the old generation only when its trigger manifest is unchanged; every non-ignored byte remains content-hashed, while one daemon-wide scan admission paces aggregate steady-state source work to 64 MiB/s and 20,000 directory entries/s, rejects trees above 100,000 entries or 128 directory levels, and cancels abandoned scans and snapshots between 64 KiB chunks
  • SIGINT and SIGTERM cleanly close each Node runner through its shutdown protocol, reap the runner's process group including descendants, release the Functions and Hub listeners, and remove the daemon's exact Hub locator without signalling unrelated processes
Not implemented (1)
  • Python (functions-framework) and Dart loader paths: refused by name, never handed to node
Notes (2)
  • a trigger family belonging to a product fireemu does not serve fails discovery by default, naming every such function; functions.unservedTriggers = "report" asks for the official emulator's carry-on instead, which prints the line and starts anyway
  • the official emulator never fails on an unsupported export: it logs `Unsupported trigger` (DEBUG) or `Unsupported function type on <name>` (WARN) and records the definition with ignored: true. fireemu keeps that inventory and parts company only on the product decision
FN-ENV-1Implemented
Implemented (10)
  • under the emulator profile, the runner inherits ordinary SCREAMING_SNAKE variables from the daemon process before dotenv, system and emulator overrides, matching wrappers such as dotenv -- firebase emulators:exec while emulator-owned names and ambient Google credentials stay isolated; the strict profile keeps the entire parent environment isolated
  • bound Pub/Sub and Emulator Hub listeners are exported to the runner as PUBSUB_EMULATOR_HOST and FIREBASE_EMULATOR_HUB using bare host:port values
  • reload snapshots retain runtime files including .env.local and .secret.local even when deployment or watcher ignore patterns match; configured ignores still control change detection
  • the dotenv chain in the official order and dialect (firebase-tools lib/functions/env.js): .env, .env.<projectId>, .env.<alias>, and emulator-only .env.local, later files overriding earlier ones
  • the official refusals, with their sentences: both a projectId and an alias file present, a line that is not an assignment, a reserved key, a key that is not SCREAMING_SNAKE, a key under a reserved prefix
  • the official quoting: single-quoted literal, double-quoted expanding \n \r \t \v \\ \' \", unquoted ending at the first #, and a quoted value allowed to cross lines
  • .secret.local for defineSecret parameters, parsed by the same dotenv rules and exposed only during an invocation of a function that declared the corresponding secret
  • .runtimeconfig.json passed through as CLOUD_RUNTIME_CONFIG; a malformed one is reported and treated as absent
  • the variables the official emulator gives a runtime: FIREBASE_CONFIG (storageBucket, databaseURL, projectId), GCLOUD_PROJECT, GOOGLE_CLOUD_QUOTA_PROJECT, FUNCTIONS_EMULATOR, K_REVISION, PORT, TZ=UTC, METADATA_SERVER_DETECTION, plus per-invocation FUNCTION_TARGET, FUNCTION_SIGNATURE_TYPE and K_SERVICE
  • defineString / defineInt / defineBoolean / defineFloat / defineList resolve as a deploy resolves them: a value from the dotenv chain, then the declared default, applied as each parameter is declared so a read at the top of a module sees it; a parameter with neither refuses the codebase, naming it and the files a value goes in (defineSecret values come from .secret.local)
Not implemented (1)
  • Google Cloud Secret Manager as a fallback for a secret .secret.local does not carry: fireemu holds no credentials and makes no network call, so the parameter resolves as an unset variable does
Notes (4)
  • the pinned firebase-functions@7.3.2 has removed functions.config(); CLOUD_RUNTIME_CONFIG is still set, as the official emulator sets it, and no supported SDK surface reads it
  • FUNCTION_TARGET, FUNCTION_SIGNATURE_TYPE and K_SERVICE name the most recently started invocation: the official emulator runs one runtime process per trigger and can set them once, and one fireemu runner serves a whole codebase
  • a codebase with local secrets serializes handler environment setup because process.env is shared; this prevents one function from observing another function's declared secret
  • fireemu also sets GOOGLE_CLOUD_PROJECT, which the official emulator does not; the Google client libraries read either name

Cloud Pub/Sub

CapabilityStatusDetails
PUBSUB-WIRE-1Implemented
Implemented (9)
  • the google.pubsub.v1 Publisher and Subscriber services over gRPC on the loopback port PUBSUB_EMULATOR_HOST names (default 8085): CreateTopic / GetTopic / ListTopics / ListTopicSubscriptions / DeleteTopic, CreateSubscription / GetSubscription / UpdateSubscription / ListSubscriptions / DeleteSubscription, Publish, Pull, StreamingPull, Acknowledge, ModifyAckDeadline and Seek (to a timestamp)
  • REST/JSON on the same port for the implemented topic, subscription, snapshot, publish, pull, acknowledge, modify-ack-deadline and seek operations; resource creation follows the annotated PUT routes, subscription FieldMask updates validate every selected value before one atomic broker update, and supported retry, dead-letter, filter and ordering fields round-trip between REST and gRPC
  • topic creation and the unsupported UpdateTopic boundary explicitly reject behavior-changing fields the broker cannot represent (schema settings, retention, KMS, storage policy, ingestion settings, message transforms and tags) before any topic mutation; name and labels remain supported
  • topic and subscription state: publish fans a message out to every subscription whose filter admits it; a subscription reports _deleted-topic_ once its topic is deleted
  • delivery semantics: ack ids and message ids assigned deterministically under the daemon seed, ack deadlines and redelivery of unacked messages on the virtual clock, nack (ModifyAckDeadline 0) for immediate redelivery, ordering keys that hold a key's later messages until the earlier one is acked, subscription filters over attributes (equality, inequality, existence, hasPrefix, NOT/AND/OR), dead-letter forwarding after maxDeliveryAttempts, and seek-to-timestamp replay
  • Pub/Sub-native subscription snapshots: CreateSnapshot / GetSnapshot / ListSnapshots / ListTopicSnapshots / UpdateSnapshot / DeleteSnapshot and seek-to-snapshot retain the source subscription's unacknowledged backlog plus every later topic publication; the lifetime is seven days minus the age of the oldest unacknowledged source message, and creation is refused below one remaining hour
  • loopback HTTP push delivery for configured subscriptions: the Pub/Sub JSON push envelope is sent sequentially by bounded workers, a 2xx response acknowledges the message, and finite failures nack the message for pull-compatible redelivery; a subscription with a retry policy waits that policy's backoff, and one without is redelivered as soon as possible, as production does, unless pubsub.pushMinimumRedeliveryIntervalMillis opts a minimum interval in (default 0); separately, push delivery carries a subscription-level backoff that consecutive failed attempts lengthen and an accepted delivery clears, which holds every message on that subscription and no other subscription; the backoff belongs to the subscription incarnation that earned it, so deleting and recreating a subscription starts it clean and a delivery still in flight for the deleted incarnation never changes the new one's throttle
  • input hardening: resource names validated against the documented Pub/Sub rules (3..=255, leading letter, the [A-Za-z0-9._~%+-] alphabet, no goog prefix), message size and attribute count bounds, and NUL rejection in attribute keys/values and ordering keys; topics, subscriptions and retained messages are bounded so a client cannot exhaust memory
  • a message published through the wire protocol also reaches subscribed Cloud Functions (onMessagePublished v2, topic().onPublish v1) through the existing FunctionsRuntime delivery path, so EVTINFRA-02 flows through real topic state without regressing the control publish route
Not implemented (3)
  • UpdateTopic: reported UNIMPLEMENTED, as the official emulator does not support it either
  • REST UpdateTopic and UpdateSnapshot, and subscription updates outside ackDeadlineSeconds and pushConfig
  • schemas (Avro or protobuf) and BigQuery / Cloud Storage subscription delivery
Notes (7)
  • conformance: the pubsub-probe records 28 parity steps and 3 documented divergences against the official pubsub-emulator 0.8.35 (conformance/pubsub-matrix.json, conformance/PUBSUB-MATRIX.md, conformance/README.md); agreement with the official Local Emulator Suite and production-authoritative divergences are classified separately
  • the documented divergences follow the production contract where the fixed official emulator differs: fireemu retains acknowledged messages for seek even when retainAckedMessages is false, CreateSnapshot returns supplied labels, and a snapshot retains publications made after it was created for replay into a new subscription on the same topic (conformance divergences.json)
  • ack deadlines and redelivery run on the virtual clock, so a test advances time with the control clock rather than sleeping, and await-idle stays deterministic
  • two separate waits govern a failed push, as in production. The per-message retry policy redelivers as soon as possible when the subscription has none, so pubsub.pushMinimumRedeliveryIntervalMillis defaults to 0 and is only an opt-in emulator protection. The subscription-level push backoff is independent of it, cannot be turned off, and throttles the whole subscription while its endpoint keeps failing; the documentation states its 100 ms to 60 s bounds (https://docs.cloud.google.com/pubsub/docs/push) but not the curve between them, so this emulator doubles from the minimum and clamps at the maximum, resetting on an accepted delivery. The exact production algorithm, and the interval production actually keeps between two push attempts, have not been observed; only the documented bounds are claimed, and the curve follows the observed one once it is measured (documented bounds, production algorithm unobserved)
  • the subscription-level push backoff is an intentional divergence from the pinned official emulator, which redelivers a failed push immediately: production throttles a failing push endpoint, and this emulator follows production. It is not registered in conformance/divergences.json because that register requires a matrix fixture and conformance/pubsub-matrix.json carries no push program at all, so no push divergence is detectable by the conformance run
  • session reset and deletion release the owned project's topics, subscriptions and Pub/Sub-native snapshots; fireemu control snapshots do not capture Pub/Sub broker state, while Pub/Sub-native snapshots remain scoped to broker subscription acknowledgement state
  • strict authentication contract A: an entirely absent Authorization header is accepted as an explicit production divergence for PUBSUB_EMULATOR_HOST SDK connections; @google-cloud/pubsub 5.3.1 determineBaseUrl_ selects insecure channel credentials and google-gax 5.0.8 _getCredentials returns them before attaching Google credential metadata (https://github.com/googleapis/google-cloud-node/blob/pubsub-v5.3.1/handwritten/pubsub/src/pubsub.ts; published google-gax build/src/grpc.js:184-202). No trusted Google OAuth verifier is configured, so every present credential is locally unsupported and fails with the recorded 401/UNAUTHENTICATED message before decoding or mutation; empty, duplicate, malformed and altered credentials are never anonymous. Valid production OAuth credentials also remain unsupported locally. The three observed REST Publisher authentication envelopes carry their recorded ErrorInfo; other credential/method combinations and exact byte layout are unobserved. The emulator profile ignores credentials as before. See docs/pubsub-authentication.md.

App Check

CapabilityStatusDetails
APPCHECK-CORE-1Implemented
Implemented (9)
  • project-scoped app registry from canonical configuration (appCheck.apps): project-number binding in both directions, one app ID per project, static debug-token digests
  • instance-isolated RS256 issuance and verification: a dedicated 2048-bit key per daemon instance from the operating system CSPRNG, kid fireemu-app-check-<public key digest>, never the Auth session key
  • the production-shaped claim set (iss, sub, both project audiences, iat, exp, jti, private fireemu_epoch) and the full verifier of specification section 11
  • virtual-clock expiry: iat <= now < exp with no leeway
  • unpredictable 128-bit project epochs; a token from another epoch or another daemon instance fails with WrongEpoch or UnknownKeyId
  • jti from the epoch plus an atomic per-epoch counter, so concurrent exchanges never share one
  • the canonical, transport-neutral X-Firebase-AppCheck classification (missing / present / malformed) of specification section 7.3
  • BaselineMode and AdmissionDecision (off, unenforced, enforced) and the one transport-neutral admission decision every product adapter asks
  • epoch rotation on session creation, reset, project deletion and snapshot restore: every token issued before the transition fails with WrongEpoch at its next verification (AC-LIFE-001)
Not implemented (2)
  • dynamic app registration (apps come only from canonical configuration and need a daemon restart)
  • limited-use tokens (APPCHECK-REPLAY-1)
Notes (2)
  • reproducible identifiers need an injected test seed (AppCheckKeySource::Seed), which is a constructor seam and is not reachable from configuration or the CLI
  • the fireemu_epoch claim is a published local divergence: local tokens are deliberately non-portable across daemon instances
APPCHECK-DEBUG-EXCHANGE-1Implemented
Implemented (8)
  • POST /v1/projects/{project}/apps/{appId}:exchangeDebugToken and the v1beta twin; {project} may be the project ID or the project number and the API key query parameter is accepted and ignored
  • constant-time SHA-256 digest comparison over the fixed-capacity digest set without an early exit, with equivalent dummy work for an unknown app
  • UUIDv4 canonicalization: hexadecimal case never changes the credential
  • one public 403 PERMISSION_DENIED "App attestation failed." for an unknown project, an unknown app, a disabled app and an unknown secret
  • limitedUse: true -> 501 UNIMPLEMENTED APP_CHECK_REPLAY_UNSUPPORTED, never a reusable session token
  • 400 INVALID_ARGUMENT for a malformed body, an unknown member, a trailing JSON value or a body above 16 KiB
  • privileged debug-token management: POST / GET /emulator/v1/projects/{p}/apps/{a}/debugTokens and DELETE .../{tokenId}; the control token is required for every method whatever the Origin, the raw secret is returned exactly once and a list shows only the token ID, name, creation time and digest prefix
  • Cache-Control: no-store on every App Check response
Not implemented (2)
  • dynamic app creation and deletion
  • limited-use exchange (APPCHECK-REPLAY-1)
Notes (3)
  • the constant-time comparison limits timing differences; it does not claim resistance to a local process-level side channel
  • debug secrets are credentials: configuration stores only their SHA-256 digest and never a raw secret
  • conformance: appcheck/exchange-and-jwks (conformance/README.md); this records agreement with the official Local Emulator Suite, never with production
APPCHECK-ENFORCE-1Implemented
Implemented (10)
  • one transport-neutral admission decision (fireemu_core_app_check::admission): the adapters resolve their route and target project, verify their own privileged credential, hand over the App Check field values and render the answer; none of them reimplements JWT semantics, the mode table or the observation shape
  • off, unenforced and enforced for Firebase Authentication (end-user Identity Toolkit and Secure Token routes), Cloud Firestore (unary gRPC, REST, the Write and Listen streams and the WebChannel transport) and Cloud Storage (the Firebase dialect: metadata, list, upload, download, update, delete, and resumable uploads bound to the app that started them)
  • stream and channel lifetime (section 13.1): a Write / Listen stream classifies its opening metadata once and is decided as soon as its first request names the database, then stays admitted for its whole life; a WebChannel classifies the opening init header block the same way, binds the channel to the admitted app and the session epoch of that admission, and closes the channel when a later envelope presents a replacement token for another app or one that does not verify. Neither token expiry nor a policy change ends an admitted stream: reconnecting is a new admission
  • resumable upload binding (section 13.2): an enforced initiation records the admitted app and the epoch of that admission in the upload session, and every later request on that session -- continuation, status query, cancel and finalization -- must present a valid token for the same app. A refused request is refused before the command is read, so it neither advances nor deletes the upload; token expiry mid-upload therefore preserves the resumable state
  • off is the absence of a policy, so a service configured off structurally cannot parse a header or record an observation
  • the explicit bypass matrix of specification section 12.2: the Firestore owner credential, Identity Toolkit Admin routes with the owner credential, the emulator inspection routes with the control token, and a Storage download URL whose token binds in constant time to the resolved bucket, object name and generation; a path shape alone never bypasses, with one owner-approved exception for server-side Storage traffic (appCheck.storageServerRequests)
  • Storage server-side requests under enforcement: production does not apply App Check to the Admin SDK, which authenticates with a service account over OAuth, so by default (appCheck.storageServerRequests "loopback", the same under every profile and productionExact) a request on a Cloud Storage JSON API route (/storage/v1, /upload/storage/v1, /download/storage/v1 and the short /b spelling) from a loopback peer addressing a loopback Host (a DNS-rebound name is refused) with no browser evidence (no Origin, Sec-Fetch-Site, Sec-Fetch-Dest, Referer or Cookie header) and no "Authorization: Firebase <token>" client credential is admitted and counted as a bypass, a resumable upload continuation included; the Firebase Storage API of the client SDKs (/v0), the XML-style object read, the form upload and any browser-shaped or non-loopback request still need a token; "capability" admits only the per-run capability (STORAGE_EMULATOR_HOST userinfo) and the owner credential
  • denial before Security Rules and before every side effect: no user, no issued or rotated credential, no consumed OOB or phone code, no MFA change, no object generation, no upload session, no Firestore mutation and no rules document-access budget consumption
  • the error contract of section 17: PERMISSION_DENIED with the public code in fireemu-code metadata on gRPC, HTTP 403 Google JSON on Firestore REST and on Auth, and the Firebase Storage JSON error envelope with 403 on Storage; the public code is APP_CHECK_REQUIRED or APP_CHECK_INVALID and never the detailed reason
  • x-firebase-appcheck on the Auth, Storage and Firestore REST CORS preflights and in the Storage accepted-header list, carried multi-valued so a duplicate can be refused
Notes (8)
  • there is no appCheck.services.functions baseline mode by design: callable enforcement is per function and is owned by APPCHECK-FUNCTIONS-1, which is what production does too
  • a project without a static appCheck.apps registration cannot use App Check at all, so an enforced service denies every request that targets it; this is the fail-closed reading of specification section 14
  • absence of a ruleset is not a bypass: the owner credential is verified by the App Check path itself, because with Security Rules disabled every Firestore caller is otherwise treated as the owner without presenting anything
  • the Storage JSON API dialect and the emulator inspection routes bypass Security Rules and their own guard more loosely than they bypass App Check (unauthenticated JSON API traffic, and an emulator route without an Origin); the App Check bypass requires the credential in both cases, so an enforced service cannot be defeated by rewriting a path
  • an enforced resumable upload needs the App Check field on every continuation, not only on the initiation, and the token has to belong to the app that started the upload
  • a WebChannel is admitted against the database its opening request names and again, inside the stream, against the database its first message names; both have to pass
  • the exact wire messages remain boundary-conformance until the fixtures of AC-CONF-001 exist
  • conformance: appcheck/unenforced-header-matrix, appcheck/enforced-header-matrix (conformance/README.md); this records agreement with the official Local Emulator Suite, never with production
APPCHECK-FUNCTIONS-1Implemented
Implemented (11)
  • under the strict profile the exchange, the JWKS and this trust boundary are served with no appCheck block (appCheck.debugTokens "any", a development default that productionExact, the emulator profile and any appCheck block turn back into registration): any well-formed UUIDv4 debug token exchanges for any app with a standard app ID, registered on first use, every product baseline stays off, and the RSA key is generated in the background after start, not on the first request; the boundary stays off, with a startup note, where an installed firebase-functions is outside 6.x and 7.x
  • the daemon proxy owns callable verification: it classifies X-Firebase-AppCheck under the canonical contract, forwards a valid token byte for byte as exactly one field, removes an invalid one before the runner can decode it, and leaves a missing one missing
  • enforceAppCheck: true answers the callable HTTP 401 UNAUTHENTICATED envelope before the runner is reached, so a denial costs no concurrency slot, no invocation record and no handler run; a valid token populates v2 request.app and v1 context.app with the app ID and the decoded claims
  • callable Authorization integrity: only a Firebase ID token verified against the target project AuthRegistry on the virtual clock and resolved as a user is reinserted. Bearer owner, service credentials, non-Bearer values and duplicate or folded fields are never callable user identities
  • every caller-supplied copy of a field the daemon owns is stripped before the trusted one is inserted: the App Check field, Authorization, the per-runner secret, and the x-callable-context-auth / x-original-auth channels firebase-functions honours under the debug switch to override v1 callable auth context
  • ordinary onRequest functions receive the raw App Check field list unclassified, duplicates included: application code owns custom-backend verification there (section 7.3)
  • the emulator profile enables FIREBASE_DEBUG_MODE and enableCors even without callable trust; skipTokenVerification is enabled only while the callable trusted protocol is active, and strict enables no CORS debug feature; the runner proves at startup that the installed firebase-functions reads those switches as expected and reports the auth-override header names it honours, which must all be ones the proxy strips
  • the runner HTTP server refuses every request when it has no per-runner secret rather than serving them unguarded, and compares the secret it does have in constant time
  • with App Check active, a configured functions.manifest is reconciled against discovery: it may not call a real callable an onRequest function, which would route the request around the trust boundary entirely
  • version-bounded loader instrumentation reads each callable enforceAppCheck and consumeAppCheckToken as the callable is declared, by hooking onCallHandler, withInit and wrapTraceContext -- the one path every v1, v2, builder and genkit spelling passes through
  • consumeAppCheckToken is three-valued and fail-closed per call: true loads the codebase with a startup note; without App Check the proxy removes X-Firebase-AppCheck before such a callable and before one whose value could not be observed, so it runs without App Check and nothing is consumed (an enforcing one answers 401), and a configured manifest whose explicit value contradicts the code, or that serves such a callable as an onRequest function, keeps loading with a warning while the token is still removed; the runner itself also removes X-Firebase-AppCheck, without the trusted protocol, before any export it discovered as such a callable, whichever path (HTTP, a task queue) the request took; with App Check a call presenting a valid token is refused with 501 UNIMPLEMENTED APP_CHECK_REPLAY_UNSUPPORTED before the runner; an undeterminable value fails startup when App Check is enabled, and it is never guessed as false
Not implemented (1)
  • limited-use consumption and alreadyConsumed (APPCHECK-REPLAY-1)
Notes (4)
  • supported firebase-functions majors are 6 and 7; outside that range, or when the internal callable modules move, the instrumentation reports itself unsupported and starting Functions with App Check enabled fails rather than guessing
  • onCallGenkit is covered because it goes through the same onCallHandler hook
  • the callable baseline is always unenforced at the daemon: it classifies and records every token so that valid app context reaches callables that do not enforce, and enforcement is the callable option
  • conformance: appcheck/unenforced-header-matrix, appcheck/enforced-header-matrix (conformance/README.md); this records agreement with the official Local Emulator Suite, never with production
APPCHECK-JWKS-1Implemented
Implemented (2)
  • GET /v1/jwks and /v1beta/jwks: an RFC 7517 JWK Set with this instance's public App Check key only
  • public on loopback and never App Check protected (bootstrap and public-key discovery)
Notes (3)
  • Cache-Control: no-store is an intentional local divergence: a daemon restart creates a new key at the same loopback URL, so a client must not retain the previous instance's JWKS
  • the response carries no private material: the modulus and the exponent only
  • conformance: appcheck/exchange-and-jwks (conformance/README.md); this records agreement with the official Local Emulator Suite, never with production
APPCHECK-OBSERVE-1Implemented
Implemented (8)
  • secret-free observations of every debug-token exchange and of every classified Auth, Firestore (unary, REST, streams and WebChannel), Storage and callable Functions request, with project, service, transport, operation, mode, credential category, privileged failure reason, verified app ID, logical time, policy generation and outcome
  • one bounded ring and one set of counters per project, created on first use and dropped whole when that project state is cleared or its session is deleted: a registered session project and the default project alike keep the 256 most recent observations of their own traffic, so a flood of requests to one project never evicts another project recent observations
  • the counters are not derived from the ring: they count every classified request of the project since its state was last reset, including the observations the ring has already dropped
  • GET /v1/sessions/{session}/appCheck/observations: the session project own counters by service, verified app ID, callable function name for the functions service, category and outcome, plus that project retained observations with their privileged reasons; another session project can neither be read here nor influence what this one is answered; the control token is required for every method whatever the Origin, and the response is Cache-Control: no-store
  • the Emulator UI App Check page (/ui/appcheck): the registered apps of the selected session project with their configured digest count and their dynamic registrations, the effective baseline mode of every service, this instance JWKS kid, debug-token registration and deletion, and the counters and recent observations of the session
  • the UI API front: GET /ui/api/appcheck/config summarises the registration (apps and modes, never a digest, a raw secret or an epoch), /ui/api/appcheck/projects/{p}/apps/{a}/debugTokens fronts the privileged management routes, and the observation route answers through the control front for the named session alone; each of them is Cache-Control: no-store, and a generated debug secret reaches the page in the creation response alone -- never in a list, a log, window.__FIREEMU__ or an observation
  • observation counters and the ring reset with the project state they describe (a reset, a project deletion and a snapshot restore clear both)
  • unverified identities aggregate into a bounded unknown bucket; no raw JWT, signature, debug secret, digest, private key or epoch value is ever recorded
Not implemented (1)
  • observations are polled, not streamed: there is no server-sent event channel for them
Notes (2)
  • the table of rings is bounded too, because a target project is resolved from a request path before anything validates it: at most 256 projects hold a ring at once, a project ID longer than 63 bytes (which no session could name) opens none, and a project the registry knows displaces the least recently used ring of one it does not rather than being crowded out by unregistered traffic
  • a project whose counter key space reaches 1024 distinct keys folds further counts into the same key without the app ID and the callable name, so the label set stays bounded without dropping a count
APPCHECK-PROVIDER-ATTESTATION-0Not supported
Notes (1)
  • Play Integrity, App Attest, DeviceCheck and reCAPTCHA are production attestation providers and are outside local emulation; a local token proves nothing about device integrity
APPCHECK-REPLAY-1Not supported
Notes (2)
  • contract: limited-use issuance, atomic consumption, concurrent replay observation and callable alreadyConsumed propagation
  • limitedUse: true fails closed with 501 APP_CHECK_REPLAY_UNSUPPORTED and never returns a reusable token
APPCHECK-SDK-WEB-1Implemented
Implemented (2)
  • tools/sdk-smoke/appcheck.mjs: the real firebase Web SDK obtains a local token through initializeAppCheck + CustomProvider against the local exchange endpoint and sends it to Firestore, Storage (a resumable upload), Auth and an enforceAppCheck callable, with the enforced refusals checked too
  • it runs in Node without browser shims: CustomProvider touches no browser global and the SDK guards its indexedDB token cache
Notes (3)
  • the SDK never dials the local exchange itself -- @firebase/app-check hard-codes the production base URL -- so the documented provider calls the daemon and returns the JWT with an expiry derived from the ttl
  • the smoke needs tools/sdk-smoke/fireemu.appcheck.json, which registers the app and the SHA-256 digest of a clearly fake debug secret
  • Android, Apple and Flutter SDKs are not exercised (AC-CONF-001)

Tooling

CapabilityStatusDetails
CTL-1Partial
Implemented (16)
  • sessions: POST /v1/sessions {project, buckets?, apiKeys?} creates a session that owns its project (Firestore databases, its default and declared Storage buckets, an Auth store, a fault plan, snapshots, text indexes); a compatibility-routed Auth namespace is provisionally displaced, the fresh session store is committed only after the initial reset, and a failed transition restores the exact displaced store; explicit and routed Auth namespaces receive an opaque daemon-incarnation lifecycle claim, project reset clears their tenant namespaces, and their issued ID tokens cannot revive across namespace reset or replacement, daemon restart, or a snapshot restore of the parent Auth store; lifecycle transitions also re-key generated refresh tokens, OOB codes, MFA handles and verification sessions so deletion and recreation cannot make stale strings designate fresh state; ordinary production-shaped stores omit the private claim, while the documented unsigned EmulatorMock profile remains caller-provided identity rather than an issued-credential lifecycle guarantee; the default session owns everything not registered; GET lists, DELETE removes; project-scoped Auth routes, declared API keys, token audiences and refresh tokens pick the project's store; ID tokens are accepted only by the project of their audience
  • clock:set
  • clock:advance
  • clock:advanceTo
  • sessions/{s}/reset (wipes the session's scope under the exclusive barrier; the default session's reset also resets the shared functions runtime; a store that refuses the wipe is reported as INTERNAL and no store is wiped)
  • sessions/{s} DELETE (wipes the project's state, then deregisters the session; a store that refuses leaves the session registered and usable)
  • sessions/{s}/snapshots (POST {name} captures the session's Firestore, Storage, Auth, fault plan and text indexes, plus the clock, both rulesets and the auto-ID generator for the default session; every part is copied before any is retained, so a store that cannot be read refuses the whole snapshot; GET lists with retained / limit / remaining; {name}:restore validates every part, takes a pre-image and applies in hook order under the reset barrier, rolling the hooks it already applied back when a later one refuses, and the default session's restore resets the functions runtime; DELETE releases what the name held)
  • sessions/{s}/snapshots budget: {MAX_SNAPSHOTS_PER_SESSION} names per session, RESOURCE_EXHAUSTED (429) beyond it with the retained set unchanged; capturing over a retained name is always admitted and releases the old copy
  • sessions/{s}/faultPlan (PUT the spec 18.2 plan, GET plan + fired history + counters, DELETE; one state per session)
  • sessions/{s}/functions and functions/{name}:run
  • sessions/{s}/pubsub/topics/{t}:publish
  • sessions/{s}:awaitIdle
  • rules load/replace/drop (admitted like data requests)
  • health
  • capabilities
  • limits
Not implemented (5)
  • per-session clocks, rules and functions (sessions share them)
  • per-session admission barriers (a reset of one session waits for every session's requests in flight)
  • snapshot files on disk (snapshots live in memory for the daemon's lifetime)
  • structural sharing of captured blobs and MVCC history (every retained snapshot is a full copy of what the session owned, which is what the per-session budget bounds)
  • capture of outstanding functions work (allowNonQuiescent snapshots drop it on restore)
Notes (1)
  • fault operations: firestore.commit / read / beginTransaction, storage.upload / read / delete / list / request, functions.deliver (duplicate; a rule with only an eventType), functions.invoke (returnError, timeout, deadLetter, delay, crashRunner, dropConnection; applies to event, HTTP, scheduled and manual invocations); actions that do not apply to an operation are refused; rules match the nth occurrence per operation, or per operation and function when a function is named; a delay keeps the other actions for when the event goes and wakes the runtime when the clock reaches it; dropConnection closes the connection (REST, Storage, functions HTTP) or resets the gRPC stream, is UNAVAILABLE over WebChannel and a failed attempt for event invocations
CTL-1-AWAIT-IDLEPartial
Implemented (1)
  • POST /v1/sessions/{s}:awaitIdle waits for pending / running / retry-waiting events, HTTP invocations and due schedule runs beyond the catch-up cap; events are enqueued inside the commit that produces them
Not implemented (2)
  • causal fences across sessions
  • Text Index work
Notes (1)
  • reset holds the session admission barrier exclusively across Firestore, Auth, Storage and the functions runtime; the runner is killed and restarted so handlers still running cannot write into the new session
CLI-01Partial
Implemented (12)
  • firebase.json for every served product: firestore in object or array (named-database) form with rules and indexes (also the legacy index spelling), storage in global object or target-based array form with .firebaserc bucket expansion, functions in object or array (multi-codebase) form with source, codebase, runtime and ignore
  • named Firestore databases load, enforce and hot-reload their own isolated rules and index files
  • target-based Storage rules load and hot-reload one isolated slot per target; --only storage:<target> follows the emulator controller by selecting Storage without narrowing the loaded target set
  • target configuration is bounded to 64 targets and 1024 concrete buckets per process; these are explicit local resource-safety budgets absent from firebase-tools 15.28.2
  • every selected Functions codebase declared in firebase.json runs in the daemon, one runner process each; --only functions:<codebase> narrows the run to one; firebase-tools 15.28.2 has no codebase-count limit, while fireemu refuses more than 32 simultaneously selected codebases before starting any runner as an explicit local resource-safety divergence
  • emulators.<name>.host and .port for firestore, auth, storage, functions, database, eventarc, tasks, hub and ui, emulators.ui.enabled, and emulators.singleProjectMode
  • loopback-only hosts, as a published divergence from firebase-tools (which binds any host as given): emulators.<name>.host and the canonical bind key accept 127.0.0.1, localhost and ::1; 0.0.0.0, :: or a LAN address exits 1 naming the key, because the daemon serves without a credential on loopback and has no routable mode
  • .firebaserc project aliases: --project resolves an alias, falls back to the default alias, and takes an unknown value as a project ID exactly as firebase --project does
  • --config reads either file: one carrying schemaVersion is the fireemu canonical configuration, anything else is a firebase.json, so `firebase emulators:exec --config firebase.json` works verbatim
  • unknown keys fail closed at every level of the sections fireemu reads
  • an emulators.<name> entry for a product fireemu does not serve is an error when --only named that product and a notice otherwise; nothing is silently dropped
  • spec/config/firebase-json.schema.json with a valid and an invalid corpus, checked by tools/config-schema-check and replayed through the loader by a unit test
Not implemented (2)
  • emulators.singleProjectMode is recorded and published but enforces nothing on its own: fireemu isolates every project into its own session, which is stricter
  • hosting and the other deferred products are not parsed beyond reporting them; the database section's predeploy / postdeploy hooks are accepted and ignored
Notes (1)
  • command-line ports override firebase.json; firebase.json overrides the canonical configuration
CLI-02Implemented
Implemented (6)
  • --only decides the lifecycle: an unselected Firestore, Storage or Functions binds no listener at all and its port stays free for whatever else wants it
  • the child environment carries exactly the canonical variables of the selected services, in the firebase-tools@15.28.2 env.ts formats: FIRESTORE_EMULATOR_HOST and FIREBASE_FIRESTORE_EMULATOR_ADDRESS as host:port, FIREBASE_AUTH_EMULATOR_HOST as host:port, FIREBASE_STORAGE_EMULATOR_HOST as host:port next to STORAGE_EMULATOR_HOST as http://host:port, FIREBASE_EMULATOR_HUB as host:port, and a synthesised FIREBASE_CONFIG
  • inherited emulator endpoints for unselected services are preserved; endpoints for selected services are overridden, so removing a routing variable cannot silently restore production routing
  • an official service fireemu does not serve (hosting, apphosting, dataconnect, extensions) is refused by --only with its scope status before anything binds; eventarc and tasks are accepted support-service selections and are no-ops without a Functions codebase, matching firebase-tools 15.28.2
  • --only functions:<codebase> selects one codebase, as the official CLI spells it
  • the functions runner sees the same selection: an unselected product leaves its variable unset there too
Notes (4)
  • there is no CLOUD_STORAGE_EMULATOR_HOST variable: the firebase-tools constant of that name emits STORAGE_EMULATOR_HOST, and fireemu sets that one
  • FIREBASE_DATABASE_EMULATOR_HOST is set as host:port when the Realtime Database is selected (--only database, emulators.database or --database-port), as firebase-tools sets it; the database is not started by default, so the default run leaves port 9000 free
  • fireemu also sets GOOGLE_CLOUD_PROJECT (the official CLI sets only GCLOUD_PROJECT); both preserve inherited emulator endpoints for unselected services
  • the Identity Toolkit socket also carries the control API, the App Check exchange and the UI API; when auth is not selected the control plane moves to an ephemeral loopback port and the configured Auth port is left free
CLI-03Implemented
Implemented (12)
  • the Emulator Hub on the official default port 4400 (emulators.hub, --hub-port, daemon.hubPort), best effort like the UI port: a busy default disables discovery, a busy explicit port is an error, and --hub-port 0 turns it off
  • GET /emulators: an object keyed by official emulator name, each entry carrying name, host, port, pid and the listen specs, with the Hub and the UI listed as upstream lists them; only services that actually bound a listener appear
  • GET /: the locator (version, origins, pid) plus this listener's host and port
  • PUT /functions/disableBackgroundTriggers and PUT /functions/enableBackgroundTriggers answering the official enabled body, or 400 when no functions codebase is loaded
  • the locator file <temp dir>/hub-<project>.json written at start with version, origins and pid, and removed at exit only while it still names this process; a live locator from another suite is left alone with a notice
  • FIREBASE_EMULATOR_HUB exported to the exec command as a bare host:port
  • @firebase/rules-unit-testing 5.0.2 discovers the suite with no explicit host: tools/sdk-smoke/rules-unit-testing.mjs runs initializeTestEnvironment with an empty configuration and exercises the rules decisions, withSecurityRulesDisabled, clearFirestore and a trigger-suppressed write
  • tools/sdk-smoke/rules-unit-testing-explicit-rules.mjs supplies explicit Firestore and Storage rules while still discovering both services through the Hub, then exercises authenticated and unauthenticated decisions on both products
  • DELETE /emulator/v1/projects/{project}/databases/{database}/documents on the Firestore port, which is what clearFirestore() calls
  • POST /_admin/export with the official body {"path", "initiatedBy"}, refusing a request that carries an Origin header exactly as upstream does; it is what fireemu emulators:export drives after finding the suite through its locator file
  • PUT /emulator/v1/projects/{project}:securityRules on the Firestore port, which is what loading rules from a test environment calls (see RULES-DIAG-1)
  • PUT /internal/setRules on the Storage port accepts the one-file rules-unit-testing body, activates a valid candidate atomically and preserves the previous rules after malformed JSON, an invalid file shape or invalid rules
Not implemented (1)
  • POST /dataconnect/clearData: fireemu serves no Data Connect emulator
Notes (2)
  • the Hub answers loopback Host headers only, which the official Hub does not check; it can disable background triggers, so a page on a routable name must not reach it by DNS rebinding
  • a suite moving from the official emulator has to do two things until the Auth gaps close: state an iat on the token authenticatedContext mints (createMockUserToken defaults to iat 0, so exp is 3600 and the token expired in 1970, which fireemu's virtual clock refuses) and create the users the contexts stand for, because fireemu resolves an ID token subject against the project Auth store
CLI-04Partial
Implemented (7)
  • emulators:start and emulators:exec as exact aliases of up and exec, and emulators:export <dir> [--project <id>] [--force] against a running suite
  • the official flag names --only, --project / -P, --config, --import, --export-on-exit, --ui (on emulators:exec), --inspect-functions and --log-verbosity, next to the fireemu port flags
  • --inspect-functions [port|true] binds Node's inspector to loopback, keeps its capability UUID out of stderr and Logging history, verifies /json/list after codebase loading, assigns independent dynamic ports to multiple codebases, serialises HTTP and background handlers while debugging, disables handler deadlines, and releases inspector ports at shutdown; a configured non-Node functions.runner and a fixed port with multiple codebases are refused before serving
  • --log-verbosity quiet | silent | info | debug (case-insensitive): quiet and silent suppress the banner and the notices, debug adds the resolved configuration
  • emulators:exec starts without the Emulator UI unless --ui is present, while emulators:start keeps the official automatic UI selection for supported services; an explicit fireemu --ui-port remains the stronger local override
  • exit codes: the command's own status from exec, 128 + signal when a signal ended it, 1 for a startup failure or a refused configuration, 2 for a usage error, and 1 when --export-on-exit fails after a command that succeeded (a failing command keeps its own status; firebase-tools 15.28.2 only warns); a refusal never starts a listener and never runs the command
  • SIGINT and SIGTERM are forwarded to the command with their identity and the whole process group is swept afterwards
Not implemented (1)
  • the official CLI takes the exec script as one positional shell string; fireemu requires an argv-safe command after --
Notes (1)
  • the official CLI takes the script as a positional argument; fireemu takes it after -- so no shell quoting is needed
CLI-05Implemented
Implemented (4)
  • the Hub background-trigger switch: disabling drops Firestore, Storage, Pub/Sub and Auth events instead of holding them, so re-enabling delivers nothing retroactively, which is what the official emulator does (its background-trigger route answers 204 and discards the body)
  • HTTP and callable invocations, manual functions/{name}:run requests and virtual-clock schedule runs keep working while background triggers are disabled: none of them is a data-change delivery
  • events already accepted keep their place in the outbox and are still delivered and retried, as upstream drains the work queue it had already taken on
  • the Logging emulator: an RFC 6455 WebSocket on the official port 4500 (emulators.logging, daemon.loggingPort, --logging-port) that streams the EmulatorLog wire format loggingEmulator.js defines, exports FIREBASE_LOGGING_EMULATOR_HOST to the exec child and the functions runner, and is advertised in the Emulator Hub GET /emulators listing. See the LOG-01 capability for the wire-format detail and divergences
Notes (1)
  • the switch is process-wide, not per session: one functions runtime serves every session
LOG-01Implemented
Implemented (7)
  • each log line is one WebSocket text frame {"level": <lowercase>, "data": <structured fields merged with trusted metadata>, "timestamp": <ms since epoch on the virtual clock>, "message": <text, VT/control characters stripped>}, as loggingEmulator.js WebSocketTransport.log produces
  • firebase-functions/logger output preserves arbitrary nested/list/scalar fields and all eight production severities (debug, info, notice, warning, error, critical, alert, emergency); a user-supplied metadata field is retained under data.metadata.user so it cannot replace trusted attribution
  • data.metadata.emulator.name tags the emulator (EmulatorLogger.forEmulator) and data.metadata.function.name tags the function (forFunction); function user output is type USER while retaining the production severity instead of the official emulator's forced info level
  • on connect a client is sent the whole retained history and then live frames, with no request/response, no filtering and no authentication; a handshake carrying a browser Origin that is not an HTTP(S) loopback origin is refused with 403 before any frame is written, while a handshake with no Origin (a non-browser client) still connects; a repeated Host or Origin header is refused rather than resolved to one of its values, and the request head must arrive whole inside a ten-second deadline and within 8 KiB or the connection is answered 408 or 431 and closed
  • the RFC 6455 handshake (Sec-WebSocket-Accept = base64(SHA-1(key + magic))), masked client frames, ping/pong and a clean close
  • every current Node codebase runner feeds its log lines into the bus tagged emulator=functions; invocation-scoped console and firebase-functions/logger output also carries data.metadata.function.name without crossing concurrent function contexts, individual runner log entries have a conservative UTF-8-safe 256 KiB source cap and a 64-level structured-data depth cap, unpaired UTF-16 surrogates fall back to protocol-safe plain text, and each emulator's own lifecycle line is tagged with emulator=<name>
  • the SHA-256-pinned official Emulator UI 1.15.0 discovers the Logging endpoint through fireemu's Hub and renders a firebase-functions/logger frame that arrives after its Logs page has connected
Notes (1)
  • divergences from the official emulator, all recorded on the logging surface: the handshake is loopback-only for both the Host header and any browser Origin, so a cross-site page cannot open the stream and read the Auth out-of-band links and SMS codes it carries (the official server has neither guard; the official UI Logs page connects from a loopback origin, and the streamed content itself stays at parity), a repeated Host or Origin header is refused instead of resolved to one of its values, the handshake head must arrive whole inside a ten-second deadline and within 8 KiB (408 or 431 otherwise, where the official server bounds neither time nor size), the retained history is bounded to 1000 frames (the official one is unbounded in memory), individual runner log entries have conservative 256 KiB source and 64-level depth caps, user metadata is nested under data.metadata.user when trusted attribution is present, function USER logs retain production severity instead of being forced to info, and control-character stripping also drops C0/C1/DEL and Unicode bidi-formatting controls from messages and structured field keys/string values, not only ANSI sequences; sanitized key collisions are retained under deterministic suffixes
DATA-1Implemented
Implemented (8)
  • the pinned firebase-export-metadata.json manifest of firebase-tools 15.28.2: the version, the per-product sections with their version, path and (Firestore) metadata_file, in the official product order
  • the Firestore managed export the emulator jar writes: every output partition named by overall_export_metadata is preflighted and imported exactly once, with a 10,000-partition safety bound, strict LevelDB record framing and masked CRC-32C validation, and the apphosting.datastore.v3 EntityProto encoding of a document, covering null, boolean, integer (two's complement), double (exact bits, so NaN and negative zero survive), string, bytes, microsecond timestamps, geo points, references, maps, arrays, the empty array as an indexed EMPTY_LIST property, the empty map as a nested EntityProto, and paths of any subcollection depth
  • auth_export/accounts.json as the identitytoolkit#DownloadAccountResponse the CLI streams to disk, with providers, custom claims, phone second factors, the reversible fakeHash:salt=<salt>:password=<plaintext> convention and auth_export/config.json
  • Identity Platform tenant accounts in auth_export/accounts-<tenant>.json, imported and exported in isolated namespaces without mixing them into accounts.json
  • storage_export/buckets.json, blobs/<id> and metadata/<id>.json, with the object bytes byte for byte and the recorded generation, metageneration, times, hashes, download tokens and custom metadata
  • a member the model does not know is parsed and retained by the export-format model; live Auth import refuses an account containing that member before startup because the running store cannot retain it, so it is never silently dropped or partially imported
  • a malformed section is refused with the byte offset or the member that caused it, never read short
  • database_export/<namespace>.json for every served Realtime Database namespace that holds data or was imported, in the format=export form firebase-tools writes, imported by writing each file as the owner with disableTriggers=true as firebase-tools does
Not implemented (3)
  • dataconnect_export (SQL Connect) is detected and refused: SQL Connect is deferred
  • a passwordHash that is not the emulator's own reversible form (a real scrypt hash from a production export) is refused rather than installed as a credential nothing can sign in with
  • an account, providerUserInfo entry, MFA/TOTP entry or Auth config containing an unknown member is refused before startup because the live Auth store cannot preserve that member; the export-format parser retains it for diagnostics
Notes (4)
  • the format is pinned by fixtures recorded from real runs of the official suite (crates/fireemu/tests/fixtures/export/, recorded by conformance/src/record-export.mjs)
  • the official managed export records no document creation or update time, so an import necessarily stamps them with the commit that installs them; the official Firestore emulator does the same
  • a Firestore database other than (default) has no place in the official format, whose export request is hard-coded to databases/(default); fireemu writes those under a fireemu member of the manifest that the official CLI ignores
  • a TOTP second factor has no official equivalent (the Auth emulator has no TOTP factor at all) and is written under totpInfo.sharedSecretKey
DATA-2Implemented
Implemented (7)
  • --import <dir> reads every section of the products --only selected into memory, including every distinct Firestore output partition referenced by overall_export_metadata, and installs them together under the exclusive admission barrier, so a corrupt record, duplicate or aliased output reference, missing partition, partition-count overflow or failure in any product leaves the empty start state and exits 1 naming the product and the path
  • a section of a product --only did not select is skipped with a notice rather than refused
  • --export-on-exit [dir] writes the export on a clean exit, on the command's failure and on SIGINT or SIGTERM; an omitted directory resolves to the --import one, and the working directory or a parent is refused
  • emulators:export <dir> finds the running suite through its Emulator Hub locator file and drives POST /_admin/export, whose body is the official {"path", "initiatedBy"} and which refuses a request carrying an Origin header
  • overwrite protection: a directory is overwritten only when it is empty or already holds a firebase-export-metadata.json
  • on Unix, every exported directory is created 0700 and every file 0600, because an Auth export carries password material and TOTP secrets
  • an import fires no Auth user event and no Storage object event: it restores state rather than producing it
Not implemented (1)
  • importing a project id other than the one the run serves installs the data under the project the artifact names, with a notice, rather than remapping it
Notes (4)
  • fireemu's own session snapshots, fault plans, text index definitions and every App Check secret (debug tokens, project epochs, the instance signing key) never reach an export directory
  • a password set through fireemu's own API has no reversible emulator form and is exported without a passwordHash rather than with an invented one; a password that arrived from an official import is written back exactly as it was read
  • Windows export publication is fail-closed: emulators:export and --export-on-exit return an error before creating or modifying any export path until an identity-bound atomic publication implementation is available
  • Unix export publication refuses a destination beneath an ancestor owned by another user, writable by group or other users, or carrying an extended ACL on macOS; use a dedicated private namespace rather than /tmp or a shared project directory