Skip to content
apptrackitPublic

About

Self hosted finance manager

Resources

Stars

8 stars

Watchers

0 watching

Forks

Repository files navigation

💰 Finance Manager

A self-hosted personal finance app for tracking accounts, transactions, investments, and cash flow. The React client runs on Cloudflare Pages, with a Workers API and D1 database. An optional MCP Worker connects your finance data to ChatGPT for analysis, reviewable transaction drafts, and saved financial forecasts.

Changelog · GitHub releases · MCP guide · Contributor and agent guide

The latest published release is v4.0. This README describes the current source tree; see the v4.0 changelog for release details.

Contents

Features

Area Capabilities
Accounts Dedicated Accounts page for cash accounts and investment holdings; multiple currencies; balance adjustments; locks; independent exclusions; archive/restore with retained history.
Search Search everything palette (Cmd/Ctrl+K) across full transaction history, pending items, accounts, categories, recurring schedules, amounts, and dates.
Transactions Income and expenses, categories and icons, linked transfers, search and date filters, split payments, save-and-add-another entry, balance adjustments, and recent-change indicators.
Upcoming transactions One-time future income and expenses, projected cash balances, and explicit confirmation or decline without changing the posted balance early.
Recurring schedules Daily, weekly, monthly, and yearly options; transaction and transfer schedules; pause/resume, end dates, occurrence limits, and a calendar view.
Investments Responsive portfolio history, allocation and sortable ETF/stock/crypto/manual holdings with native trends, coverage alerts, normalized free sources, dated manual values and exact linked cash trades.
Analytics Configurable widgets for cash trends, AI financial forecasts, income/expense trends and breakdowns, individual account trends, Money Map, and top expenses.
AI financial forecasts Saved HUF forecasts with 7/30/90-day ranges, daily cash paths, report history, source-data freshness, and privacy-aware narratives.
MCP review Prepare income, expense, or cash transfer drafts in conversation; list and correct ordinary drafts or transfer pairs; review and confirm or decline them in the app.
Settings Reporting currency, category management, navigation visibility, light/dark modes, Original/Monochrome/Red Filter themes, startup privacy, cache controls, and CSV/JSON export.
PWA Installable app, responsive desktop/mobile layouts, cached assets, and service-worker updates. Financial writes require an API connection.

Budget management was retired in v3.0. See the v3.0 changelog and the migration note before upgrading an older deployment.

How financial data works

Posted, upcoming, and review transactions

Cash income is positive and expenses are negative. Posted transactions affect account balances; pending and cancelled transactions do not.

Transaction state Stored balance Upcoming cash projections Next action
Posted Included Part of the starting balance/history Edit or delete through the app
Pending, upcoming Unchanged Included where upcoming projections are shown Confirm when due, or decline
Pending, mcp_review Unchanged Excluded; review previews are separate Review in the app, then confirm when due or decline
Cancelled Unchanged Excluded Retained as cancelled data

Future-dated ordinary transactions remain pending until confirmed. Confirmation is guarded against duplicate balance updates and cannot post a future-dated transaction. The client sends its local calendar date in X-Client-Date so the API and browser agree on what is due.

Transfers link both sides of a movement and are excluded from income/expense aggregates. Cash-linked investment purchases and sales store reciprocal linked_transaction_id values on the cash transaction and its buy/sell record in investment_transactions. Standalone investment records keep this field null. Linked trade creation, edits, and deletion update both records, native cash/holding balances, and audit entries in one guarded D1 batch. Edit the pair through its cash transaction; deleting either record removes the pair and reverses both movements once.

Migration 017 backfills unambiguous existing investment links without changing financial values, balances, or financial-source revision. Ambiguous or inconsistent legacy links remain unchanged and require repair before pair mutation. The app records purchases and sales through the shared /investment-trades writer, with exact cash, included fees and validated execution facts. Unsupported standalone investment posting is read-only; legacy unverified pairs need reconciliation before mutation.

Investments and currencies

Matching recorded purchases/sales now supply unit history directly; zero-start holdings need no separate opening date. A dated starting position is for imported units missing from the trade ledger. Verified old linked cash payments can support reporting cost, while standalone purchase currency/fees remain explicitly unavailable. See recorded-history rules.

