https://github.com/dasgoutam/personal-finance
Self hosted personal finance application with double-entry accounting
https://github.com/dasgoutam/personal-finance
finance-management raspberry-pi
Last synced: 30 days ago
JSON representation
Self hosted personal finance application with double-entry accounting
- Host: GitHub
- URL: https://github.com/dasgoutam/personal-finance
- Owner: dasgoutam
- Created: 2026-03-13T14:20:25.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-06-17T16:03:03.000Z (about 1 month ago)
- Last Synced: 2026-06-17T18:05:42.898Z (about 1 month ago)
- Topics: finance-management, raspberry-pi
- Language: Svelte
- Homepage:
- Size: 219 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Personal Finance
A self-hosted, single-user personal finance dashboard built with SvelteKit, SQLite, and proper double-entry bookkeeping.
## Stack
| Layer | Choice |
|---|---|
| Framework | SvelteKit 2 + TypeScript |
| Styling | Tailwind CSS |
| Runtime adapter | `@sveltejs/adapter-node` |
| Database | SQLite via `better-sqlite3` |
| ORM / migrations | Drizzle ORM + drizzle-kit |
| Auth | Lucia v3 (session-based, single user) |
| Password hashing | Argon2id via `@node-rs/argon2` |
| Deployment | Docker Compose on Raspberry Pi (linux/arm64) |
## Schema overview
```
accounts — chart of accounts (hierarchical via parent_id)
type: asset | liability | equity | income | expense
currency: ISO 4217 (EUR, INR, …)
transactions — a dated, described business event
journal_entries — individual debit/credit legs of a transaction
∑ amount per currency must equal 0 ← double-entry constraint
commodities — tradeable instruments (ETFs, stocks, mutual funds)
prices — historical price series per commodity
```
Amounts are stored as **integers in the smallest currency unit** (euro-cents for EUR, paise for INR). This avoids floating-point rounding issues.
Commodity quantities use **milli-units** (1 000 = 1 share/unit).
## Local development
### Prerequisites
- Node 20+
- `npm`
### Setup
```bash
# 1. Install dependencies
npm install
# 2. Copy environment config
cp .env.example .env
# Edit .env to set your username/password
# 3. Generate & apply migrations
npm run db:generate # creates drizzle/migrations/ SQL files (already included)
npm run db:migrate # applies migrations to ./data/finance.db
# 4. Seed with sample data (Germany/India dual-currency accounts)
npm run db:seed
# 5. Start dev server
npm run dev
```
Open http://localhost:5173 and log in with the credentials you set in `.env`
(defaults: `admin` / `changeme`).
### Available npm scripts
| Script | Description |
|---|---|
| `npm run dev` | Start Vite dev server with HMR |
| `npm run build` | Build production bundle |
| `npm run preview` | Preview production build locally |
| `npm run check` | Run `svelte-check` type checker |
| `npm run db:generate` | Generate SQL migrations from schema changes |
| `npm run db:migrate` | Apply pending migrations |
| `npm run db:push` | Push schema directly to DB (dev shortcut, no migration files) |
| `npm run db:seed` | Seed the database with sample accounts and transactions |
| `npm run db:studio` | Open Drizzle Studio (web UI for the database) |
## Double-entry constraint
The constraint is enforced at the application layer in `src/lib/server/finance.ts`.
`createTransaction()` rejects any transaction where the sum of `amount` values,
grouped by `currency`, is not exactly zero.
```
EUR legs: +400 000 (salary credit to bank) + -400 000 (income debit) = 0 ✓
INR legs: +500 000 (ETF purchase debit) + -500 000 (NRE debit) = 0 ✓
```
For multi-currency transactions (e.g. wire transfers where you send EUR and
receive INR) each currency group must independently sum to zero — you model the
exchange as two matching legs per currency.
## Seed data highlights
The seed script (`scripts/seed.ts`) creates accounts for a person living in
Germany with EUR as primary currency and INR for Indian investments:
**Assets (EUR):** DKB Girokonto · DKB Tagesgeld · Bargeld
**Assets (INR):** Zerodha Portfolio · PPF Account · NRE Savings Account
**Liabilities:** Visa Kreditkarte
**Income:** Gehalt · Freiberufliche Einnahmen · Dividenden · Investment Returns (INR)
**Expenses:** Miete · Strom & Internet · Lebensmittel · Restaurant · ÖPNV ·
Gesundheit · Freizeit · Kleidung · Transfergebühren
**Commodities:** NIFTYBEES · HDFC Flexi Cap Fund · INFY (Infosys)
**Transactions:** ~25 sample transactions spanning Jan 2024 – Mar 2025,
including salary, rent, groceries, PPF deposit, ETF purchases (with commodity
tracking), remittance via Wise, and PPF interest credit.
## Raspberry Pi deployment
### Build and run with Docker Compose
```bash
# On the Pi (or cross-compile from x86 with Docker buildx):
docker compose up -d --build
```
The SQLite database is stored in a named Docker volume (`finance_data`) so it
survives container rebuilds.
### Seed inside the container
```bash
# First run only — migrate + seed
docker compose exec finance node -e "
const { execSync } = require('child_process');
execSync('npm run db:migrate', { stdio: 'inherit' });
"
# Then seed (run the seed script directly with tsx)
docker compose run --rm finance sh -c "npm run db:seed"
```
Or copy a pre-seeded database file into the volume:
```bash
docker cp ./data/finance.db \
$(docker compose ps -q finance):/data/finance.db
```
### Reverse proxy (nginx example)
```nginx
server {
listen 80;
server_name finance.local;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `DATABASE_URL` | `./data/finance.db` | Absolute path to the SQLite file |
| `FINANCE_USERNAME` | `admin` | Username created by `db:seed` |
| `FINANCE_PASSWORD` | `changeme` | Password created by `db:seed` |
| `NODE_ENV` | `development` | Set to `production` in Docker |
| `PORT` | `3000` | HTTP port the server listens on |
| `HOST` | `0.0.0.0` | Bind address |
## Project structure
```
src/
app.css Tailwind entry point
app.d.ts App.Locals type augmentation for Lucia
hooks.server.ts Session validation on every request
lib/
server/
auth.ts Lucia instance + type registration
finance.ts createTransaction() with double-entry guard
db/
index.ts better-sqlite3 + Drizzle setup
schema.ts All table definitions + inferred types
routes/
+layout.server.ts Pass user to all pages
+layout.svelte Nav bar
+page.server.ts Root redirect (→ /dashboard or /login)
login/ Login form
logout/ POST action to invalidate session
dashboard/ Recent transactions + account balances
scripts/
migrate.ts Standalone migration runner
seed.ts Sample data seeder
drizzle/
migrations/ SQL migration files (generated by drizzle-kit)
Dockerfile Multi-stage ARM64 image
docker-compose.yml Pi deployment
```