Work / Chitragupt / Wiki / Decisions
2026-06-28-phase-7a-app-check-enforcement
Decisioncanonicalverified 2026-06-28
DECISION.2026-06-28.PHASE-7A-APP-CHECK-ENFORCEMENTPhase 7a App Check: enforcement on with a custom web provider
Decision
App Check enforcement is on as of Phase 7a:
- Callable layer —
onZodCalldefaultsenforceAppCheck: 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 viaenforceAppCheck: false, because the caller has no Firebase identity yet and so cannot mint a token bound to it. Emulator + CI environments bypass enforcement viaAPP_CHECK_ENFORCEMENT_DISABLED=true. - Web client —
apps/website/src/api/firebase.tsinitializes App Check on the firstgetFirebaseApp()call using a FirebaseCustomProvider. The provider POSTs to a new HTTPS endpoint (mintAppCheckTokeninapps/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_TOKENor the globalFIREBASE_APPCHECK_DEBUG_TOKENconsole flag), so the emulator doesn't need a real mint. - Storage + Firestore rules —
hasAppCheck()helpers are defined in both rule files but are not yet wired intosignedIn(). 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 anappCheckTokenstub. 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—enforceAppCheckdefaults totrue;APP_CHECK_ENFORCEMENT_DISABLED=truebypasses 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 asmintAppCheckToken.apps/functions/src/auth/email-otp.ts,waitlist/waitlist.ts,grievance/file.ts— explicitenforceAppCheck: falsewith a one-line comment explaining why.apps/website/src/api/firebase.ts—initializeAppCheckwithCustomProviderruns on firstgetFirebaseApp(). ReadsNEXT_PUBLIC_APP_CHECK_TOKEN_ENDPOINT(optional) and defaults tohttps://asia-south1-{projectId}.cloudfunctions.net/mintAppCheckToken.firebase/firestore.rulesandfirebase/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
- .context/features/index.md — Phase 7a exit criterion
- 2026-06-10-no-recaptcha-ever
- 2026-06-10-app-check-enforcement-deferred (superseded by this decision)
Every project of mine is written down like this.
Read the résumé