Portfolio is a self-hosted portfolio tracker for long-term investors.
The product is built around a few explicit rules:
- transactions are the canonical source of truth
- analytical views are rebuildable read models
- SQLite is the runtime database
- JSON export, import, backup, and restore remain first-class
- the product is optimized for a single-user self-hosted setup
- accounts, instruments, transactions, targets, and reusable CSV import profiles
- holdings, allocation drift, and contribution-first rebalance guidance
- dashboard contribution planner with suggested splits and manual asset-class previews
- daily history and performance in
PLN,USD, and gold MWRR,TWR, real return, and benchmark-relative comparison- benchmark configuration with shipped defaults, target-mix, and unlimited custom references
- EDO valuation via
edo-calculator - ETF, FX, and benchmark history via
stock-analyst - fallback market-data snapshots surfaced as
STALEor degraded coverage instead of pretending freshness - canonical state export/import with preview diff, warnings, and blocking validation
- server backups, restore workflow, audit trail, and read-model cache diagnostics
- target-history visibility through audit events
- optional single-user password auth
- installable PWA shell for phone and tablet use
- active mobile views refresh on app resume after a longer pause
portfolio/
├── AGENTS.md
├── docker-compose.yml
├── docker-compose.market-data.remote.yml
├── docker-compose.market-data.self-hosted.yml
├── docker-compose.full-stack.example.yml
├── docs/
├── apps/
│ ├── api/
│ │ └── portfolio-domain/
│ └── web/
└── README.md
docker compose --profile app up -d --buildThis starts:
portfolio-apiportfolio-web- SQLite on a named Docker volume
- JSON backups on a separate named Docker volume
Endpoints:
- UI:
http://127.0.0.1:4174 - API:
http://127.0.0.1:18082
In the default local compose mode:
- live market data is off
- OpenAPI UI is off
- auth is off
- Docker sets
PORTFOLIO_AUTH_SECURE_COOKIE=true, which is the right default when the app later sits behind HTTPS
PORTFOLIO_STOCK_ANALYST_API_URL=https://your-stock-analyst.example/api
PORTFOLIO_STOCK_ANALYST_UI_URL=https://your-stock-analyst.example
PORTFOLIO_EDO_CALCULATOR_API_URL=https://your-edo-calculator.exampledocker compose \
-f docker-compose.yml \
-f docker-compose.market-data.remote.yml \
--profile app up -d --builddocker compose \
-f docker-compose.yml \
-f docker-compose.market-data.self-hosted.yml \
--profile app up -d --buildThis adds:
stock-analyststock-analyst-backend-yfinanceedo-calculator
On ARM hosts, the override currently pins upstream market-data services to linux/amd64. If those images become multi-arch later, remove or override PORTFOLIO_MARKET_DATA_PLATFORM.
cp docker-compose.full-stack.example.yml docker-compose.full-stack.yml
docker compose -f docker-compose.full-stack.yml up -dUse this path when you want a published-image deployment rather than a repo build.
Portfolio is intentionally SQLite-only.
Key invariants:
- one API process per database file
- transactions remain canonical
- history and returns stay rebuildable
- market-data snapshots are resilience data, not source of truth
- backups and exports stay portable JSON
Default application config in apps/api/src/main/resources/application.yaml is conservative:
marketData.enabled=falseopenapi.uiEnabled=falseauth.enabled=false
Compose overrides decide the real runtime mode. That keeps local raw app startup safe by default and makes market-data behavior explicit instead of accidental.
Portfolio has two import modes: MERGE and REPLACE.
- accounts, instruments, transactions, and app preferences are upserted by id or key
- if the snapshot omits
targets, the current target allocation is preserved - if the snapshot contains
targets, that section replaces the target allocation as one set - if the snapshot omits
importProfiles, current profiles are preserved - if the snapshot contains
importProfiles, they merge by id, but final profile names must stay unique
- requires explicit
REPLACEconfirmation - clears the current write model before loading the snapshot
- creates a safety backup automatically before the destructive step
Preview is not a cosmetic dry run. It uses the same validation path as the real import and returns:
- blocking issues
- warnings
- section-by-section diff counts
- skip/preserve semantics for targets and import profiles
If preview says the snapshot is valid, import should not later fail on a hidden business rule that preview skipped.
- keep secrets and upstream URLs in a local
.env, not in Git - prefer the example compose file or your own compose wrapper for real deployment
- use
PORTFOLIO_AUTH_SECURE_COOKIE=truebehind HTTPS - keep
PORTFOLIO_OPENAPI_UI_ENABLED=falseunless you actively need the docs UI - treat
REPLACEimport or restore as a maintenance action, not a casual workflow
If you want an empty reset:
docker compose down --volumes --remove-orphans
docker compose up -dPORTFOLIO_MARKET_DATA_ENABLEDPORTFOLIO_STOCK_ANALYST_API_URLPORTFOLIO_STOCK_ANALYST_UI_URLPORTFOLIO_EDO_CALCULATOR_API_URLPORTFOLIO_GOLD_API_URLPORTFOLIO_GOLD_API_KEYPORTFOLIO_MARKET_DATA_STALE_AFTER_DAYS
PORTFOLIO_STOCK_ANALYST_UI_URL is optional. Set it only when you want the UI to open an external stock-analyst page for instruments backed by that upstream.
PORTFOLIO_BACKUPS_ENABLEDPORTFOLIO_BACKUPS_DIRECTORYPORTFOLIO_BACKUPS_INTERVAL_MINUTESPORTFOLIO_BACKUPS_RETENTION_COUNT
PORTFOLIO_READ_MODEL_REFRESH_ENABLEDPORTFOLIO_READ_MODEL_REFRESH_INTERVAL_MINUTESPORTFOLIO_READ_MODEL_REFRESH_RUN_ON_START
PORTFOLIO_OPENAPI_UI_ENABLED
PORTFOLIO_AUTH_ENABLEDPORTFOLIO_AUTH_PASSWORDPORTFOLIO_AUTH_SESSION_SECRETPORTFOLIO_AUTH_SESSION_COOKIE_NAMEPORTFOLIO_AUTH_SECURE_COOKIEPORTFOLIO_AUTH_SESSION_MAX_AGE_DAYS
API:
cd apps/api
./gradlew test detektWeb:
cd apps/web
npm run lint
npm test
npm run buildThe README screenshots are generated with Playwright against a seeded instance:
cd apps/web
PORTFOLIO_E2E_BASE_URL=http://127.0.0.1:4174 npx playwright test e2e/screenshots.spec.tsOutput lands in docs/screenshots/. The script seeds a demo portfolio, captures screens in en-GB locale, then restores the original data.
- docs/architecture.md: system shape, runtime boundaries, verification model
- docs/domain-model.md: canonical entities, derived models, and invariants
- docs/runbook.md: deployment, health checks, backup/restore, auth guardrails
- docs/roadmap.md: short active priorities only




