AidFinder
Back to dashboard

af-frontend

Aid Finder Frontend

Next.js 16 / React 19 / TypeScript Apollo client at usaidfinder.com — disaster map, dynamic form renderer, AI processing portal, admin panel.

Domain role
Survivor-facing web app (consumer of af-backend-go-api GraphQL + af-map tiles + Firebase Auth)
Last updated
2026-04-07
Lines of code
24,980
API style
GraphQL (client-side consumer only; frontend exposes NO API of its own)

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

  • QUERYListForms / Form / FormPrefill / FormSubmission / FormSubmissionsForm schema + submissions
  • MUTATIONSubmitFormPersist + validate submission, return presigned S3 URLs
  • MUTATIONFinalizeFormSubmissionMark uploaded, kick AI
  • MUTATIONContinueAgentResume Agent 2
  • MUTATIONCreatePlaidLinkToken / LinkBankAccountPlaid bank linking
  • QUERYMe / Login / UpdateMeAccount management
  • QUERYDashboardUser submissions + stats
  • QUERYActiveDisastersDisasters list for map
  • QUERYLocation.Detail / PopupDetailMap popups + location detail
  • QUERYAdmin 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

overall health3.0 / 5acceptable
4Module overview / clarity of intent
4External dependencies
4API endpoints
3Database schema
3Backend services
2WebSocket / real-time
4Frontend components
4Data flow clarity
2Error handling & resilience
3Configuration
3Data refresh patterns
2Performance
4Module interactions
3Troubleshooting / runbooks
1Testing & QA
2Deployment & DevOps
2Security & compliance
4Documentation & maintenance
3Roadmap clarity

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 localStorage and forwarded as an x-admin-passphrase header (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 isAdmin claim, and in parallel adopt Vitest + Playwright for the submit→finalize→processing happy path.
  • Blocking unknowns: No CI/deploy pipeline exists in-repo (.github/workflows returns 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

#DimensionScore (1–5)Justification
1Module overview / clarity of intent4Prop-build YAML + README + component-tree.md clearly describe surfaces and flows.
2External dependencies4Pinned versions, modern stack; Firebase public config hardcoded in src/lib/config.ts:16-22 is intentional but worth noting.
3API endpoints4Pure GraphQL client via codegen; typed operations next to .graphql files. No own API surface.
4Database schema3N/A persistent; Apollo cache config is minimal but correct (src/lib/api/graphql/apollo.ts:8-17).
5Backend services3N/A — pure client; score neutral.
6WebSocket / real-time25 s polling only (form-submission-portal/index.tsx:26,40); no subscriptions, wasteful and high-latency.
7Frontend components4Feature-sliced layout, dynamic-form is a strong abstraction; a few leaky any-ish casts in render-field.tsx.
8Data flow clarity4Single Apollo client per tree, auth-link well-isolated (src/lib/api/graphql/links/auth-link.ts).
9Error handling & resilience2No React error boundaries / error.tsx; auth-link retries but has no backoff cap and can loop on repeated ERR_UNAUTHORIZED.
10Configuration3src/lib/config.ts centralizes env, but NEXT_PUBLIC_USE_MOCK_DATA missing from .env.example (noted in YAML tech_debt).
11Data refresh patterns3Polling + on-demand; no SWR/cache invalidation strategy beyond cache.evict({fieldName:'formPrefill'}) in application-form/index.tsx:165.
12Performance2No measurements, no bundle budget, React Compiler on but legacy useMemo still present (render-field.tsx:52-74).
13Module interactions4Clean boundaries with af-backend-go-api, af-map, Firebase, Mapbox, Plaid.
14Troubleshooting / runbooks3Runbook exists (data/af-frontend/runbook.md) but no alerts to trigger it.
15Testing & QA1Zero test framework in package.json; Storybook is not an automated gate.
16Deployment & DevOps2No CI config in repo, no Dockerfile, no vercel.json — deploy is invisible to the repo.
17Security & compliance2Admin passphrase in localStorage, no CSP/security headers visible, no cookie-consent, no Sentry to catch XSS regressions.
18Documentation & maintenance4README + companion docs are unusually thorough for a frontend repo.
19Roadmap clarity3section_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 by src/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.
  • Strength: Centralized Apollo auth-link with single-flight token refresh.

    • Location: src/lib/api/graphql/links/auth-link.ts:28-116.
    • Why it works: isRefreshing flag + pendingRequests queue avoids thundering-herd token refreshes. Triggered off both ERR_UNAUTHORIZED in 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.
  • Strength: Apollo cache type policy for union + custom keyFields.

    • Location: src/lib/api/graphql/apollo.ts:8-17 (FormField possibleTypes, Household.bqHouseholdId keyFields).
    • Why it works: Correct handling of GraphQL unions and non-id entities is a frequent bug source; solved deliberately here.
  • 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.

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 localStorage and sent as x-admin-passphrase on 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 admin custom claim verified server-side; drop the passphrase. Stop-gap: move to HttpOnly cookie 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.json has 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:241 renders null in prod for an unknown field component, nobody hears it.
  • Evidence: YAML §14 monitoring_alerts.applicable: false; no @sentry/* in package.json.
  • Suggested change: Add Sentry (browser + source maps) and wire global-error.tsx + per-route error.tsx boundaries.
  • 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, handleTokenRefresh refreshes 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 cites src/features/dashboard/pages/dashboard/parts/circular-progress/index.tsx and src/features/map/....
  • Suggested change: Sweep and remove; add eslint-plugin-react-compiler rule.
  • 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.aiAgentLatestState in af-backend-go-api and use useSubscription.
  • 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

  1. 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-29 vs src/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?
  2. Observation: resolveFormPath and resolveFileValue reconstruct 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?
  3. 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.

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.tsx uses 'options' in field / 'placeholder' in field runtime checks instead of an exhaustive discriminated union)
  • Golden hammer
  • Vendor lock-in
  • Stovepipe
  • Missing seams for testing (hardcoded fetch to 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 (passphrase in localStorage)
  • 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-patternLocation (file:line)SeverityRecommendation
1Secret in localStorage + missing real authnsrc/features/admin/api/client/apollo.ts:10; src/features/admin/components/layout/login/index.tsx:34P0Firebase Auth + admin claim (A.4.1)
2Copy-paste duplication of Apollo client setupsrc/features/admin/api/client/apollo.ts:9-29 vs src/lib/api/graphql/links/auth-link.tsP2Extract shared link factory
3Leaky abstraction via in-checks on FormField unionsrc/lib/components/dynamic-form/parts/render-field.tsx:108,167,186,208,211P2Exhaustive switch on __typename
4Retry storm potential in auth-linksrc/lib/api/graphql/links/auth-link.ts:104-108,147-158P1Per-op attempt counter
5Missing testing seam for file uploadssrc/features/application/components/application-form/index.tsx:152-156P1Inject fetch/uploader dependency
6Primitive obsession — RHF field paths as dot-stringssrc/features/application/components/application-form/index.tsx:26-35P2Typed helper / branded type
7Magic localStorage key 'passphrase'same files as row 1P2Constant (obsolete once A.4.1 lands)
8Legacy useMemo under React Compilersrc/lib/components/dynamic-form/parts/render-field.tsx:52-74P2Remove per project rule

A.7 Open Questions

  1. Q: Is the admin passphrase scheme a deliberate interim pending Firebase-admin-claim migration, or the intended end state?
  2. Q: Where does CI actually live? .github/workflows is empty.
  3. Q: What are the measured LCP/TTI/bundle size for /explore?
  4. Q: Is a subscription on FormSubmission feasible 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.yml or 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 codegen or pnpm build.

A.9 Risks & Unknowns

A.9.1 Known risks

#RiskLikelihoodImpactMitigation
1Admin passphrase leakage via XSS, screen share, or git historyMHA.4.1
2Silent production regressions on submit flowHHA.4.2
3Undetected client crashes / blank screensMMA.4.3
4Auth-link retry loop on persistent ERR_UNAUTHORIZEDLMA.4.4
5Missing cookie-consent / CCPA controlsMMAdd banner before EU/CA launch
6Polling load on backend during large activationsMMA.4.6
7Deploy pipeline opaque to the repoMMCommit 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 itemQuadrantInterestRemediation
1Admin passphrase auth in localStorageReckless & DeliberateHighFirebase admin claim (M)
2Zero automated testsReckless & DeliberateHighVitest + Playwright (M)
3No error tracking / RUMReckless & InadvertentMediumSentry + error.tsx (S)
4No CI config in repoPrudent & InadvertentMediumCommit ci.yml (S)
5Duplicated Apollo client setup (main vs admin)Prudent & InadvertentLowExtract factory (S)
6Legacy useMemo/useCallback under React CompilerPrudent & InadvertentLowSweep + eslint rule (S)
7NEXT_PUBLIC_USE_MOCK_DATA missing from .env.examplePrudent & InadvertentLowAdd it (S)
8Client-only auth guard (no middleware.ts)Reckless & DeliberateMediumMiddleware checking Firebase __session cookie (M)
9Admin edit handlers stub (console.log) + mock data arraysPrudent & Deliberate (WIP)High when enabledWire admin mutations (M)
105 s polling for AI statePrudent & DeliberateMediumGraphQL subscription (M)
11No error boundaries / error.tsxReckless & InadvertentMediumAdd per route group (S)

A.11 Security Posture (lightweight STRIDE)

CategoryThreat present?Mitigated?Gap
Spoofing (identity)Yes (admin)Partial for users (Firebase), No for adminAdmin passphrase is not identity (A.4.1)
Tampering (integrity)Yes (form submissions)Yes — server validates + presigned S3 URLsClient does not verify S3 PUT status (application-form/index.tsx:152-156)
RepudiationYes (admin actions)NoNo per-admin identity under shared passphrase
Information DisclosureYes (PII in form)PartialIn-memory only; admin passphrase XSS-exposed
Denial of ServiceN/A client-sidePolling amplifies backend load
Elevation of PrivilegeYes (admin)NoAny browser knowing the passphrase is "admin"

A.12 Operational Readiness

CapabilityPresent / Partial / MissingNotes
Structured logsMissingBrowser console only
MetricsMissingNo RUM / Vercel Analytics
Distributed tracingMissingNo trace-id propagation
Actionable alertsMissingNothing to alert on
RunbooksPresentdata/af-frontend/runbook.md
On-call ownership definedPartialYAML lists authors; no rotation
SLOs / SLIsMissingNone
Backup & restore testedN/AStateless client
Disaster recovery planPartialDeployment doc references rollback
Chaos / failure testingMissing

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: FormSubmission polled 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 RenderField switch safely; understanding applyPrefill/buildFieldGroupInfo contract; unwritten React Compiler memo-sweep rule.
  • Tribal knowledge: CI/deploy pipeline (literally not in repo); admin passphrase rotation procedure.
  • Actions: Write docs/dynamic-form.md extension guide; commit CI config; ADR for admin auth interim decision.

A.16 Compliance Gaps

RegulationRequirementStatusGapRemediation
GDPR / CCPACookie consent, DSARMissingNo banner, no DSAR flowAdd before EU/CA traffic
FEMA Privacy ActNo unnecessary PII persistenceMet
Plaid Data Use PolicyCredentials in Plaid iframeMet
Mapbox ToSNo PII overlayed on tilesMet todayRisk if per-household overlays addedPolicy check before adding

A.17 Recommendations Summary

PriorityActionOwnerEffortDepends on
P0Replace admin passphrase with Firebase Auth + isAdmin claim; remove localStorage 'passphrase'frontend lead + backend leadMBackend admin-claim
P0Adopt Vitest + Playwright and cover submit → finalize → processing happy pathfrontend teamM
P1Add Sentry + per-route error.tsx + global-error.tsxfrontend teamS
P1Cap auth-link token-refresh retries per operationfrontend teamS
P1Commit CI config (.github/workflows/ci.yml) running lint + typecheck + tests + bundle-sizefrontend team + af-infraSA.4.2
P1Add middleware.ts checking Firebase __session cookie before rendering (protected) routesfrontend teamM
P1Migrate FormSubmission polling to GraphQL subscriptionfrontend + backendMBackend subscription support
P2Extract shared Apollo link factory used by both main and admin clientsfrontend teamSA.4.1
P2Sweep legacy useMemo/useCallback; add eslint-plugin-react-compiler rulefrontend teamS
P2Refactor RenderField to exhaustive discriminated-union switch on FormField.__typenamefrontend teamS
P2Add NEXT_PUBLIC_USE_MOCK_DATA to .env.example and document itanyoneS
P2Wire admin disaster/program CRUD mutations (replace console.log stubs)admin feature ownerMAdmin GraphQL mutations
P2Add GDPR/CCPA cookie-consent banner before EU/CA launchproduct + frontendSLegal input
P2Write docs/dynamic-form.md extension guidedynamic-form ownerS

Environment variables

NamePurpose
API_URL*Main GraphQL endpoint
ADMIN_API_URLAdmin GraphQL endpoint
ADMIN_API_PASSPHRASEAdmin passphrase header default
MAPBOX_TOKEN*Mapbox GL access token
MAPBOX_STYLE_URL*Custom Mapbox style URL
FIREBASE_AUTH_EMULATOR_URLLocal Firebase emulator (dev)
SHOW_PREFILL_BUTTONDebug prefill button
NEXT_PUBLIC_USE_AGGREGATESCounty-level aggregate map mode
NEXT_PUBLIC_USE_MOCK_DATAMock data mode