Architecture
A Next.js application hosts shared review rules. Full reviews are stored as versioned JSON; lists read derived database columns.
Runtime boundaries
The current stack is TypeScript with Next.js 16 and React 19, Tailwind CSS 4 for styling,
Zod for runtime input validation, Drizzle for PostgreSQL queries, and Clerk for sign-in.
Bun installs dependencies and runs scripts/tests; Playwright runs browser journeys. Exact
versions are in package.json.
Browser
Client feature components + ReviewProvider
shared command validation and calculation for immediate feedback
fetch /api/* for saves and later list pages
Next.js server
Server pages: authenticate, query, pass initial data to client components
API handlers: authenticate, parse, check access, invoke commands/stores
features/reviews: commands and save coordination
programs/snap-negative: calculation, checks, field codes, policy
lib/db: SQL reads and transactional writes
PostgreSQL: snapshots + review_index + inbox state + policy tables
lib/documents: local disk or private object storage
A server page executes on the server and can load initial data directly from a store. A
file marked "use client" owns browser interaction and state. Shared
calculation and command functions run in both places, but the server reruns them before
accepting a save. Keep database credentials and object-storage clients in server modules.
Two kinds of saved data
snap_review_snapshots stores a complete SnapReview per database
version. review_index stores one current row per review: list fields, current
version, and report eligibility. That row is a projection, meaning data derived from the
snapshot. Append updates both together, plus inbox events and save-side touch state.
Inbox and catalog do not load snapshot JSON. Search joins the current snapshot only for fields it needs beyond the index. Reports read only the report projection. Downloads load current snapshots and recheck export eligibility.
Identity and versions
| Value | Purpose |
|---|---|
review.id / reviewKey(review) |
Opaque UUID for storage and links. No program, year, or review number is decoded from it. |
(program, fiscal_year, review_number) |
Separately unique business identity; used to match imported samples. |
StoredReview.version |
Database concurrency check. Each accepted command batch appends the next version. |
review.revision |
Domain revision used to determine whether stored validation is current. |
facts.inputsRevision |
Computation-input revision; Final Results refer to the revision they used. |
calcVersion, checkVersion |
Immutable implementation IDs controlling calculation and edit-check behavior. |
policyId, policy |
Dated amount-table identity and saved table copy. |
New imports use UUIDv7; deterministic fixtures use UUIDv5. Migration 0007 assigned random
UUIDs to existing records. Number-only and old FFY:number routes return 404.
See review identity.
Authorization
Clerk identifies the user. Iris builds an Actor from metadata, then checks
role capabilities and row access. reviewScope applies visibility in SQL;
canAccessReview applies it to a loaded review. Store methods such as
load are low-level and do not authorize their callers. Hiding a navigation
link is not an access check.
Dependencies and tradeoffs
Domain commands separate React interaction from validation and workflow. Program
calculation and checks avoid Next, Clerk, and database imports; the program directory
still imports some review feature types/helpers, so it is not an independent package.
Drizzle keeps SQL and selected columns explicit. getDb uses a
pg pool for localhost/127.0.0.1 and Neon HTTP elsewhere; its local batch
adapter opens a transaction.
There is no background projector, command-replay store, or websocket service. These are observations about the current implementation, not permanent prohibitions. Read Tradeoffs before extending a boundary.