Skip to content

Requirements

About this document

Audience: project owner, team, and agentic tools.
Purpose: an authoritative, checkable list of what the system must do and the constraints under which it must do it.

Scope markers

  • MMVP. Built and shipping in v1. Schema, API, code all present.
  • D — Designed-for / Deferred. Not built in v1, but the architecture must accommodate it without rewrites.
  • X — Out of scope. Will not be built; architecture does not go out of its way to support it.

Every requirement is referenceable by its ID (e.g., R-TRX-001). Use these IDs in commits, PRs, and agent prompts.


1. Functional Requirements

1.1 Identity & Authentication

ID Requirement Scope
R-IDN-001 Users can register with a username and password. Username uniqueness is enforced instance-wide. M
R-IDN-002 Users can log in with username + password and receive a JWT access token + refresh token. M
R-IDN-003 Users can log in via Google OAuth. The Google account's stable identifier is stored as the user's username. M
R-IDN-004 Users can authenticate by linking their Telegram account to an app account, then issuing commands in the bot. M
R-IDN-005 Each user is issued 12 single-use recovery codes at signup. They can be used to reset a forgotten password without email infrastructure. M
R-IDN-006 Users can regenerate their set of recovery codes; doing so invalidates the previous set. M
R-IDN-007 Users can opt in to TOTP-based 2FA (Google Authenticator-compatible). D
R-IDN-008 Users can register WebAuthn / passkey credentials as an alternative authentication factor. D
R-IDN-009 Users can log out, which revokes the current refresh token. M
R-IDN-010 Refresh tokens rotate on use; an access token can be obtained from a valid refresh token without re-authentication. M
R-IDN-011 A user can list and revoke their active sessions. M
R-IDN-012 A user can view, edit, and delete their own profile (display name, locale, primary timezone). M

1.2 Families & Membership

ID Requirement Scope
R-FAM-001 On registration, a user has a private family auto-created. This family is undeletable but can be hidden in the UI. M
R-FAM-002 A user always belongs to at least one family. M
R-FAM-003 A user can create additional families and is the initial owner of each. M
R-FAM-004 A family has a name, a base currency, and a primary timezone, all editable by the owner. M
R-FAM-005 An owner can invite other users to a family by username. M
R-FAM-006 An owner can generate a shareable invite link with a baked-in role. M
R-FAM-007 Invitees see pending invitations in any client (web, Telegram bot) and can accept or reject them. M
R-FAM-008 A family supports three roles: owner, editor, viewer. M
R-FAM-009 An owner has full control: manage members, change roles, edit/delete any data, delete the family (except a private family). M
R-FAM-010 An editor can create, edit, and delete data, but can only delete entries they themselves created. M
R-FAM-011 A viewer has read-only access across all family data. M
R-FAM-012 An owner can transfer ownership to another member. The transfer is a swap: the previous owner becomes an editor; the new owner gets owner rights. D
R-FAM-013 A family can have only one owner at a time. M
R-FAM-014 A user can leave a family. The owner cannot leave without first transferring ownership. M
R-FAM-015 When a member is removed or leaves, data they authored remains in the family with attribution preserved. M
R-FAM-016 When a user deletes their own account: their private family and any owner-only families with no other members are deleted; account deletion is blocked otherwise until ownership is transferred or families are removed. M
R-FAM-017 All transactions, accounts, categories, tags, budgets, goals, and reports are scoped to a family. M

1.3 Accounts

ID Requirement Scope
R-ACC-001 Users can create accounts of these types: cash, debit, credit, investment. M
R-ACC-002 Cash accounts have a non-negative balance invariant; transactions that would violate it are rejected. M
R-ACC-003 Debit accounts allow any balance value. M
R-ACC-004 Credit accounts have a credit limit; balance can go negative down to -credit_limit. M
R-ACC-005 Investment accounts hold positions and a cash balance, restricted to investment-related transaction kinds. M
R-ACC-006 Crypto wallets are supported as a distinct account type, holding crypto assets and an optional cash balance. D
R-ACC-007 Each account has a single base currency, set on creation, immutable thereafter. M
R-ACC-008 Each account has a stored balance, synchronously updated within the same database transaction as any financial transaction touching it. M
R-ACC-009 Account balance recomputation is available as an admin/operator tool that detects and reports drift but does not silently self-heal. M
R-ACC-010 Accounts can be soft-deleted; soft-deleted accounts and their transactions are excluded from balances and reports but remain queryable for audit. M
R-ACC-011 Loans and debts can be tracked as informational entries (counterparty, principal, direction, interest rate, dates, status). They do not affect net worth and have no associated transactions. D

