Skip to content
Ritesh FirodiyaGet in touch

Work / Chitragupt / Wiki / Decisions

2026-06-28-phase-7a-app-check-enforcement

Decisioncanonicalverified 2026-06-28

DECISION.2026-06-28.PHASE-7A-APP-CHECK-ENFORCEMENT

Phase 7a App Check: enforcement on with a custom web provider

Decision

App Check enforcement is on as of Phase 7a:

  1. Callable layer — onZodCall defaults enforceAppCheck: true. Every signed-in callable rejects requests without a valid App Check token. The four pre-auth public callables (requestEmailOtp, verifyEmailOtp, joinWaitlist, fileGrievance) opt out explicitly via enforceAppCheck: false, because the caller has no Firebase identity yet and so cannot mint a token bound to it. Emulator + CI environments bypass enforcement via APP_CHECK_ENFORCEMENT_DISABLED=true.
  2. Web client — apps/website/src/api/firebase.ts initializes App Check on the first getFirebaseApp() call using a Firebase CustomProvider. The provider POSTs to a new HTTPS endpoint (mintAppCheckToken in apps/functions/src/auth/app-check-token.ts) that returns an Admin-SDK-minted token after an origin allow-list check and a per-IP-per-minute rate limit. Local dev / Playwright runs use App Check's debug-token flow (NEXT_PUBLIC_APPCHECK_DEBUG_TOKEN or the global FIREBASE_APPCHECK_DEBUG_TOKEN console flag), so the emulator doesn't need a real mint.
  3. Storage + Firestore rules — hasAppCheck() helpers are defined in both rule files but are not yet wired into signedIn(). Flipping the rule-side check on is a one-line change in each file; it is gated on the website CustomProvider being live in prod and on the firestore-rules test harness gaining an appCheckToken stub. The intent is documented in the rule comments so the operational flip happens with the next prod deploy and is not lost.

This supersedes 2026-06-10-app-check-enforcement-deferred, which deferred enforcement because the only Firebase-supplied web provider was ReCaptcha (banned by 2026-06-10-no-recaptcha-ever). The CustomProvider path closes that gap without reintroducing ReCaptcha.

Why

Phase 7a is a launch-blocking exit criterion (see .context/features/index.md §7a). The earlier deferral was driven by the lack of a web provider, not by an objection to App Check itself. A Firebase CustomProvider hitting an Admin-SDK mint endpoint is the documented escape hatch for projects that can't use ReCaptcha — it's not cryptographic device attestation, but combined with an origin allow-list and an IP rate limit it materially raises the cost of scripted callable abuse from outside chitragupt.ai.

The four public-callable opt-outs are deliberate: pre-auth flows have no Firebase identity to bind a token to. Anti-abuse on those endpoints already comes from the per-email-hash dedup (waitlist) and the IP rate limit (grievance, email-OTP).

Impact

  • apps/functions/src/_lib/callable.ts — enforceAppCheck defaults to true; APP_CHECK_ENFORCEMENT_DISABLED=true bypasses for emulator.
  • apps/functions/src/auth/app-check-token.ts (new) — HTTPS endpoint that mints App Check tokens via Admin SDK, gated by origin + per-IP rate limit. Exported as mintAppCheckToken.
  • apps/functions/src/auth/email-otp.ts, waitlist/waitlist.ts, grievance/file.ts — explicit enforceAppCheck: false with a one-line comment explaining why.
  • apps/website/src/api/firebase.ts — initializeAppCheck with CustomProvider runs on first getFirebaseApp(). Reads NEXT_PUBLIC_APP_CHECK_TOKEN_ENDPOINT (optional) and defaults to https://asia-south1-{projectId}.cloudfunctions.net/mintAppCheckToken.
  • firebase/firestore.rules and firebase/storage.rules — hasAppCheck() helper defined; rule-side use is intentionally deferred to the next operational flip (documented inline).
  • Env additions required in apps/functions/.env.production: APP_CHECK_APP_ID = the web app id from Firebase console (NOT the project id). Without it the mint endpoint returns 500.

Status

Active.

Sources

Every project of mine is written down like this.

Read the résumé