Market-priced holdings distinguish share/coin quantity from cash value. Their buy/sell history lives in investment_transactions; current portfolio valuation uses quantities, market quotes, and currency conversion. Manual assets use a separately managed value. Individual account trend charts keep the account's native currency, while aggregate views use the selected reporting currency.

Market quotes and exchange rates come from external services and may be unavailable or delayed. The epic’s market input contract implements shared API/MCP quote normalization, bounded caching, actual source/session timestamps and dated ECB FX without a paid provider. Investment metrics use a shared calculator with documented complete/partial aggregate policies; contributors should follow the financial rules in AGENTS.md when changing calculations.

The investment rework's versioned accounting contract retains the dated pre-implementation audit and specifies opening quantities, exact cash/fees, remaining cost basis, manual valuations, historical FX, and estimated return calculations. Its synthetic worked examples are checked by the API suite. The current source implements those rules through shared calculators and guarded writers; see the integrated validation and rollout plan for evidence and remaining release steps.

The epic’s holding model and account setup implement explicit ETF/category/pricing selection, dated openings and safe current-position corrections. Migration 018 preserves existing balances and records while marking incomplete history. Cash-linked trades and dated manual values are implemented in their own epic steps; shared metrics and historical series consume that explicit opening coverage.

Ordinary cash writes, pending confirmation, linked cash-transfer edits/deletions and account adjustments use guarded atomic batches with decimal balance arithmetic and audit. Account snapshot guards and bounded retries preserve concurrent trades; stale account correction drafts retain their original saved balance/version. Decimal balance recovery handles narrowly verified old arithmetic residue during a successful mutation without rounding genuine fractional cash or crypto units.

Recurring schedules

The API Worker runs recurring processing using the configured daily cron (0 0 * * *, midnight UTC). It creates posted transactions and updates balances for eligible schedules, respecting locks, archived accounts, end dates, remaining occurrences, and last-processed dates. Its ledger writes, balances and occurrence consumption commit together, including under repeated execution. Monthly days beyond the end of a month clamp to its final day.

Yearly schedules keep their selected month across saves, API execution, client calendars, and MCP projections. Legacy schedules without a saved month fall back to their creation month until edited.

MCP and AI financial forecasts

The optional MCP Worker accesses D1 directly. Most tools read bounded financial summaries or transaction history. Its writes are limited to stored proposals, pending MCP review draft creation and correction, and immutable forecast snapshots.

  1. prepare_mcp_transaction_drafts validates and previews up to 20 income/expense items, then stores an expiring proposal.
  2. After you approve the preview, create_mcp_transaction_drafts creates pending MCP review drafts using the proposal ID. Retrying the same successful proposal does not duplicate the drafts.
  3. Review those drafts in Finance Manager. Only confirmation in the app posts them and updates balances.

The MCP can also list unresolved review drafts, showing each cash transfer pair once. To fix a mistake, it prepares a complete before/after preview for up to 20 edits or declines; after your confirmation, it applies the proposal atomically. Ordinary drafts and transfer pairs use separate correction tools. A decline removes the draft or both transfer legs from the active review queue without deleting their audit history. Changes to review drafts alone do not change balances, projections, or forecast freshness.

For cash-to-cash transfers, prepare_mcp_transfer_drafts previews both account legs. Different-currency transfers require the amount sent and the amount received; MCP does not calculate either from a market rate. After you confirm the complete preview, create_mcp_transfer_drafts creates linked pending review drafts. The app and MCP list show one review item per transfer. You can edit its accounts, both amounts, date, and note while it is pending; changing accounts requires both amounts to be entered. The MCP uses prepare_mcp_transfer_corrections and apply_mcp_transfer_corrections to edit or decline both legs after your confirmation. Posting remains an app action. Investment transfers remain in the app.

For forecasts, an AI client obtains context with get_financial_outlook_context and publishes a snapshot with create_financial_outlook_snapshot. The current format stores actual cash history and a daily 90-day HUF forecast. Source revisions and timestamps determine freshness; saved reports remain immutable. The API and Analytics UI read those reports without generating them automatically.

