Cash-position dashboard for a small company: it turns a raw bank export into a monthly cash-flow statement, a daily balance curve, OHLC-style monthly ranges and runway KPIs.
I built it to follow the treasury of my own consulting company. The repository ships with a fully fictional, generated ledger; real bank data never leaves my machine.
- Integer-cent money. Amounts are parsed from strings into integer cents and stay integers through every aggregation. Floats appear only when formatting for display.
- Timezone-proof calendar. Transactions are bucketed by their booking date as written in the
export (
YYYY-MM-DD), never converted to instants, so a+02:00booking at midnight cannot slip into the previous month. - Reconciliation invariants, tested. For every month:
opening + net = closing, and category totals add up to the net flow (uncategorized lines are classified by cash direction to keep it so). - Fail-fast ingestion. The ledger is validated with a Zod schema at startup. A malformed amount or an unknown category type stops the API instead of producing a wrong report.
- Pure domain core. All treasury logic lives in a framework-free library shared by the API and the UI; the NestJS and Vue layers stay thin.
apps/
api/ NestJS REST API: GET /api/treasury/report?to=YYYY-MM&months=N
frontend/ Vue 3 + Vuetify + Chart.js dashboard
libs/
treasury/ Pure TypeScript domain: parsing, money, calendar, report, KPIs
tools/
generate-demo-data.mjs Seeded generator for the fictional ledger
data/
demo/ Committed fictional ledger (default data source)
private/ Real exports, git-ignored
flowchart LR
file[(Ledger JSON)] --> repo[JsonFileTransactionsRepository<br/>Zod validation]
repo --> ctrl[TreasuryController]
ctrl --> lib[["@thoth/treasury<br/>buildTreasuryReport()"]]
ctrl -- TreasuryReport JSON --> ui[Vue dashboard]
lib -. shared types & formatting .-> ui
The ledger source is behind a TransactionsRepository port, so the JSON adapter can be replaced by
a database or a bank API without touching the domain or the controller.
Requirements: Node.js 20+ and npm 10+.
npm install
npm start # API on :3333, dashboard on http://localhost:4200| Command | What it does |
|---|---|
npm test |
Unit and HTTP tests (Vitest + Jest/Supertest) |
npm run lint |
ESLint on every project |
npm run typecheck |
tsc / vue-tsc on every project |
npm run build |
Production builds |
npm run demo-data |
Regenerates the deterministic demo ledger |
Point the API at any export that follows the same format (extra fields are ignored):
{
"id": "5e658d9d-…",
"occurred_at": "2025-01-15T00:00:00.000+01:00",
"amount": "-4599",
"description": "SOFTWARE SUBSCRIPTION",
"category_name": "Software",
"category_type": "EXPENSE"
}THOTH_TRANSACTIONS_FILE=data/private/transactions.json npm startdata/private/ is git-ignored.
GET /api/treasury/report
| Query | Default | Constraint |
|---|---|---|
to |
month of the latest transaction | YYYY-MM |
months |
12 |
integer 1 – 36 |
Returns the window, the per-month summary (opening, closing, high, low, income, expenses, net,
per-category totals), one balance point per day, and KPIs (closing balance, net change, average
monthly spend, runway). Invalid queries get a 400 listing each failing field.
- Forecast: project recurring flows to estimate the balance for upcoming months
- Transaction drill-down from a table cell
- Bank import adapters (CSV, open banking) behind the repository port
- Persistence (PostgreSQL) once imports are incremental
