all docsdocs/ARCHITECTURE.md

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:

  1. 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.
  2. 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 FrankieDataSource interface; 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.
  • ICardRepository / IssuerRepository / EventRepository / TransferPartnerRepository are segregated; FrankieDataSource composes them.

Rules of the road:

  • No fetch() in components. All data access via lib/data.
  • No business math in components. Valuation/stats live in lib/domain as 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: apps/web/src/app/theme.css — fonts, type scale, spacing unit, primitive palette, semantic (shadcn) tokens, glass tokens, radii. globals.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.