See the MCP guide for tools, authentication, setup, and the complete workflow. This project does not require an OpenAI API key or run a model itself.

Investment values, remaining cost, net invested and realized/unrealized gains use shared accounting rules. Currency modes recompute acquisition-date FX; unknown legacy history stays unavailable. Market Today is a covered daily-close estimate. The combined portfolio history chart reconstructs dated units, prices, FX and appraisals, including sold and archived holdings, and reports covered range gain and daily-linked Estimated return. Missing history stays visible as gaps. The responsive overview combines this chart with portfolio summaries, allocation and sortable holdings; category filters affect only the table, and unavailable values retain explicit coverage. The holding panel and trade editor add native execution markers, current remaining-cost references, exact cash previews and version-protected linked edits/deletion.

Architecture

flowchart LR
    Browser[React client on Pages] -->|Origin + Access session| API[Hono API Worker]
    API --> DB[(Cloudflare D1)]
    Cron[Daily cron] --> API
    AI[ChatGPT / MCP client] -->|Cloudflare Access| MCP[MCP Worker]
    MCP --> DB
Loading

The API and MCP Worker share the deployed D1 database but have separate authentication and application code. The API follows middleware → controller → service → repository. React uses browser routing, component state, context, and a shared finance-data hook, with additional fetching inside feature modules.

Accounts, search, and archive

The Accounts page groups cash/bank accounts, investments, and archived accounts. Desktop navigation includes collapsible Cash/Investments shortcuts showing all accounts sorted by converted value, independent account scrolling, compact balances, and Search everything; mobile uses bottom navigation, More, and a header search button. The sidebar and Settings expose light/dark mode independently of the existing color themes.

Initial Dashboard, Analytics, and Investments reads use text-free skeletons shaped like their widgets, with a left-to-right gradient shimmer. Analytics respects saved widget visibility; reduced-motion preferences disable the shimmer. Once data has loaded, background refreshes keep the page visible with a small fixed refresh indicator. Investment portfolios and the first page of saved forecast reports stay in memory across navigation and refresh when reopened; a browser reload starts a new session. Failed refreshes retain the previous complete snapshot with retry feedback.

Dashboard loads posted cash and investment transactions only for its selected dates, in bounded pages. Complete history loads on demand when you open Analytics or Search everything. Charts and search wait for all history pages before using the new snapshot; failed refreshes retain the last complete snapshot with a retry warning. Changing Dashboard dates does not reload hidden Analytics. See the history loading measurements for fixture results and the pagination tradeoff.

Open Search everything with the sidebar/header button or Cmd/Ctrl+K. Its command palette searches full posted history plus pending transactions, account/category names, recurring schedules, native amounts/currencies, and dates. Results open read-only transaction details or existing account/recurring editors; category results and View all matching transactions open filtered full-history lists. Quick actions and visible page destinations appear before typing. N and the transaction plus buttons still open transaction creation. Search respects privacy and unfinished financial forms, with retry controls for unavailable data. Search drafts are not saved in app preferences or URL query parameters.

Archive requires an unlocked account with exactly zero native balance/holding and no unresolved pending transactions (including MCP reviews and linked review pairs). Archive preserves identity, ledger, investment history, exclusions, and exports. It pauses every recurring schedule using the account as source or destination; restore makes the account available again but does not resume schedules. Archived accounts are read-only until restored and are omitted from active account selectors and current market valuations. Historical posted activity and balance reconstruction continue to include them.

PATCH /accounts/:id/archive and /restore are idempotent and audited. Migration 016 enforces the same write restrictions for API and MCP. Permanent deletion requires a typed account name in the UI and is blocked when linked transfers exist; use archive to retain those records. JSON export is an archive format, not an app restore feature.

See redesign implementation notes for the prototype mapping and verification boundaries.

Navigation and saved views

Opening / or freshly launching the installed app opens /dashboard in the current local month. Explicit links, refreshes, and resumed tabs keep their URL. Dashboard is always available; optional hidden sections fall back to Dashboard. Old last-page preferences no longer control startup.

