PisoPilot is a privacy-first, mobile-first personal finance PWA for self-hosted use. It combines double-entry-style ledger mechanics, zero-based budgeting, recurring bill management, goals, reporting, offline entry, imports, and confirm-before-save AI assistance in one installable application.
This repository is published as a portfolio project. It demonstrates production-oriented full-stack engineering across financial data integrity, offline synchronization, responsive product design, container hardening, database migrations, and encrypted operations.
- Cash, bank, e-wallet, credit-card, loan, investment, property, and other asset accounts
- Exact minor-unit money handling with PHP as the default base currency and explicit exchange-rate snapshots
- Income, expense, refund, transfer, split, pending, cleared, and reconciled transactions
- Safe transaction editing with linked bill-payment integrity and optimistic concurrency
- Zero-based monthly envelopes, rollover, ready-to-assign calculations, and card-payment envelopes
- Optional 50/30/20 planning and actual-spending view with editable ratios, six-month trends, reviewed allocation, and safe guided rebalancing
- Complete bill management: partial payments, reminders, skip/restore, and recurring-series scopes
- Recurring-series pause, resume, and end lifecycle while preserving paid history
- Savings goals, debt payoff planning, and loan, savings, and retirement calculators
- Spending, cash-flow, net-worth, account, and budget reports
- CSV preview, mapping, deduplication, plus JSON and CSV exports
- Receipt upload and vision extraction into an editable proposal
- Local AI assistance whose suggestions always require explicit user confirmation
- IndexedDB caching, queued offline mutations, idempotent sync, and expiring offline grants
- Six persistent visual themes and an installable phone-first PWA experience
PisoPilot deliberately does not include bank credential storage, automatic bill payment, public account registration, autonomous AI writes, or shared-household access.
- Monetary values are stored as integer minor units; floating-point arithmetic is never used for balances.
- 50/30/20 targets use deterministic largest-remainder rounding, so every cent of planned income is assigned exactly once.
- Needs, wants, savings, debt minimums, and excess debt payments are classified explicitly; uncertain transfers remain visible as setup warnings rather than being silently guessed.
- Balances and reports are derived from signed ledger postings rather than mutable cached totals.
- Bill payments remain linked to ledger transactions, so edits and deletions recalculate bill status safely.
- Fully paid bill occurrences are immutable; corrections happen through their payment transactions.
- Recurring edits support occurrence-only and this-and-future scopes while retaining paid or partially paid history.
- Offline writes use client mutation IDs, durable outbox storage, optimistic versions, and server conflict records.
- AI output is treated as an untrusted proposal and never performs arithmetic or writes autonomously.
- Production containers run without root privileges, with dropped capabilities, a read-only filesystem, and loopback-only publishing.
flowchart LR
Phone[Phone / browser PWA] -->|HTTPS over private network| Edge[Tailscale Serve]
Edge --> API[Fastify API]
API --> DB[(PostgreSQL)]
API --> Files[Receipt volume]
API --> AI[Optional local AI gateway]
Worker[pg-boss worker] --> DB
Worker --> Files
Backup[Encrypted backup job] --> DB
Backup --> Files
| Area | Technology | Responsibility |
|---|---|---|
apps/web |
React, Vite, TanStack Query, Dexie, Wouter | PWA interface, local cache, and offline outbox |
apps/api |
Fastify, Drizzle ORM, pg-boss | API, validation, ledger rules, sync, and background work |
packages/shared |
TypeScript, Zod | Shared contracts and exact money helpers |
| PostgreSQL | PostgreSQL 17 | Durable financial and application state |
| Deployment | Docker Compose, Tailscale, systemd, age | Private ingress, services, and encrypted backups |
Requirements: Node.js 22+, npm, and PostgreSQL.
cp .env.example .env
# Replace the example database password and generate a random offline-grant secret.
npm ci
npm run db:migrate
npm run db:seed
npm run devThe development configuration disables Tailscale header enforcement and uses a placeholder local identity. Never reuse development secrets in production.
Quality checks:
npm run typecheck
npm test
npm run lint
npm run build
npm audit --omit=devThe included deployment profile targets an unprivileged Debian 12 Proxmox container with Docker, Tailscale, and a host-mounted backup destination. Treat the provided container ID, storage sizes, and network settings as examples and review them for your own host.
On the Proxmox host:
deploy/scripts/provision-lxc.shAfter copying the repository to /opt/pisopilot inside the container:
cd /opt/pisopilot
cp deploy/.env.production.example deploy/.env
# Edit deploy/.env with your identity, timezone, and optional AI endpoint.
install -d -m 0700 /etc/pisopilot/secrets
openssl rand -hex 32 > /etc/pisopilot/secrets/postgres_password
openssl rand -hex 48 > /etc/pisopilot/secrets/offline_grant_secret
printf '%s' 'YOUR_DEDICATED_AI_GATEWAY_KEY' > /etc/pisopilot/secrets/openwebui_api_key
chgrp 10001 /etc/pisopilot/secrets/*
chmod 0640 /etc/pisopilot/secrets/*
deploy/scripts/deploy.sh
tailscale up --hostname=pisopilot
tailscale serve --bg http://127.0.0.1:3000The API accepts only the exact identity configured by TAILSCALE_OWNER_LOGIN. Docker publishes the application on loopback; Tailscale Serve is the intended production ingress. Do not expose port 3000 directly to an untrusted network because identity is supplied through the trusted proxy header.
AI support is optional. Set AI_ENABLED=false when no local gateway is configured; all other functionality remains available.
docker compose -f /opt/pisopilot/deploy/compose.yml ps
docker compose -f /opt/pisopilot/deploy/compose.yml logs -f --tail=200 app worker
/opt/pisopilot/deploy/scripts/deploy.sh
systemctl start pisopilot-backup.serviceThe systemd timer creates nightly PostgreSQL and receipt archives, verifies checksums, encrypts the archive with age, and applies a 90-day retention policy. Store the matching age private key outside the application host; backups cannot be recovered without it.
For a restore, decrypt into a temporary directory, verify SHA256SUMS, restore PostgreSQL with pg_restore, recover the receipt volume, and verify /healthz before reopening access.
- Private-network identity enforcement; no password form or public registration flow
- Docker file secrets kept outside images and source control
- Content Security Policy, HSTS in production, frame blocking, rate limits, and sensitive-header redaction
- Upload type and 10 MB request limits for receipt attachments
- Short-lived offline grants with explicit local-data clearing
- Optimistic concurrency and audit/sync records for mutable financial entities
- Confirm-before-save AI proposals with expiration
See SECURITY.md for responsible disclosure and deployment assumptions.
This is a working personal project, not regulated financial software and not a hosted financial service. Use test data when evaluating it.
No open-source license is currently granted. Public visibility is provided for portfolio review and technical evaluation; copyright remains with the repository owner.