System architecture
Reference · 2026-08-30. How Frankie's pieces fit, today and through the roadmap phases. Companions: ../ARCHITECTURE.md (repo structure), ../plans/database-design.md (schema), ../plans/card-data-ingestion.md (pipeline).
The system at a glance
┌──────────────────────────────────────────────┐
issuer pages, │ INGESTION (Phase 0–1) │
terms PDFs, ────▶ │ change detection → fetch → LLM extraction │
CFPB data │ → validation → human review → publish() │
└───────────────────┬──────────────────────────┘
│ service role, one transaction
▼
┌──────────────────────────────────────────────┐
│ SUPABASE (Postgres + Storage) │
│ L1 evidence: sources, extractions, raw docs │
│ L2 truth: versioned rows (terms, offers, │
│ rewards, benefits, partners) │
│ L3 derived: card_events │
│ RLS: public read on L2/L3, pipeline-only L1 │
└───────────────────┬──────────────────────────┘
│ anon key, read-only
▼
┌──────────────────────────────────────────────┐
│ WEB APP (@frankie/web on Vercel) │
│ getDataSource() ──▶ FrankieDataSource │
│ ├─ MockDataSource (@frankie/mocks) ← MVP │
│ └─ SupabaseDataSource ← post-seed │
│ lib/domain: pure stats + valuation │
│ pages: dashboard · cards · compare · │
│ issuers · changes · analyst · admin │
└──────────────────────────────────────────────┘
│ later
▼
API product · AI analyst · consumer agent
One asset (the card graph), many consumers — the Channel 3 playbook (see ../PRD.md §Thesis).
Components
Web app — Next.js 16 App Router on Vercel, server components by
default. All reads go through the repository interfaces in
apps/web/src/lib/data; all business math is pure functions in
lib/domain; all styling is tokens from theme.css. No app-side writes to
the database exist or are planned — the app is a read model.
Database — Supabase Postgres, three layers (evidence → versioned truth
→ derived events) with the publish function as the only write path into
layers 2/3. Schema in supabase/migrations/, seed generated from
@frankie/mocks. Raw fetched documents live in Supabase Storage,
hash-addressed, pointed at by sources.storage_path.
Ingestion pipeline (not yet built — Phase 0/1) — scripts in a future
packages/pipeline, run by GitHub Actions cron: change detection on
~250 tracked issuer URLs → fetch + archive → LLM extraction to the schema
contract → validation gates → human review queue → publish_extraction().
The pipeline holds the service key; it is the only component with write
access.
Docs surface — /admin renders docs/ from the repo at request time;
the repo itself is the CMS.
Data flow contracts
- UI ↔ data: the
FrankieDataSourceinterface. Swapping mock → Supabase changes one factory (lib/data/index.ts) and zero pages. The generated seed makes the two sources start identical, which is the adapter-equivalence test. - Pipeline ↔ database: the extraction JSON contract (defined in Phase
0) +
publish_extraction(). Nothing else may mutate layer 2/3 — "history is sacred" is enforced by grants, not convention. - Database ↔ future API product: the same layer 2/3 tables exposed via PostgREST or a thin API app later; no separate data model.
Environments & deploys
| Concern | Today | Notes |
|---|---|---|
| Hosting | Vercel (apps/web, root dir setting) | CI (lint/typecheck/build) on GitHub Actions |
| Domain | frankie.cards | metadataBase set in layout |
| Data | NEXT_PUBLIC_DATA_SOURCE=mock | flip to supabase per env after seeding |
| Database | none yet | supabase init + db reset locally; link + db push for cloud |
| Secrets | none in the app | anon key is public by design; service key lives only in pipeline CI |
Security model
- Anon key + RLS read-only policies = the public app can never write.
- Evidence/staging tables have no anon policies (deny by default).
- Service key never ships to the browser or the web app runtime; it exists only in pipeline jobs.
- Auth (Supabase Auth) arrives in PRD Phase 2 for gated features (exports,
alerts); RLS policies already distinguish
anonvsauthenticated.
How the phases change this picture
- Phase 0–1 (data foundation): pipeline + database go live; web flips
DATA_SOURCE. No page changes. - Phase 3 (change tracking): the changes feed and history timelines
start rendering real
card_eventsfrom the publish path — same UI. - Phase 4 (AI analyst): a query-compilation layer (NL → structured filters over layer 2/3) behind the existing analyst page. Reads only.
- API product: keyed read access to the same tables/views; rate limits and plans at the edge, not in the schema.