Dashboard filters use from/to (inclusive YYYY-MM-DD dates), range=allTime, category (category ID, transfer, all-expenses, or all-income), sort (date, amount-high, amount-low), and view (list, calendar). No dates means the current local month; explicit dates keep bookmarks fixed. Calendar ranges normalize to the month containing the start date; All Time uses the list view. For example, /dashboard?from=2026-10-01&to=2026-10-31&view=calendar.

Analytics filters use period (month, year, allTime), month (YYYY-MM) or year (YYYY), mode (actual, projected), incomeCategory/expenseCategory (matching category IDs), and resolution (default, quarter, year, as supported by the period). For example, /analytics?period=year&year=2026&resolution=quarter. Defaults are omitted; invalid values recover to valid defaults. Projected bookmarks wait for successful loading and fall back to Actual when no eligible upcoming row exists. Changing periods returns to Actual.

Browser Back/Forward restores applied filters. Search text and financial form drafts stay in the current page rather than the URL; open financial editors prompt before navigation and browsers warn before reload/close. Theme, privacy, and widget visibility remain independent saved preferences.

Cloudflare Pages serves direct page paths using its SPA fallback (do not add a top-level 404.html without replacing that behavior). The PWA manifest is generated from client/vite.config.ts as site.webmanifest, with launch URL /dashboard and scope /. The service worker can serve the cached app shell on document paths, excluding API/assets/file requests. Offline pages still show data availability errors where necessary, and writes require an API connection. Existing installed apps may require a browser-managed manifest update; resuming an existing window does not count as a fresh launch.

Layer Stack
Client React 19, TypeScript, Vite 7, Tailwind CSS 4, Recharts, Lucide, date-fns
API Cloudflare Workers, Hono, Zod, TypeScript
MCP Separate Cloudflare Worker with JSON-RPC tool handling and Access JWT verification
Storage Cloudflare D1 (SQLite), ordered SQL migrations, audit records
Market data Yahoo Finance HTTP adapter; current FX from open.er-api.com; dated ECB FX via Frankfurter
Verification Vitest, Testing Library/jsdom, TypeScript, client ESLint, GitHub Actions
finance/
├── api/
│   ├── migrations/          # Ordered schema changes; apply the complete sequence
│   ├── src/index.ts         # Live API routes, dependency wiring, and cron
│   ├── src/controllers/     # HTTP handlers
│   ├── src/services/        # Business rules
│   ├── src/repositories/    # D1 queries
│   ├── src/validators/      # Request schemas
│   ├── src/tests/           # API regression tests
│   └── wrangler.toml.example
├── client/
│   ├── src/App.tsx          # Shared layout and finance data context
│   ├── src/navigation/     # Routes, validated URL filters, editor guard
│   ├── src/pages/          # Dashboard and Analytics route bindings
│   ├── src/hooks/           # Shared finance data and refresh handling
│   ├── src/components/      # Dashboard, analytics, investments, settings
│   ├── src/context/         # Theme, privacy, and alerts
│   ├── src/lib/             # Amount parsing and review helpers
│   └── public/_headers     # Pages CSP/security/cache headers
├── mcp/
│   ├── src/                # Protocol, tools, auth, calculations, and tests
│   ├── scripts/            # Staging smoke test
│   └── README.md           # MCP setup and tool contract
├── .github/workflows/ci.yml
├── .deploy-config.example
├── AGENTS.md               # Coding conventions and financial invariants
├── CHANGELOG.md            # Published releases and unreleased changes
├── scripts/
│   ├── deploy.mjs          # One deployment CLI for all targets
│   └── deploy.test.mjs     # Scope, failure handling, and bundle checks
└── package.json            # npm workspace scripts and app version

Local development

Prerequisites

  • Node.js 22.23.2 (the tested CI version, recorded in .node-version) and npm. The package engine range also permits newer supported Node releases; use the pinned version for consistent local integration tests.
  • Git. Wrangler is installed with the workspace dependencies.
  • A Cloudflare account to provision the D1 database and deploy Workers/Pages. Local API development uses Wrangler's local D1 state.