1.4 Transactions

This section covers regular transactions on non-investment accounts (cash, debit, credit). Investment-account transactions have their own kinds and storage - see §1.5 Investment Transactions. Both transaction types share these baseline behaviors (logical date, amount, status, soft-delete, optimistic locking).

In this document, the uppercase labels used for investment transaction kinds are shorthand for the lowercase enum values stored in the database and returned by the API. Regular transaction kinds remain the lowercase values income, expense, and transfer.

ID Requirement Scope
R-TRX-001 Transactions have three kinds for non-investment accounts: income, expense, transfer. M
R-TRX-002 A transfer between two accounts is modeled as two linked transactions sharing a transfer_group_id. Each side records its own native amount; the FX rate is implicit. M
R-TRX-003 Each transaction has: a logical date (calendar date, no time), an amount in account currency, a category, optional tags, an optional note, an author, a creation timestamp, an updated timestamp, and a version. M
R-TRX-004 Cross-currency transfers require an exchange rate. The user enters the rate manually in MVP. M
R-TRX-005 The system can fetch FX rates from external providers (NBU, ECB, etc.) for popular currencies. D
R-TRX-006 The user always enters a transaction in the account's native currency, as it appears on the source statement. Original-currency notes are informational only. M
R-TRX-007 Transactions support a pending vs cleared state. M
R-TRX-008 Transactions can have file attachments (receipts, statements). D
R-TRX-009 Recurring/scheduled transactions auto-generate per their schedule, with notifications on generation. D
R-TRX-010 Transactions can be soft-deleted. Audit log retains the deletion event. M
R-TRX-011 Transactions support optimistic concurrency via a version field; conflicting updates return HTTP 409 with current state. M

1.5 Investment Transactions

Investment transactions live in their own table (investment.investment_transaction), separate from regular transactions (ledger.transaction). They share the transfer_group mechanism with regular transactions for cross-account transfer pairs. See Data Model §5.2.

The transaction kinds in this section are stored and exposed as lowercase enum values: buy, sell, dividend, deposit_cash, withdraw_cash, fee, and split. The uppercase forms in prose are just human-readable labels.

ID Requirement Scope
R-INV-001 Transactions on investment accounts are restricted to: BUY, SELL, DIVIDEND, DEPOSIT_CASH, WITHDRAW_CASH, FEE, SPLIT. M
R-INV-002 A BUY decreases account cash and increases position quantity. Average cost per unit is recalculated. M
R-INV-003 A SELL increases account cash and decreases position quantity. Average cost per unit is unchanged. A position with zero quantity is soft-deleted. M
R-INV-004 A DIVIDEND increases account cash and is linked to a symbol; it does not affect position quantity. M
R-INV-005 DEPOSIT_CASH and WITHDRAW_CASH move cash into/out of the investment account. They participate in cross-account transfer pairs (sharing a transfer_group_id). M
R-INV-006 A FEE decreases account cash. It is tagged but not linked to a position. M
R-INV-007 A SPLIT is recorded as a ratio (e.g., 4:1). It updates the position's quantity and average cost per unit proportionally without affecting cash. M
R-INV-008 Reinvested dividends are recorded as two separate transactions (a DIVIDEND and a BUY). The system does not auto-link them. M
R-INV-009 Positions in non-account-currency require an FX rate at the time of BUY/SELL, same mechanism as cross-currency transfers. M

1.6 Symbols & Pricing

