Skip to content
stackvs18Public

Repository files navigation

Budget

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.

Budget: items grouped by category on the left, balance after plan and running numbers on the right

  • 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.

Quick start

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:5050

For working on the code, npm start runs the Vite dev server with hot reload on the same port (don't run both at once).

Using it

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).

How the numbers work

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.

Settings

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

Where your data lives

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 over data.json.
  • Not on GitHub: data.json, data.*.json and backups/ 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 data API

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.

Deploy

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.


Development

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).

Project structure

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

Adding a setting

  1. Add the field to Settings or Display in src/types.ts, and a default in seed.json.
  2. Validate it in normalizeSettings / normalizeDisplay in src/lib/migrate.ts, so older files get the default.
  3. Read it with useData((d) => d.settings.yourField) and add a control in src/components/settings/.

Stack

React 19, TypeScript, Vite, Tailwind CSS 4, zustand, dnd-kit, Geist and Geist Mono (bundled locally), lucide icons, Vitest.

Contributors

Languages