Run commands from the repository root unless a command explicitly changes directory.

Install and configure

git clone https://github.com/apptrackit/finance.git
cd finance
npm ci

# New checkout only: copy templates without replacing existing local configuration.
cp -n api/wrangler.toml.example api/wrangler.toml
cp -n api/.dev.vars.example api/.dev.vars
cp -n client/.env.example client/.env.local

Set the DB binding's database name and ID in api/wrangler.toml to your D1 database. Keep the binding name DB and the existing Worker compatibility settings.

Set local API values in api/.dev.vars:

API_AUTH_MODE=public
ALLOWED_ORIGINS=http://localhost:5173

Set client values in client/.env.local:

VITE_API_AUTH_MODE=public
VITE_API_DOMAIN=localhost:8787

First start or intentional database reset

npm run dev deletes the API's existing local database state. Its API setup step removes api/.wrangler/state, installs dependencies, and applies every SQL migration before starting the servers. Use it for a fresh local database, not for a routine restart.

npm run dev

To run only the local frontend against the private production API, keep client mode private, use the remote API hostname, explicitly allow http://localhost:5173 on the API, and run npm run dev -w client. Private remote requests bypass the Vite proxy to use the API's Access cookie. See local frontend with production API.

Restart while preserving local data

Start these in separate terminals after the local schema has been initialized:

# Terminal 1: API without setup/reset
(cd api && npx wrangler dev src/index.ts)
# Terminal 2: client
npm run dev -w client

Vite forwards /api requests to VITE_API_DOMAIN and removes that prefix. Confirm the target before editing data: a development client can be configured to call a remote API.

For local MCP work, follow mcp/README.md, copy its Wrangler example, and run npm run dev -w mcp. API and MCP local state directories are separate by default; configure a shared persistence directory and distinct ports when testing both against one local database.

Configuration

File / location Settings Purpose
api/wrangler.toml DB binding, Worker settings, cron Local API configuration, copied from the tracked example
api/.dev.vars API_AUTH_MODE=public, ALLOWED_ORIGINS Explicit local public mode and allowed browser origins
client/.env.local VITE_API_AUTH_MODE, VITE_API_DOMAIN Client mode must match the API; domain is a hostname with optional port and no scheme
.deploy-config PROJECT_NAME, DATABASE_NAME, DATABASE_ID, API_AUTH_MODE, API_ACCESS_*, ALLOWED_ORIGINS, saved API_URL Private configuration for root deployment scripts
.deploy-config (optional MCP) DEPLOY_MCP, MCP_WORKER_NAME, MCP_ACCESS_TEAM_DOMAIN, MCP_ACCESS_AUD, MCP_ALLOWED_EMAIL Optional MCP deployment and Cloudflare Access configuration
mcp/wrangler.toml DB, CF_ACCESS_TEAM_DOMAIN, CF_ACCESS_AUD, optional ALLOWED_EMAIL Generated by root deployment or copied from its example for standalone MCP work

Use .deploy-config.example for the full deployment template. Actual configuration and secrets are gitignored. VITE_* values are embedded in the client bundle at build time and are visible to people who can access it; changing them requires rebuilding the client.

Testing and builds

# All workspace tests plus compiled Worker/D1 integration tests
npm test

# Unit/regression tests for every workspace
npm test --workspaces

# Disposable local database, migration, authentication, and financial workflows
npm run test:integration

# Deployment orchestration and local Worker bundle checks; no remote writes
npm run test:deploy

# Individual suites
npm test -w api
npm test -w client
npm run test:mcp

# API TypeScript check
npx tsc --noEmit -p api/tsconfig.json

# Typecheck all workspaces and the integration harness
npm run typecheck

# Client TypeScript check and production/PWA build
VITE_API_AUTH_MODE=public VITE_API_DOMAIN=localhost:8787 npm run build

# MCP TypeScript check
npm run build:mcp

# Client lint
npm run lint -w client -- --max-warnings=0

API/client watch mode is available with npm run test:watch -w api or npm run test:watch -w client. Root build builds only the client; MCP build is a typecheck. The client build uses tsc -b to check referenced TypeScript projects.

