Skip to content

Backend Architecture

The backend is a FastAPI application running on uvicorn with async SQLAlchemy 2.0. It provides a REST API for net-worth tracking, authentication, data import/export, budgeting, and investment management.

backend/app/
├── main.py # App factory, lifespan, router registration, middleware
├── config.py # Pydantic BaseSettings (loads from .env)
├── database.py # Async SQLAlchemy engine, session factory, get_db
├── dependencies.py # get_current_user (dual auth: OIDC + local JWT + API token)
├── errors.py # Structured error responses (APIError, handlers)
├── i18n.py # Backend translations (fr/en/es/de/it)
├── worker.py # arq worker settings (optional Redis-based background tasks)
├── middleware/ # Request id, metrics, security headers, rate limiting, body limit, CSRF, audit logging, FX flags
├── models/ # 40 model files (47 mapped classes, 50 tables) + base/mixins/enums
├── routers/ # 42 API routers
├── schemas/ # Pydantic request/response schemas
├── services/ # 91 service modules and the backup_import package (business logic)
└── utils/ # JWT, cookies, OIDC client helpers
File Responsibility
main.py Creates the FastAPI app, registers the exception handlers and nine middleware (Metrics outermost → … → CORS innermost, see Middleware), includes all routers, and starts up to 14 lifespan background tasks. Also closes the shared OIDC HTTP client on shutdown.
config.py Settings(BaseSettings) loads from .env via Pydantic Settings; all configuration in one place
database.py Creates the async engine (create_async_engine), session factory (async_sessionmaker), and get_db dependency (always commits on success, rolls back on exception); sets SQLite WAL/FK PRAGMAs via event listener
dependencies.py get_current_user validates JWT from cookie or API token from Authorization: Bearer ptr_... header; supports local (HS256) and OIDC (RS256) tokens; handles token refresh including OIDC refresh token rotation; imports validate_api_token and upsert_from_claims at top level
errors.py APIError exception class and four handlers (api_error_handler, http_exception_handler, validation_exception_handler, unhandled_exception_handler) producing {"error": {"code", "message", "detail"?}}
i18n.py translate(key, locale, **kwargs) for backend error messages; supports fr/en/es/de/it, falls back to fr
worker.py arq WorkerSettings with 10 cron jobs; used when REDIS_URL is set; otherwise lifespan tasks handle scheduling

42 routers registered in main.py. The table below lists each router, its URL prefix, and the endpoints it exposes.

