Profiles and productionExact
Compatibility profiles
Section titled “Compatibility profiles”| Profile | Behavior |
|---|---|
strict |
Matches production Firebase behavior and constraints as closely as possible within the supported scope. The default, and recommended, including when migrating from the official emulator. |
emulator |
Targets the behavior of a pinned version of the official emulator (firebase-tools 15.28.2). Use it only to check alignment with the official emulator, for example as an extra test target. |
The emulator profile accepts behavior that production refuses, so a test can pass there and fail against production. Use strict for your tests.
schemaVersion = 1profile = "strict"{ "schemaVersion": 1, "profile": "strict"}Where Fireemu departs from production on purpose
Section titled “Where Fireemu departs from production on purpose”Matching production is Fireemu’s goal, but not every production behavior is worth reproducing by default. Some only add setup to a test: production needs signed tokens and signing keys, a feature enabled through an Admin API call, or an hour of waiting. Copying them exactly would make Fireemu harder to use in CI and on a developer’s machine without catching more bugs.
For these behaviors, the default in both profiles is a development default that works without setup. Each one has its own setting, and each is listed here, so a difference from production is always a documented choice rather than a gap.
To get production’s behavior for all of them at once, set productionExact to true (strict profile only). An explicit setting still decides over it:
schemaVersion = 1profile = "strict"productionExact = true
[auth]allowUnsignedCustomTokens = true{ "schemaVersion": 1, "profile": "strict", "productionExact": true, "auth": { "allowUnsignedCustomTokens": true }}| Development default | Setting | With productionExact |
|---|---|---|
| Unsigned emulator custom tokens are accepted | auth.allowUnsignedCustomTokens |
Refused |
Firestore, Storage and Realtime Database Security Rules accept @firebase/rules-unit-testing’s unsigned mock ID token |
auth.allowUnsignedIdTokens |
Refused, as production refuses an unsigned token |
| A Realtime Database instance without a rules file is open to every request, as the official emulator serves it | database.defaultAccess ("open") |
"locked": only the emulator’s owner credential reads and writes, as production admits only administrators to a new database in locked mode |
| Identity-provider sign-in accepts fixture assertions and the sign-in widget | auth.idpAssertions ("fixture") |
"signed": OIDC ID tokens must verify with an auth.idpSigners key, SAML responses with the provider’s certificates |
| The Eventarc listener admits any credential or none | eventarc.authentication ("permissive") |
"production": a missing, malformed or JWT-shaped bearer is refused with production’s 401 |
A tenant ID can be any readable name, such as tenant-a |
auth.tenantIdFormat ("readable") |
"generated": production’s generated form, such as acme-x7k2q |
Multi-tenancy is on: the Admin API creates tenants and auth.tenants loads without an updateConfig call |
auth.multiTenant.allowTenants (true) |
Unset until enabled, as in a new production project: tenant routes are refused and auth.tenants needs allowTenants: true |
Multi-factor authentication is on, SMS and TOTP, without a console toggle, an updateConfig call or an auth.mfa block |
auth.mfa (state: "ENABLED") |
Off until enabled, as in a new production project: enrollment is refused with OPERATION_NOT_ALLOWED : SMS based MFA not enabled. or TOTP based MFA not enabled. |
An end user’s Firestore Listen stream stays open however long it is held |
firestore.listenStreamLifetime ("unlimited") |
"oneHour": ended with INTERNAL an hour after it opened |
App Check works without registering an app: any well-formed debug token (a UUIDv4) exchanges for any app, so an enforceAppCheck callable can be called with no appCheck block; a missing or malformed token is still refused |
appCheck.debugTokens ("any") |
"registered": only apps from appCheck.apps with a registered debug-token digest exchange, and App Check is not served without an appCheck block. An appCheck block restores it too |
| Dead-letter forwarding needs neither a project number nor IAM grants | pubsub.deadLetterAuthorization ("implicit") |
"iam": a message is forwarded only after the Pub/Sub service agent is granted roles/pubsub.subscriber on the subscription and roles/pubsub.publisher on the dead-letter topic. Naming pubsub.projectNumbers selects it too |
The startup banner and fireemu capabilities report whether productionExact is set, and the effective value and source of each setting in the table.
Most projects do not need productionExact; it is an advanced switch for a stricter comparison with production. A query whose composite index is not defined is not among these defaults: strict refuses it as production does, and firestore.missingIndexes: "report" is an explicit opt-out that runs it and reports the missing index on standard error. productionExact does not change that key.
Other development conveniences
Section titled “Other development conveniences”These differ from production and are not switched by productionExact:
| Development behavior | How to get production’s behavior |
|---|---|
Pub/Sub accepts requests without any credential, so clients that use PUBSUB_EMULATOR_HOST work |
Set pubsub.authentication.localBearerToken; requests without a credential are then refused as production refuses them. See Pub/Sub and Eventarc. |
localhost is in the default authorized domains for action-link continue URLs; a new production project may not include it |
Declare auth.authorizedDomains, which replaces the defaults |
| Storage: a signed URL or a POST policy is accepted with any signature (its expiry and conditions are checked), because verifying one needs the signing service account’s key | Not available |
| Storage: the JSON API serves a request that has no credential, or any credential other than a Firebase ID token, as the owner, because a local run cannot judge an access token | Not available |
Storage: the first write to a bucket nothing declared is served and creates the bucket, with one note: line on standard error; production answers 404 |
List the bucket in storage.buckets; the 404 itself is not available |
Storage: a bucket-level public grant (IAM allUsers) cannot be made through an API |
List the bucket in storage.publicBuckets |
Storage: the public URL (GET /{bucket}/{object}) is served without a credential to a request that sends x-goog-api-client, because the Go client library (and the Firebase Admin Go SDK) reads through it with no credential against an emulator host; a browser fetch is refused |
Not available |
Storage: every Origin is reflected in the CORS headers, where production needs a CORS configuration on the bucket for browser downloads |
Not available |
Storage: a Security Rule may call firestore.get() and firestore.exists() without the Firestore access grant production needs for the Storage service agent |
Not available |
Storage: both default bucket names, <project>.appspot.com and <project>.firebasestorage.app, exist, where a production project has one of them |
Set storage.defaultBucket to the one your project has; the other name is then noted on its first write and its bucket resource answers 404 |
Every sign-in method starts enabled, email-link sign-in included; a new production project enables none (firebase deploy --only auth only turns on the providers it lists, never off) |
Declare auth.signIn to match your project, such as email.passwordRequired: true to refuse email-link sign-in |
Built-in identity providers such as google.com sign in without being enabled in the console. oidc.* and saml.* providers must still be configured and enabled, or sign-in is refused as production refuses it |
auth.idpAssertions: "signed" (set by productionExact) refuses the built-in providers |
Any Web API key is accepted until the project declares one; production answers API_KEY_INVALID for a key of another project |
Declare auth.apiKeys |
| Phone verification sends a code to any number with no reCAPTCHA, Play Integrity or APNs proof, which production requires outside test numbers | No setting. Client SDKs run their verifier in emulator mode, so the app path is exercised; a raw REST client that omits the proof passes here |
A reCAPTCHA ENFORCE state is stored and returned but not enforced, so a sign-in without a reCAPTCHA token is accepted |
No setting |
accounts:batchCreate has no row limit; the Admin SDK checks 1000 users per call before it sends |
No setting |
| Sign-up rate limits (production limits new accounts per IP) are not simulated | Set auth.quotaSimulation to auth.quotaSimulation.mode = "enforce" |
Firestore serves a request naming a project other than the daemon’s from that project’s own empty data, which keeps parallel tests apart; a typo in a projectId splits data instead of failing as production’s PERMISSION_DENIED does |
Set projects.unknownProjects to "refuse" |
The Admin SDK’s verifyIdToken inside a function accepts an unsigned ID token, because the SDK skips signature verification while FIREBASE_AUTH_EMULATOR_HOST is set; production refuses it |
No setting. See Auth |
A Pub/Sub push endpoint may be plain http:// on loopback (127.0.0.1, localhost, [::1]); production requires HTTPS |
Use a non-loopback https:// URL in the subscription; Fireemu delivers push messages only to loopback, so there is no setting that makes it reach one |
.env.local in a Functions codebase is applied after .env and .env.<projectId> although production never deploys it |
Put the value in .env or .env.<projectId> instead; a key that exists only in .env.local is unset in production |
A function that fails with retry enabled is retried 4 times in total (events.maxAttempts, 10 s to 600 s backoff on the virtual clock), then dead-lettered; production retries for up to 24 hours (second generation) or 7 days (first generation) |
Raise events.maxAttempts; no value reproduces a time window |
A function that is not public (invoker: "private" or a list of service accounts) and a task-queue function accept any bearer that looks like a Google-issued identity token (a JWT whose iss is https://accounts.google.com) on the public function URL, because Google’s verification of its signature and of its IAM role cannot run locally. A Firebase Auth ID token is refused with 401, as Cloud Run refuses it. A call without credentials is refused with 403, as production refuses it. The console and the control API call such functions with the control token |
There is none; send a call without credentials to see the refusal. ingressSettings is not enforced |
Composite indexes from firestore.indexes.json are READY at once, and ones created through the Admin API after about 1.5 seconds; production builds an index for minutes. The CREATING state, and the “currently building” refusal of a query that needs the index, are kept |
No setting. Poll the index until it is READY, as against production; advancing the virtual clock also finishes a build |
| Managed export, import and bulk-delete operations are done on the first poll; production takes minutes | No setting. Poll the operation until done, as against production |
| A transaction that holds a lock another writer is waiting for is rolled back after 60 seconds without activity (wall clock). Production’s own idle limit is about 110 to 123 seconds, and what it does to a contended holder earlier is not recorded | No setting |
Choosing a setup
Section titled “Choosing a setup”- Most projects:
strictwith the development defaults. Your tests run without keys or setup calls, and still meet production’s validation, refusals and error shapes. - Before a release, or in a separate CI job:
strictwithproductionExact: true, to catch what the development defaults let through, such as an unsigned token. - Checking the official emulator:
emulator, as an additional target only.