CI checks workflow syntax, all three workspaces' tests, deployment orchestration, API/MCP types, the client production/PWA build, client lint, and compiled API/MCP integration against shared local D1. Integration tests cover fresh and populated database upgrades, signed Access authentication, financial state transitions, retries, rollback, transfers, and recurring execution. They use disposable state and fixed external-service responses; no Cloudflare credentials or local configuration are required. Failed runs retain JUnit reports and Worker diagnostics.

See the testing guide for the coverage boundaries, adding meaningful regressions, and the CI passed check to require in branch protection. Client lint excludes generated output and follows the rules enabled in client/eslint.config.js; some legacy typing/React rules remain disabled there.

The MCP staging smoke test (npm run test:staging -w mcp) creates a real review draft in the configured staging database. See the MCP verification instructions before using it.

Deployment

Cloudflare setup

Provision a D1 database and a Pages project, then authenticate Wrangler with npx wrangler login. Private mode requires Cloudflare Access on the frontend and API and verified user identity. Prefer a same-site custom API domain; explicit workers.dev URLs are supported subject to browser cookie restrictions. Public mode deliberately allows unauthenticated demo access. Follow the API Access deployment guide before deploying. Optional MCP requires a custom hostname and its own Access application with Managed OAuth; see the MCP deployment guide.

All deployment commands use scripts/deploy.mjs. The Node CLI works on macOS and Linux and reuses your existing gitignored .deploy-config. The former root deploy.sh and deploy-client.sh scripts have been removed.

Deployment commands

Command What it deploys
npm run deploy Pending remote migrations, API, client, and MCP if enabled in saved configuration
npm run deploy:client Client only, using the saved project name, API URL, and key
npm run deploy:api API Worker only; checks that database migrations are current
npm run deploy:mcp MCP Worker only; checks that database migrations are current
npm run deploy:migrations Pending remote migrations only
npm run deploy:api -- --migrations Pending migrations, then API only; also supported for deploy:mcp
npm run deploy -- --with-mcp Full release including MCP; also saves that preference
npm run deploy -- --no-mcp Full API/client deployment with MCP skipped; also saves that preference

Use the named commands above, or forward a target with npm run deploy -- client. Flags require the extra separator: npm run deploy -- --client works; npm run deploy --client is parsed by npm and is rejected to avoid accidentally running a full release. deploy:mcp now means MCP only; use --with-mcp for the former full-release behavior.

Append -- --plan to any named command to see its scope without prompts, builds, remote calls, or file changes. For example, npm run deploy:api -- --plan shows an API-only deployment with read-only migration verification.

Only settings required by the selected target are requested. Full releases remember whether MCP is included; targeted MCP deployments do not change that preference. Missing MCP settings can still be imported from an existing mcp/wrangler.toml. Before remote writes, the CLI runs the selected workspace tests, typechecks/builds, and local Worker bundle checks. API/MCP-only commands stop if migrations are pending; apply them separately or explicitly add --migrations.

Worker configuration and API secret files are temporary and cleaned up on success, failure, or interruption. API secrets upload with the new Worker version. Existing Wrangler and client .env files are preserved; client settings are injected through the build process environment, and the built CSP origin is updated without platform-specific shell commands. A configured API_URL, including a custom domain, is preserved. A first full release without one discovers it after deploying the API and rebuilds the client with the discovered URL.

PROJECT_NAME is the Pages project name in .deploy-config. All Pages deployments explicitly target main (production). Deploying from another branch prompts for confirmation; --yes explicitly accepts that production deployment. In non-interactive use, fill in .deploy-config first and provide --with-mcp or --no-mcp if a full release has no saved MCP preference.

Database migrations

The schema is defined by the full ordered sequence in api/migrations, currently 001-init.sql through 016-account-archive.sql. Deployment uses the custom migration_history table to skip applied migrations. Create a new numbered SQL file for schema changes instead of editing applied files; one-time ALTER TABLE statements should not be rerun manually.

Upgrade note: migration 012-remove-budgets.sql permanently drops the retired budget tables and clears the budget navigation preference. Back up any budget data you need before deploying the current branch over an older installation.

