Savoney
Personal finance, typed end to end
Language
TypeScript
Frontend
React 19 · TanStack Query · Recharts
Backend
Express 5 · Mongoose 9
Data
MongoDB 8
Shared
Zod
01The problem
Expense trackers either demand a discipline nobody keeps, or collect everything and tell you nothing.
If an app asks for my attention every day, what does it owe me back?
02The solution
A TypeScript monorepo where one set of Zod schemas drives the client, the server and the API docs, so none of them can drift.
Built end to end: auth, transactions, budgets, goals and analytics.
03Architecture
A React 19 client and an Express 5 API over MongoDB 8, sharing a single package of Zod schemas and types.
One package of Zod schemas that the client, the server and the OpenAPI document all derive from, so none of the three can drift. Connections: @savoney/shared to React 19 client (forms); @savoney/shared to Express 5 API (validates); @savoney/shared to /api/docs (generates); React 19 client to Express 5 API (REST); React 19 client to /api/auth (rotate); /api/auth to MongoDB 8 (token family); Express 5 API to MongoDB 8 (aggregation).
scroll the diagram sideways →
04Technical depth
- Refresh tokens are single-use; presenting one twice revokes the entire session family.
- Access tokens live 15 minutes, in memory, never localStorage.
- The refresh cookie is SameSite=Strict and scoped to /api/auth, which makes the API CSRF-safe by design.
- Passwords use Argon2id; money is stored as integer minor units, never floats.
I can't stop a token being stolen, but I can make using it announce itself.