The problem
Most finance apps stop at "income minus expense". Real money in Vietnam is messier: a salary in a bank account, daily spending in cash and e-wallets, a credit card on its own statement cycle, a phone on 0% installments, a loan paid every month. Chili keeps all of it in one place, with every balance exact.
It is in beta at chili.tlait.online with a Vietnamese UI, and 10 beta users already use it for their own money. I built it alone: from an empty repository to production in about three months, then 31 releases in the first three weeks.
Scope
- Wallets of every kind, including credit cards with statement cycles and installments.
- Transactions, monthly budgets, saving goals, and debts and loans with repayment schedules.
- A cash-flow forecast for the month, and monthly and yearly reports.
- An AI assistant that turns a sentence or a screenshot into a transaction.
Architecture
A pnpm + Turborepo monorepo. web and api both import shared, never each other.
packages/shared Zod schemas · types · enums · error codes (imports nobody)
▲ ▲
apps/web apps/api
React 19 + Vite Express 5
React Router 7 Controller → Service → Repository
TanStack Query Prisma 7 → PostgreSQL
Each API area (wallets, transactions, budgets, …) has a controller for HTTP, a service for the business rules, and a repository, the only layer that touches Prisma. Production is three Docker containers on one VPS: Caddy, the API and PostgreSQL.
Key technical decisions
The service layer is the center of the app
The web app, the AI assistant and scheduled jobs all call the same service methods, and every method checks that the data belongs to the user. So the AI gets no special path to the database: it is just another caller.
AI as a translator, not a back door
The AI's tools are generated from the shared Zod schemas, so its input passes the same checks as the forms. The user id comes from the session, never from the model, and each action runs once even if a request is retried. A screenshot is only read into a draft; nothing is saved until the user confirms.
The model is an open one I host myself with Ollama, so no user's finances go to a third-party AI service. I picked it by its score on 76 Vietnamese test inputs, such as "ăn trưa 50k tiền mặt" (lunch, 50k, cash), not by a few chats that went well.
One schema, checked on both sides
The same Zod schemas validate on the client and again on the server, so forms, API and AI tools cannot drift apart. Errors share one shape too, so the UI can show exactly which rule was broken.
Money and time are never approximate
Money is stored as Decimal(19, 4) and sent as strings, never as JavaScript floats. Dates are resolved in Asia/Ho_Chi_Minh, so a card statement or a budget month never shifts because the server runs in UTC.
Each rule is tested once, at the lowest level that proves it
Unit tests cover business rules and components. Integration tests run services against a real PostgreSQL for rollbacks, races and locks. Playwright covers only what needs the browser, the API and the database together, such as balances, transfers and a full month of use.
Shipping it like a team product
Working alone, nobody else catches what I forget, so the process is written down:
- Small, finished slices. Each feature ships complete, with a changelog entry.
- Decisions on paper. Choices worth remembering are written down with the reasoning.
- Checked releases. CI runs types, lint, more than 3,000 unit tests, the build and end-to-end tests on a real database. The database is backed up every night.
- AI with rules. I work with Claude Code inside architecture and testing rules I wrote, and I review every change.
From Pocketly to Chili
Chili is a rewrite. The first version, Pocketly (Next.js, Server Actions, Prisma, early 2026), proved the finance logic, but its business rules were mixed into the request handling, so nothing besides the UI could safely change money. Chili moved the rules into a separate API built around the service layer, with room for the AI assistant planned from the start.