Firebase App Check for Expo React Native
Verify that requests hitting Cloud Functions, Firestore, and Storage actually come from your app, not a script replaying a leaked API key. This covers what App Check protects, why it needs a custom Expo dev client, react-native-firebase setup with Play Integrity and App Attest, and a rollout that will not lock out real users.
The short version
App Check proves a request came from an unmodified build of your registered app running on a genuine device. It does not know who the user is. Add @react-native-firebase/app-check in a custom Expo dev client, initialize it with Play Integrity on Android and App Attest on iOS, leave Firebase Auth and your security rules exactly as they are, then watch the App Check metrics before you flip enforcement on.
It is tempting to reach for App Check as a stand-in for real security. Treat it as an extra filter against bots, scrapers, and requests replaying a stolen public API key — not a replacement for authentication or authorization.
1. What App Check actually verifies
App Check issues a short-lived token that attests the calling client is your genuine, unmodified app running on a real device, using a platform attestation service: Play Integrity on Android and App Attest (with a DeviceCheck fallback on older devices) on iOS. The token says nothing about which account is signed in.
It is a good fit for stopping scripted abuse that hits your backend directly with a copied public API key, requests replayed from a decompiled or repackaged build, and traffic that never went through your published app at all. It cannot do per-user rate limiting, authorization, or anything that depends on knowing who is asking — that is still Firebase Auth's job.
2. Keep Auth and security rules exactly as they are
App Check and Firebase Auth answer different questions and both stay in place. A Cloud Function can see both signals side by side:
exports.createOrder = onCall({ enforceAppCheck: true }, async (request) => {
const uid = request.auth?.uid; // who is calling
const appId = request.app?.appId; // what app they are calling from
if (!uid) throw new Error("Sign in required");
// request.app is only populated once the token passes verification
});If you have not set up Auth-aware Firestore rules yet, do that first — see Firebase Auth in Expo React Native and Firestore Rules and Offline Data. App Check adds a layer on top; it is not a shortcut around either one.
3. Why this needs a custom dev client, not Expo Go
The Firebase JS SDK's App Check module ships a ReCAPTCHA provider built for web pages; it has no native attestation and cannot produce a Play Integrity or App Attest token from a mobile client. Real attestation lives in native code inside @react-native-firebase/app-check, which means Expo Go cannot run it.
You need expo-dev-client and a native rebuild any time you add these modules or change their configuration — a plain JS/OTA update is not enough. If you have not built a development client before, the EAS Build and Submit checklist covers the commands.
4. Install and configure the native modules
npx expo install @react-native-firebase/app @react-native-firebase/app-check
npx expo install expo-build-properties expo-dev-clientRegister the config plugins in app.json so prebuild wires up the native projects, including the static frameworks iOS needs for react-native-firebase:
{
"expo": {
"plugins": [
"@react-native-firebase/app",
"@react-native-firebase/app-check",
[
"expo-build-properties",
{ "ios": { "useFrameworks": "static" } }
]
]
}
}Rebuild the dev client after any change here — npx expo prebuild --clean locally, or eas build --profile development for a device you cannot plug in.
5. Initialize App Check with real attestation providers
Configure the react-native-firebase provider before any protected Firebase call, as early as possible in app startup:
import { getApp } from "@react-native-firebase/app";
import { initializeAppCheck, ReactNativeFirebaseAppCheckProvider } from "@react-native-firebase/app-check";
const provider = new ReactNativeFirebaseAppCheckProvider();
provider.configure({
android: {
provider: __DEV__ ? "debug" : "playIntegrity",
debugToken: process.env.EXPO_PUBLIC_APP_CHECK_ANDROID_DEBUG_TOKEN,
},
apple: {
provider: __DEV__ ? "debug" : "appAttestWithDeviceCheckFallback",
debugToken: process.env.EXPO_PUBLIC_APP_CHECK_IOS_DEBUG_TOKEN,
},
});
export const appCheck = await initializeAppCheck(getApp(), {
provider,
isTokenAutoRefreshEnabled: true,
});The debug tokens are managed the same way as any other build-time value — see EAS Build environment variables for Firebase and Expo if you have not set up per-environment secrets yet.
Play Integrity needs a Play-recognized install
A sideloaded EAS internal-distribution APK is not always recognized by Play Integrity the same way a Play-installed build is. Keep the debug provider for local and internal-distribution testing, and validate the playIntegrity provider against a build installed from at least an internal testing track before you rely on it.
6. Protect callable and HTTPS Cloud Functions
Set enforceAppCheck: true on functions that should reject requests without a valid token, before your handler code runs:
const { onCall } = require("firebase-functions/v2/https");
exports.redeemInviteCode = onCall({ enforceAppCheck: true, region: "europe-west2" }, async (request) => {
const uid = request.auth?.uid;
if (!uid) throw new Error("Sign in required");
// request.app is guaranteed present here; enforceAppCheck already
// rejected anything without a valid token.
});For endpoints where a captured token could be replayed — redeeming a one-time code, claiming a reward — call getLimitedUseToken() on the client and consumeAppCheckToken in the function so each token can only be spent once.
7. Turn on Firestore and Storage enforcement without an outage
Firestore and Storage do not read App Check status from your security rules; enforcement is a per-product switch under Firebase Console → App Check → APIs. Flipping it immediately rejects every request without a valid token, including from app versions that predate this change.
- Ship the App Check-enabled build and let it reach the bulk of active users first.
- Watch the App Check Metrics tab for the split between verified, unverified, and stale requests per product.
- Only enable Enforce once verified traffic dominates, starting with the least critical product.
- Keep the toggle in mind as your fastest rollback if verified traffic unexpectedly drops after a release.
8. Debug tokens for simulators and CI
Simulators, emulators, and CI runners cannot pass real device attestation, so App Check has a debug provider for exactly this case. On first run, the SDK logs a debug token to the console; register it under Project Settings → App Check → Manage debug tokens.
- Give each developer's simulator its own debug token so one can be revoked without breaking everyone else.
- Use a separate token for CI, stored the same way as other CI secrets.
- Never ship a debug token in a production build — the conditional on
__DEV__in the setup above keeps that split.
9. Production rollout checklist
- Ship the App Check build without enforcement and watch the Metrics tab for several days before changing anything.
- Validate the Play Integrity provider against a Play-track build, not only a sideloaded internal build.
- Confirm the App Attest capability made it into the native iOS project after prebuild.
- Enable enforcement one product at a time, least critical first.
- Document who can flip enforcement back off, and make sure they know where the switch is.
Related tutorials
Need this built for you? I take on contract Expo and React Native work — see how I can help.