ID Requirement Scope
R-SYM-001 Symbols are user-created. Symbols with an external identifier (ISIN, CoinGecko ID, etc.) are deduplicated instance-wide and shared across all users. M
R-SYM-002 Symbols without an external identifier are private to the creating user (visible across all their families but not to other users on the instance). M
R-SYM-003 Each symbol records: ticker, asset class (stock, etf, bond, crypto, mutual_fund, commodity, other), natural currency (nullable for crypto), external ID + type (nullable), display name, user-defined flag. M
R-SYM-004 Crypto multi-chain disambiguation uses distinct symbols (e.g., USDT-TRC20 vs USDT-ERC20), each with its own external ID. D
R-SYM-005 Prices are stored as a history: (symbol_id, as_of_date, price, currency, source). Manual updates create new history rows. M
R-SYM-006 The "current price" of a symbol is the most recent price-history row for it. M
R-SYM-007 The system can fetch prices from external providers (CoinGecko, exchanges) for symbols with external identifiers. D
R-SYM-008 Symbol valuation in family base currency uses both price history (in symbol's natural currency) and FX history (natural currency to base currency) at the same as_of_date. M

1.7 Categories & Tags

ID Requirement Scope
R-CAT-001 Categories support a two-level hierarchy (parent + optional children). M
R-CAT-002 New families are seeded with a default set of common categories. M
R-CAT-003 Categories are fully editable: users can add, rename, reparent (within the two-level limit), and soft-delete. M
R-CAT-004 Soft-deleting a category does not delete transactions referencing it; the category remains visible in historical reports. M
R-CAT-005 Tags are flat (no hierarchy), free-text, family-scoped, fully user-editable. M
R-CAT-006 A transaction has exactly one category and zero or more tags. M

1.8 Imports

ID Requirement Scope
R-IMP-001 Users can import transactions from a CSV file conforming to the application's defined import format. M
R-IMP-002 CSV imports are processed asynchronously via the worker; users see status updates and a final summary (success count, error rows). M
R-IMP-003 Importers are pluggable via an interface. New importers can be added without modifying core code. M
R-IMP-004 An LLM-based importer can extract transactions from arbitrary statement formats (PDF, CSV with non-standard headers, free text). D
R-IMP-005 Bank-specific API integrations are pluggable per API instance and can be added by self-hosters as their own code. D

1.9 Reports

ID Requirement Scope
R-RPT-001 A user can view current balances per account and the family's total in base currency. M
R-RPT-002 A user can view spending broken down by category over a chosen period. M
R-RPT-003 A user can view income vs expense over time at chosen granularity (day, week, month, year). M
R-RPT-004 A user can view net worth over time, computed from all accounts (including investment positions valued at historical prices). M
R-RPT-005 A user can browse and filter transactions by date range, account(s), category(ies), tag(s), amount range, and free-text search. Filters combine with AND. M
R-RPT-006 A user can save filter sets as named custom views, including non-AND boolean combinations. D
R-RPT-007 Budgets: a user can set monthly limits per category and track progress. D
R-RPT-008 Savings goals: a user can define a goal (target amount, deadline, optional source account) and track contribution progress. D
R-RPT-009 A simplified cash-flow forecast projects expected balances forward based on recurring transactions. D
R-RPT-010 Investment portfolio view: total value, per-position value, percentage growth, absolute growth — all in family base currency. M

1.10 External Services & API Keys

ID Requirement Scope
R-EXT-001 An API instance has a configuration file listing external services. Each entry has a name, an API key, a scope (reader or writer), and a description. M
R-EXT-002 External services authenticate via their API key in a request header. The auth middleware verifies the key against the config. M
R-EXT-003 An external service can act on behalf of any user on the instance. The user_id and family_id are supplied in the request; the API does not validate ownership beyond key validity. M
R-EXT-004 The trust model is documented: an API key is a system credential; the key holder is fully trusted. Self-hosters are responsible for key security. M
R-EXT-005 The Telegram bot is one such external service, shipped by the project, registered against the project's hosted instance and runnable against any self-hosted instance. M
R-EXT-006 Per-API-key rate limits can be configured. M

1.11 Telegram Bot

ID Requirement Scope
R-BOT-001 A user links their Telegram account to their app account by issuing a one-time link command in the bot, providing a code obtained in the web UI. M
R-BOT-002 A linked user can quick-add transactions via natural-language commands (e.g., /add 50 uah coffee). M
R-BOT-003 A linked user can check current account balances. M
R-BOT-004 A linked user can view simple charts (e.g., spending by category for the current month). M
R-BOT-005 The bot delivers notifications: pending family invitations, import completion, large transactions, etc. (Notification system itself is D; the bot's notification capability becomes available when notifications are built.) M / D
R-BOT-006 A user can accept or reject family invitations directly from the bot. M

1.12 CLI

ID Requirement Scope
R-CLI-001 A CLI tool authenticates via API key (registered as an external service) and can perform batch imports and exports. D

1.13 Audit & History

ID Requirement Scope
R-AUD-001 Every create, update, and delete on family-scoped entities (transactions, accounts, categories, tags, members, roles) generates an audit log entry. M
R-AUD-002 An audit log entry records: timestamp, actor user_id, family_id, entity type, entity id, action, before and after state. M
R-AUD-003 Owners of a family can view the full audit log for their family. M
R-AUD-004 Editors and viewers can view audit entries they are permitted to see (i.e., entries about entities they can read). M
R-AUD-005 An audit log retention policy can be configured per family (e.g., delete entries older than N days). D

1.14 Notifications

ID Requirement Scope
R-NOT-001 Pending family invitations are surfaced in any client by polling the API; clients display them until accepted or rejected. M
R-NOT-002 A general notification system supports multiple delivery channels (in-app, Telegram bot, future email/push) with per-user channel preferences. D

1.15 GDPR & Data Portability

ID Requirement Scope
R-GDP-001 A user can export all data they have access to: as an owner, the entire family's data; as an editor, only entries they themselves authored. M
R-GDP-002 Exports are produced as a downloadable bundle (JSON or CSV) re-importable into another instance of the application. M
R-GDP-003 A user can request hard deletion of their account. The system processes per R-FAM-016 (private family deleted; ownership transfers required for other families). D

1.16 Operator Surface

ID Requirement Scope
R-OPS-001 An OPERATOR_TOKEN can be set via environment variable. The operator credential is not stored in the database. M
R-OPS-002 Operator endpoints expose: instance health (/health/info), user metadata (count, registration dates — no PII deep-dive), API key management, ability to disable a user account. M
R-OPS-003 Operator endpoints do not allow access to user data (transactions, accounts, etc.). Operator auth is separate from user auth and is bootstrapped from the OPERATOR_TOKEN environment variable. M
R-OPS-004 Prometheus-compatible metrics endpoint for operator use. D
R-OPS-005 The browser-based operator panel authenticates by exchanging OPERATOR_TOKEN at a dedicated operator session endpoint for a short-lived HttpOnly cookie session. Protected operator health/admin endpoints accept that cookie; the raw token is not stored server-side. M

2. Non-Functional Requirements

2.1 Architecture & Extensibility

ID Requirement Scope
N-ARC-001 The backend is a modular monolith composed of bounded contexts. Each context has its own domain, application, and infrastructure layers. M
N-ARC-002 The architecture style is Hexagonal (Ports & Adapters): domain at the center, infrastructure at the edges, dependencies pointing inward. M
N-ARC-003 The domain layer has no imports from infrastructure, frameworks, or third-party libraries other than language standard libraries and small pure utility libraries. M
N-ARC-004 Bounded contexts communicate primarily via versioned domain events. Direct cross-context calls are limited to deliberately public read APIs exposed by each context. M
N-ARC-005 The event bus is in-process for MVP but exposes an interface broker-ready from day 0; switching to RabbitMQ/Redis Streams is a configuration change, not a rewrite. M
N-ARC-006 Domain events are persisted to an events table for retry, audit, and broker migration. M
N-ARC-007 Each domain event has a version field; subscribers can handle multiple versions for safe schema evolution. M
N-ARC-008 All extension points (importers, FX providers, price providers, file storage, notification channels) are defined as ports with at least one MVP adapter. M

2.2 Data & Storage

ID Requirement Scope
N-DAT-001 All persistent data is stored in PostgreSQL. SQLite is not supported. M
N-DAT-002 Each bounded context owns a dedicated database schema. No cross-schema foreign keys. Cross-context references are by ID, validated at the application layer. M
N-DAT-003 Monetary amounts are stored as integer minor units with an associated currency code. Floating-point types are not used for money. M
N-DAT-004 Crypto quantities support at most 6 decimal places of precision. M
N-DAT-005 All entity IDs are UUIDv7. M
N-DAT-006 Soft-delete (a deleted_at timestamp) is used for transactions, accounts, categories, and positions. Hard-delete is used for tags, notifications, and (per R-GDP-003) GDPR deletions. M
N-DAT-007 All shared mutable entities have a version integer for optimistic concurrency control. M
N-DAT-008 Transaction logical date is stored as a calendar date (no time, no timezone). Created/updated/audit timestamps are TIMESTAMPTZ in UTC. M
N-DAT-009 Reports use the family's primary timezone for period boundaries; user display preferences affect rendering only. M
N-DAT-010 Migrations are forward-only and idempotent. Major-version migrations may include destructive changes documented in UPGRADING.md. M

2.3 API

ID Requirement Scope
N-API-001 The API is REST over HTTPS, in English. M
N-API-002 URI-based versioning: /v1/.... v1 is committed-stable; only additive changes within a major version. M
N-API-003 Errors follow a single response shape with stable string codes, HTTP status, optional details, and a request_id. M
N-API-004 Mutation endpoints (POST, PATCH, DELETE) accept an Idempotency-Key header. Responses are cached for 24 hours per (idempotency_key, user_id). M
N-API-005 Updates to mutable shared entities require a version value; mismatch returns HTTP 409 with the current entity state. M
N-API-006 Lists are paginated with cursor-based pagination. Filtering is by query parameter; AND-only in MVP. M
N-API-007 An OpenAPI spec is auto-generated from request/response schemas and published at /v1/openapi.json. M
N-API-008 Per-user and per-API-key rate limits are configurable; exceeding them returns HTTP 429 with Retry-After. M

2.4 Security & Privacy

ID Requirement Scope
N-SEC-001 Passwords are hashed with a memory-hard algorithm (Argon2id or scrypt) with per-user salt. M
N-SEC-002 Recovery codes are stored hashed; only their plaintext is shown once at generation. M
N-SEC-003 JWT access tokens have short lifetimes (≤ 15 minutes); refresh tokens are stored server-side and rotate on use. M
N-SEC-004 API keys are stored hashed; only their plaintext is shown once at creation. M
N-SEC-005 The application enforces that one user cannot read or modify another user's data outside of shared families. M
N-SEC-006 All secrets (DB password, JWT secret, OAuth client secret, operator token) are configured via environment variables. The application refuses to start if any required secret is missing or contains a placeholder value. M
N-SEC-007 The application provides full GDPR-style data export and deletion. M
N-SEC-008 TLS termination is provided by Caddy in self-hosted and cloud profiles via automatic ACME. M
N-SEC-009 Personally identifying data (display name, locale, etc.) is per-user; no PII is stored at the family level. M

2.5 Internationalization

ID Requirement Scope
N-I18-001 The API responds in English. Error codes are stable strings; clients perform localization. M
N-I18-002 The web UI supports at least Ukrainian and English. D
N-I18-003 Currency formatting and number formatting in the UI follow the user's locale. D
N-I18-004 Date formatting in the UI follows the user's locale. D

2.6 Observability

ID Requirement Scope
N-OBS-001 Logs are written to stdout in JSON format with required fields: timestamp, level, message, request_id, and (where applicable) user_id, family_id, context, error. M
N-OBS-002 Errors are reported to Sentry when a Sentry DSN is configured. M
N-OBS-003 Each request gets a unique request_id propagated through logs and returned in the response. M
N-OBS-004 GET /health returns 200 if the process is up. GET /health/ready checks DB and Redis connectivity. GET /health/info (operator-protected) returns extended status. M
N-OBS-005 Prometheus metrics for request rates, queue depth, error counts, and DB pool stats. D

2.7 Deployment

ID Requirement Scope
N-DEP-001 The system runs in three deployment profiles from the same source tree: cloud, self-hosted server, fully local. Differences are limited to environment variables and optional infrastructure swaps. M
N-DEP-002 All processes (API, worker, web, telegram bot) are distributed as Docker images. M
N-DEP-003 A docker-compose.yml provides a working out-of-the-box deployment for self-hosters. Profile-specific overrides are provided as docker-compose.<profile>.yml. M
N-DEP-004 Database migrations run via an init container before the API process starts. M
N-DEP-005 Caddy serves the web SPA at / and proxies the API at /api/*. Same origin. M
N-DEP-006 A documented pg_dump-based backup procedure is provided for self-hosters; a sample backup container is included in the compose file (commented out by default). M
N-DEP-007 Tagged Docker images per release; docker compose pull && docker compose up -d is the upgrade path. M
N-DEP-008 First-run setup completes in under five minutes for a self-hoster on a fresh VPS. M

2.8 Performance

ID Requirement Scope
N-PRF-001 A typical API read (e.g., list 50 transactions) responds in under 200ms p95 on a small VPS with up to 10,000 transactions per family. M
N-PRF-002 A typical API write (e.g., create a transaction) responds in under 300ms p95. M
N-PRF-003 Report queries have a Redis-backed cache with a 10–15 minute TTL, invalidated on relevant writes via event subscriptions. M
N-PRF-004 Account balance reads are O(1); the stored balance is updated synchronously inside the same DB transaction as financial mutations. M

2.9 Reliability

ID Requirement Scope
N-REL-001 Database writes that span multiple rows (e.g., creating a transaction and updating an account balance) are wrapped in a single SQL transaction. M
N-REL-002 Domain event handlers are idempotent (event ID used as deduplication key). M
N-REL-003 Background jobs are retryable with exponential backoff; permanently-failed jobs are written to a dead-letter table with full context for inspection. M
N-REL-004 The application starts in a degraded-but-safe mode if Redis is temporarily unavailable: idempotency, rate limiting, and report caching are bypassed; the user-visible API remains functional. D

2.10 Development & Maintainability

ID Requirement Scope
N-DEV-001 The repository is an Nx monorepo. Apps live under apps/; bounded contexts under contexts/; shared libraries under libs/. M
N-DEV-002 Shared types live in libs/shared-types. They contain types only — no logic. M
N-DEV-003 Domain layers can be tested without a database, HTTP server, or any external service. M
N-DEV-004 All public functions in domain and application layers have explicit return types. M
N-DEV-005 A single migration tool owns the schema. Other services (Go, Python) read the schema but never run migrations. M
N-DEV-006 Coding conventions, naming rules, and file layout are documented in AGENTS.md. M

3. Explicit Non-Goals

These are listed here, with X scope, so that they cannot be quietly added later:

ID Non-Goal
X-001 Tax calculation, reporting, or any tax-jurisdiction-specific logic.
X-002 Bill payment or any actual movement of money outside the application.
X-003 Peer-to-peer money transfer between users of the application.
X-004 Expense splitting between users (Splitwise-style).
X-005 Subscription detection or management.
X-006 Credit score tracking or credit reporting integration.
X-007 Full investment analytics (asset allocation, risk metrics, rebalancing suggestions, IRR calculations beyond simple growth percentage).
X-008 Professional or business expense tracking (categorization for accounting, invoice generation, GST/VAT handling).
X-009 Multi-tenant administrative tooling beyond the operator surface defined in R-OPS.
X-010 A native mobile application. The web SPA may evolve into a PWA; that is the mobile story.
X-011 Email-based features (notifications, password reset, marketing). The system does not depend on SMTP.
X-012 Real-time collaboration (live presence, simultaneous editing).

4. Open Items

Decisions deferred to a later phase; tracked here so they don't get lost.

ID Item Decision Owner Trigger
O-001 HTTP framework choice (Hono / Express / NestJS / AdonisJS) Tech stack round Before backend infrastructure implementation begins
O-002 ORM / query layer choice (Drizzle / Kysely / other) Tech stack round Before backend infrastructure implementation begins
O-003 Web client framework (React vs Vue) Tech stack round Before web implementation begins
O-004 Telegram bot language (TypeScript vs Python) Tech stack round Before bot implementation begins
O-005 LLM provider strategy for D-scope LLM importer Future design pass When R-IMP-004 is being built
O-006 Notification channel implementations beyond in-app and Telegram Future design pass When R-NOT-002 is being built