Cacheboard #
Cacheboard is a finance dashboard for one household that you run on your own machine. It brings accounts, monthly budgets, cash flow, investments, and retirement planning into one browser app.
Features #
- Accounts and net worth: combined balances, assets, debts, balance history, and account dates. Disabled accounts are excluded from totals.
- Monthly budget and cash flow: a main tab with a month picker, income, spending, net cash flow, category targets, and 12 months of history. Month links support reload and browser Back. The current month is marked as month to date.
- Budget editing: add or remove categories, enter dollar targets, use recent spending as a starting point, save, or discard. Targets repeat each month; edits stay in place across tab changes and background refreshes.
- Transactions: search, filter by account, category, date, or money in/out; sort, change categories, and export CSV. Transfers are marked and excluded from spending and income totals.
- Bank data: SimpleFIN imports balances and transactions, avoids duplicates, matches pending transactions, and applies category rules. Deployment mode checks for stale data when the app opens. Connection setup uses terminal commands.
- CSV imports: preview and import transactions or investment holdings. The browser expects named CSV columns; custom column mappings use the API.
- Investments: holding values, portfolio shares, and gains or losses against purchase cost, with dates. Holdings and prices come from imported data.
- Retirement planning: multiple income streams, employer matches, spending phases, and tax, inflation, and return assumptions. The Agent can run repeated market simulations and compare contribution changes. There is no separate retirement-planning tab.
- Home estimate: a manual value, date, source, and link included in net worth.
- Agent workspace: saved conversations with charts, tables, spending comparisons, and retirement results. Finance tools calculate the numbers; the model explains them. A model server is required for chat.
- Local operation: Docker or a source checkout, a local SQLite database, and a browser UI with small-screen layouts and dark mode.
Rough comparison: Mint and Personal Capital #
Mint is the reference here for everyday spending and budgets, using Intuit's published Mint feature list. Personal Capital is the reference for wealth tracking and planning; its tools are now part of Empower Personal Dashboard. This is a rough comparison of the tools available in each app.
- Daily spending (Mint) — built: transaction review, category budgets, monthly spending, income, and net worth. Cacheboard's budget tab puts these monthly checks together. Subscription tracking and automatic spending alerts are TODO gaps.
- Wealth tracking (Personal Capital) — partial: account totals, net worth, holdings, allocation, and retirement calculations are built. Dedicated planning screens and deeper portfolio analysis are TODO gaps compared with Empower's tools.
- Local ownership — on purpose: you run the app and hold its database. There is no hosted Cacheboard account, product-offer feed, or adviser sales flow. Bank sync still uses SimpleFIN; chat uses the model endpoint you choose.
- Read-only financial assistant — on purpose: the Agent can inspect and explain data and run scenarios. It cannot change your records, move money, trade, or choose for the household. The app's own forms allow data edits.
- Manual home value — on purpose: the owner supplies the estimate and its source. Cacheboard does not scrape property sites.
- Budget depth — TODO gap: targets repeat across all months. There are no month-specific target versions, rollovers, savings goals, or upcoming-bill forecasts. These limits are not a finished budgeting philosophy.
- Editing tools — TODO gap: individual category edits are in the UI; rule management and bulk transaction edits are API-only. Custom category management and transaction splitting have no UI.
- Access and setup — TODO gap: one household dataset, no sign-in, no separate member permissions, and no native mobile app. Bank linking and account setup are less complete than a hosted consumer app. USD is the only supported currency for bank sync.
Missing features and unresolved scope are listed in the feature TODO list. A TODO is not a delivery promise.
Code layout #
Cacheboard is built with Gleam. The API runs on Erlang/OTP with Mist, Wisp, and SQLite. The browser app uses Lustre and compiles to JavaScript. Money is stored and sent as integer cents; the server listens locally by default.
The repository contains three Gleam projects:
shared/holds wire records, JSON encoders, finance semantics, and retirement projection math. It compiles to Erlang and JavaScript.server/owns SQLite through one serializing actor, runs migrations, exposes the HTTP API, serves the production browser bundle, and provides CLI commands.web/is the Lustre application. Chart.js is pinned and bundled locally.
Product direction and the evidence behind it are documented in
docs/product-research.md. Architecture and local
model research are documented in
docs/research-context.md.
Run with Docker #
Docker is the portable deployment path. It builds a fixed browser bundle and Erlang shipment, runs as a non-root user, keeps all mutable data in one volume, and exposes a container health check.
Docker is an alternative installation, not this Mac's production service.
Its commands are not part of the native Mac verification checks. On this Mac,
set CACHEBOARD_PORT=4302 in .env before starting Docker so it cannot compete
with production on port 3000. Use port 4302 in the browser for that setup.
cp .env.example .env
docker compose build
# For an existing configured volume:
docker compose up -d
Open http://127.0.0.1:3000. Deployment mode requires an existing database at
/data/cacheboard.sqlite with saved household settings. A missing file or an
empty household stops startup; it cannot open onboarding or create a replacement.
For a new, empty installation only, stop the service and run setup on loopback in development mode, finish the household form, then stop it and start deployment mode:
docker compose stop cacheboard
docker compose run --rm --service-ports -e CACHEBOARD_ENV=development cacheboard
# After setup, stop the foreground process with Ctrl-C.
docker compose up -d
Do not use this setup command to repair an existing production installation.
A missing household needs a local check of the configured volume and database.
Never run docker compose down -v against household data; -v deletes the volume.
Settings also accepts an optional home estimate with its as-of date, source, and link. The value is stored locally and included in net worth; Cacheboard does not scrape Zillow.
The Compose port binds to loopback by default. On a trusted local network, set
CACHEBOARD_BIND=0.0.0.0 in .env. The named cacheboard-data volume survives
container replacement. SQLite requires one running Cacheboard instance and a
local persistent volume; do not place its WAL-mode database on NFS or another
network filesystem.
The container reaches a model server on the host through
host.docker.internal. The model server must listen on an interface reachable
from Docker, or LM_BASE_URL must name another reachable machine.
Source requirements #
The verified toolchain on this Mac is managed by mise:
- Gleam 1.18.1
- Erlang/OTP 28.4
- Bun 1.4.0, used by Lustre's production bundler
Run all commands below from the repository root unless a block says otherwise.
Install mise, Python 3.11 or newer, Git, and Apple's command-line developer tools
(xcode-select --install on a new Mac). Playwright also needs Node.js on PATH.
The app tool versions are pinned in .mise.toml; Python and Node are host tools.
Install and check the selected tools:
mise trust .mise.toml
mise install
mise current
mise exec -- gleam --version
mise tasks ls
Seed and run #
Start the live-reloading browser app and a separate sample API:
mise run dev
Open http://127.0.0.1:5173. Its API proxy targets 127.0.0.1:4300.
Both listeners bind only to loopback. The command builds current code, creates
a temporary sample database, and starts both processes. Ctrl-C stops them and
removes that sample database. Restart this command after server-code changes;
browser-code changes reload through Lustre.
The sample contains six accounts, 468 transactions, 216 monthly balance
snapshots, budgets, holdings, rules, and settings, dated 2026-07-30.
The command clears inherited app, bank, and model environment variables before
setting its own sample paths. It does not copy bank credentials. Chat needs a
model server at the default local endpoint.
To check startup and the development proxy, then exit automatically:
mise run dev --check
Development refuses occupied ports. Build commands also refuse to overwrite
web/dist if the installed production service still points there.
Production on this Mac #
Production is the macOS LaunchAgent com.cacheboard, listening on
127.0.0.1:3000. Its two addresses are:
- Local: http://localhost:3000/
- Tailscale: https://averys-mac-studio.tailcb772b.ts.net/
ops/macos/production.json is the source of truth
for the service file, URLs, port, database, data directory, release directory,
logs, mise, and Tailscale CLI. It contains paths only, with no finance data or
credentials. Do not search for other databases or pick one by size or date.
This Mac: deploy and verify #
Show the saved production identity and the installed release without reading household records:
mise run prod:show
The installed service uses the checkout's server/build/erlang-shipment and
web/dist when show reports checkout build (no release ID). Builds in those
locations can affect the live app. Use the isolated release build below. A
release installed through this command has a fixed commit ID and its own server
and browser files under the release directory; editing the checkout cannot
change those files.
Build a reviewed, committed checkout into a new absolute directory:
mise trust .mise.toml
mise run prod:build /tmp/cacheboard-candidate
python3 ops/macos/production.py render /tmp/cacheboard-candidate > /tmp/cacheboard-candidate.plist
plutil -lint /tmp/cacheboard-candidate.plist
mise run prod:build (also ./deploy.sh) builds committed application source in a temporary directory using
mise, then writes a release with file checksums and its Git commit ID. It never
starts a service, creates a household database, or writes the checkout's live
build folders. It refuses uncommitted changes or an existing destination.
The release has server code, migrations, public assets, and tool versions only.
Python 3 is required for the release tools.
Local operator command: after checking the household in your own browser, install the reviewed release from a local terminal:
mise run prod:install /tmp/cacheboard-candidate
Installation checks that the installed service still names the recorded data
paths, checks for saved household settings, and makes a private SQLite backup
in the data directory's backups/ folder. SQLite's backup API includes committed
writes from its WAL file (the file holding recent writes). The installer copies
the release into its fixed directory, preserves extra service environment
settings, and restarts the existing LaunchAgent through mise. It does not seed,
move, replace, or restore the household database. Server startup runs schema
migrations only after the existing-household check passes.
A service reload retries launchd error 5 for up to 15 seconds because unloading
can return before the old process exits. The same retry applies to recovery.
A local startup or asset-check failure restores the prior service file and
checks its health after restarting it. If recovery fails, the command reports
both failures; it does not claim success. The exact prior file is saved as .plist.previous beside the
service file. Database rollback is never automatic: schema changes can require
a local operator to review the backup before restoring it. A Tailscale-only
failure is reported while the healthy local release stays running.
Check both URLs after installation:
mise run prod:verify /tmp/cacheboard-candidate
This checks /healthz for the exact commit ID and checks every public asset
against the release checksums. It never calls finance APIs. A health check alone
does not establish that the expected household is present; that check stays in
the local operator's browser.
Onboarding and data boundaries #
- Setup opens only for the explicit
SETTINGS_NOT_CONFIGUREDAPI response in a new development or test household. Generic 404s, server errors, and network failures show a settings error and cannot load setup defaults. - A household that has loaded or saved settings stays out of setup for that page session. Old setup links return to Overview. Reload reads the saved settings.
- Deployment mode requires an explicit absolute
CACHEBOARD_DBpath. SQLite opens it withmode=rw, which cannot create a file. Missing files, invalid databases, and missing household settings stop startup before migrations. Missing settings during a running deployment return HTTP 503, not onboarding. - Coding agents use source, service identity, release metadata, public assets,
and synthetic fixtures. They must not open real databases, backups, bank
credentials, finance APIs, or the production app in a browser. They may build,
render, and verify releases; the local operator runs
install. - These agent rules are workflow limits. The app has no sign-in; filesystem and network access must be restricted outside the app to enforce data isolation.
Service and Tailscale recovery #
Use python3 ops/macos/production.py show for the exact saved paths. The service
runs in gui/$(id -u), starts at login, and is kept alive by macOS. Service
commands use /opt/homebrew/bin/mise exec -- because launchd does not load an
interactive shell or add Erlang to PATH.
Use the main Tailscale app's CLI:
/Applications/Tailscale.app/Contents/MacOS/Tailscale serve status
Its saved HTTPS route forwards to http://127.0.0.1:3000. If that route is absent,
the local operator can restore it with:
/Applications/Tailscale.app/Contents/MacOS/Tailscale serve --bg http://127.0.0.1:3000
Preserve unrelated routes. The Homebrew Tailscale daemon and
~/.config/tailscale-kimi/tailscaled.sock belong to Kimi at
kimi.tailcb772b.ts.net. The bare tailscale command selects that CLI, so use
the full app path above. Cacheboard uses tailnet-only Serve. Do not enable Funnel.
Separate development and browser tests #
Use mise run dev for current code and temporary sample data. Its frontend
proxy targets sample port 4300. Production owns port 3000.
The browser fixtures create sample databases and choose free ports. To test a release without rebuilding the live checkout's assets:
mise run test:browser --assets /tmp/cacheboard-candidate/public
Each fixture explicitly sets its temporary database path, overriding inherited configuration. Production-mode browser tests use synthetic households only.
Production-like sample app #
Run the packaged server and browser files in deployment mode with a temporary sample household:
mise run prod:like /tmp/cacheboard-candidate
Open http://127.0.0.1:4301. Ctrl-C stops the server and removes its sample data.
There is no live reload or Tailscale route. The command validates the release
checksums and serves the packaged files. Deployment mode requires the seeded
household and refuses a missing database, just as production does.
mise run prod:like /tmp/cacheboard-candidate --check
mise run test:release /tmp/cacheboard-candidate
The first command checks startup and assets, then exits. The second checks
restart preservation and missing-data errors. Browser tests with --assets
use the current checkout's server and the supplied browser bundle; the release
test and production-like app use the packaged server.
Frontend structure #
The browser app keeps its root model-view-update wiring in web/src/cacheboard/app/.
Each route has a module under web/src/cacheboard/pages/, with larger views
split into feature-local submodules. Shared visual elements live in
web/src/cacheboard/components/ and web/src/cacheboard/ui/; API decoders and
encoders live in web/src/cacheboard/api/.
The existing design system is in web/assets/styles.css. Add small, composable
layout or spacing adjustments with Tailwind utilities in Gleam class lists.
web/src/cacheboard_web.css is the Tailwind entry point, and Lustre builds it
alongside the JavaScript bundle.
Build and test #
These root commands are defined in .mise.toml and ops/workflow.py:
mise run build
mise run test
(cd web && mise exec -- bunx playwright install chromium)
mise run test:browser
The focused tests cover the exact mulberry32 sequence on Erlang and JavaScript, frozen retirement projection output, the bounded agent loop, the frozen tool catalog and model-facing tool-result format, transaction filters and mutations, CSV deduplication and atomic imports, recorded SimpleFIN payloads, GFM configuration, and markdown sanitization.
The browser tests start separate local servers with temporary empty or seeded sample databases. They cover setup, saved-household route guards, server errors, and production mode. They find controls by visible roles, names, and labels.
Deployment tests cover release integrity, fixed service/data paths, private backups, and recovery after a simulated service failure:
python3 -m unittest discover -s ops -p 'test_*.py'
mise run test:release /tmp/cacheboard-candidate
mise run test:launchd
The release smoke test starts the packaged server twice on a free loopback port, checks its commit and public assets, compares sample settings and transactions across the restart, checks HTTP 503 when sample settings disappear, and confirms that a missing database stops startup. It uses a temporary synthetic household and removes it when done.
test:launchd requires macOS and a logged-in GUI session. It creates a disposable
service that takes three seconds to stop and checks that reload succeeds.
It never unloads production. test:browser builds first unless --assets is
supplied. The lower-level web script bun run test:e2e only runs Playwright;
it does not build assets.
Keeping these instructions accurate #
Change the command implementation, its tests, and this README in the same
commit. Run the affected command with sample data before calling it verified.
Keep production paths in ops/macos/production.json and tool versions in
.mise.toml. Use mise tasks ls to list commands; use prod:show to identify
the installed release. Documentation cannot guarantee future tool behavior;
the checked-in commands and repeatable checks are the maintenance contract.
SimpleFIN #
These are local-operator commands; they access bank data. Use the exact data
directory and database listed by mise run prod:show. Do not run them in a
sample environment or leave their paths implicit. For this Mac:
cd server
export CACHEBOARD_DATA_DIR='/Users/avery/Library/Application Support/Cacheboard'
export CACHEBOARD_DB='/Users/avery/Library/Application Support/Cacheboard/cacheboard.sqlite'
SIMPLEFIN_SETUP_TOKEN='…' mise exec -- gleam run -m cacheboard/cli/simplefin_claim
mise exec -- gleam run -m cacheboard/cli/simplefin_verify
The access URL is stored as simplefin-access.json in the data directory with
mode 0600. In deployment mode, each page load checks the last successful
SimpleFIN update. Saved dashboard data appears at once, and a new update runs in
the background when the last one is at least 12 hours old. A completed update
reloads the dashboard data.
Synchronization is deployment-only. It can also be run by hand:
CACHEBOARD_ENV=deployment mise exec -- gleam run -m cacheboard/cli/simplefin_sync
Run that command in the same local terminal and server/ directory as the
explicit exports above. Close that terminal afterward so other commands do
not inherit production paths. Manual sync uses its own database connection;
do not run it alongside an import or another sync.
The manual command asks how to classify new remote accounts. Automatic updates classify clear account names, including spending, checking, savings, credit, investment, and loan accounts. An unclear new account stops the automatic update with an error so it can be classified with the manual command first.
Synchronization accepts USD accounts, updates balance snapshots, deduplicates remote transactions, replaces matching pending transactions, applies categorization rules, and detects cross-account transfers.
Configuration #
| Variable | Default | Purpose |
|---|---|---|
CACHEBOARD_DATA_DIR |
data/ relative to the working directory |
Directory holding the database and SimpleFIN credentials |
CACHEBOARD_DB |
Development: $CACHEBOARD_DATA_DIR/cacheboard.sqlite; deployment: required |
Explicit absolute existing database path in deployment |
CACHEBOARD_STATIC_DIR |
../web/dist when run from server/ |
Built browser bundle directory |
CACHEBOARD_HOST |
127.0.0.1 |
Listener address; loopback always listens too (tailscale serve) |
PORT |
3000 |
HTTP port |
CACHEBOARD_TODAY |
2026-07-30 |
Synthetic seed date |
CACHEBOARD_ENV |
unset | deployment requires an existing household and enables SimpleFIN sync; development allows initial setup and seeding |
CACHEBOARD_RELEASE |
development |
Commit ID returned by /healthz; set by the release installer |
SIMPLEFIN_SETUP_TOKEN |
unset | One-time SimpleFIN claim token |
LM_BASE_URL |
http://localhost:1234/v1 |
OpenAI-compatible local model endpoint |
LM_MODEL |
ternary-bonsai-27b-mlx |
Local model |
LM_TEMPERATURE |
unset (provider default) | Sampling temperature for the agent loop; do not set to 0 |
LM_TURN_TIMEOUT_MS |
300000 |
Maximum total assistant-turn time |
There is no authentication. Keep the default loopback listener unless access is restricted by the deployment environment.
Repository hygiene #
Real financial data and credentials live outside the repository. .gitignore
and .dockerignore exclude environment files, databases, SQLite WAL files,
SimpleFIN credentials, dependency caches, and build output. .env.example
contains names and safe defaults only. The synthetic seed is development-only
and exits unless CACHEBOARD_ENV=development is set explicitly.