Security rules answer "is this user allowed to do that?" They do not answer "is this request coming from your app at all?" Anyone can read your Firebase web config out of a JS bundle, sign up as a legitimate user, and then talk to Firestore directly from a script — at whatever rate they like, from whatever machine they like. Rules are satisfied. Your invoice is not.
App Check is the piece that closes that gap. It attests that a request came from an unmodified instance of your app on a real device or a real browser, using reCAPTCHA Enterprise on web, App Attest / DeviceCheck on Apple platforms, and Play Integrity on Android. Backends check the attestation token before serving.
It is also the single Firebase feature most likely to take your app down if you turn it on in one step. The failure mode is not subtle: enforcement flips, attestation fails for some slice of your real traffic, and those users get permission-denied on every read. We have seen it done wrong twice. Both times the cause was the same — enforcing before reading the metrics.
Here is the rollout order we use.
1. Ship the client SDK first, enforce nothing
Initialize App Check in the client and deploy. With enforcement off, tokens are minted and reported but nothing is rejected. This is the whole point of the first phase: you are collecting data, not adding a gate.
import { initializeApp } from 'firebase/app';
import {
initializeAppCheck,
ReCaptchaEnterpriseProvider,
} from 'firebase/app-check';
const app = initializeApp(firebaseConfig);
initializeAppCheck(app, {
provider: new ReCaptchaEnterpriseProvider(RECAPTCHA_SITE_KEY),
isTokenAutoRefreshEnabled: true,
});
isTokenAutoRefreshEnabled: true matters. Tokens expire (an hour by default); without auto-refresh, long-lived sessions — a dashboard left open overnight — start failing once you do enforce.
Call initializeAppCheck before you touch Firestore, Storage, or callable Functions. Requests issued during initialization go out without a token, and in monitoring mode they show up as unverified traffic that you will spend an afternoon trying to attribute.
2. Handle local development and CI deliberately
Emulators, unit tests, and Cypress runs cannot attest. Use debug tokens, and keep them out of production builds:
if (import.meta.env.DEV) {
self.FIREBASE_APPCHECK_DEBUG_TOKEN = true;
}
That prints a debug token to the console on first run; register it in the console under App Check → Apps → Manage debug tokens. Register one per developer and one per CI runner, name them after the machine, and delete them when a laptop or runner is retired. A debug token is a permanent bypass of App Check for whoever holds it — treat the list like an access list, review it quarterly, and never paste one into a shared doc.
Server-side code using the Admin SDK is unaffected: admin credentials bypass App Check entirely. If a backend of yours talks to Firestore through a client SDK — a Node script, an SSR renderer — it will fail once you enforce. Find those before phase 4.
3. Read the metrics before you flip anything
After a week or two of monitoring, the App Check page shows, per service, the split between verified, unverified, and outdated-client requests. The last bucket is the one that decides your timeline: it is traffic from app versions shipped before you added the SDK. On the web you can drain it in days. On mobile you cannot — users update when they update, and a long tail of installs will sit on the old build for months.
So the rule is arithmetic, not judgment. Do not enforce until unverified plus outdated traffic is a number you are willing to break, and you know who is in it. If mobile is 4% outdated after six weeks, you are choosing between a forced-update prompt and locking out 4% of users. Make that choice on purpose.
4. Enforce one service at a time
Enforcement is per product — Firestore, Realtime Database, Storage, Cloud Functions callables, Authentication — and you should treat each as a separate change with its own bake time. Start with the service whose metrics are cleanest and whose blast radius is smallest, usually Storage or a single callable Function. Watch denial rates for a day or two, then move on. Firestore last, because it is the one every screen depends on.
For Gen 2 callable Functions, enforcement can also be declared in code, which keeps it in review alongside everything else:
import { onCall, HttpsError } from 'firebase-functions/v2/https';
export const submitOrder = onCall(
{ enforceAppCheck: true },
async (request) => {
if (!request.auth) {
throw new HttpsError('unauthenticated', 'Sign in required.');
}
// request.app is present and verified here
},
);
For HTTP functions and Cloud Run services there is no automatic enforcement — verify the header yourself:
import { getAppCheck } from 'firebase-admin/app-check';
const token = req.header('X-Firebase-AppCheck');
if (!token) return res.status(401).send('missing app check token');
try {
await getAppCheck().verifyToken(token);
} catch {
return res.status(401).send('invalid app check token');
}
Add { consume: true } to verifyToken for replay protection on high-value endpoints — payment initiation, invite redemption, anything you would hate to see executed twice. Replay protection requires limited-use tokens on the client (getLimitedUseToken()), costs an extra round trip, and is worth it on maybe three routes in a typical app. Do not turn it on globally.
5. Know what App Check does not do
It is device attestation, not authorization. A determined attacker on a rooted device, or one relaying tokens from a genuine client, can still get through. App Check raises the cost of scripted abuse from "curl in a loop" to "maintain a fleet of real devices," which is enough to end almost all of the scraping and quota-burning traffic we see in practice.
What it does not do is protect data. If your rules let any authenticated user read users/{anyUid}, App Check means that data is now stolen exclusively by your own app. Rules stay the control plane; App Check just decides who gets to knock. Ship both, tested, in that order.
The rollout in one page
- Add the SDK, auto-refresh on, enforcement off. Deploy.
- Register debug tokens for dev machines and CI. Audit the list.
- Watch the per-service metrics until unverified plus outdated traffic is a number you accept.
- Enforce per service, smallest blast radius first, Firestore last.
- Verify tokens manually in HTTP functions and Cloud Run; add replay protection only on high-value routes.
The entire exercise is usually a day of engineering spread across a month of waiting. The waiting is the part teams skip, and it is the part that keeps your users signed in.