concise

concise

apps.detail.types.app from FostersDigital's Repository

apps.detail.sections.overview

Concise is a minimalist, self-hosted personal finance / net-worth tracker. One small Node.js process serves BOTH the web UI and the REST API from a single internal port — 3000 inside the container (there is no separate API port; the UI calls /api on the same port). Map a host port to container 3000 with "WebUI Port" below. All state is a single SQLite file; validated backups are written to the mapped backup directory. Set PUID/PGID to the user that owns your appdata share (Unraid: 99 / 100). Access over plain HTTP on your LAN requires COOKIE_SECURE=false (the template default); set it to true only when serving Concise behind an HTTPS reverse proxy.

Concise

A minimalist personal finance tracker. Black theme, gold accents, mobile-first.

Track assets and liabilities, watch your net worth over time, automate recurring financial movements, and keep market-valued holdings up to date — all in a fast, private, self-hostable app.

Features

  • Dashboard — total assets, liabilities, and net worth with an interactive history graph (1M / 3M / 6M / YTD / 1Y / 5Y / 10Y / 20Y / All, full-screen mode), a trend line with an adjustable rolling-average window (slider next to the graph; stable across range changes), graph smoothing that ramps sparse manual revaluations over the gap instead of one-day cliffs, and age markers on 5-year-plus ranges (set your birth year in Settings → Calculation). A Nominal / Real toggle re-expresses the graph and the percent-change badges in today's money (rough inflation adjustment), so long-range growth reflects real progress rather than nominal drift.
  • Historical view mode — drag the red circle under the graph to pin the whole app to a past date: every page shows the portfolio exactly as it stood then (later entries vanish, values are as-of), survives navigation, and a floating reset button returns to today.
  • Assets & liabilities — multiple entries per class, each with a unique emoji (💵 cash, 📈 stock investments, 🏠 property, 🚗 vehicles, 🪙 crypto, 🥇 precious metals with gold/silver/platinum/palladium sub-selection, 📦 other / 🏦 mortgage, 💸 loans, 💳 credit cards, 🎓 student loans, ⚖️ other), full value history per entry, optional backdating on creation (with an optional present-day value), a running total of each side beside the page heading, a current per-unit price on each market holding, and a badge marking any entry that an active recurring increase/decrease applies to.
  • Valuation methods per category — cash is always a manual figure; anything tradable can be market-priced (symbol × quantity with a verification step) across many instruments on several exchanges (London, US, Europe, crypto, spot metals), with prices converted from the instrument's currency into yours; property can auto-apply a country's yearly average price change; vehicles can auto-depreciate by age from their manufacture date (anchored on a present-day value when you give one). Backdated auto-valued entries are backfilled with one historically accurate value per day — entries whose history the provider cannot price are flagged on the page.
  • Multi-currency — pick your display currency; switching re-denominates your whole portfolio and history at rough exchange rates, and any value pulled in a foreign currency is converted before it is used.
  • History editing — Settings → History lists every historic entry across all holdings for editing or deletion, and accepts legacy wealth points ("on X date my net worth was Y") that appear on the graph.
  • Recurring movements — scheduled fixed or percentage changes (salary into savings, mortgage payments, compounding interest, …) on daily, weekly, monthly, quarterly or yearly cadences, applied automatically.
  • Goals & prediction — set a target net worth or pick a liability to pay off; each goal tracks progress, a projected ETA, and a suggested monthly contribution (one click turns a payoff goal's suggestion into a recurring payment). Prediction mode projects the whole portfolio forward to a chosen horizon — the cards, breakdowns and graph all reflect the future — and draws a gold marker line at each enabled goal's projected date, labelled with the goal name and a progress hover (toggle a goal's line on or off in Settings → Goals).
  • Multi-user — self-service account creation, session auth, rate limiting, and audit logging. A "not financial advice" disclaimer appears on sign-up and in Settings → Legal.
  • Database backups — validated point-in-time copies of the SQLite file, taken automatically on a configurable interval (on by default), on startup when the last one is stale, or on demand from Settings → Backup. A bounded number are kept (manual and automatic pruned together). See BACKUP.md.

Tech stack

Node.js 24 + TypeScript + Express 5 + SQLite (node:sqlite) on the backend; React 19 + Vite + Tailwind CSS v4 + Recharts on the frontend. Tests with Vitest, supertest, Testing Library, and Playwright. See ARCHITECTURE.md.

Run locally

Requires Node.js ≥ 24.

npm install
npm run db:migrate    # create data/concise.db
npm run db:seed       # demo user + sample portfolio
npm run dev           # API on :3000, web on :5173 (proxied)

Open http://localhost:5173 and log in with the demo account:

username: demo
password: demo

Scripts

Command What it does
npm run dev Backend (tsx watch) + frontend (Vite) together
npm run db:migrate Apply SQL migrations
npm run db:seed Seed demo user and sample portfolio
npm test All unit + integration tests
npm run build Production build (server bundle + static web)
npm start Run the production build (serves API + web)

Running in production

The whole app is one Node process. Build, then start:

npm run build     # server bundle (esbuild) + static web (Vite)
npm start         # serves the API and the built SPA on $PORT (default 3000)

Set NODE_ENV=production (enables Secure cookies — serve over HTTPS) and a persistent DB_PATH. Behind a reverse proxy, set TRUST_PROXY=1 so client IPs are correct, and TRUSTED_ORIGINS if the frontend is hosted on a different origin. Concise takes its own validated backups (automatic + manual; see BACKUP.md) into BACKUP_DIR — keep that on persistent storage and copy it off-host for disaster recovery. See ARCHITECTURE.md.

Env var Default Purpose
PORT 3000 HTTP port
DB_PATH ../data/concise.db SQLite file location
BACKUP_DIR <dirname(DB_PATH)>/backups Where database backups are written
NODE_ENV development production enables Secure cookies
SESSION_TTL_HOURS 336 (14 days) Session lifetime
COOKIE_SECURE (prod: true) Send Secure cookies (requires HTTPS)
TRUST_PROXY 0 Hops to trust for client IP
TRUSTED_ORIGINS (none) Extra CSRF-trusted origins, comma-sep
API_RATE_LIMIT 300 API requests per IP per minute
LOGIN_RATE_LIMIT 10 Login attempts per IP per 15 minutes
SEED_ON_START 0 1 (re)seeds the demo account at startup
PRICE_PROVIDER real Market price source: real (live Yahoo Finance quotes, no API key) or simulated (deterministic offline)
LOG_LEVEL info Log verbosity: fatal/error/warn/info/debug/trace/silent

Run with Docker

The repo ships a multi-stage Dockerfile (it builds the server bundle and static SPA, then assembles a slim runtime image) and a docker-compose.yml template. The image runs as a non-root user, applies migrations on startup, and stores the SQLite database on a volume mounted at /data.

cp .env.docker.example .env     # adjust for your deployment
docker compose up -d --build

Open http://localhost:3000. Set SEED_ON_START=1 in .env to create the demo account (demo / demo) on first run. Testing over plain HTTP on localhost? Uncomment COOKIE_SECURE=false in .env first, or logins silently fail (see the HTTPS note below).

Without Compose:

docker build -t concise .
docker run -d --init --name concise -p 3000:3000 -v concise-data:/data concise
  • Serve over HTTPS. COOKIE_SECURE defaults to true, so session cookies require HTTPS — put Concise behind a TLS-terminating reverse proxy (Caddy, nginx, Traefik). For local plain-HTTP testing on localhost only, add -e COOKIE_SECURE=false (otherwise logins silently fail). Behind a proxy also set TRUST_PROXY=1 (correct client IPs) and TRUSTED_ORIGINS if the SPA is served from another origin.
  • The database is the /data volume; Concise writes its own validated backups to /data/backups (automatic + manual — see BACKUP.md). Copy the volume off-host as well for true disaster recovery.
  • The container runs as a non-root user and exposes GET /api/health for the built-in healthcheck and external probes. A richer GET /api/health/detailed reports the UI, server and database status (never any financial data) for dashboards and monitors — see HEALTHCHECK.md.

See .env.docker.example for all tunables.

Health checks

Concise exposes a simple liveness probe (GET /api/health{ ok: true }) and a detailed readiness probe (GET /api/health/detailed, checking UI, server and database) for unraid / Docker / Uptime Kuma. Neither ever reports financial data. See HEALTHCHECK.md.

Logs

Concise writes structured JSON logs to stdout (via pino), so docker logs, journald or a log shipper just work. Every /api/* request carries a correlation id — sent back as the x-request-id header and included in a request completed log line (method, path, status, duration, client IP, user id) — so an issue can be traced end to end. Logs report only operational facts, never financial data, passwords or session tokens. Per-request lines are emitted at info; set LOG_LEVEL to debug to additionally log liveness health-check polls, or silent to disable.

Security

Concise hashes passwords with scrypt, uses revocable opaque sessions, scopes every query by user, validates all input with zod over parameterised SQL, and applies CSRF checks, rate limiting and a strict CSP. The full security posture, how to report a vulnerability, and a hardening checklist for self-hosters are in SECURITY.md. Concise is a tracking tool, not financial advice — see the in-app disclaimer on sign-up and in Settings → Legal.

apps.marketingCta.appInstallTitle

apps.marketingCta.appInstallDescription

apps.installHelp.stepOpen apps.installHelp.stepSearchApp apps.installHelp.stepReview apps.installHelp.stepInstall

apps.detail.sections.categories

apps.downloadStats.title

1,185
apps.downloadStats.metrics.totalDownloads

apps.detail.sections.related

apps.detail.related.exploreCategories

apps.detail.related.exploreAll

apps.detail.sections.details

apps.detail.details.repository
observe1234/concise-wealth-tracker:latest
apps.detail.details.lastUpdated2026-08-09
apps.detail.details.firstSeen2026-08-10

apps.detail.sections.runtime

apps.detail.details.webui
http://[IP]:[PORT:3000]/
apps.detail.details.network
bridge
apps.detail.details.shell
sh
apps.detail.details.privileged
false
apps.detail.details.extraParams
--init

apps.detail.sections.configuration

WebUI PortPorttcp

Host port published for Concise. The container always listens on 3000 internally, serving BOTH the web UI and the /api REST API from that one port (there is no separate API port). Change the host (left) side only — leave the container side as 3000.

apps.detail.config.target
3000
apps.detail.config.default
3000
apps.detail.config.value
3000
AppdataPathrw

Persistent storage for the SQLite database (concise.db). Must be writable by PUID:PGID. Backups go to the separate 'Backup Directory' mapping below.

apps.detail.config.target
/data
apps.detail.config.default
/mnt/user/appdata/concise-wealth-tracker
apps.detail.config.value
/mnt/user/appdata/concise-wealth-tracker
Backup DirectoryPathrw

Where validated database backups are written (automatic + manual). Point the host (left) side at any share you like — e.g. a dedicated backups share or a second disk — to keep copies off the database's own folder. The container side is fixed at /backups (the BACKUP_DIR variable points here). Must be writable by PUID:PGID.

apps.detail.config.target
/backups
apps.detail.config.default
/mnt/user/appdata/concise-wealth-tracker/backups
apps.detail.config.value
/mnt/user/appdata/concise-wealth-tracker/backups
PUIDVariable

User ID that owns the appdata share. Unraid default is 99 (nobody). Files in /data are created as this user.

apps.detail.config.default
99
apps.detail.config.value
99
PGIDVariable

Group ID that owns the appdata share. Unraid default is 100 (users).

apps.detail.config.default
100
apps.detail.config.value
100
COOKIE_SECUREVariable

Send session cookies only over HTTPS. Leave 'false' for plain-HTTP LAN access (e.g. http://tower:3000) or login will silently fail. Set 'true' ONLY when Concise is behind an HTTPS reverse proxy.

apps.detail.config.default
false
apps.detail.config.value
false
BACKUP_DIRVariable

Container path Concise writes backups to. Leave this as /backups — it must match the container side of the 'Backup Directory' volume above; change the HOST folder there to relocate backups, not this.

apps.detail.config.default
/backups
apps.detail.config.value
/backups
TZVariable

Timezone for log timestamps. Concise's own date calculations are always UTC, so this is cosmetic.

apps.detail.config.default
Etc/UTC
apps.detail.config.value
Etc/UTC
UMASKVariable

File-creation mask for the database and backups.

apps.detail.config.default
022
apps.detail.config.value
022
TRUST_PROXYVariable

Number of reverse-proxy hops to trust for client IPs (rate limiting, audit log). Set to 1 behind a single proxy; 0 when exposed directly.

apps.detail.config.default
0
apps.detail.config.value
0
TRUSTED_ORIGINSVariable

Comma-separated extra origins the CSRF check should trust (e.g. https://money.example.com). Only needed if the UI is served from a different origin than the API.

SESSION_TTL_HOURSVariable

Session lifetime in hours (default 336 = 14 days).

apps.detail.config.default
336
apps.detail.config.value
336
API_RATE_LIMITVariable

Max API requests per client IP per minute.

apps.detail.config.default
300
apps.detail.config.value
300
LOGIN_RATE_LIMITVariable

Max login attempts per client IP per 15 minutes.

apps.detail.config.default
10
apps.detail.config.value
10
PRICE_PROVIDERVariable

'real' fetches live quotes from Yahoo Finance (no API key). 'simulated' uses a deterministic offline simulation (for air-gapped use; prices will not match the market).

apps.detail.config.default
real
apps.detail.config.value
real
LOG_LEVELVariable

Log verbosity (structured JSON to the container log): fatal, error, warn, info, debug, trace, silent.

apps.detail.config.default
info
apps.detail.config.value
info
SEED_ON_STARTVariable

Set to 1 to (re)create the demo account (demo / demo) on every start. Leave 0 for real use.

apps.detail.config.default
0
apps.detail.config.value
0