Skip to content

Latest commit

 

History

History
127 lines (100 loc) · 5.95 KB

File metadata and controls

127 lines (100 loc) · 5.95 KB

budgetr (web)

Self-hosted personal finance — accounts, transactions, budgets, and a full investments suite (holdings, options analytics, realized gains/tax lots, dividends, allocation) plus cashflow forecasting and reporting. Next.js 16 + TypeScript, a local SQLite database (Drizzle), and Plaid / Finnhub / Yahoo for data. Everything runs and stays on your machine — the SQLite file never leaves.

Quick start

Node.js 20.9 or newer is required by Next.js 16. The npm scripts will use a compatible Homebrew Node installation when an older system Node appears first on macOS; BUDGETR_NODE_BINARY can point to another compatible executable.

cd web
npm install
npm run setup      # creates .env.local, generates an encryption key, migrates + seeds the DB
npm run dev        # http://localhost:3000

npm run setup is idempotent — re-run it any time. Out of the box the app boots against Plaid Sandbox, so you can click Connect account and log in with the test credentials user_good / pass_good to explore with fake data. To link your real bank, add Plaid credentials (below).

Environment

npm run setup copies env.example.env.local and fills in APP_ENCRYPTION_KEY for you. The keys that matter:

Variable Required Notes
PLAID_CLIENT_ID / PLAID_SECRET to link accounts From the Plaid dashboard. The app boots without them; you just can't connect a bank.
PLAID_ENV yes sandbox (default) or production.
APP_ENCRYPTION_KEY yes 32-byte hex; encrypts Plaid access tokens at rest. Auto-generated by setup.
DATABASE_PATH no Defaults to ./data/budgetr.db.
FINNHUB_API_KEY no Enables live intraday prices on Investments; falls back to end-of-day without it.
PLAID_PRODUCTS no Required Link products (default transactions). Link only shows institutions supporting all of these.
PLAID_OPTIONAL_PRODUCTS no Pulled when available (default investments) but never block linking.
PLAID_COUNTRY_CODES no Default US.
ATTACHMENTS_DIR no Receipt storage; defaults to ./data/attachments.

Linking a real bank (production)

Plaid access tokens are environment-scoped — a sandbox token is invalid in production and vice versa — so switching means re-linking, not flipping a flag:

  1. Request production access in the Plaid dashboard and grab the production PLAID_SECRET.
  2. In .env.local: set PLAID_ENV=production and swap in the production secret.
  3. npm run db:reset-items to drop sandbox links, then restart and re-link from Accounts → Connect account.

Commands

Command What it does
npm run dev Dev server at localhost:3000.
npm run build / npm run start Production build / serve.
npm run setup First-run bootstrap (env + key + migrate + seed).
npm test / npm run test:watch Run / watch the Vitest unit tests.
npm run lint ESLint.
npm run sync Pull the latest transactions/holdings/balances from Plaid into the DB.
npm run db:generate Generate a Drizzle migration after editing db/schema.ts.
npm run db:migrate Apply pending migrations.
npm run db:seed Seed the default categories (idempotent).
npm run db:studio Open Drizzle Studio to browse the DB.

Testing

Pure business logic is unit-tested with Vitest — colocated lib/*.test.ts files covering the trickiest deterministic modules: recurring cashflow projection, option-symbol parsing/risk, Black-Scholes greeks, realized tax lots (FIFO/LIFO/wash-sale), and asset allocation.

npm test

DB-backed query and action layers are intentionally kept thin over these pure modules, which is where the test value concentrates.

Project layout

app/         Next.js App Router pages (server components) + route handlers
components/  Client components (charts, tables, editors, dialogs)
lib/         Business logic — queries.ts (reads), actions.ts (mutations),
             and pure modules (forecast, recurrence, options, tax-lots, …)
db/          Drizzle schema + generated SQL migrations
scripts/     setup, seed, sync, and maintenance scripts

Data flows one way: server-component pages call synchronous lib/queries.ts functions (better-sqlite3 is sync); mutations are Server Actions in lib/actions.ts that write to SQLite and revalidatePath("/", "layout").

Docker (self-hosting)

cd web
cp env.example .env.local   # fill in PLAID_* and set APP_ENCRYPTION_KEY (openssl rand -hex 32)
docker compose up -d        # http://localhost:3000

The SQLite database and receipt attachments live in the budgetr-data volume (/data in the container). Migrations run automatically on start, so upgrading is git pull && docker compose up -d --build.

Warning: the app has no authentication — it's designed for one person on a trusted machine/network. If you expose it beyond localhost, put it behind a reverse proxy with auth (Caddy basic_auth, Tailscale, Authelia, etc.).

Running as a background app (optional)

desktop/ contains a launchd + Caddy setup that runs next start as a login service behind a local hostname (see desktop/README.md), and an Electron packaging target (npm run package). These are optional — npm run dev is all you need to work on the app.

Packaged (Electron) builds never bundle your .env.local. They read secrets from a per-user file — ~/Library/Application Support/budgetr/budgetr.env — which is auto-created on first launch with a fresh APP_ENCRYPTION_KEY; add your Plaid/Finnhub keys there and relaunch. In the app, Settings → Open Settings File (⌘,) opens it directly and Settings → Relaunch to Apply Settings restarts. This is what makes a publicly distributed DMG safe: no shared encryption key, no baked-in credentials.