Architecture Overview
This page is a deliberately small architecture overview. It follows a subset of the arc42 template, keeping only the sections that earn their place for a small self-hosted monolith. Sections that arc42 defines but Portfolixir does not need yet are listed under Omitted sections with the reason.
To avoid duplication, this page links to existing documents instead of restating them:
- product behaviour: Product Documentation
- agent and contributor rules:
AGENTS.md - API and MCP contract: API and MCP
- local deployment: Home Deployment
- recorded decisions: Architecture Decisions
1. Introduction and Goals
Portfolixir is a self-hosted Phoenix application for local portfolio tracking. It keeps securities, portfolios, depots, cash accounts, manual transactions, derived holdings, and quote history in one auditable local app.
Top quality goals, in priority order:
- Auditability — financial state is reproducible from immutable inputs.
- Correctness of money — exact decimal arithmetic, never floats.
- Smallness — the scope stays narrow and the codebase stays readable.
- Local-first operation — runs fully on the operator’s own machine.
Explicit non-goals: it is not a broker, bank, trading, payment, order, or rebalance platform. See “Non-goals today” in the Product Documentation.
2. Constraints
- Elixir/Phoenix monolith plus a thin TypeScript MCP companion.
- PostgreSQL as the only data store.
Decimalfor all persisted money, quantities, prices, fees, taxes, and FX rates. Floats are not allowed for persisted financial values.- No external LLM calls from the app.
- No market-data network calls in tests; synthetic fixtures only.
- No stored API keys in source; local bearer tokens come from the environment.
- Architecture decisions must not change silently — they are recorded as ADRs.
The binding rule set lives in
AGENTS.md
(“Hard Rules”, “Security Boundaries”); this page only summarises it.
3. Context and Scope
Portfolixir runs locally and talks to a small set of external systems. All network egress is for read-only market data and logos.
+------------------------+
Web browser ---> | |
| Portfolixir | ---> PostgreSQL (local)
MCP client --> MCP | (Phoenix monolith) |
companion --> JSON | | ---> Yahoo Finance (quote history)
API ----------------> | /api/v1 + Web UI | ---> CoinGecko (crypto search)
| | ---> Portfolio Perf.(security search)
+------------------------+ ---> Wikipedia (logo lookup)
- Inbound: a human via the web UI, and integrations via
/api/v1. The MCP companion is itself a client of/api/v1; it never reaches the database or Elixir contexts directly. - Outbound: read-only quote and search providers plus logo lookup. There are no write integrations to banks, brokers, wallets, or payment systems.
4. Solution Strategy
| Goal | Strategy |
|---|---|
| Auditability | Holdings and trades are derived from an immutable transaction history, never stored as mutable state. |
| Money correctness | Decimal everywhere; decimals serialise as strings across the API and MCP boundary. |
| Smallness | A modular monolith with a few bounded contexts instead of services. |
| Local-first | Single Docker Compose stack; the MCP companion is optional and separable. |
| Safe integrations | The MCP companion is a thin wrapper over the public JSON API only. |
These choices are recorded individually in the Architecture Decisions log.
5. Building Block View
Level 1 — system
The OTP application supervises (lib/portfolixir/application.ex):
Portfolixir.Repo— Ecto/PostgreSQL access.Phoenix.PubSub— in-process messaging.Task.Supervisor(LogoSupervisor) — background logo fetches.Portfolixir.Catalog.LogoDiscovery— background queue that discovers missing security logos.Portfolixir.Catalog.QuoteSync— scheduler that pulls daily closes on a configurable interval.Portfolixir.Fx.RateSync— opt-in scheduler that refreshes exchange rates (ECB) on a configurable interval.PortfolixirWeb.Endpoint— HTTP, LiveView, and JSON API.
Level 2 — domain contexts
| Context | Responsibility | Key modules |
|---|---|---|
Portfolixir.Catalog |
Securities, quote history, quote sync, online search, logos | security, quotes, quote_sync (Yahoo), security_search (CoinGecko, Portfolio Performance) |
Portfolixir.Portfolios |
Portfolios, cash accounts, depots, valuation, target weights and allocation | portfolio, cash_account, securities_account, valuation, target, targets, allocation |
Portfolixir.Ledger |
Manual transactions, derived holdings (with cost basis and P&L), FIFO trades | transaction, positions, trade_matcher |
Portfolixir.Classifications |
Custom and built-in (asset-class, currency) classification trees and assignments | classification, category, assignment |
Portfolixir.Fx |
Exchange rates and multi-currency conversion (EUR hub) | exchange_rate, fx, rate_sync (ECB) |
Portfolixir.Imports |
Portfolio Performance CSV/JSON v1 import (parse, preview, apply) | portfolio_performance (csv/json parsers), preview, applier, mapping |
PortfolixirWeb |
LiveViews, JSON API controllers, auth plug, components | live/*, controllers/api/v1/*, plugs/api_auth_plug |
mcp-server/ |
TypeScript MCP companion wrapping /api/v1 |
server.ts, tools.ts, api-client.ts, http.ts |
Domain contexts stay separate from LiveViews, controllers, and MCP wrapper code. The dependency direction is one-way: web and MCP depend on contexts; contexts do not depend on the web layer.
8. Crosscutting Concepts
- Decimal money — all financial fields use
Decimal; API/MCP responses serialise them as strings. See ADR-0003. - Derived holdings — current positions and FIFO trades are computed from transactions on read. See ADR-0004.
- Idempotent imports — Portfolio Performance imports preview before applying and use content hashes to skip duplicates on re-run; applying is atomic.
- Quote provider split — search uses Portfolio Performance (stocks/ETFs) and CoinGecko (crypto); quote history uses Yahoo Finance for both. See ADR-0005.
- Local bearer auth —
/api/v1requiresPORTFOLIXIR_API_TOKEN; the MCP HTTP transport requiresPORTFOLIXIR_MCP_TOKEN. No keys live in source. - Internationalisation and theming — System/Light/Dark themes, accent choice, and EN/DE language are runtime preferences and never affect persisted financial values.
- Test isolation — tests use synthetic fixtures and fake providers; no real network calls.
11. Risks and Technical Debt
- External provider drift — Yahoo/CoinGecko/Portfolio Performance are unofficial endpoints and can change shape or rate-limit without notice.
- Single-portfolio assumption — several flows assume one working portfolio; broadening this would touch multiple contexts.
- No runtime quality gates documented — performance and availability targets are intentionally unspecified for a local single-user app.
9. Architecture Decisions
Decisions are recorded as short ADRs in the
Architecture Decisions log. Per
AGENTS.md,
architecture decisions must not change silently; add or supersede an ADR when a
decision changes.
Omitted arc42 sections
Kept out on purpose to stay lightweight:
- 6. Runtime View — the supervision tree in section 5 plus the request flow in API and MCP cover the few notable runtime paths.
- 7. Deployment View — covered by Home Deployment
and the root
docker-compose.yml. - 10. Quality Requirements — no formal quality scenarios for a local single-user app beyond the goals in section 1.
- 12. Glossary — domain terms are defined inline in the Product Documentation.