OllinDocs
Data flow & contracts

How a screen gets its numbers

Fetch layer → frozen wire contracts → view models → components, with no shortcuts.

Every number on every screen travels one pipeline, and each stage has exactly one job:

1. Fetch layer

src/lib/api/client.ts (postflop) and src/lib/api/preflop-client.ts (preflop) are the only modules that talk to the network. Both read OLLIN_STUDY_API_URL (trailing slash stripped) and share one apiGet pattern: return the parsed body, or null on any failure — env unset, non-2xx, or a network throw. Every fetcher then decides between live data and the mock fallback, and every result is tagged { data, source: "live" | "mock" } so pages can raise the demo ribbon.

Solve data is immutable per dataset stamp, so caching is generous (next: { revalidate } per request):

RequestRevalidate
GET /v1/families (catalog)300s
GET /v1/families/{fam} (detail)300s
GET /v1/families/{fam}/boards/{board} (node document)3600s
GET /v1/preflop (catalog)300s
GET /v1/preflop/{depth} (depth pack)3600s, plus an in-process 1h memo

The aggregate flop report has no endpoint yet: fetchFamilyAggregate fans out over the per-board endpoint with bounded concurrency (limit 12) and builds the report client-side from each document's server-sent grid169. Failed boards become mix: null — rendered as gaps, never fabricated.

2. The contract mirror

The API's wire contracts are frozen v1 documents, defined once in the API repo and mirrored file-for-file client-side:

API schema (source of truth)App mirror
ollin-study-api/schema/node-document.v1.tssrc/lib/api/contract.ts
ollin-study-api/schema/preflop.v1.tssrc/lib/api/preflop-contract.ts

Breaking changes require a v2 document and a new schema file — v1 is never mutated in place. The two contracts are documented in full on the next pages: NodeDocument v1 and Preflop Pack v1.

3. View models

src/lib/api/view-model.ts and preflop-view-model.ts derive everything display-side from wire documents: action labels and colors, display ordering, 169-class grouping, lineage, pot geometry, URL state. Components never touch wire shapes — a component that needs a new number gets it from a view model, which derives it from the contract. Details: View models.

4. Components

Server components (src/app/(product)/study/…) call the fetch layer, build view models, and pass plain view objects into client components (src/components/study/*, src/components/preflop/*). Client-side state is interaction only (hover, pin, filters, search); data state lives in the URL (?depth=, ?line=) and the server render.

Why this shape

  • One derivation per number. The 13×13 grid is aggregated by the server (grid169); the client renders it and never recomputes it from per-combo data when a live document is present.
  • Honest fallback. Every page knows whether it rendered live or mock data and shows the ribbon accordingly — there is no path where fabricated numbers render unlabeled.
  • Contract-first evolution. The tree endpoint already exists (as a typed 501), the UI already renders its slots (locked). When coverage arrives, nothing above the fetch layer changes.

On this page