A minimal, monochrome budget planner. List what you want to buy, keep a history of money in and out across your accounts, and see exactly where your balance lands against the lowest you're willing to let it go.
- Plan before you spend. Mark items Planned to see your balance drop in real time; tick them when you buy them, and the spending goes into your history.
- A money history like a bank statement. Money in, spending, and moves between your own accounts (cash withdrawals, UPI or shop wallet top-ups), with the balance after every line.
- Every account in one place. Bank, cash at home, wallets: each with its own balance and history, all adding up to Balance now.
- A floor, not just a budget. Set the lowest your balance should ever reach; the app tells you how far above or below it your plan leaves you.
- Yours to shape. Categories with optional limits, priorities, expected money, currency and number style, light/dark theme, density.
- Private, synced, installable. Host it free on Vercel behind your password, install it from Chrome on your phone and laptop, and both stay in sync (DEPLOY.md).
- Fast and quiet. Black on white, one typeface family, no animations, about 93 KB of gzipped JavaScript.
Requires Node.js 22.18 or newer (nodejs.org).
On Windows, double-click start.bat: it installs dependencies the first time, then opens http://127.0.0.1:5050. Keep its window open while you use the app.
Elsewhere:
npm install
npm run build && npm run serve # http://127.0.0.1:5050For working on the code, npm start runs the Vite dev server with hot reload on the same port (don't run both at once).
| To | Do this |
|---|---|
| Add an item | Type in the top bar: name, category, priority, price, then Enter. A new category name creates that category. |
| Add to a category | + Add item under any category. |
| Plan it | Click its status (Idea → Planned → Bought). After plan drops by its price. |
| Mark it bought | Tick the checkbox. It moves to the bottom of its category and a Spent line goes into the history (paid from your first account). Untick to put it back to Planned and take the line out. |
| Edit anything | Click the name or price in the row. Enter in a price jumps to the next one. |
| Reorder | Drag the ⋮⋮ handle, within a category or into another one (empty categories included). Keyboard: focus the handle, Space, arrow keys, Space. Works while sorting by List order. |
| Delete | The bin icon at the end of the row. Undo from the message that appears. |
| More options | The chevron opens quantity, category, priority, status, note and link. |
| Set your balances | Settings → Accounts: type what each account holds right now (first-time setup). After that the history keeps them up to date. |
| Record money | History in the header: pick Spent, Money in or Move / cash out, type what it was and the amount, Enter. |
| See one account | Click an account in the History sidebar (or pick it above the list) to see only its lines and running balance. |
| Fix a history line | Click it: date, kind, amount, accounts, and which list items it paid for. The bin deletes it, with Undo. |
| Jump to a category | Click its name in the right column. |
Price fields accept a number or a quick sum: type 199*2+2-1 and press Enter to get 399 (press Enter again to add the item or move on). + - * /, brackets, x/×, ÷ and % all work, so a 10% discount on two ₹199 tees after ₹131 of wallet balance is (199*2-131)*90% (or 90*(199*2-131)/100). Letters are ignored. Grouping is added for you (15000 shows as ₹15,000).
Balance now = every account's balance = opening amounts + money in − spent (moves just shift money between accounts)
After plan = balance now + counted expected money − planned
Budget = balance now + counted expected money − lowest I'll go (or a fixed amount)
Left to spend = budget − planned
Setting an account's balance when it already has history adds a Balance correction line for the difference, so the history always adds up and you can see what's missing from it.
The plan is marked Tight when less than 15% of the budget is left (adjustable), and Over budget when you go past the budget or below your floor.
| Tab | What you can change |
|---|---|
| Money | Plan name, lowest I'll go, budget mode (everything above floor or a fixed amount), tight threshold, currency symbol, number style (Indian, international, European), decimals |
| Accounts | Your accounts and what each holds right now; which one pays when you tick an item as bought |
| Expected | Money you're waiting for, with a switch for whether it counts yet, and a Received button that moves it into the history |
| Categories | Rename, reorder, per-category spending limits, delete (items move to another category) |
| Priorities | Rename, reorder (used by Sort → Priority), add, delete |
| Display | Light / dark / system theme, comfortable or compact rows |
| Data | Export a backup (.json), import one, export items or the money history (.csv), sign out on this device, start over |
| Running as | Saved to | Shared across devices |
|---|---|---|
start.bat / npm run serve (local) |
data.json in the project folder, plus a safety copy in the browser |
Yes, with your other devices over Tailscale (see DEPLOY.md) |
npm run serve on a server |
data.json in DATA_DIR, plus a safety copy in the browser |
Yes, for anyone with the password |
| Vercel with Upstash Redis (see DEPLOY.md) | The database, behind your password, plus a safety copy in the browser | Yes, automatically, on every device you sign in on |
| A static host without a data server | This browser only | No. Use Export / Import to move it |
Nothing you type is lost if the server stops. Every change is written to the browser first, then to data.json a moment later. If the server isn't running (say you closed the start.bat window), a black banner says so, changes wait in the browser, and they're written to data.json as soon as the server is back: either straight away in the same tab, or the next time you open the app ("Recovered").
- Backups: every save also updates
backups/data-YYYY-MM-DD.json, one per day, last 30 days. To roll back, stop the app and copy one overdata.json. - Not on GitHub:
data.json,data.*.jsonandbackups/are git-ignored, so pushing code never uploads your numbers. Back up the project folder (or use Export backup) if you want a copy elsewhere. - Several devices: each device picks up the others' changes when you return to it (and every 15 s). If two devices save at once, the second sees "Changed on another device" with a Use mine option, so nothing is overwritten silently.
- Older files (including the first version's format) are upgraded and validated automatically on load and on import.
The app only ever talks to one endpoint: GET /api/data returns the plan as JSON (or null), and PUT /api/data replaces it, refusing (409) if another device saved since. Locally that's server/data-api.ts writing data.json; on Vercel it's api/data.js storing it in Upstash Redis behind BUDGET_PASSWORD. The front end is the same either way.
Step-by-step guide: DEPLOY.md. In short: import the repo into Vercel (free, private repos are fine), connect a free Upstash Redis database from Vercel's Storage tab, set BUDGET_PASSWORD, redeploy, then import your data.json and install the app from Chrome on each device.
The production server (npm run serve) takes PORT, HOST, DATA_DIR, BUDGET_PASSWORD and OPEN_BROWSER. It refuses to listen on a public interface without a password, and serves /healthz for uptime checks.
| Script | |
|---|---|
npm start |
Dev server, opens the browser |
npm run dev |
Dev server without opening a browser |
npm test |
Unit tests (Vitest) |
npm run typecheck |
TypeScript, no output |
npm run build |
Typecheck and production build into dist/ |
npm run serve |
Production server for dist/ (see Deploy) |
npm run check |
Typecheck, tests and build: what CI runs |
GitHub Actions runs typecheck, test and build on every push and pull request (.github/workflows/ci.yml).
api/
data.js Vercel function: GET/PUT /api/data ↔ Upstash Redis, password check, daily backups
server/
data-api.ts GET/PUT /api/data ↔ data.json (used by the dev server and serve.ts)
serve.ts production server: static files, data API, password, health check
public/
sw.js service worker: makes the app installable; never caches your data
manifest.webmanifest, icon-*.png app name and icons for installing
src/
types.ts the data model
store/budget.ts zustand store, actions, debounced auto-save
store/ui.ts view state (search, open panels)
lib/calc.ts plan maths (after plan, budget, left to spend)
lib/ledger.ts money history maths (account balances, running balance)
lib/money.ts currency formatting and the numbers-only input parser
lib/migrate.ts upgrades and validates saved or imported data
lib/api.ts storage: data server (with the password), or browser storage on static hosts
lib/reorder.ts drag-and-drop moves
components/list/ add bar, category sections, item rows
components/summary/ right-hand numbers column
components/history/ the money history page
components/settings/ settings panel tabs
index.css theme: one ink colour at three strengths
seed.json the blank starting plan
render.yaml Render blueprint
- Add the field to
SettingsorDisplayinsrc/types.ts, and a default inseed.json. - Validate it in
normalizeSettings/normalizeDisplayinsrc/lib/migrate.ts, so older files get the default. - Read it with
useData((d) => d.settings.yourField)and add a control insrc/components/settings/.
React 19, TypeScript, Vite, Tailwind CSS 4, zustand, dnd-kit, Geist and Geist Mono (bundled locally), lucide icons, Vitest.