Upgrade note: migration 016-account-archive.sql adds account archiving and database guards shared by the API and MCP Workers. After it is applied, locked or archived accounts cannot be deleted, and accounts with linked transfers cannot be permanently deleted; archive them instead.

Data exports

Settings CSV exports the posted cash ledger (transactions), including both cash transfer legs and cash legs of investment purchases/sales. It preserves transaction/account/category IDs, linked IDs, account currencies, signed native amounts, descriptions, and spending-estimate exclusions. Pending/cancelled rows and the separate investment buy/sell history are excluded. Linked rows are labelled Transfer rather than income/expense.

Settings JSON uses authenticated GET /export, a single read-only D1 batch for a consistent database snapshot. Its finance-manager-data-export format has exportVersion: 1, an ISO exportedAt, schemaVersion (latest applied migration filename without .sql), the ordered migrations list, and a manifest with table row counts, explicit exclusions, and restoreSupported: false. This replaces the earlier partial version: "1.0" extract. The data object includes all rows and columns from:

  • accounts, categories, and transactions in every state, including upcoming work, cancelled rows, and MCP review drafts.
  • investment_transactions, recurring_schedules, and app_settings.
  • audit_log, immutable financial_outlook_snapshots, and financial_data_revision.
  • Durable mcp_draft_batches, mcp_draft_correction_runs, and mcp_transfer_correction_runs (provenance/replay records).

Rows preserve database values exactly: SQLite booleans remain numeric, stored JSON fields remain strings, and IDs, links, quantities, currencies, review metadata, and timestamps are retained. Audit logs and forecast snapshots are always included, even if empty; there are no silently omitted optional tables.

The client adds browserSettings containing only known saved app preferences: reporting currency, navigation, color theme and light/dark mode, sidebar group collapse choices, startup/legacy privacy, analytics widget visibility, and remembered transaction form account/category choices. Values are stored strings or null when unset (the app uses its default). Privacy cookies take precedence over local storage, matching the app. Browser preferences cover the exporting browser only and are read after the database snapshot. Applied page filters live in URLs and the retired last-page preference is not exported.

Expiring MCP proposal tables are explicitly excluded because their IDs authorize temporary actions; created review drafts and completed replay records are included. Migration ledger rows, SQLite infrastructure, arbitrary browser storage, authentication cookies, API keys, and environment/deployment secrets are excluded. Never store credentials in app_settings, which contains user app preferences and is exported in full.

Both formats stop on failed reads or an incomplete/unsupported response and show an error. The endpoint refuses more than 10,000 rows in any included table or more than 16 MiB of serialized JSON; the client also enforces the JSON size limit after adding browser preferences/formatting. It never returns a truncated export. CSV uses the same snapshot and therefore the same endpoint limits. For larger datasets and database recovery, use a D1 database export/backup. App import and restore are not implemented or verified. Downloads contain unmasked financial data even when privacy mode is enabled.

API reference

The live route registry is api/src/index.ts. Requests go to the Worker root; /api is only the client development proxy prefix.

All ordinary requests, including / and /version, require an exact Origin allowed by ALLOWED_ORIGINS. In private mode they also require a verified Cloudflare Access assertion. Public mode requires no identity. Allowed OPTIONS requests need no login; authenticated top-level GET /auth/login navigation can omit Origin and validates its return destination. The browser wrapper also adds X-Client-Date: YYYY-MM-DD for date-sensitive transaction actions.

