Skip to main content

๐Ÿ›ก๏ธ Runbook: Enable Firebase App Check

  • Severity: ๐ŸŸก Hardening (no active incident)
  • Why: App Check attests that calls to Firestore, Storage, and the callable functions come from your genuine apps, not a scraped API key + curl. Today none of the clients enroll App Check and no callable enforces it, so the Firestore/Storage rules are the only barrier against an attacker holding a (public, by-design) Firebase web API key.
  • Blocked on a human: App Check needs provider keys generated in consoles (reCAPTCHA, Apple, Google Play). Do those first, then the code is one line per surface.

[!WARNING] Never turn on enforceAppCheck before clients are sending tokens. Enabling enforcement first will reject every real user. Follow the phased order below: enroll clients โ†’ watch the App Check "unverified" metrics in the console for a full release cycle โ†’ only then flip enforcement on.

1. Generate the provider keys (console work)โ€‹

SurfaceProviderWhere
Web (app.acesense.io, admin)reCAPTCHA Enterprise or reCAPTCHA v3Firebase Console โ†’ App Check โ†’ register each web app; add domains app.acesense.io, admin.acesense.io, localhost
iOSApp Attest (prod) / DeviceCheckFirebase Console โ†’ App Check โ†’ iOS app; no extra key, uses the app's entitlement
AndroidPlay IntegrityFirebase Console โ†’ App Check โ†’ Android app

The reCAPTCHA site key is public (safe to embed). Keep the secret key in the console only.

2. Enroll the clientsโ€‹

Web โ€” acesense-admin: โœ… ALREADY WIRED. It calls initializeAppCheck with a ReCaptchaV3Provider, gated behind VITE_APPCHECK_SITE_KEY (and skipped in dev). It is inert until you set that env var to your reCAPTCHA v3 site key and rebuild/redeploy โ€” no code change needed. To turn it on: set VITE_APPCHECK_SITE_KEY in the admin deploy env, pnpm build, firebase deploy --only hosting:admin.

acesense-landing no longer writes to Firestore from the browser, so it has no App Check enrollment code today.

Flutter โ€” acesense-frontend: โœ… ALREADY WIRED (web + iOS verified). firebase_app_check is added and lib/main.dart calls FirebaseAppCheck.instance.activate(...) (web=ReCaptchaV3, iOS=App Attest, Android=Play Integrity), gated behind --dart-define=ENABLE_APP_CHECK=true. The iOS native integration is confirmed (flutter build ios --no-codesign builds Runner.app with the pod). To turn it on, build with:

flutter build <target> --dart-define=ENABLE_APP_CHECK=true \
--dart-define=APPCHECK_RECAPTCHA_KEY=<web reCAPTCHA v3 key>

iOS/Android need no key (App Attest / Play Integrity). Off until then.

3. Watch (do not enforce yet)โ€‹

Ship steps 1โ€“2. In Firebase Console โ†’ App Check, watch the verified vs unverified request ratio per product (Firestore, Storage, Functions) across a full release cycle (web is instant; iOS/Android lag until users update). Wait until verified โ‰ˆ 100% of legitimate traffic.

4. Enforceโ€‹

  • Firestore / Storage: flip "Enforce" per product in the console.
  • Callables โ€” acesense-auth-function: spread ...appCheckOptions() into the onCall({ region: REGION, ... }) options (already wired on requestUploadPath and coach callables). Set function env ENFORCE_APP_CHECK=true to enforce; default is off. Start with the high-value, app-only callables (requestUploadPath, createCoachLink, addCoachNote, the admin* callables). Do NOT enforce on the public REST surface (apiServer, mcpServer) โ€” those are authenticated by API keys, not App Check, and have no app to attest.

Callable enforcement: requestUploadPath and all coach callables spread appCheckOptions() from shared/app-check.ts โ€” enforcement activates only when ENFORCE_APP_CHECK=true on the function env; default is off.

Rollbackโ€‹

Enforcement is a console toggle (off in seconds) and the callable option is a one-line revert + redeploy. The client activate() call is harmless to leave in place even when enforcement is off.

Wave 2 โ€” client + callable wiring (2026-07)โ€‹

  • Flutter client: lib/main.dart calls FirebaseAppCheck.instance.activate(...) via appCheckOptions() from lib/config/app_check_flags.dart. Gated behind --dart-define=ENABLE_APP_CHECK=true; web additionally requires --dart-define=APPCHECK_RECAPTCHA_KEY=<reCAPTCHA v3 site key>.
  • Callables: acesense-auth-function spreads appCheckOptions() from shared/app-check.ts into onCall options. Set function env ENFORCE_APP_CHECK=true to enforce; default is off. Do not enforce on public REST surfaces (apiServer, mcpServer).