Personal project
School Finance Platform
A web application replacing three manual documents used by a municipal school's parent-teacher association, with official report generation and AI-assisted receipt reading. In production.
- Role
- Sole developer
- Period
- 2025 – present
- Stack
- NestJS
- Next.js
- TypeScript
- PostgreSQL
- TypeORM
- Turborepo
- Vertex AI
- Gemini
- Tailwind CSS
- Railway
The problem
The head teacher of a municipal school ran the parent-teacher association’s finances across three documents: a Word file for the annual statement, with twelve months stacked inside it, and two Excel spreadsheets acting as cash books — one for cash, one for instant transfers.
The same figure was typed in up to three times. Balances were added by hand. An arithmetic error in March propagated silently through to the annual statement, and the only way to find it was to re-add the year.
The person doing this is not a technical user, and the reports she produces are statutory. Those two facts, together, decided almost everything about the design.
The approach
A Turborepo monorepo — a NestJS API and a Next.js application — sharing a package of typed enums and DTOs, so the contract between the two sides is checked at compile time rather than discovered in production. Multi-tenant by school, with an admin module for managing schools and users.
Three AI features, all of them suggestions:
- Receipt extraction — a photo or PDF of a receipt pre-fills the expense form, via structured output against a typed schema.
- Batch extraction — several receipts at once.
- Month-end review — before generating the statement, deterministic heuristics run (a month with no transfers, an expense with no receipt, a class with no contributions) and the model writes the findings up in plain language, each one linking to the screen that fixes it. It changes no data whatsoever.
Decisions worth defending
The AI suggests; the head teacher confirms. Nothing a model produces is written to a financial record without a human reviewing it on screen first. A sanitisation layer with its own test suite validates and normalises the model’s output before it is allowed to touch the form, and it is built to never invent: a field it cannot verify comes back empty rather than plausible.
The cost is that the fastest possible flow — photograph a receipt, walk away — is not available. That flow was never acceptable. The user is accountable for these numbers to a municipal authority, and an extraction that reads 1.860,00 as 1860.00 in the wrong currency convention is both entirely plausible and a real problem. Confirmation is what makes the feature usable by someone who cannot audit the model.
The LLM SDK is confined to one provider class, with a null implementation behind it. Controllers and services know only an extraction interface. When credentials are absent, a null provider answers a friendly 503, and the application remains completely usable without any AI at all — because a school with an expired API key still has to file its accounts on time. A dedicated test asserts the SDK does not leak out of that layer, since the natural drift is for one convenient import to appear in a service and for the abstraction to quietly stop being one.
Migrations run at startup and the seed is idempotent. Redeploying should not require remembering a manual step, on a project maintained by one person in their spare time for a user who cannot be asked to wait for it.
The design system is a versioned document. Palette, type scale, spacing, components, do’s and don’ts, and responsive rules — 36 KB of it — written before the interface, because retrofitting consistency onto twenty screens costs more than deciding it once.
Scale
- ~9,600 lines of API, ~11,000 lines of web
- 18 modules, 11 entities, 9 migrations
- 15 application routes, 19 test files
- 142 commits, 19 features specified before implementation