Architecture
How the Frankie monorepo is put together, and why.
Layout
frankie/
├── agentic-team/ # AI role docs (portable kit — don't edit here)
├── docs/ # PRD, plans, research, reference, this file
├── mocks/ # @frankie/mocks — the fixture dataset (workspace pkg)
├── packages/
│ ├── shared-types/ # @frankie/shared-types — domain model (TS only)
│ └── tsconfig/ # @frankie/tsconfig — shared compiler configs
├── apps/
│ └── web/ # @frankie/web — Next.js 16 app (the whole MVP)
├── supabase/ # DB migrations + generated seed (see its README)
├── pnpm-workspace.yaml · turbo.json · package.json
System-level view (app ↔ database ↔ pipeline, environments, security): reference/system-architecture.md. Database schema rationale: plans/database-design.md.
pnpm workspaces + Turborepo per the house standard
(agentic-team/references/monorepo_structure.md), with two deliberate
deviations, both sanctioned by the founder for MVP:
- No
apps/api(NestJS) — the MVP has no backend. Pages query a repository layer that today reads mock data and later queries Supabase directly. A dedicated API server is added only when ingestion lands. - No
packages/eslint-config— one app, one flat config (apps/web/eslint.config.mjs). Extract when a second app appears.
mocks/ lives at the root (not under packages/) by request — it's a
first-class, replaceable stand-in for the future database.
Data flow (ports & adapters)
page (server component)
→ getDataSource() lib/data/index.ts (factory, env-driven)
→ FrankieDataSource lib/data/repositories.ts (interfaces)
├─ MockDataSource lib/data/mock/ ← MVP (reads @frankie/mocks)
└─ SupabaseDataSource lib/data/supabase/ ← post-MVP seam (README)
→ pure domain services lib/domain/{market-stats,valuation}.ts
→ components render tokens + data
SOLID mapping:
- S — repositories fetch, domain services compute, formatters format
(
lib/format.ts), components render. - O/D — pages depend on the
FrankieDataSourceinterface; swapping mock → Supabase touches only the factory (lib/data/index.ts). - L — interfaces are async (written for the real backend) so the Supabase implementation is substitutable; no throwing stub is shipped.
- I —
CardRepository/IssuerRepository/EventRepository/TransferPartnerRepositoryare segregated;FrankieDataSourcecomposes them.
Rules of the road:
- No
fetch()in components. All data access vialib/data. - No business math in components. Valuation/stats live in
lib/domainas pure functions (unit-testable, reused across pages). - Server components by default;
"use client"only for interactivity (table, compare board, charts, sidebar active state). - Shared domain types live in
@frankie/shared-types— never redeclared.
Theme system
Every design token lives in one file:
— fonts, type
scale, spacing unit, primitive palette, semantic (shadcn) tokens, glass
tokens, radii. apps/web/src/app/theme.cssglobals.css is plumbing only (Tailwind wiring, .glass
helpers). Components never contain raw colors or sizes — semantic
utilities only. See DESIGN.md.
Mock data
@frankie/mocks = 27 cards, 33 events, 11 issuers, 20 transfer partners.
Generated from the CardIntel prototype seed
(reference/claude-design-spec.md) via
mocks/transform-seed.mjs, plus hand-curated co-brand cards. Illustrative,
not verified — the UI carries a "mock data" marker; the schema is the real
contract.
Verification
pnpm typecheck · pnpm lint · pnpm build (turbo across the workspace),
CI in .github/workflows/ci.yml. Tests: domain services are the designated
unit-test surface; suites land with the first real data adapter, when
behavior can regress silently.