Method Path Purpose
GET / Authenticated health check
GET /version App version from root package.json
GET /auth/session Check API session and deployment mode
GET /auth/login Complete API sign-in and return to an allowed frontend URL
GET, POST /accounts List or create accounts
PUT, DELETE /accounts/:id Update or delete an account
PATCH /accounts/:id/lock Lock an account
PATCH /accounts/:id/unlock Unlock an account
PATCH /accounts/:id/archive Archive an account (idempotent; pauses its schedules)
PATCH /accounts/:id/restore Restore an archived account (schedules stay paused)
GET, POST /transactions List posted transactions or create a transaction
GET /transactions/paginated Paginated posted history
GET /transactions/date-range Posted history within a date range
GET /transactions/from-date Posted history from a starting date
GET /transactions/upcoming Pending upcoming transactions and MCP review drafts
POST /transactions/:id/confirm Confirm an eligible pending transaction
POST /transactions/:id/decline Cancel a pending transaction without a balance change
PUT, DELETE /transactions/:id Update or delete a transaction
GET, POST /categories List or create categories
PUT, DELETE /categories/:id Update or delete a category
POST /categories/reset Restore default categories
GET, POST /investment-transactions List or create investment buy/sell records
DELETE /investment-transactions/:id Delete an investment record
GET /transfers/exchange-rate Retrieve an exchange rate
POST /transfers Create a linked transfer
GET /dashboard/net-worth Account/net-worth summary
GET /dashboard/spending-estimate Existing spending-estimate API calculation
GET /financial-outlook/latest Latest saved financial forecast and freshness
GET /financial-outlook-snapshots Cursor-paginated forecast history
GET /market/search Search market symbols
GET /market/quote Fetch a market quote
GET /market/chart Fetch market price history
GET, POST /recurring-schedules List or create recurring schedules
GET, PUT, DELETE /recurring-schedules/:id Read, update, or delete a schedule
GET, PUT /settings/navigation Read or save navigation visibility
POST /test-scheduled-task Run recurring processing immediately; writes transactions and balances

Example local request with the local API explicitly configured as public:

curl --fail-with-body \
  -H 'Origin: http://localhost:5173' \
  http://localhost:8787/accounts

Security and privacy

  • Single-user deployment: the app has one shared ledger and allows the configured email in private mode. It does not provide separate user accounts or tenant isolation.
  • Frontend access: Cloudflare Access must protect the frontend in private mode. It must be configured in Cloudflare; frontend environment variables are not a secret vault.
  • API checks: exact allowed-origin enforcement, verified Access JWT authentication in private mode, in-memory per-IP rate limiting (300 requests per 60 seconds per Worker instance), Zod validation on core mutation routes, and structured error/audit facilities. CORS is not an identity check. Private mode verifies the token signature, issuer, API audience, expiry/not-before time, subject, and allowed email before handlers run. Only explicit public mode skips identity verification; use a separate Worker and database for demos.
  • MCP checks: the Worker verifies Access JWT signature, issuer, audience, expiry, and optional email allowlist. Its workers.dev hostname is disabled. DISABLE_ACCESS_AUTH=true is for local testing only.
  • Browser protections: client/public/_headers defines CSP and other security/cache headers. Deployment fills in the API origin.
  • Privacy mode: the eye toggle masks displayed financial values; startup settings can hide all values or net worth. Preferences persist locally. Masking does not remove data from API responses or encrypt browser caches.

Troubleshooting

  • 401 / session errors: use Sign in to establish the API hostname's Access cookie, then retry. Check the API Access policy, team origin, audience and allowed email. See API Access troubleshooting.
  • 503 AUTH_CONFIG_INVALID: private Access configuration is missing/invalid, or the mode is unsupported. The API deliberately stays closed.
  • 403 Origin errors: send an allowed Origin header, including for curl requests, and check ALLOWED_ORIGINS against the actual frontend URL and port.
  • Local data disappeared: root/API npm run dev reruns the reset script. Use the data-preserving restart commands.
  • A development edit affected remote data: check VITE_API_DOMAIN and the Vite proxy target before making further changes.
  • Stale UI or data after deployment: inspect service-worker caching; Settings includes cache management and force-reload controls. Cached assets do not provide offline transaction writes.
  • Empty or stale AI forecast: a connected MCP client must publish a report first. Later ledger changes can make it stale; the UI does not automatically request a replacement.

For the derived metrics and nonmutating preview endpoints, see Investment metrics.

For investment execution and migration 019, see Direct cash investment trades.

For implementation conventions, financial invariants, and change-specific checks, see AGENTS.md. For historical changes and upgrade context, see CHANGELOG.md.

For dated manual appraisals, whole-asset ownership/cash events and migration 020, see Manual investment values.

About

Self hosted finance manager

Resources

Stars

8 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages