Basis of preparation
Everything on these six sheets is drawn from the repository itself — 22,437 lines of
TypeScript under src/, three migrations, the committed configuration, the
specification documents, and a 16,649-line test suite.
Each assertion carries a basis marker so you can tell how firmly it is held down. The distinction matters, and in this repository it turns out to matter more than usual: there are places where the specification, the test suite and the shipped code each say something different. Where they disagree, this document follows the code and records the disagreement in the register of exceptions.
| Marker | Meaning |
|---|---|
| Source | Read from a file under src/. This is what the
application actually does, and it outranks every other marker. |
| Test | Pinned by an assertion in __tests__/. The named symbol,
signature, literal string or status code is quoted from a test that exercises it.
A test records what the code was expected to do when the test was
written — which is not always what it does now. |
| Migration | Read from drizzle/*.sql or the Drizzle metadata
snapshots. This is the schema as actually applied. |
| Config | Read from a committed configuration file —
jest.config.ts, drizzle.config.ts,
components.json, eslint.config.mjs. |
| Spec | Stated in PRD.md, TASKS.md,
CLAUDE.md or DEPLOY.md, but not independently
corroborated by a test or a migration. |
This is a technical description of a private internal system. No customer, vendor, employee or counterparty identities appear anywhere in this document, and no balances, payroll figures or headcounts are reproduced. Every number printed here counts code: tables, columns, tools, routes, tests.
Nature of operations
The system answers a narrow question continuously: where is the money, where did it go, and how long does it last. It does so without asking anyone to do bookkeeping. Two upstream systems are polled on a schedule; their records are normalised into a single transaction table; a language model assigns each counterparty to a category; and the resulting ledger is read by both a conventional dashboard and a conversational agent.
It is deliberately single-tenant. PRD.md states “Single company (no
multi‑tenant)”, and the schema bears this out — accounts,
transactions, categories and settings carry no
organisation column. Only the chat tables are scoped to a user.
The four capabilities
- Bank synchronisation
-
A typed client for the Mercury API at
https://api.mercury.com/api/v1walks every account, pages through transactions 500 at a time by offset, and upserts them by their upstream identifier. Duplicate suppression is enforced by a unique constraint ontransactions.mercury_id, not by application logic alone.1 - Automatic classification
- Uncategorised transactions are grouped by counterparty, normalised, matched against previously learned patterns, and only the genuine remainder is sent to Claude in batches. A manual override can be written back as a rule, so the same vendor is never re-inferred twice.
- Payroll ingestion
-
Payroll reaches the system indirectly. Gusto posts journal entries into QuickBooks
Online; the app connects to QuickBooks over OAuth 2.0, queries
JournalEntryrecords, and reverse-engineers each entry's debits and credits back into gross pay, taxes, deductions and net pay. - Conversational analysis
- A Claude agent is given 20 tool definitions over the same ledger — queries, metrics, trends, anomalies, vendor rollups, payroll summaries and a sandboxed arithmetic evaluator. Responses stream to the browser as server-sent events, with conversations persisted per user.
-
drizzle/0000_round_payback.sqlline 66:CONSTRAINT "transactions_mercury_id_unique" UNIQUE("mercury_id"). The corresponding behaviour — a second sync reportingcreated: 0and a non-zeroskipped— is asserted in__tests__/server/sync/transactions.test.ts. ↩
Consolidation of sources
Two external ledgers are consolidated into one. Neither is treated as authoritative in place: both are copied into Postgres, and every downstream reader — dashboard, agent, cron job — reads the local copy through Drizzle. The upstream APIs are touched only by the sync layer.
← Scroll to see the full diagram →
src/, recovered from the test suite's @/ imports. The dashed
line marks the scheduler's control relationship rather than a data flow. Note that
categorisation is reached two ways: fired asynchronously by a completed sync, and
independently on its own ten-minute cron as a backstop.
Principal figures
Counts of the artefacts that make up the system. Nothing here is an estimate; each line is countable from a file in the repository.
| Line item | Count | Basis |
|---|---|---|
| Persistence | ||
| Tables at migration 0002 | 12 | Migration |
| Columns across all tables | 94 | Migration |
| Foreign-key constraints | 8 | Migration |
| Unique constraints | 3 | Migration |
| Migrations applied | 3 | Migration |
| Seeded default categories | 12 | Test |
| Agent surface | ||
Tool definitions in financialTools | 20 | Source |
| — financial tools | 12 | Source |
| — payroll and compensation tools | 8 | Source |
| — of which have a handler | 20 | Source |
| Tool-loop iteration cap | 10 | Source |
| Server-sent event types on the chat stream | 5 | Test |
| Suggested prompts offered on an empty chat | 4 | Test |
| Interface | ||
Dashboard routes in navItems | 6 | Source |
| API route files | 27 | Source |
| — production | 18 | Source |
| — debug, unauthenticated | 9 | Source |
| Time-range options on Expenses | 4 | Test |
| Month-range options on Cash Flow | 4 | Test |
| Codebase | ||
TypeScript files under src/ | 149 | Repository |
| Lines of source | 22,437 | Repository |
| Runtime dependencies | 29 | Config |
| npm scripts | 13 | Config |
| Assurance | ||
Test files under __tests__/ | 50 | Repository |
| Lines of test | 16,649 | Repository |
| API routes with a route test | 5 of 27 | Repository |
describe blocks | 353 | Repository |
it / test blocks | 1,088 | Repository |
| Coverage threshold, all four metrics | 80% | Config |
Tests recorded in the TASKS.md summary2 |
1,056 | Spec |
Sources: drizzle/meta/0002_snapshot.json, src/lib/chat/tools.ts, src/components/layout/sidebar.tsx, src/server/db/seed.ts, src/app/api/, package.json, jest.config.ts, TASKS.md. The tool count is read from the source array, not from the test that counts it — see exception 1.
-
Two figures appear in
TASKS.mdand they do not agree. Task 8.3 records a full-suite run of 870 passed; the Test Summary table at the foot of the same file totals 1,056. The gap is consistent with the summary table having been extended by later phases (Phase 9 and Phase 10) without the run being repeated. The mechanically countable figure — 1,088itblocks — is close to the summary total but includes 15 blocks inside adescribe.skipand a live-API integration suite that is skipped unless credentials are present. ↩
Variance noted. TASKS.md task 1.1 describes the initial migration as
“7 tables”. The migration file 0000_round_payback.sql in fact
creates 8: accounts, categories,
category_patterns, chat_conversations,
chat_messages, settings, transactions,
users. Where the prose and the SQL disagree, this document follows the SQL.
Technology basis
Twenty-nine runtime dependencies and nineteen development dependencies, with the versions
as declared in package.json. This is a current-generation stack: React 19 and
Next.js 16 on the framework side, Tailwind 4 and Zod 4 alongside.
| Layer | Package | Version | Role in the system |
|---|---|---|---|
| Runtime | |||
| Framework | next | 16.1.0 | App Router; six pages, 27 API routes, middleware |
| UI runtime | react / react-dom | 19.2.3 | Server and client components |
| Database driver | @neondatabase/serverless | 1.0.2 | HTTP Postgres client, suited to serverless functions |
| ORM | drizzle-orm | 0.45.1 | Schema definition and every query |
| Auth | @clerk/nextjs | 6.36.5 | clerkMiddleware, session lookup, user sync |
| Webhooks | svix | 1.82.0 | Signature verification on the Clerk webhook |
| Model access | @anthropic-ai/sdk | 0.71.2 | Categorisation and the tool-using agent |
| Validation | zod | 4.2.1 | Request schemas at the API boundary |
| Charts | recharts | 2.15.4 | Cash flow, expense breakdown, payroll series |
| Markdown | react-markdown / remark-gfm | 10.1.0 / 4.0.1 | Rendering streamed assistant replies |
| Panels | react-resizable-panels | 2.1.9 | The resizable chat push-panel |
| Primitives | @radix-ui/react-* | 9 packages | Dialog, dropdown, scroll-area, tabs, tooltip and more |
| Icons | lucide-react | 0.562.0 | Icon set named in components.json |
| Theming | next-themes | 0.4.6 | Light/dark toggle in the header |
| Dates | date-fns | 4.1.0 | Relative timestamps in the conversation list |
| Spreadsheets | xlsx | 0.18.5 — reads the payroll report exports held in payrollexport/ | |
| Development | |||
| Language | typescript | 5 | strict: true, noEmit, path alias @/* → ./src/* |
| Test runner | jest | 30.2.0 | With jest-environment-jsdom and Testing Library |
| Migrations | drizzle-kit | 0.31.8 | Backs the five db:* scripts |
| Styling | tailwindcss | 4 | Via @tailwindcss/postcss; no tailwind.config file |
| Total declared | 29 dependencies + 19 devDependencies | ||
Hosting is the one item not evidenced by a dependency: vercel.json declares the two cron entries, and DEPLOY.md describes a Vercel deployment, but nothing in the code requires it.
Reporting surfaces
Six dashboard routes, exported as a single navItems array from
src/components/layout/sidebar.tsx so that the sidebar and its tests read the
same list. Each entry carries a title, an href and an icon.
| # | Title | Route | Principal content |
|---|---|---|---|
| 1 | Overview | / | KPI cards: cash balance, burn rate, runway, net change |
| 2 | Cash Flow | /cash-flow | Inflow/outflow series; month-range selector, 6m / 12m / ytd / all |
| 3 | Expenses | /expenses | Category breakdown; time-range selector, 30d / 90d / ytd / all |
| 4 | Payroll | /payroll | Payroll runs, monthly totals, per-employee breakdown |
| 5 | Transactions | /transactions | Filterable, paginated table with inline category editing |
| 6 | Settings | /settings | Mercury key, sync status and history, category management, QuickBooks connection |
Source: __tests__/components/layout/sidebar.test.tsx, which imports navItems directly and asserts each title and href.
The chat panel is not a route. It is mounted at layout level so that a conversation
survives navigation between the six pages — a decision recorded in
TASKS.md 5.4 and visible in the context tests, where the message history is
held in a provider rather than in any page component.
Observation. The sidebar test that enumerates “the correct navigation
links” checks five of the six items and omits Payroll, even though a
neighbouring test iterates the full navItems array. Payroll is the most
recently added route; the assertion list appears not to have been extended with it.
Coverage of the item itself is therefore weaker than the surrounding tests suggest.