Gogentic Finance · internal engineering record

Sheet 1 of 6

Statement of Position

A single-company executive finance dashboard. It draws transactions from a business bank account and payroll from an accounting ledger, normalises both into one Postgres schema, classifies spend with a language model, and exposes the result to a tool-using agent.

Framework
Next.js 16.1.0
Source files
149
Source lines
22,437
Tables
12
Agent tools
20
API routes
27
1

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.

Basis markers used throughout
MarkerMeaning
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.
Note — scope of this record

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.

2

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/v1 walks 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 on transactions.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 JournalEntry records, 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.
  1. drizzle/0000_round_payback.sql line 66: CONSTRAINT "transactions_mercury_id_unique" UNIQUE("mercury_id"). The corresponding behaviour — a second sync reporting created: 0 and a non-zero skipped — is asserted in __tests__/server/sync/transactions.test.ts.
3

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.

Exhibit 1
Consolidation topology of Gogentic Finance Two external sources, Mercury Bank API and QuickBooks Online API, flow through an ingest layer of TypeScript modules into a single Neon Postgres schema of twelve tables, which is read by a Next.js dashboard and a Claude agent. Vercel Cron drives the two scheduled ingest paths. SOURCE SYSTEMS INGEST & NORMALISE LEDGER OF RECORD READERS Mercury Bank API api.mercury.com/api/v1 accounts · transactions offset pages of 500 QuickBooks Online quickbooks.api.intuit.com JournalEntry query, OAuth 2.0 payroll posted upstream by Gusto as journal entries server/sync/ syncAccounts, syncTransactions runFullSync, runIncrementalSync lib/ai/categorize counterparty grouping, 50-vendor batches to Claude lib/quickbooks/sync parseGustoJournalEntry Neon Postgres Drizzle ORM · 12 tables accounts transactions categories category_patterns payroll_runs payroll_line_items employees chat_conversations chat_messages, users, settings, quickbooks_… Dashboard 6 routes: Overview, Cash Flow, Expenses, Payroll, Transactions, Settings Claude agent 20 tool definitions SSE stream to browser per-user conversations and message history async, non-blocking SCHEDULER Vercel Cron bearer-authenticated GET endpoints */5 → /api/cron/sync */10 → /api/categorize drives ingest

← Scroll to see the full diagram →

Consolidation topology. Module names are the real import paths under 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.
4

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.

Schedule of principal figures
Line item Count Basis
Persistence
Tables at migration 000212Migration
Columns across all tables94Migration
Foreign-key constraints8Migration
Unique constraints3Migration
Migrations applied3Migration
Seeded default categories12Test
Agent surface
Tool definitions in financialTools20Source
— financial tools12Source
— payroll and compensation tools8Source
— of which have a handler20Source
Tool-loop iteration cap10Source
Server-sent event types on the chat stream5Test
Suggested prompts offered on an empty chat4Test
Interface
Dashboard routes in navItems6Source
API route files27Source
— production18Source
— debug, unauthenticated9Source
Time-range options on Expenses4Test
Month-range options on Cash Flow4Test
Codebase
TypeScript files under src/149Repository
Lines of source22,437Repository
Runtime dependencies29Config
npm scripts13Config
Assurance
Test files under __tests__/50Repository
Lines of test16,649Repository
API routes with a route test5 of 27Repository
describe blocks353Repository
it / test blocks1,088Repository
Coverage threshold, all four metrics80%Config
Tests recorded in the TASKS.md summary2 1,056Spec

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.

  1. Two figures appear in TASKS.md and 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,088 it blocks — is close to the summary total but includes 15 blocks inside a describe.skip and 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.

5

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.

Schedule of principal dependencies, from package.json
Layer Package Version Role in the system
Runtime
Frameworknext16.1.0App Router; six pages, 27 API routes, middleware
UI runtimereact / react-dom19.2.3Server and client components
Database driver@neondatabase/serverless1.0.2HTTP Postgres client, suited to serverless functions
ORMdrizzle-orm0.45.1Schema definition and every query
Auth@clerk/nextjs6.36.5clerkMiddleware, session lookup, user sync
Webhookssvix1.82.0Signature verification on the Clerk webhook
Model access@anthropic-ai/sdk0.71.2Categorisation and the tool-using agent
Validationzod4.2.1Request schemas at the API boundary
Chartsrecharts2.15.4Cash flow, expense breakdown, payroll series
Markdownreact-markdown / remark-gfm10.1.0 / 4.0.1Rendering streamed assistant replies
Panelsreact-resizable-panels2.1.9The resizable chat push-panel
Primitives@radix-ui/react-*9 packagesDialog, dropdown, scroll-area, tabs, tooltip and more
Iconslucide-react0.562.0Icon set named in components.json
Themingnext-themes0.4.6Light/dark toggle in the header
Datesdate-fns4.1.0Relative timestamps in the conversation list
Spreadsheetsxlsx0.18.5 — reads the payroll report exports held in payrollexport/
Development
Languagetypescript5strict: true, noEmit, path alias @/* → ./src/*
Test runnerjest30.2.0With jest-environment-jsdom and Testing Library
Migrationsdrizzle-kit0.31.8Backs the five db:* scripts
Stylingtailwindcss4Via @tailwindcss/postcss; no tailwind.config file
Total declared29 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.

6

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.

navItems, in declared order
#TitleRoutePrincipal content
1Overview/KPI cards: cash balance, burn rate, runway, net change
2Cash Flow/cash-flowInflow/outflow series; month-range selector, 6m / 12m / ytd / all
3Expenses/expensesCategory breakdown; time-range selector, 30d / 90d / ytd / all
4Payroll/payrollPayroll runs, monthly totals, per-employee breakdown
5Transactions/transactionsFilterable, paginated table with inline category editing
6Settings/settingsMercury 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.