Router Prefix Endpoints
health.py (none) GET /health, GET /health/db
metrics.py (none) GET /metrics (Prometheus text format; bearer METRICS_TOKEN, 404 when unset)
auth.py (none) GET /auth/oidc/start, GET /auth/callback, POST /auth/login, POST /auth/logout, POST /auth/register, POST /auth/login/token, POST /auth/mobile/exchange, `GET
sessions.py /api/v1/auth/sessions GET (paginated, X-Total-Count), DELETE /{id}, POST /revoke-others; the caller’s own browser sessions
accounts.py /api/v1/accounts CRUD, reorder, restore, archive, permanent delete
admin_users.py /api/v1/admin/users Admin-only user management: list, lock/unlock, role changes, invites (create, list, revoke, purge), email change, MFA disable, audit view and export
alerts.py /api/v1/alerts, /api/v1/alert-rules Paginated visible notifications fired by the alert check (newest first, read and unread), their unread count, and read, dismiss and read-all actions; GET and PUT of the three alert rules (defaults filled in; PUT re-evaluates the user); GET and PUT /api/v1/notification-preferences, the channel of each notification type
api_tokens.py /api/v1/tokens CRUD (ptr_ prefixed tokens)
audit_logs.py /api/v1/audit-logs Paginated query
planned_transactions.py /api/v1/planned-transactions CRUD, due/overdue checks, reminders (merged former bills + recurring transactions), payee_id (set or clear) on create and update with the payee returned as payee, /subscriptions (active repeating expenses with yearly amounts in the base currency and price changes)
budget.py /api/v1/budget Budgets CRUD, PUT/DELETE /budgets/{budget_id}/month-limits/{month}, monthly/range summary, analytics, spendable, savings rate (GET /savings-rate)
categories.py /api/v1/categories CRUD, reorder
categorization_rules.py /api/v1/categorization-rules CRUD, rule-based auto-categorization of transactions; size caps and a regex probe on save (422 rule_too_many_nodes, rule_too_deep, rule_regex_invalid, rule_regex_timeout); preview reports a regex timeout in the X-Rule-Timed-Out header
envelopes.py /api/v1/envelopes Envelope CRUD, contributions, mark paid, free-to-use figure; GET /payday-plan lists, per pay schedule that at least one envelope or goal uses (the default schedule first, then by next payday), the next payday, the per-payday amount of each envelope and goal and their total
funding_schedules.py /api/v1/funding-schedules Pay schedules: create, list, read, edit, delete (the default schedule cannot be deleted until another default is chosen or it is cleared, and a schedule that envelopes or goals still pick is refused with 409 funding_schedule_in_use and the number of items using it)
goal_templates.py /api/v1/goal-templates Contribution templates: create, list, read, edit, delete (a template that goals still use is refused with 409 goal_template_in_use and the number of goals using it)
chart_annotations.py /api/v1/chart-annotations CRUD for user annotations overlaid on charts
charts.py /api/v1/charts net-worth, account-trends, stacked-by-type, stacked-by-category, statistics, delta-heatmap, waterfall
data_import.py /api/v1/import parse (CSV/ODS/XLSX/JSON/OFX), commit
exchange_rates.py /api/v1/exchange-rates CRUD, filterable/sortable paginated list, /refresh (auto-FX), /fetch (single-pair historical)
export.py /api/v1/export CSV, JSON, ODS, XLSX, backup
institutions.py /api/v1/institutions CRUD, aggregated balances, soft-delete with cascade, archive/unarchive, restore, permanent delete, empty trash
investment.py /api/v1/investments Holdings + dividends CRUD (a dividend is edited with PATCH /holdings/{holding_id}/dividends/{dividend_id}), portfolio summary, price refresh
market_data.py /api/v1/market-data Ticker lookup/price fetch (Yahoo Finance) used by investment holdings
net_worth_thresholds.py /api/v1/net-worth-thresholds CRUD and projected dates
reconciliations.py /api/v1/budget Reconcile an account against a statement: POST /reconciliations/preview, POST /reconciliations, POST /reconciliations/{id}/undo, GET /reconciliations
recurring_snapshots.py /api/v1/recurring-snapshots CRUD
reports.py /api/v1/reports GET/PATCH /settings (the saved assumptions), GET /financial-independence, GET /cash-flow, GET /simulation (the seeded retirement simulation, optional current_age, retirement_age, retirement_length, volatility and seed query values over the saved settings; 422 simulation_age_required, invalid_retirement_age; 503 simulation_busy when both run slots stay taken for five seconds), POST /run (runs a report definition and stores nothing; 422 report_too_large, report_too_many_intervals); all read-only apart from the settings PATCH
savings_goals.py /api/v1/savings-goals CRUD, account linking
snapshots.py /api/v1/snapshots CRUD, counts, restore, batch delete by date
saved_filters.py /api/v1/saved-filters List (paginated), create, rename, delete
saved_reports.py /api/v1/saved-reports List (paginated), create, update (rename and/or definition), delete; 409 saved_report_name_taken and saved_report_limit, 404 saved_report_not_found
dashboards.py /api/v1/dashboards List (paginated), create, POST /initialize (creates the first dashboard, idempotent), PUT /reorder, update (rename and/or layout), delete; 409 dashboard_limit, dashboard_last and dashboard_card_conflict, 422 dashboard_order_invalid, 404 dashboard_not_found
subcategories.py /api/v1/subcategories CRUD, reorder
system.py (none) GET /api/v1/system/info
tags.py /api/v1/tags CRUD, tag assignment to transactions
payees.py /api/v1/payees Paginated list (X-Total-Count) with search over names and aliases, ids lookup and name/usage sort, create, rename keeping the old name as an alias, default category, delete unused, GET /{id}/category-suggestion, merge preview and merge
transaction_trash.py /api/v1/budget Transaction trash: GET /transactions/deleted/list, POST /transactions/{id}/restore, DELETE /transactions/{id}/permanent, DELETE /transactions/deleted/empty
transfers.py /api/v1/budget Transfers between two accounts: POST /transfers (create a linked pair), POST /transfers/link (link two existing transactions), POST /transactions/{id}/unlink, GET /transfers/suggestions (paginated, read-only)
transactions.py /api/v1/budget Transactions CRUD, filtering/sorting, export (CSV/ODS/XLSX), batch delete/update, bank-file import (OFX, QFX, QIF, CAMT and generic CSV with a column mapping: parse and commit pairs under /transactions/import/), Boursorama CSV category import
transaction_splits.py /api/v1/budget PUT /transactions/{id}/splits (replace the lines), DELETE /transactions/{id}/splits (remove them), POST /transactions/merge (merge two duplicates)
webhooks.py /api/v1/webhooks CRUD (HMAC-signed)

40 model files in app/models/ (47 mapped model classes plus 3 association tables — several files, e.g. budget.py and investment.py, define more than one class), all registered with Base.metadata via app/models/__init__.py for Alembic autogenerate.

Model File Table Purpose
User user.py users User with integer PK, OIDC sub, local auth, locale, base currency
Account account.py accounts Financial account with soft-delete, type, currency, liquidity horizon, on_budget (an off-budget account is left out of budgets, analytics and the spendable pool) and exclude_from_net_worth (the account stays visible but is left out of every net worth total); csv_import_mapping holds the CSV column mapping last confirmed for the account (SQL NULL when unset)
Snapshot snapshot.py snapshots Balance record per account per date; unique index on (account_id, date) where deleted_at IS NULL
Category category.py categories User-owned spending category (with subcategories) used by transactions, budgets, rules and splits, with colour, sort order and exclude_from_spending
Institution institution.py institutions User-owned financial institution grouping with soft-delete (deleted_at) and archive (archived_at); cascade soft-delete/restore to linked accounts and snapshots; partial unique index on (user_id, name) where deleted_at IS NULL
RecurringSnapshot recurring_snapshot.py recurring_snapshots Scheduled snapshot creation rules (weekly/monthly)
Envelope, EnvelopeContribution envelope.py envelopes, envelope_contributions Money set aside for a non-monthly expense, and the amounts added to it
FundingSchedule funding_schedule.py funding_schedules A user’s pay schedule: a name and a cadence (dates only, no amount); at most one default per user, names unique per user without regard to case
GoalTemplate goal_template.py goal_templates A user’s reusable contribution: a fixed amount per paycheck or per month; names unique per user without regard to case; it never moves money
SavingsGoal savings_goal.py savings_goals Savings target with optional deadline and linked accounts
savings_goal_accounts savings_goal.py savings_goal_accounts M2M association table (SavingsGoal ↔ Account)
Budget budget.py budgets Monthly spending limit per category with rollover
BudgetMonthLimit budget.py budget_month_limits A budget’s own limit for one month; a month without a row uses the budget’s default limit
Transaction budget.py transactions Income/expense transaction linked to account; status (pending, cleared, reconciled), deleted_at (trash), reconcile_id (the reconciliation that locked it), transfer_group_id (opaque id shared by the two legs of a transfer, NULL otherwise) and payee_id (the transaction’s payee, NULL when its text names nobody)
Reconciliation reconciliation.py reconciliations One account reconciled against a statement (statement date and balance, the snapshot it wrote or replaced, undone_at)
TransactionSplit transaction_split.py transaction_splits One category line of a split transaction (category, optional subcategory, signed amount, position); removed with its parent
PlannedTransaction planned_transaction.py planned_transactions Recurring/scheduled transaction with due date, reminders, overdue detection (merged former Bill + RecurringTransaction); weekend_shift and skipped_dates hold the weekend option and the skipped anchors
PlannedSuggestionDismissal planned_suggestion_dismissal.py planned_suggestion_dismissals A suggested planned transaction the user dismissed, keyed by account, transaction type and description key; removed with its account
CategorizationRule categorization_rule.py categorization_rules Rule-based auto-categorization of transactions, with optional category, subcategory, tag and suggested-label actions
ChartAnnotation chart_annotation.py chart_annotations User annotation overlaid on chart timelines
Tag tag.py tags User-owned tag applicable to transactions
Payee, PayeeAlias payee.py payees, payee_aliases A user’s payee (merchant or counterparty a transaction points at, unique per user on its case-folded name_key, optional default category and subcategory) and the other spellings that resolve to it (alias_key unique per user)
SavedFilter saved_filter.py saved_filters Named transaction filter set of one user, stored as JSON; unique per (user_id, name)
ReportSettings report_settings.py report_settings One row per user: investable accounts (JSON list, no foreign key), withdrawal rate, real return and an optional yearly spending override; no row means the defaults
SavedReport saved_report.py saved_reports Named report definition of one user, stored as JSON; unique per (user_id, name)
Dashboard dashboard.py dashboards Named dashboard of one user: tab order and card layout stored as JSON; at most 8 per user
NetWorthThreshold net_worth_threshold.py net_worth_thresholds Net-worth threshold; is_past mirrors the latest alert check and is written by it
Holding investment.py holdings Investment holding (ticker, quantity, cost basis, price)
Dividend investment.py dividends Dividend payment for a holding
ExchangeRate exchange_rate.py exchange_rates Manual or auto-fetched FX rate per (from, to, date)
ApiToken api_token.py api_tokens Long-lived API token (SHA-256 hashed, ptr_ prefix)
Webhook webhook.py webhooks Webhook endpoint config with HMAC secret
AuditLog audit_log.py audit_logs Persisted audit event (method, path, status, IP, duration)
AlertRule, Alert, NotificationPreference alert.py alert_rules, alerts, notification_preferences A user’s saved alert rule settings, the alerts those rules fired (one row per occurrence, dedup_key unique per user, with the user’s read and dismissed marks), and a user’s channel per notification type
RevokedToken revoked_token.py revoked_tokens JWT blacklist entry (jti, user_sub, expires_at)
TotpRecoveryCode totp_recovery_code.py totp_recovery_codes One-time two-factor recovery code: plain lookup id, Argon2 hash of the secret half, used_at; unique per (user_id, lookup_id)
UserSession user_session.py user_sessions Tracked browser session: random sid, expires_at, revoked_at, last_seen_at (written at refresh rotation), coarse browser/OS label and /24 or /48 network
DeltaCache delta_cache.py delta_cache Precomputed daily net-worth delta for heatmap
UserInvite user_invite.py user_invites Pending admin-issued invite token for new user registration
PasswordResetToken password_reset_token.py password_reset_tokens Single-use token for local-account password reset
EmailChangeToken email_change_token.py email_change_tokens Single-use token for confirming an email address change
TransactionDuplicateDismissal transaction_duplicate_dismissal.py transaction_duplicate_dismissals A pair of transactions the user declared “not duplicates”, stored ordered (a < b), unique per user; cascades on permanent delete
WebhookDelivery webhook_delivery.py webhook_deliveries Per-attempt webhook delivery record with retry/backoff state

91 service modules in app/services/ containing business logic, separated from routers.

Service File Responsibility
account_service account_service.py Account CRUD, soft-delete, reorder, archive/restore
account_closing_service account_closing_service.py Close an account inside the request transaction: a transfer of the remaining balance, a snapshot for each account on the transfer date, then the archive; every refusal comes before the first write
account_validation account_validation.py Shared account input validation helpers
admin_user_service admin_user_service.py Admin user management: list, lock/unlock, role changes
alert_service alert_service.py Evaluates each user’s alert rules, net worth thresholds and snapshot reminders in the worker, stores one alert per occurrence, posts alert.fired to the user’s webhooks and emails the types set to email or both after commit, and serves the read and dismissed state and the channel filter behind the bell
api_token_service api_token_service.py Token generation (ptr_ prefix), hashing, validation
audit_log_service audit_log_service.py Audit log creation, paginated query, cleanup
auth_service auth_service.py Local login, registration, password change, failed-login lockout
backup_service backup_service.py SQLite sqlite3.backup() snapshot creation
balance_forecast_service balance_forecast_service.py Day-by-day liquid balance forecast from the opening balance and planned transactions (read-only); get_what_if_forecast replays it with up to five hypothetical one-off, weekly or monthly items and returns the base and simulated series plus the change in lowest and end-of-horizon balance, storing nothing
payday_plan_service payday_plan_service.py Builds the payday plan, grouped by pay schedule, from the envelopes and goals and the schedule each uses (its pick, else the default) (read-only)
payday payday.py Resolves the pay schedule an item uses (its pick, else the user’s default), counts the paydays of a schedule between two dates, honouring the weekend shift, and gives the next payday
funding_schedule_service funding_schedule_service.py Creates, lists, reads, edits and deletes a user’s pay schedules, works out each one’s next payday and counts the envelopes and goals that pick it, refusing a delete while any do
goal_template_service goal_template_service.py Stores a user’s contribution templates, counts the goals that use each, and projects a goal’s completion date from a template and the goal’s pay schedule on every read (nothing is stored)
bank_formats bank_formats.py Bank-specific CSV format detection and parsing
planned_schedule planned_schedule.py Pure recurrence rules for planned transactions, shared by the auto-post sweep, manual payment and the forecast. A date is an anchor and the weekend shift only maps it to the effective date it is posted on. The sweep pre-filters on anchors widened by SHIFT_REACH_DAYS (two days), then filters exactly on effective dates. Stepping over a skipped anchor never counts toward max_occurrences, and a pointer that lands on a skipped anchor advances lazily, without posting
planned_transaction_service planned_transaction_service.py Planned transaction CRUD, due/overdue checks, reminders
planned_suggestion_service planned_suggestion_service.py Suggested planned transactions, computed on demand and read-only: ledger rows are grouped by account, type and description, a weekly, monthly, quarterly or yearly gap pattern is detected with named thresholds (GAP_BANDS_DAYS, SUGGESTION_MIN_OCCURRENCES, SUGGESTION_AMOUNT_TOLERANCE, a 24-month lookback), and patterns a planned item already covers or the user dismissed are left out. Dismissals are the only thing stored
subscription_service subscription_service.py Yearly normalisation, base-currency conversion and price-change detection for the Subscriptions view
budget_service budget_service.py Budget CRUD, rollover, analytics, monthly/range summary
cash_flow_service cash_flow_service.py The cash flow graph of a period (read-only): flow rows grouped by category and converted to the base currency, income sources (the top 5, the rest as Other) into one hub that flows out to the spending categories (the top 8, the rest as Other) and, for a surplus, a Savings node; a shortfall is a Deficit node on the source side. Synthetic nodes carry a kind and no label, so the client translates them
camt_import_service camt_import_service.py CAMT.052/053 reader: defusedxml with any DOCTYPE refused, depth, element and statement bounds, booked entries only (unreadable and foreign-currency entries counted as skipped), rows and external ids, one preview per account and currency through previews_from_statements
categorization_rule_service categorization_rule_service.py Rule-based auto-categorization CRUD and matching (including the RuleRun that skips a slow regex rule for the rest of one import)
category_seed_data category_seed_data.py Default category/subcategory seed data for new users
category_service category_service.py Category CRUD, reorder
category_validation category_validation.py Shared category input validation helpers
chart_annotation_service chart_annotation_service.py Chart annotation CRUD
chart_service chart_service.py Chart data aggregation, delta cache refresh
csv_category_import_service csv_category_import_service.py Bulk category import from CSV
csv_import_service csv_import_service.py Generic bank CSV import: preview under a column mapping (saved, preset or requested) and commit through the shared bank-file core; remembers the mapping on the account
custom_css_service custom_css_service.py Validation of a user’s custom theme, a pure allow-list with no database access: the colour tokens every skin defines plus --radius are the only properties, each value must match the grammar of its property (a colour, or a length for --radius), the length is checked first (MAX_LENGTH, 8000 characters), and only the canonical re-serialised --name: value; text is stored, never the raw input. A refusal is a 422 custom_css_invalid whose detail carries the line, the property and the reason, with a message in the user’s locale. Used by the profile update, by backup restore and, for its constants, by GET /api/v1/auth/custom-css/rules
email_service email_service.py Transactional email sending (invites, password reset, email change) via aiosmtplib
exchange_rate_service exchange_rate_service.py FX rate CRUD, auto-fetch from frankfurter.app
export_service export_service.py CSV/JSON/ODS/XLSX/backup export
fire_service fire_service.py The financial independence projection in today’s money (read-only): the target is annual spending divided by the withdrawal rate, and the balance grows with the real return and the average monthly savings of the completed months of the last year, for at most 600 months. The accounts come from the request, then the saved settings, then the default (the live, off-budget accounts that are not liabilities); the starting balance is the latest snapshot of each account in the base currency, and one without a rate is left out and counted The projection returns no_history, then no_accounts, then no_spending (a yearly spending of zero, whose target of zero any balance would reach) before it projects anything.
import_parsing import_parsing.py Standard-library helpers shared by the CSV, QIF and CAMT importers: strict amount, date and decoding parsers, stable synthetic external ids, text normalisation, MAX_IMPORT_ROWS
import_service import_service.py File parsing (CSV/ODS/XLSX/JSON), commit
institution_service institution_service.py Institution CRUD, soft-delete with cascade to accounts/snapshots, restore with cascade, archive/unarchive with cascade to accounts, permanent delete, empty trash, aggregated balances
investment_service investment_service.py Holdings/dividends CRUD, portfolio summary, daily price refresh
invite_service invite_service.py Admin-issued user invite creation and acceptance
ledger_scope ledger_scope.py Row scopes for every transaction read (five: live, cash, flow, saving, guard) and the statement builders services use instead of querying transactions directly. live() excludes trashed rows; cash() keeps everything live (balances see transfer legs and off-budget accounts); flow() also drops transfer legs and rows of off-budget accounts, so budgets and analytics never count a transfer; saving() selects the on-budget leg of a transfer whose other leg sits on an off-budget account (savings rate only); guard() includes trashed rows. transfer_candidates is the leg query behind the transfer suggestions. select_flow_lines is select_flow with the split lines expanded, for category readers only: a parent with lines yields one row per line, any other row yields itself, and the category and amount are read through the LINE_CATEGORY_ID, LINE_SUBCATEGORY_ID and LINE_AMOUNT expressions. Totals and the cash scope stay on the parent row, because the join would count a parent once per line
net_worth_scope net_worth_scope.py The one predicate saying which accounts count toward net worth: counted(), true for an account that is not excluded from net worth. Every net worth total applies it (the net worth and adjusted series, stacked by type, waterfall, concentration, liquidity, grid totals, thresholds, the delta heatmap, spendable, the pooled balance forecast and the default FIRE set); per-account views, latest-per-account, alerts, reminders, savings goals and portfolio views never do. tests/test_net_worth_scope_guard.py fails when a snapshot read or a latest-per-account call is not classified, or when a total does not use it, and tests/test_net_worth_exclusion.py proves each total equals the same user without the excluded accounts
liquid_balance liquid_balance.py Opening balance of the liquid account pool (latest snapshot plus later ledger rows), default pool and stale-snapshot threshold
market_data_service market_data_service.py Ticker lookup and price fetching (Yahoo Finance)
merge_service merge_service.py Merge two duplicate transactions: the kept one is filled from the other’s empty fields, tags are united and the other goes to the trash; reconciled rows, transfer legs and split parents refuse (409 merge_blocked)
net_worth_threshold_service net_worth_threshold_service.py Threshold CRUD and projected dates (crossings are notified by alert_service)
ofx_import_service ofx_import_service.py OFX and QFX file parsing via ofxtools, statement preview and commit; previews_from_statements builds the per-account preview that the CAMT reader shares
recovery_code_service recovery_code_service.py One-time two-factor recovery codes: generation (ten codes, the secret half stored as an Argon2 hash), normalisation, consumption with exactly one verify per attempt (a dummy hash when nothing matches) and an atomic single use, status and deletion
recurring_snapshot_service recurring_snapshot_service.py Rule CRUD and the sweep that reschedules due rules (the reminder itself is raised by alert_service)
report_service report_service.py Scheduled net-worth report generation and emailing
report_builder_service report_builder_service.py Runs a report definition and persists nothing: ledger rows are grouped by one or two dimensions (category, account, time interval) and measured (expenses, income, net, count). An interval is always the x axis; series keep the top 7 and a non-interval x axis the top 50, the rest folded into Other. Dimensions and measures are closed enums mapped to Python functions, never SQL built from strings
report_rows report_rows.py The one path from ledger rows to converted figures for the reports. It reads only through the ledger scopes: select_flow, so a transfer leg or an off-budget account never counts, and select_flow_lines only when a category dimension is grouped, so a split transaction counts in each line’s own category. The statement never joins accounts or categories itself. Each (day, account, category, type) group is converted with the rate of its own date; a group with no rate is left out and counted (excluded_transactions, missing_rate_currencies). Interval buckets and the folding into Other are computed in Python, and two caps refuse a report: 100,000 grouped rows (report_too_large) and 367 interval buckets (report_too_many_intervals)
report_settings_service report_settings_service.py Report settings: the defaults without a row, a read that never writes, saved account ids filtered to the caller’s own live, non-archived, non-liability accounts on every read and write, and a first save inside a savepoint so two concurrent first saves leave one row
monte_carlo_service monte_carlo_service.py The retirement simulation, in today’s money and deterministic for a seed: yearly growth is log-normal (arithmetic mean the real return, arithmetic standard deviation the volatility), the yearly savings are added until the retirement age, then the yearly spending is drawn down for the retirement length. Success is a balance still above zero at the end of the last retirement year; the 10th, 25th, 50th, 75th and 90th percentiles are kept per year. The path count is fixed at 5,000 (never a parameter), each call builds its own random.Random(seed), and the pure-Python loop runs in run_in_threadpool behind a semaphore of two runs: a caller waits at most five seconds, then gets 503 simulation_busy, and the database transaction is ended before the wait so a queued run holds no connection. Balance, spending, savings and return come from resolve_fire_inputs, so the figures match the financial independence report. Stores nothing It returns the same no_history, no_accounts and no_spending statuses, in that order, with no success rate.
qif_import_service qif_import_service.py QIF file preview read with the user’s date order and number format; rows commit through the shared bank-file commit
rule_actions rule_actions.py Applies a matched categorization rule’s category, subcategory, tag and suggested-label actions to a transaction, shared by rule apply and the importers
rule_limits rule_limits.py Save-time caps on a rule’s condition tree (30 nodes, 4 levels) and the regex probe, raised as RuleLimitError
rule_regex rule_regex.py The only module that compiles or runs a user-supplied rule pattern: pattern and text length caps, compile guard, 50 ms search timeout (RegexTimeout) and the hostile-sample probe
reconcile_service reconcile_service.py Reconcile preview, commit and undo: difference from the latest reconcile, snapshot or opening balance, lock of the window’s cleared transactions, statement-date snapshot
running_balance running_balance.py Per-row running balance and snapshot drift for one account, a window SUM() OVER over the cash scope anchored on the latest snapshot
saved_filter_service saved_filter_service.py Saved filter list, create, rename and delete, scoped to one user; unique names and the 100-filter cap
saved_report_service saved_report_service.py Saved report list, create, update and delete, scoped to one user; unique names and the 100-report cap. An update applies the rename first, so a refused rename leaves the definition untouched
dashboard_service dashboard_service.py Dashboard list, first-dashboard creation, create, update, delete and reorder, scoped to one user; a per-user write lock, the 8-dashboard cap, the one-dashboard-per-card check and the hidden-card merge on delete
savings_goal_service savings_goal_service.py Savings goal CRUD, linked account progress
savings_rate_service savings_rate_service.py Monthly savings rate in the base currency: income and expenses over the flow scope, plus the amount moved to off-budget accounts (saving scope) counted as saved (read-only)
scheduling_service scheduling_service.py Shared helpers for lifespan/arq scheduled tasks (e.g. get_users_with_active_accounts)
session_service session_service.py Tracked browser sessions: start at cookie sign-in, last_seen_at at refresh rotation, list (with the synthetic legacy entry), revoke one or all others (stamping the legacy cutoff), daily purge; keeps a coarse browser/OS label and a /24 or /48 network, never the raw User-Agent or address
snapshot_service snapshot_service.py Snapshot CRUD, counts, batch operations
split_service split_service.py The one place that saves or removes the category lines of a transaction: 2 to 20 non-zero lines summing to the parent’s amount, a NULL parent category, no split of a reconciled row or a transfer leg
spendable_service spendable_service.py End-of-month spendable figure: liquid balance minus uncovered planned expenses and budget reserves (read-only). Counts on-budget accounts only (the balance forecast and net worth keep off-budget ones), and nets a recorded future-dated transfer pair between pooled accounts out of expected income and expenses
subcategory_service subcategory_service.py Subcategory CRUD, reorder
system_service system_service.py System info aggregation
payee_service payee_service.py Payee CRUD (list with search over names and aliases, usage count and sort, create, rename keeping the old name as an alias, default category, delete unused, merge with a preview of the affected transactions) and name normalization, the one grouping rule shared with migration 120
payee_validation payee_validation.py Payee ownership checks (resolve_owned_payees, resolve_owned_payee): an id of another user is a 404 payee_not_found
tag_service tag_service.py Tag CRUD, assignment to transactions
tag_validation tag_validation.py Shared tag input validation helpers
totp_service totp_service.py TOTP secret generation/verification (pyotp) for 2FA
transaction_import_core transaction_import_core.py Shared create path for bank-file imports (external-id de-duplication, a savepoint per row, rule actions, import summary)
transaction_lock transaction_lock.py The single guard every transaction writer calls: assert_mutable raises 409 transaction_locked for a reconciled row (its transfer_group_id is a locked field too), partition_locked splits a batch, release_account_locks runs before a permanent account or institution delete
transaction_trash transaction_trash.py Transaction trash: list, restore, permanent delete, empty
transaction_service transaction_service.py Transaction CRUD, filtering/sorting, export queries, batch operations, base-currency conversion
transfer_service transfer_service.py Transfers: create a linked pair, link and unlink existing transactions, find the peer leg, attach transfer_peer to list rows, suggest likely pairs, and find_inconsistent_groups (a group must have two legs, one trash state and two different accounts)
user_service user_service.py User upsert from claims (only updates non-None fields), profile update, password change
backup_import backup_import/ JSON backup restore, split by section (accounts, catalog, planning, savings, transactions, views) with a shared preview and restore context; import_service delegates to it
duplicate_service duplicate_service.py Probable-duplicate detection and dismissals
envelope_service envelope_service.py Envelope CRUD, contributions, pay with idempotency key
loan_payoff_service loan_payoff_service.py Loan payoff projection from the account’s rate and payment
metrics_service metrics_service.py In-process Prometheus counters/histograms, job outcomes (record_job) and the Redis queue gauge behind GET /metrics
net_worth_projection net_worth_projection.py Net worth projection
payee_suggestion_service payee_suggestion_service.py Category suggestion for a payee (default or history)
pivot_grid pivot_grid.py Snapshot/institution grid pivot
sorting sorting.py Shared sort helpers
spreadsheet_parsing spreadsheet_parsing.py CSV/ODS/XLSX parsing helpers (parse_bank_csv)
webhook_service webhook_service.py Webhook CRUD, HMAC-signed delivery, retry sweep

Up to 14 background tasks are scheduled in main.py via asyncio.create_task during the FastAPI lifespan. When REDIS_URL is set, the equivalent work runs as arq cron jobs in the worker container instead (see worker.py’s 11 cron jobs).

Task Schedule Description
daily-backup Every 24h SQLite sqlite3.backup() to ./data/backups/patrimoine_<timestamp>.db; skipped for PostgreSQL; disabled when AUTO_BACKUP=false
recurring-snapshots Every 1h Checks recurring_snapshots table and reschedules due rules (no snapshot is created and no email is sent)
planned-transaction-sweep Every 1h Posts the planned transactions that fall due (weekend shift and skipped dates applied) and sends reminders
fx-refresh Every 24h Auto-fetches exchange rates from frankfurter.app for users with non-EUR accounts
audit-cleanup Every 24h Deletes audit logs older than audit_log_retention_days (default 90)
revoked-token-purge Every 24h Deletes expired rows of revoked_tokens
webhook-delivery-cleanup Every 24h Deletes webhook delivery history older than WEBHOOK_DELIVERY_RETENTION_DAYS (default 30)
session-purge Every 24h Deletes user_sessions rows whose expires_at is more than 30 days past
delta-cache-refresh Every 1h Recomputes daily net-worth deltas for heatmap visualization
alert-check Every 30 min Evaluates each user’s alert rules, net worth thresholds and due snapshot reminders, stores one alert per occurrence and posts alert.fired to the user’s webhooks (the arq cron runs it at :00 and :30)
price-refresh Every 24h Auto-refreshes current holding prices from Yahoo Finance, deduped globally by ticker
scheduled-reports Every 1h Sends due scheduled net-worth report emails (the arq cron runs this once daily at 07:00 UTC instead)
webhook-delivery-retry Every 60s Retries pending webhook deliveries; the only retry mechanism guaranteed present since the arq worker is optional

The arq worker does not run session-purge, revoked-token-purge or webhook-delivery-retry; they stay in the API process.

All tasks log exceptions via add_done_callback(_log_task_exception) and continue running. The daily backup task is only created when auto_backup is True (opt-in, default False) — see Backup & Restore for details.

Settings are defined in app/config.py as a Pydantic BaseSettings subclass, loaded from environment variables and .env file.

Setting Type Default Description
database_url str sqlite+aiosqlite:///data/patrimoine.db SQLAlchemy async database URL
debug bool False Enables debug logging and SQL echo
app_name str Patrimoine Application name (FastAPI title)
oidc_issuer str http://localhost:9000/application/o/patrimoine OIDC provider issuer URL (trailing slash auto-stripped)
oidc_client_id str "" OIDC client ID
oidc_client_secret SecretStr "" OIDC client secret
oidc_enabled bool False OIDC is active only when this and oidc_client_id are set
oidc_redirect_uri str "" OIDC redirect URI; derived as FRONTEND_URL + /auth/callback when empty
frontend_url str http://localhost:8080 Frontend origin (CORS)
jwt_secret SecretStr "" HS256 signing secret for local JWTs (required, 32+ characters)
jwt_algorithm str HS256 JWT algorithm for local tokens
access_token_expire_minutes int 30 Access token TTL
refresh_token_expire_days int 7 Refresh token TTL
registration_enabled bool False Whether new user registration is allowed
trusted_proxy bool True Trust X-Real-IP header from nginx
e2e_test_mode bool False Auth bypass for Playwright tests (requires DEBUG=True)
audit_log_retention_days int 90 Days before audit logs are auto-deleted
auto_backup bool False Enable automatic SQLite backup on startup + every 24h (opt-in)
redis_url str "" Redis URL for arq worker (empty = asyncio fallback); also enables the worker job counts in GET /metrics
metrics_token SecretStr "" Bearer token for GET /metrics (32+ characters; empty disables the endpoint)
max_request_body_bytes int 2 MiB Request body cap
max_import_body_bytes int 12 MiB Request body cap for import endpoints
db_pool_size / db_max_overflow int 5 / 10 Connection pool for PostgreSQL
trusted_proxies str "" (private networks) Peers allowed to set X-Forwarded-* / X-Real-IP
max_session_days int 30 Lifetime of a tracked session
login_max_attempts / login_lockout_minutes int 5 / 15 Failed-login lockout (login_lockout_max_minutes 240, login_account_max_attempts 25)
max_tokens_per_user int 10 API token cap
webhook_delivery_retention_days int 30 Webhook delivery history retention
smtp_host, smtp_port (587), smtp_user, smtp_password, smtp_from Outgoing email
admin_emails, admin_subs str "" Bootstrap admins
expose_api_docs, docs_url, site_url, help_url API docs exposure and documentation links
mobile_oidc_client_id, mobile_oidc_issuer str "" Mobile app OIDC client
jwt_accept_legacy_key bool True Accept tokens signed with the raw JWT_SECRET (issued before the signing key was derived from it)
frankfurter_api_url, yahoo_search_url, yahoo_chart_url str public endpoints Market-data sources
e2e_bypass_token SecretStr "" Test-mode bypass token (generated at startup when empty)

The list is a selection; the full reference is Environment Variables.

Key Python packages (from pyproject.toml):

Package Version Constraint Purpose
fastapi[standard] >=0.162.0,<0.163.0 Web framework
uvicorn[standard] >=0.54.0,<0.55.0 ASGI server
sqlalchemy[asyncio] >=2.1.6,<2.2.0 ORM (async)
aiosqlite >=0.22.1,<0.23.0 Async SQLite driver
alembic >=1.20.1,<1.21.0 Database migrations
pydantic >=2.14.1,<2.15.0 Data validation
pydantic-settings >=2.16.1,<2.17.0 Settings management
PyJWT[crypto] >=2.15.1,<3.0.0 JWT encoding/decoding (with crypto extras for RS256)
httpx >=0.28.1,<0.29.0 Async HTTP client (OIDC, FX fetch)
odfdo >=3.27.3,<3.28.0 ODS file parsing/export
openpyxl >=3.1.5,<3.2.0 XLSX file parsing/export
pwdlib[argon2] >=0.3.1,<0.4.0 Argon2 password hashing
email-validator >=2.3.0,<3.0.0 Email address validation for Pydantic EmailStr fields
pyotp >=2.10.0,<3.0.0 TOTP generation/verification for 2FA
cryptography >=50.0.2,<51.0.0 Cryptographic primitives backing JWT/OIDC and password hashing
aiosmtplib >=5.1.3,<6.0.0 Async SMTP client for transactional email
ofxtools >=1.1.1,<2.0.0 OFX file parsing for bank statement import
regex >=2026.9.29,<2027.0.0 Time-limited matching for the regex rule operator (the standard re module cannot interrupt a search)
defusedxml >=0.7.1,<0.8.0 Hardened XML parsing for CAMT bank statement import

Optional dependencies:

Extra Package Purpose
postgres asyncpg PostgreSQL async driver
redis arq Background task queue
dev pytest, pytest-asyncio, pytest-timeout, pytest-rerunfailures, pytest-cov, pytest-xdist, ruff Testing and linting