Skip to content

Vision

About this document

Audience: project owner and team. Read first, refer back occasionally.
Purpose: capture why this project exists, who it's for, and the principles that decide tradeoffs when this document doesn't.
Status: living document. Update when the product's identity changes, not when features change.


1. What this is

A personal finance tracker built around shared families rather than individual users. A family is the primary unit of data ownership: every transaction, account, category, and report belongs to a family, and family members collaborate on it.

The application is API-first. The same backend serves a web SPA, a Telegram bot, an optional CLI, and any external services users build or integrate themselves.

It is open-source and runs in three deployment profiles from the same codebase: a managed cloud instance the project operates, a self-hosted server a user puts on their own VPS, and a fully local install on a personal machine. All three use the same Docker images and the same compose file with profile-specific overrides.

2. What this isn't

This is not a professional or business expense tracker. It is not an investment analytics platform, a tax tool, a bill-payment service, an expense-splitting app, or a credit-monitoring product. We acknowledge these adjacent product categories exist, are useful, and are not us.

Concretely excluded from the design:

  • Tax calculation and reporting
  • Bill payment and money movement between users
  • Expense splitting (Splitwise-style)
  • Subscription detection or management
  • Credit score tracking
  • Full investment analytics (we track value and growth, nothing more)
  • Direct bank connections as a primary feature (opportunistic only)

Non-goals are load-bearing

If a feature request lands in any of these categories, the answer is "not us" — not "later."

3. Who it's for

Three concrete user shapes guide design decisions:

The solo tracker. One person managing their own money, possibly across many accounts and currencies. They want a clean, fast, complete record of their financial life and the ability to slice it however they like. They use the web SPA and possibly the Telegram bot for quick entry on the go.

The family unit. Two to six people who share finances to varying degrees — couples, parents and adult children, flatmates, small households. They want shared visibility, role-based permissions, and the ability for each member to add transactions without stepping on each other's data.

The technically-curious self-hoster. A developer or power user who wants to own their data, doesn't trust a stranger's cloud with their financial history, and is comfortable running Docker. They might also build small custom external services against the API.

The application is not designed for organizations, accountants, or anyone needing audit-grade financial controls. The "family" abstraction can be repurposed for very small informal teams, but professional use is not a goal.

4. Core product principles

These are the tiebreakers when design choices conflict.

Privacy is foundational, not a feature. Users' financial data is among the most sensitive data they have. We treat it accordingly: no cross-family leakage, no analytics of user behavior shipped to third parties, full GDPR-style export and delete, and a self-host path that requires no trust in us at all.

The domain is the product. Money, accounts, transactions, categories, families — these concepts and their rules are where the value lives. Everything else (frameworks, infrastructure, UI flourishes) is a means to express the domain accurately and let users work with it efficiently.

Designed for extension over time. This is a pet project that may grow. Every architectural decision is checked against the question "if this becomes popular and we want to add X in two years, will we curse our past selves?" Concrete consequences: pluggable importers, broker-ready event bus, swappable storage backends, versioned events, schemas reserved for deferred features.

Three deployment profiles from one codebase. Cloud, self-hosted, and local must all work from the same source tree. Differences are limited to environment variables and optional infrastructure swaps. We do not ship a "lite" version or a "pro" version.

Open by default. The API is documented and stable. The data is exportable in a re-importable format. The code is open-source. Users own what they put in and can leave at any time.

Domain logic before infrastructure. Build the rich domain model first. Wire it to the database, HTTP, and queues afterward. The domain layer must be testable in isolation, with no framework or infrastructure imports.

Explicit non-goals are real boundaries. Saying "no" to features is part of the design. The non-goals list in section 2 is load-bearing — it prevents drift into competing product categories where we'd be a worse version of an existing tool.

5. The shape of the system

A short description so that someone reading only this document has a working mental model:

  • A TypeScript API is the single backend. Internally it is a modular monolith organized into bounded contexts (Identity, Family, Ledger, Investment, Currency, Import, Reporting, Audit, Notification). Contexts communicate primarily through versioned domain events. The architecture is hexagonal: pure domain at the center, infrastructure adapters at the edges.
  • A TypeScript worker runs background jobs (CSV imports, future async tasks) consuming from Redis-backed queues. It shares the API's codebase and contexts.
  • A Telegram bot is the first official "external service" — it talks to the API like any third-party would, via an API key. Users running the hosted instance use the project's bot; self-hosters can run their own.
  • A web SPA (React or Vue, decided later) is a separate static app, served by Caddy at the same origin as the API. It uses the same public API as everything else.
  • PostgreSQL holds all persistent data. Redis handles job queues, idempotency caching, rate limiting, and short-lived report caches. Caddy terminates TLS and serves the web client. No Kubernetes; just Docker Compose.

A user belongs to one or more families, always at least their own private family (auto-created, undeletable). Each family contains accounts (cash, debit, credit, investment), transactions on those accounts, categories, tags, and audit history. Currency conversions, exports, and reports operate at the family level using a family-chosen base currency.

6. What "good" looks like

Concrete signals that the project is on track:

  • A new contributor can clone the repo, run docker compose up, and be using the app within five minutes.
  • Adding a new bounded context, a new importer, or a new external service does not require touching unrelated parts of the codebase.
  • The domain layer can be exercised by tests with no database, no HTTP server, and no external mocks.
  • A user can move their entire data set to a different instance of the app via the export/import feature.
  • The hosted instance and a self-hosted instance run identical code.

Success signal

An agentic coding tool, given a focused task and the right docs, can implement a feature without needing an architect's intervention.

7. What "good" doesn't look like

Drift indicators

  • Logic leaking from the domain layer into route handlers or repositories.
  • Bounded contexts reaching into each other's database tables.
  • Cloud-only features that don't work for self-hosters.
  • Features added to compete with tools we explicitly said we're not.
  • Documentation that doesn't match the code, or code that wasn't designed against the documentation.

8. Where to go next