Next.js 16 App Router SPA. Apollo Client treats the backend GraphQL schema as source-of-truth (codegen via @graphql-codegen/cli). Firebase Auth handles email/password sign-in client-side; JWT injection happens in an Apollo auth link with token-refresh on 401. The dynamic form is rendered 100% from server-defined sections/fields/conditions/repeated-groups via a generic renderer in src/lib/components/dynamic-form. The processing screen polls FormSubmission every 5s and frames a VNC iframe served by the AI container. A separate admin panel uses its own Apollo client guarded by an x-admin-passphrase header from localStorage.
Role in the system: The public surface of usaidfinder.com — entry point at /explore
Surfaces:
- (public)/explore — disaster map + FAQ
- (public)/disaster/[id] — disaster detail
- (public)/application/[locationId]/programs — public program selection
- (public)/(protected)/dashboard — user submissions + documents
- (public)/(protected)/me — profile
- (public)/(protected)/application/[locationId]/form — dynamic form renderer
- (public)/(protected)/application/[locationId]/processing — AI agent VNC + polling
- admin/disasters, admin/programs — separate Apollo + passphrase auth (CRUD partially WIP)
User workflows
Discover & apply
Survivor reaches the dynamic form ready to fill
Submit form
Survivor lands on /processing screen which begins polling
AI processing & continue
FEMA application submitted; user can review the VNC frame for 48h
Form prefill
Form pre-populated; survivor edits only changed answers
Admin manage disasters/programs
Admin sees stub CRUD; mutations not yet wired
API endpoints
- QUERY
ListForms / Form / FormPrefill / FormSubmission / FormSubmissionsForm schema + submissions - MUTATION
SubmitFormPersist + validate submission, return presigned S3 URLs - MUTATION
FinalizeFormSubmissionMark uploaded, kick AI - MUTATION
ContinueAgentResume Agent 2 - MUTATION
CreatePlaidLinkToken / LinkBankAccountPlaid bank linking - QUERY
Me / Login / UpdateMeAccount management - QUERY
DashboardUser submissions + stats - QUERY
ActiveDisastersDisasters list for map - QUERY
Location.Detail / PopupDetailMap popups + location detail - QUERY
Admin DashboardAdmin metrics
Third-party APIs
af-backend-go-api (GraphQL)
Main API
af-backend-go-api (Admin GraphQL)
Admin CRUD
Mapbox GL v3
Disaster map, tilesets, geocoding
Firebase Auth
Email/password sign-in
Plaid Link v4
Bank account linking inside form
Service dependencies
af-backend-go-api
GraphQL data source
af-map (indirect)
Mapbox tilesets consumed via mapbox://helpishere.counties
Firebase Auth
Identity provider
Mapbox
Tile rendering + geocoding
Plaid
Bank linking flow
Analysis
af-frontend — Prop-Build Analysis
Document Type: Critical Review & Analysis (companion to prop-build-template.md)
Scope: Per-Repo
Subject: af-frontend (aid-finder/af-frontend)
Reviewer(s): Claude (automated code review)
Date: 2026-04-09
Version: 0.1
Confidence Level: Medium
What would raise confidence: running pnpm build + bundle analyzer locally, access to Vercel/CloudFront RUM, an interview with the frontend lead, and sight of the external CI/deploy pipeline config.
Inputs Reviewed:
- Prop-build doc:
data/af-frontend.yaml - Companion docs:
data/af-frontend/{component-tree,data-flow,api-examples,runbook,deployment}.md - Code commit SHA:
8b7acf98842a737bcf7d1c9dada0659e87cb2938(develop) - Dashboards / metrics: none available (no RUM/APM configured)
- ADRs / design docs: none found in repo
- Interviews: none
A.1 Executive Summary
- Overall health: Modern, well-structured Next.js 16 / React 19 SPA with a genuinely elegant server-driven dynamic-form core, but undermined by a total absence of automated tests, no error tracking, and a weak admin auth story.
- Top risk: Admin panel "auth" is a single shared passphrase read from
localStorageand forwarded as anx-admin-passphraseheader (src/features/admin/api/client/apollo.ts:9-18,src/features/admin/components/layout/login/index.tsx:34). Anyone who can inspect a browser gets every admin mutation — see A.4.1. - Top win / thing worth preserving: The server-driven dynamic form (
src/lib/components/dynamic-form/parts/render-field.tsx+application-form/index.tsx) lets the backend introduce new fields/sections/conditions without a frontend deploy. This pattern should be preserved and propagated. - Single recommended next action: Replace admin passphrase auth with Firebase Auth + a backend
isAdminclaim, and in parallel adopt Vitest + Playwright for the submit→finalize→processing happy path. - Blocking unknowns: No CI/deploy pipeline exists in-repo (
.github/workflowsreturns 404 per the YAML §16), no bundle size or Lighthouse numbers, no production error rates — impossible to score A.12/A.14 confidently.
A.2 Health Scorecard
| # | Dimension | Score (1–5) | Justification |
|---|---|---|---|
| 1 | Module overview / clarity of intent | 4 | Prop-build YAML + README + component-tree.md clearly describe surfaces and flows. |
| 2 | External dependencies | 4 | Pinned versions, modern stack; Firebase public config hardcoded in src/lib/config.ts:16-22 is intentional but worth noting. |
| 3 | API endpoints | 4 | Pure GraphQL client via codegen; typed operations next to .graphql files. No own API surface. |
| 4 | Database schema | 3 | N/A persistent; Apollo cache config is minimal but correct (src/lib/api/graphql/apollo.ts:8-17). |
| 5 | Backend services | 3 | N/A — pure client; score neutral. |
| 6 | WebSocket / real-time | 2 | 5 s polling only (form-submission-portal/index.tsx:26,40); no subscriptions, wasteful and high-latency. |
| 7 | Frontend components | 4 | Feature-sliced layout, dynamic-form is a strong abstraction; a few leaky any-ish casts in render-field.tsx. |
| 8 | Data flow clarity | 4 | Single Apollo client per tree, auth-link well-isolated (src/lib/api/graphql/links/auth-link.ts). |
| 9 | Error handling & resilience | 2 | No React error boundaries / error.tsx; auth-link retries but has no backoff cap and can loop on repeated ERR_UNAUTHORIZED. |
| 10 | Configuration | 3 | src/lib/config.ts centralizes env, but NEXT_PUBLIC_USE_MOCK_DATA missing from .env.example (noted in YAML tech_debt). |
| 11 | Data refresh patterns | 3 | Polling + on-demand; no SWR/cache invalidation strategy beyond cache.evict({fieldName:'formPrefill'}) in application-form/index.tsx:165. |
| 12 | Performance | 2 | No measurements, no bundle budget, React Compiler on but legacy useMemo still present (render-field.tsx:52-74). |
| 13 | Module interactions | 4 | Clean boundaries with af-backend-go-api, af-map, Firebase, Mapbox, Plaid. |
| 14 | Troubleshooting / runbooks | 3 | Runbook exists (data/af-frontend/runbook.md) but no alerts to trigger it. |
| 15 | Testing & QA | 1 | Zero test framework in package.json; Storybook is not an automated gate. |
| 16 | Deployment & DevOps | 2 | No CI config in repo, no Dockerfile, no vercel.json — deploy is invisible to the repo. |
| 17 | Security & compliance | 2 | Admin passphrase in localStorage, no CSP/security headers visible, no cookie-consent, no Sentry to catch XSS regressions. |
| 18 | Documentation & maintenance | 4 | README + companion docs are unusually thorough for a frontend repo. |
| 19 | Roadmap clarity | 3 | section_19_roadmap.planned lists items but no dates/owners. |
Overall score: 3.0 — strong on architecture/docs (1, 7, 8, 13, 18), dangerously weak on testing, observability, and admin security (9, 12, 15, 16, 17).
A.3 What's Working Well
-
Strength: Server-driven dynamic form renderer — entire form schema lives on the backend and a generic client renders it.
- Location:
src/lib/components/dynamic-form/parts/render-field.tsx:102-248; orchestrated bysrc/features/application/components/application-form/index.tsx:68-206. - Why it works: New field types, conditions, and repeated groups ship without a frontend deploy. Unknown components fall back sanely (
render-field.tsx:241-247). - Propagate to: Admin CRUD forms in
src/features/admin/...that currently reinvent their own.
- Location:
-
Strength: Centralized Apollo auth-link with single-flight token refresh.
- Location:
src/lib/api/graphql/links/auth-link.ts:28-116. - Why it works:
isRefreshingflag +pendingRequestsqueue avoids thundering-herd token refreshes. Triggered off bothERR_UNAUTHORIZEDin result extensions (:149-152) and 401/UNAUTHENTICATED network errors (:163-171). - Propagate to: Admin Apollo client (
src/features/admin/api/client/apollo.ts) which does NOT get this benefit.
- Location:
-
Strength: Apollo cache type policy for union + custom keyFields.
- Location:
src/lib/api/graphql/apollo.ts:8-17(FormFieldpossibleTypes,Household.bqHouseholdIdkeyFields). - Why it works: Correct handling of GraphQL unions and non-
identities is a frequent bug source; solved deliberately here.
- Location:
-
Strength: Polling pauses when the VNC iframe is visible.
- Location:
src/features/application/components/form-submission-portal/index.tsx:34,40,66. - Why it works: Avoids fighting the user's login.gov session and saves backend QPS.
- Location:
A.4 What to Improve
A.4.1 P0 — Admin panel protected only by shared localStorage passphrase
- Problem: Admin authentication is a single shared secret stored in
localStorageand sent asx-admin-passphraseon every admin GraphQL request. No per-user identity, no rotation, no audit, no expiry, and localStorage is XSS-readable. - Evidence:
src/features/admin/api/client/apollo.ts:9-18;src/features/admin/components/layout/login/index.tsx:32-37. YAML §17 acknowledges but scores "low-sensitivity". - Suggested change: Firebase Auth with an
admincustom claim verified server-side; drop the passphrase. Stop-gap: move toHttpOnlycookie set by a backend login endpoint. - Estimated effort: M.
- Risk if ignored: One XSS or screenshot → full disaster/program CRUD compromise once mutations are wired.
A.4.2 P0 — No automated tests of any kind
- Problem:
package.jsonhas no Vitest/Jest/Playwright. The submit → presigned S3 PUT → finalize → polling pipeline is the money path and entirely unverified. - Evidence:
src/features/application/components/application-form/index.tsx:96-193;src/lib/components/dynamic-form/parts/render-field.tsx:102-247. - Suggested change: Vitest for
helpers/+ dynamic-form unit tests; Playwright for one happy-path e2e against a mocked backend. - Estimated effort: M.
- Risk if ignored: Any refactor can silently break submissions; regressions only caught by survivors failing to file.
A.4.3 P1 — No client-side error tracking or RUM
- Problem: No Sentry / Datadog RUM / Vercel Analytics SDK. If
render-field.tsx:241rendersnullin prod for an unknown field component, nobody hears it. - Evidence: YAML §14
monitoring_alerts.applicable: false; no@sentry/*inpackage.json. - Suggested change: Add Sentry (browser + source maps) and wire
global-error.tsx+ per-routeerror.tsxboundaries. - Estimated effort: S.
- Risk if ignored: Production regressions only surface through support tickets.
A.4.4 P1 — Auth-link has no retry budget or backoff
- Problem: On repeated
ERR_UNAUTHORIZED,handleTokenRefreshrefreshes and re-forwards. No per-operation attempt counter. - Evidence:
src/lib/api/graphql/links/auth-link.ts:147-158. - Suggested change: Per-operation retry counter capped at 1; on second failure, logout and surface an error.
- Estimated effort: S.
- Risk if ignored: Low-rate but real risk of retry-storm against Firebase + backend.
A.4.5 P2 — Legacy manual memoization left behind after enabling React Compiler
- Evidence:
src/lib/components/dynamic-form/parts/render-field.tsx:52-74; YAML citessrc/features/dashboard/pages/dashboard/parts/circular-progress/index.tsxandsrc/features/map/.... - Suggested change: Sweep and remove; add
eslint-plugin-react-compilerrule. - Estimated effort: S.
A.4.6 P2 — Polling should become a GraphQL subscription
- Evidence:
src/features/application/components/form-submission-portal/index.tsx:26,40. - Suggested change: Add a subscription on
FormSubmission.aiAgentLatestStatein af-backend-go-api and useuseSubscription. - Estimated effort: M (cross-repo).
- Risk if ignored: Continued poor UX during the AI processing stage; unnecessary backend QPS.
A.5 Things That Don't Make Sense
-
Observation: The admin Apollo client gets its own copy-pasted auth-link with no token-refresh logic, while the main client has a sophisticated single-flight refresh link.
- Location:
src/features/admin/api/client/apollo.ts:9-29vssrc/lib/api/graphql/links/auth-link.ts. - Question for author: Is the passphrase intended to go away soon, or should the admin client inherit the main auth-link machinery now?
- Location:
-
Observation:
resolveFormPathandresolveFileValuereconstruct RHF field paths by hand inline.- Location:
src/features/application/components/application-form/index.tsx:26-57. - Question for author: Should these move into
helpers/so tests (when they exist) can target them directly?
- Location:
-
Observation:
cache.evict({ fieldName: 'formPrefill' })is called unconditionally on all success branches after submit.- Location:
application-form/index.tsx:165. - Question for author: Intentional? "May not be eligible" branch also evicts.
- Location:
A.6 Anti-Patterns Detected
A.6.1 Code-level
- God object / god function
- Shotgun surgery
- Feature envy
- Primitive obsession (passphrase as raw string, field paths as dot-strings)
- Dead code
- Copy-paste / duplication (admin Apollo client re-implements a stripped-down auth-link)
- Magic numbers / unexplained constants (raw
'passphrase'localStorage key is not named) - Deep nesting
- Long parameter lists
- Boolean-flag parameters
A.6.2 Architectural
- Big ball of mud
- Distributed monolith
- Chatty services
- Leaky abstraction (
render-field.tsxuses'options' in field/'placeholder' in fieldruntime checks instead of an exhaustive discriminated union) - Golden hammer
- Vendor lock-in
- Stovepipe
- Missing seams for testing (hardcoded
fetchto presigned S3 URLs; no injectable HTTP client)
A.6.3 Data
- None observed in this client repo.
A.6.4 Async / Ops
- Poison messages
- Retry storms / no backoff (auth-link has no per-op cap)
- Missing idempotency keys
- Hidden coupling via shared state
- Work queues without visibility (polling has no metric)
A.6.5 Security
- Secrets in code / localStorage (
passphraseinlocalStorage) - Missing authn/z on internal endpoints (admin "auth" is a shared string)
- Overbroad IAM roles
- Unvalidated input crossing trust boundary
- PII/PHI in logs or error messages
- Missing CSRF / XSS / SQLi / SSRF — unable to confirm CSP headers without deploy config; flagged in A.9.2
A.6.6 Detected Instances
| # | Anti-pattern | Location (file:line) | Severity | Recommendation |
|---|---|---|---|---|
| 1 | Secret in localStorage + missing real authn | src/features/admin/api/client/apollo.ts:10; src/features/admin/components/layout/login/index.tsx:34 | P0 | Firebase Auth + admin claim (A.4.1) |
| 2 | Copy-paste duplication of Apollo client setup | src/features/admin/api/client/apollo.ts:9-29 vs src/lib/api/graphql/links/auth-link.ts | P2 | Extract shared link factory |
| 3 | Leaky abstraction via in-checks on FormField union | src/lib/components/dynamic-form/parts/render-field.tsx:108,167,186,208,211 | P2 | Exhaustive switch on __typename |
| 4 | Retry storm potential in auth-link | src/lib/api/graphql/links/auth-link.ts:104-108,147-158 | P1 | Per-op attempt counter |
| 5 | Missing testing seam for file uploads | src/features/application/components/application-form/index.tsx:152-156 | P1 | Inject fetch/uploader dependency |
| 6 | Primitive obsession — RHF field paths as dot-strings | src/features/application/components/application-form/index.tsx:26-35 | P2 | Typed helper / branded type |
| 7 | Magic localStorage key 'passphrase' | same files as row 1 | P2 | Constant (obsolete once A.4.1 lands) |
| 8 | Legacy useMemo under React Compiler | src/lib/components/dynamic-form/parts/render-field.tsx:52-74 | P2 | Remove per project rule |
A.7 Open Questions
- Q: Is the admin passphrase scheme a deliberate interim pending Firebase-admin-claim migration, or the intended end state?
- Q: Where does CI actually live?
.github/workflowsis empty. - Q: What are the measured LCP/TTI/bundle size for
/explore? - Q: Is a subscription on
FormSubmissionfeasible in af-backend-go-api?
A.8 Difficulties Encountered
- Difficulty: No CI/deploy config in repo.
- Impact: Cannot verify build steps, deploy gates, rollback timing, or whether lint/typecheck block merge.
- Fix: Commit
.github/workflows/ci.ymlor link from README.
- Difficulty: No tests to read as executable spec.
- Impact: Intent inferred from happy-path code alone.
- Difficulty: No runtime telemetry.
- Impact: A.12, A.13, A.14 are qualitative only.
- Difficulty: Did not run
pnpm codegenorpnpm build.
A.9 Risks & Unknowns
A.9.1 Known risks
| # | Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|---|
| 1 | Admin passphrase leakage via XSS, screen share, or git history | M | H | A.4.1 |
| 2 | Silent production regressions on submit flow | H | H | A.4.2 |
| 3 | Undetected client crashes / blank screens | M | M | A.4.3 |
| 4 | Auth-link retry loop on persistent ERR_UNAUTHORIZED | L | M | A.4.4 |
| 5 | Missing cookie-consent / CCPA controls | M | M | Add banner before EU/CA launch |
| 6 | Polling load on backend during large activations | M | M | A.4.6 |
| 7 | Deploy pipeline opaque to the repo | M | M | Commit CI config |
A.9.2 Unknown unknowns
- Storybook visual regression coverage — not reviewed; risk Low.
- Mapbox feature (
src/features/map/) — not reviewed in depth; risk Medium (landing surface). - CSP / security headers (Next middleware,
next.config.ts) — not opened; risk Medium. helpers/internals (applyPrefill,processPayload,mapErrorCodesToMessage) — not reviewed; risk Medium.- Accessibility of the dynamic form — no tooling signal; risk Medium.
A.10 Technical Debt Register
| # | Debt item | Quadrant | Interest | Remediation |
|---|---|---|---|---|
| 1 | Admin passphrase auth in localStorage | Reckless & Deliberate | High | Firebase admin claim (M) |
| 2 | Zero automated tests | Reckless & Deliberate | High | Vitest + Playwright (M) |
| 3 | No error tracking / RUM | Reckless & Inadvertent | Medium | Sentry + error.tsx (S) |
| 4 | No CI config in repo | Prudent & Inadvertent | Medium | Commit ci.yml (S) |
| 5 | Duplicated Apollo client setup (main vs admin) | Prudent & Inadvertent | Low | Extract factory (S) |
| 6 | Legacy useMemo/useCallback under React Compiler | Prudent & Inadvertent | Low | Sweep + eslint rule (S) |
| 7 | NEXT_PUBLIC_USE_MOCK_DATA missing from .env.example | Prudent & Inadvertent | Low | Add it (S) |
| 8 | Client-only auth guard (no middleware.ts) | Reckless & Deliberate | Medium | Middleware checking Firebase __session cookie (M) |
| 9 | Admin edit handlers stub (console.log) + mock data arrays | Prudent & Deliberate (WIP) | High when enabled | Wire admin mutations (M) |
| 10 | 5 s polling for AI state | Prudent & Deliberate | Medium | GraphQL subscription (M) |
| 11 | No error boundaries / error.tsx | Reckless & Inadvertent | Medium | Add per route group (S) |
A.11 Security Posture (lightweight STRIDE)
| Category | Threat present? | Mitigated? | Gap |
|---|---|---|---|
| Spoofing (identity) | Yes (admin) | Partial for users (Firebase), No for admin | Admin passphrase is not identity (A.4.1) |
| Tampering (integrity) | Yes (form submissions) | Yes — server validates + presigned S3 URLs | Client does not verify S3 PUT status (application-form/index.tsx:152-156) |
| Repudiation | Yes (admin actions) | No | No per-admin identity under shared passphrase |
| Information Disclosure | Yes (PII in form) | Partial | In-memory only; admin passphrase XSS-exposed |
| Denial of Service | N/A client-side | — | Polling amplifies backend load |
| Elevation of Privilege | Yes (admin) | No | Any browser knowing the passphrase is "admin" |
A.12 Operational Readiness
| Capability | Present / Partial / Missing | Notes |
|---|---|---|
| Structured logs | Missing | Browser console only |
| Metrics | Missing | No RUM / Vercel Analytics |
| Distributed tracing | Missing | No trace-id propagation |
| Actionable alerts | Missing | Nothing to alert on |
| Runbooks | Present | data/af-frontend/runbook.md |
| On-call ownership defined | Partial | YAML lists authors; no rotation |
| SLOs / SLIs | Missing | None |
| Backup & restore tested | N/A | Stateless client |
| Disaster recovery plan | Partial | Deployment doc references rollback |
| Chaos / failure testing | Missing | — |
A.13 Test & Quality Signals
- Coverage: 0 / 0 — no framework.
- Untested critical paths: submit → presigned S3 upload → finalize → processing polling; dynamic form rendering; auth-link token refresh;
applyPrefill/processPayload. - Missing test types: [x] unit [x] integration [x] e2e [x] contract [x] load [x] security/fuzz
A.14 Performance & Cost Smells
- Hot paths:
/explore(Mapbox GL initial load),/application/.../form(RHF + large schema),/processing(5 s poll). - Suspected bottlenecks: Mapbox GL + Apollo + styled-components bundle; RHF rerenders on repeated groups; polling.
- Wasteful queries:
FormSubmissionpolled every 5 s unconditionally. - Cache surprises:
cache.evict({fieldName:'formPrefill'})on every submit branch forces refetch.
A.15 Bus-Factor & Knowledge Risk
- Only-person: Cannot determine from code alone; YAML lists 4 authors. Dynamic-form architecture likely 1–2 of them.
- What breaks: Evolving the
RenderFieldswitch safely; understandingapplyPrefill/buildFieldGroupInfocontract; unwritten React Compiler memo-sweep rule. - Tribal knowledge: CI/deploy pipeline (literally not in repo); admin passphrase rotation procedure.
- Actions: Write
docs/dynamic-form.mdextension guide; commit CI config; ADR for admin auth interim decision.
A.16 Compliance Gaps
| Regulation | Requirement | Status | Gap | Remediation |
|---|---|---|---|---|
| GDPR / CCPA | Cookie consent, DSAR | Missing | No banner, no DSAR flow | Add before EU/CA traffic |
| FEMA Privacy Act | No unnecessary PII persistence | Met | — | — |
| Plaid Data Use Policy | Credentials in Plaid iframe | Met | — | — |
| Mapbox ToS | No PII overlayed on tiles | Met today | Risk if per-household overlays added | Policy check before adding |
A.17 Recommendations Summary
| Priority | Action | Owner | Effort | Depends on |
|---|---|---|---|---|
| P0 | Replace admin passphrase with Firebase Auth + isAdmin claim; remove localStorage 'passphrase' | frontend lead + backend lead | M | Backend admin-claim |
| P0 | Adopt Vitest + Playwright and cover submit → finalize → processing happy path | frontend team | M | — |
| P1 | Add Sentry + per-route error.tsx + global-error.tsx | frontend team | S | — |
| P1 | Cap auth-link token-refresh retries per operation | frontend team | S | — |
| P1 | Commit CI config (.github/workflows/ci.yml) running lint + typecheck + tests + bundle-size | frontend team + af-infra | S | A.4.2 |
| P1 | Add middleware.ts checking Firebase __session cookie before rendering (protected) routes | frontend team | M | — |
| P1 | Migrate FormSubmission polling to GraphQL subscription | frontend + backend | M | Backend subscription support |
| P2 | Extract shared Apollo link factory used by both main and admin clients | frontend team | S | A.4.1 |
| P2 | Sweep legacy useMemo/useCallback; add eslint-plugin-react-compiler rule | frontend team | S | — |
| P2 | Refactor RenderField to exhaustive discriminated-union switch on FormField.__typename | frontend team | S | — |
| P2 | Add NEXT_PUBLIC_USE_MOCK_DATA to .env.example and document it | anyone | S | — |
| P2 | Wire admin disaster/program CRUD mutations (replace console.log stubs) | admin feature owner | M | Admin GraphQL mutations |
| P2 | Add GDPR/CCPA cookie-consent banner before EU/CA launch | product + frontend | S | Legal input |
| P2 | Write docs/dynamic-form.md extension guide | dynamic-form owner | S | — |
Environment variables
| Name | Purpose |
|---|---|
API_URL* | Main GraphQL endpoint |
ADMIN_API_URL | Admin GraphQL endpoint |
ADMIN_API_PASSPHRASE | Admin passphrase header default |
MAPBOX_TOKEN* | Mapbox GL access token |
MAPBOX_STYLE_URL* | Custom Mapbox style URL |
FIREBASE_AUTH_EMULATOR_URL | Local Firebase emulator (dev) |
SHOW_PREFILL_BUTTON | Debug prefill button |
NEXT_PUBLIC_USE_AGGREGATES | County-level aggregate map mode |
NEXT_PUBLIC_USE_MOCK_DATA | Mock data mode |
