BillTracker/docs/Engineering_Reference_Manua...

105 KiB
Raw Blame History

Engineering Reference Manual — Bill Tracker

Status: Current code reference Last Updated: 2026-07-05 Version: 0.40.0 Primary stack: Node.js 22 + Express 4 (CommonJS), React 19 + Vite 5 (TypeScript, strict + noUncheckedIndexedAccess), Tailwind CSS 3 + shadcn/ui, TanStack Query 5, Sonner, SQLite via better-sqlite3 12, @simplewebauthn/* 13, otplib 13, openid-client 5

This manual reflects the current application code in server.js, routes/, services/, middleware/, db/, client/, package.json, Dockerfile, docker-compose.yml, tsconfig.json, eslint.config.mjs, and vite.config.mjs. It is written as a current-state reference, not a changelog. The previous version of this manual (v0.28.1) is fully superseded.

Notable changes since the previous manual:

  • Client migrated JSX → TSX. Full strict TypeScript with noUncheckedIndexedAccess. allowJs + checkJs:false keep legacy .js running while .ts/.tsx are type-checked. npm run typecheck wired into CI.
  • Branded money types. client/lib/money.ts exposes Cents / Dollars branded types — a typed caller can no longer format cents as dollars (the 100× bug) by accident.
  • React 18 → React 19 with babel-plugin-react-compiler for automatic memoization. Vite 5 with vite-plugin-pwa for service-worker / installable PWA.
  • Node 18 → Node 22 in Dockerfile; node:22-bookworm in CI.
  • New features: SimpleFIN bank sync, WebAuthn + TOTP 2FA, calendar feed (/api/calendar/feed.ics), subscription catalog + recommendations, spending categories / budgets, snowball plans (lifecycle), push notifications (ntfy / Gotify / Discord / Telegram), push notification column on users, encrypted secrets at rest with HKDF-derived key, money-cents migration (in progress), category groups, banker-rule auto-attribute.
  • Schema: migrations v0.2v1.06. Rollback map covers v0.44v1.04.

1. System Overview

Bill Tracker is a self-hosted bill management application. It supports:

  • Local username/password authentication, optional single-user mode, optional Authentik/OIDC login, and optional WebAuthn / FIDO2 + TOTP two-factor authentication.
  • User-scoped bills, categories (with category_groups and spending_enabled flag), payments, monthly bill overrides, monthly income, and starting cash buckets.
  • Admin user management, backup/restore, cleanup, auth-mode/OIDC configuration, status checks, migration rollback, and SimpleFIN bank-sync admin configuration.
  • Spreadsheet, user-SQLite, OFX, and CSV transaction import workflows with preview/apply split.
  • CSV, Excel, and user-SQLite export workflows; an iCalendar feed (/api/calendar/feed.ics) of upcoming bills gated by per-user tokens.
  • SMTP-based bill due notifications plus ntfy / Gotify / Discord / Telegram push notifications.
  • SimpleFIN-based bank sync (data sources, financial accounts, transactions, match suggestions, auto-categorize, merchant rules, merchant-store matches).
  • Subscription catalog (top-200 known services), per-user recommendations, declined-subscription hints, recommendation feedback.
  • Spending module: categorized transactions, rules, monthly budgets, income breakdown, copy-month workflow.
  • Snowball plans: lifecycle (active / paused / completed / abandoned), debt-order projection (snowball/avalanche/minimum-only), payoff scenarios.
  • React 19 SPA in TypeScript with branded Cents / Dollars money types, lazy-loaded routes, global QueryCache error toasts, and CSRF-protected JSON APIs.

Runtime flow:

  1. server.js initializes SQLite through db/database.js, runs db/migrations/versionedMigrations.js plus legacyReconcileMigrations.js, seeds defaults/admin user, cleans expired sessions, then starts Express.
  2. Express applies securityHeaders, optional CORS, express.json({ limit: '100kb' }), cookieParser, csrfTokenProvider, the patched res.json error formatter, and an unauthenticated GET /api/health probe.
  3. Route files under routes/ validate input, enforce ownership through req.user.id, and use service modules for business logic. WebAuthn and TOTP challenges use a 515 minute TTL in webauthn_challenges and totp_challenges.
  4. React 19 under client/ calls /api/* through client/api.ts with credentials: include and CSRF headers on mutating methods; the CSRF token is fetched once from /api/auth/csrf-token and cached in memory.

2. Project Layout

  • server.js — Express entry point and route mounting.
  • routes/ — HTTP API handlers:
    • auth.js — login, logout, logout-all, password change, TOTP setup/enable/disable/challenge/status, WebAuthn status, login history, CSRF token endpoint, mode, acknowledge-version/privacy.
    • authOidc.js — OIDC login and callback.
    • bills.js — bills CRUD, auto-mark paid, history ranges, soft-delete, recently-deleted restore.
    • payments.js — payments CRUD, status matching, snowball handling, bank-override accounting, bulk/quick.
    • categories.js — category CRUD, sort order, spending/bill split, category groups.
    • tracker.js — monthly tracker data, bucket resolution, cycle handling, drift, autopay suggestions, amount suggestions, safe-to-spend, three-month averages.
    • summary.js — summary stats, income, starting amounts, bank-tracking overlay, skip override.
    • analytics.js — expense reports, category breakdown, month-window filtering.
    • settings.js — user settings, admin config, notification settings.
    • notifications.js — notification management (admin SMTP + push).
    • profile.js — user profile, notification settings, change-password, export URLs, import history, login history.
    • import.js — CSV / Excel / user-SQLite / OFX import workflows.
    • export.js — CSV, Excel, SQLite export.
    • status.js — system status, health, runtime.
    • dataSources.js — data sources CRUD with sync status (manual / file_import / provider_sync).
    • transactions.js — transaction CRUD, match/ignore/commit actions, manual entry, pending flag, geolocation.
    • matches.js — match suggestions, rejection tracking.
    • snowball.js — snowball projection, settings, debt order, plan lifecycle (create / list / active / pause / resume / complete / abandon).
    • subscriptions.js — user subscriptions, recommendations (list / decline / match-bill / create), transaction matches, catalog links, custom descriptors.
    • spending.js — categorized spending summary, transactions, budgets (PUT / copy / month-rollover), category rules, income.
    • calendarFeed.js — public iCalendar feed (GET /feed.ics) per-user token-gated.
    • admin.js — users (incl. username change), backups, cleanup, auth-mode, bank-sync-config, migrations rollback.
    • about.js, aboutAdmin.js — public + admin markdown about view.
    • privacy.js — public privacy page.
    • version.js — public version, history, update-status.
  • services/ — business logic modules. See §4.
  • middleware/ — auth guards, CSRF, rate limits, security headers, error formatting.
  • db/schema.sql — base SQLite schema.
  • db/database.js — DB connection, migrations runner, defaults, settings, rollback support.
  • db/migrations/versionedMigrations.js — ordered, dependency-aware migrations v0.2v1.06.
  • db/migrations/legacyReconcileMigrations.js — column-level reconciliation for unversioned columns that pre-date the migration framework.
  • db/subscriptionCatalogSeed.js — top-200 US subscriptions dataset used by v0.65 / v0.69 / v0.95.
  • workers/dailyWorker.js — scheduled notification/cleanup worker entry.
  • client/ — React 19 SPA in TypeScript (see §7).
  • dist/ — generated Vite build output served by Express.
  • Dockerfile, docker-compose.yml, docker-entrypoint.sh — container deployment.
  • tsconfig.json — TypeScript config (strict + noUncheckedIndexedAccess, allowJs).
  • eslint.config.mjs — flat ESLint config, react-hooks + react-refresh + typescript-eslint.
  • vite.config.mjs — Vite + React Compiler + PWA + Vitest config.

3. Backend Entry Point and Middleware

server.js

Server defaults:

  • Port: PORT || 3000.
  • Bind host: BIND_HOST || '0.0.0.0'.
  • Static frontend: dist/.
  • Optional CORS: enabled when CORS_ORIGIN is set; comma-separated allowlist with credentials: true.
  • TRUST_PROXY (env): when set to true/yes/on trusts 1 hop; numeric → trust that many hops; otherwise the raw value (e.g. loopback, CIDR). Unset/false = no proxy trust. Required behind nginx/Traefik so req.ip and req.secure reflect the real client.
  • Session cleanup: runs on startup and every SESSION_CLEANUP_INTERVAL_MS || 86400000 ms.
  • Admin seed: INIT_ADMIN_USER || admin; INIT_ADMIN_PASS or generated/default behavior in db/database.js.
  • Environment variables INIT_ADMIN_USER and INIT_ADMIN_PASS (or INIT_REGULAR_USER + INIT_REGULAR_PASS) skip the first-login flow entirely by pre-seeding users with first_login=0 flags via setup/firstRun.js.
  • Optional regular-user seed: INIT_REGULAR_USER + INIT_REGULAR_PASS; password must be at least 8 chars.
  • First-run flow: if userCount === 0 after migrations, setup/firstRun.run(db) is invoked.
  • After app.listen, cleanupExpiredSessions is scheduled, bankSyncWorker.start() runs, the backup scheduler is started, and workers/dailyWorker.start() schedules the daily jobs.

Global middleware order:

  1. securityHeaders
  2. optional cors (only if CORS_ORIGIN)
  3. express.json({ limit: '100kb' }) — import routes override this per-endpoint
  4. cookieParser()
  5. csrfTokenProvider
  6. errorFormatter (patches res.json)
  7. GET /api/health — unauthenticated liveness probe
  8. mounted API routers with route-level rate-limit/auth/CSRF middleware
  9. GET /login.html → 302 /login, express.static(DIST)
  10. SPA fallback GET * serving dist/index.html after ensuring a CSRF token cookie
  11. final JSON error handler for malformed JSON / body-size / runtime errors

Authentication middleware

middleware/requireAuth.js exports:

  • requireAuth: attaches req.user from bt_session; in single-user mode attaches the configured active regular user without a session.
  • requireUser: permits user and admin roles but blocks the default admin account from tracker access.
  • requireAdmin: requires req.user.role === 'admin'.

CSRF middleware

middleware/csrf.js:

  • Cookie name: CSRF_COOKIE_NAME || bt_csrf_token.
  • Header name: x-csrf-token.
  • Defaults: CSRF_HTTP_ONLY === true only when explicitly configured, CSRF_SAME_SITE || strict, CSRF_SECURE !== false.
  • The SPA expects CSRF_HTTP_ONLY=false so client/api.ts can read the token cookie and send x-csrf-token; do not enable httpOnly CSRF cookies unless token delivery changes.
  • The SPA also calls GET /api/auth/csrf-token (handled by routes/auth.js) on first request and caches the token in memory; the cookie is the source of truth and the body mirrors it.
  • csrfTokenProvider sets a token cookie on responses.
  • csrfMiddleware validates mutating requests unless req.csrfSkip is set. Token may come from header, query, or body and must match cookie.
  • Failures return 403 and audit csrf.failure.

Rate limits

Defined in middleware/rateLimiter.js:

  • Login: 10 / 15 min.
  • Login-by-username (extra layer): separate limiter, applied alongside loginLimiter.
  • Password changes: 5 / 15 min.
  • Import: 20 / 15 min.
  • Export: 30 / 15 min.
  • Admin actions: 30 / 15 min.
  • OIDC: 20 / 15 min.
  • Backup operations: 5 / 60 min.
  • Demo-data clear: 3 / 15 min.

A skipRateLimitIfNoUsers(limiter) wrapper is applied to /api/auth/login so a brand-new install with zero users is not locked out by the login limiter.

Rate-limit responses are JSON: { "error": "..." }.

Security headers

middleware/securityHeaders.js sets CSP with a per-request nonce plus common hardening headers. Static SPA scripts/styles must comply with the generated CSP.

Error formatting

middleware/errorFormatter.js standardizes route responses into JSON with error, code, and optional field. Common statuses: 400 validation, 401 auth, 403 forbidden, 404 not found, 409 conflict, 429 rate limit, 500 server error.


4. Services

services/authService.js

  • Cookie: bt_session.
  • Session lifetime: 7 days. Session tokens are hashed at rest in sessions (column populated in v0.94); the cookie value is a random opaque ID and is looked up by hash.
  • Password hashing: bcrypt, cost 12.
  • Functions:
    • login(username, password) — verifies active local user, rejects OIDC-only accounts, cleans expired sessions for that user, creates a new session, updates last_login_at, and returns {sessionId, user} or null.
    • createSession(userId) — creates a session for an OIDC-provisioned or existing local user after validating that the user is active.
    • logout(sessionId) — deletes one session.
    • getSessionUser(sessionId) — returns public user fields when the session exists, is unexpired, and belongs to an active user.
    • rotateSessionId(oldSessionId, userId) — validates the current session, deletes it, and inserts a replacement session in a transaction. Password changes use this to prevent session fixation.
    • invalidateOtherSessions(userId, keepSessionId) — deletes all sessions for a user except the supplied current session; when keepSessionId is null, deletes every session for that user.
    • pruneExpiredSessions() — deletes expired sessions.
    • publicUser(user) — strips password/session secrets and normalizes booleans.

Password changes through /api/auth/change-password update the password hash, clear must_change_password, stamp last_password_change_at, invalidate all other sessions, rotate the current session ID when a valid session cookie is present, set a replacement bt_session cookie, and audit password.change.

/api/auth/logout-all calls invalidateOtherSessions(req.user.id, null), deletes the current cookie session if present, audits logout.all, clears bt_session, and returns {success:true}.

services/totpService.js

TOTP / RFC-6238 authenticator-app 2FA:

  • newSecret()otplib 20-char base32 secret.
  • generateQrCode(secret, username) — returns {uri, qr_data_url} for the SPA to display.
  • verifyToken(encryptedSecret, token) — decrypts the per-user secret, strips whitespace, calls verifySync.
  • Setup flow: getTotpSetup → user scans QR → enableTotp(plainSecret, token) validates a sample token, encrypts and stores the secret, returns 8 recovery codes.
  • disableTotp(userId) — clears totp_enabled, totp_secret, recovery codes.
  • Recovery codes are stored hashed in users.totp_recovery_codes (JSON).
  • totpStatus(userId) returns {enabled, hasSecret, recoveryCodesRemaining}.
  • Login flow: POST /api/auth/totp/challenge accepts {username, password} and returns a 5-minute challenge_id stored in totp_challenges; the SPA submits it with the 6-digit code on the next request.

services/webauthnService.js

WebAuthn / FIDO2 security-key 2FA via @simplewebauthn/server:

  • WEBAUTHN_RP_ID (env, default localhost) and WEBAUTHN_ORIGIN (env, default http://localhost:${PORT}).
  • createRegistrationChallenge(db, userId, username) — generates a 32-byte webauthn_user_id (base64url), excludes already-registered credentials, stores the challenge in webauthn_challenges with 15-minute TTL, returns {options, challengeId}.
  • verifyRegistration(db, userId, challengeId, response, credentialName) — verifies the registration, stores the credential (credential_id, public key, sign count, transports, backup flags, AAGUID, friendly name) and returns the credential id.
  • createAuthenticationChallenge(db, userId) — for login: returns assertion options + challengeId.
  • verifyAuthentication(db, userId, challengeId, response) — verifies, updates sign count, audits webauthn.login.
  • Supports attestationType: 'none', residentKey: 'preferred', userVerification: 'preferred', algorithms -7 (ES256) and -257 (RS256).
  • WEBAUTHN_RP_ID must match the browser-visible hostname, or registration/assertion will fail.

services/oidcService.js

Handles Authentik/OIDC login with openid-client:

  • Reads settings first, then env fallbacks: issuer URL, client ID/secret, redirect URI, token auth method, scopes, provider name, admin group, auto-provision flag.
  • The OIDC client secret is encrypted at rest (migration v0.79) using services/encryptionService (HKDF-derived key from v0.78).
  • Builds PKCE login state in oidc_states.
  • Discovers provider and caches OIDC client for 1 hour.
  • Exchanges callback code, verifies nonce/state, maps claims to local user.
  • Role mapping uses configured admin group; default role is user.
  • Client secret is write-only through admin settings.

services/encryptionService.js

  • assertEncryptionReady() — throws if no master key is configured.
  • encryptSecret(plain) / decryptSecret(cipher) — symmetric encryption with HKDF-derived key.
  • The key derivation was upgraded from SHA-256 to HKDF in v0.78; migrations v0.77 (SMTP password) and v0.79 (OIDC secret) wrapped any pre-existing plaintexts.
  • reEncryptWithEnvKey() is invoked by the migration runner when an admin sets ENCRYPTION_KEY after install; called from the runtime error handler when a legacy ciphertext is detected.

services/loginFingerprint.js

  • Captures ip, user_agent, browser, os, device_type, device_fingerprint on every login.
  • The ip and user_agent columns are encrypted at rest (migration v0.84).
  • success (v0.85) tracks failed-attempt history separately from successful logins.
  • A session_fingerprint is stored on each successful login so the SPA can detect a different device for the same session and trigger a re-auth challenge.

services/backupService.js

  • Backup directory: BACKUP_PATH || backups/.
  • Valid backup IDs match managed prefixes only: bill-tracker-backup, pre-restore, imported-backup, scheduled-backup.
  • Creates SQLite backups with checksum and metadata.
  • Validates uploads by SQLite integrity_check and optional SHA-256 checksum.
  • Restore creates a pre-restore backup before swapping DB.
  • Path traversal is prevented by ID regex and path.relative checks.

services/backupScheduler.js

  • Validates daily/weekly schedule, HH:MM time, retention count 1-365.
  • Stores schedule settings in settings.
  • Uses node-cron, supports run-now, reload, next-run status, and retention cleanup.

services/cleanupService.js

Cleanup tasks:

  • Expired import sessions.
  • Stale temp SQLite export files in OS temp dir.
  • Orphan .partial/.upload backup files.
  • Optional old import-history pruning.

Settings are stored in settings; run results are stored as JSON.

services/notificationService.js

  • Builds SMTP transport from global notification settings (SMTP password is encrypted at rest, migration v0.77).
  • Sends test email to an admin-provided address.
  • Runs due-bill notifications for 3 days, 1 day, due today, and overdue.
  • De-duplicates sends via notifications unique key.
  • Recipients come from user notification settings when enabled/allowed or global recipient settings.
  • Push notifications: ntfy, Gotify, Discord webhook, Telegram bot — configured per-user in users columns (migration v0.80); dispatched alongside or in place of SMTP when configured.

services/spreadsheetImportService.js

  • Accepts XLSX buffers only, max 10 MB, max 5,000 rows.
  • Detects monthly sheets, headers, categories, bills, amounts, and ambiguous rows.
  • Creates import_sessions preview records with 24-hour TTL.
  • Apply step creates/updates user-owned categories, bills, payments, monthly state, and import history.

services/ofxImportService.js

  • OFX / QFX bank-statement import (separate from CSV/Excel). Uses node-ofx-parser; falls back to manual buffer parsing when the package is not installed.
  • Produces import_sessions previews and commits to transactions with source_type='file_import'.
  • 24-hour TTL on preview sessions; results write to import_history.

services/userDbImportService.js

  • Accepts user SQLite export files up to 50 MB.
  • Requires export metadata and known tables.
  • Sanitizes categories, bills, payments, monthly state, and starting amounts.
  • Preview stores an import session; apply maps export IDs to current user-owned IDs.

services/transactionService.js

Transaction data source and transaction row helpers:

  • ensureManualDataSource(db, userId) creates/retrieves a user-specific manual data source (type='manual', provider='manual', name='Manual Entry').
  • decorateDataSource(row) removes encrypted_secret, adds source_label and source_type_label.
  • decorateTransaction(row) adds source_label, source_type_label, and embedded data_source object with safe fields.
  • getSourceTypeLabel(type) returns labels: manual → Manual, file_import → File import, provider_sync → Provider sync.
  • sourceLabel(source) constructs human-readable source labels for manual entries or provider names.
  • transactions schema carries a pending flag (v1.01) for SimpleFIN pending transactions.

services/transactionMatchService.js + services/transactionMatchState.js

  • matchTransactionToBill(transactionId, billId, userId) — links a transaction to a bill, creates a corresponding payments row with payment_source='provider_sync', runs accounting override logic.
  • unmatchTransaction(transactionId, userId) — reverses the link and deletes the auto-created payment (or marks it accounting_excluded).
  • ignoreTransaction / unignoreTransaction — set/match-status transitions.
  • transactionMatchState.js is the state-machine helper that powers match/unmatch/ignore/unignore.

services/matchSuggestionService.js

  • listMatchSuggestions(userId, transactionId, billId, limit, offset) — returns scored suggestions.
  • rejectMatchSuggestion(userId, transactionId, billId) — records the rejection in match_suggestion_rejections.
  • suggestionCounts(userId) — dashboard counts.
  • Rejections are stored in match_suggestion_rejections (created in v0.62) so a transaction/bill pair is never re-suggested after rejection.
  • After sync, autoMatchForUser(userId) runs merchant-rule + merchant-store-match + match-suggestion passes in order.

services/csvTransactionImportService.js

CSV import workflow for transactions:

  • Parses CSV with quoted-field support and quote doubling.
  • previewCsvTransactions(userId, buffer, options) returns headers, sample rows, suggested field mapping, errors, and creates a 24-hour TTL import session (max 25k rows).
  • suggestMapping(headers) auto-detects field mappings from header names against posted_date, transacted_at, amount, description, payee, memo, category, account, transaction_type, currency, etc.
  • commitCsvTransactions(userId, importSessionId, mapping) imports rows into the transactions table with source_type='file_import', auto-creates a CSV data source and financial accounts per unique account name.
  • Stable deduplication via SHA-256 hash: csv:id: prefix for explicit transaction IDs, csv:hash: prefix from date+amount+description+payee+account.
  • Imports record to import_history with counts and details.
  • FIELD_LABELS maps field keys to user-friendly labels for validation messages.

services/bankSyncService.js + services/simplefinService.js + services/bankSyncConfigService.js + services/bankSyncWorker.js

  • SimpleFIN bridge (simplefinService.js): wraps the SimpleFIN protocol — claimSetupToken, fetchAccountsAndTransactions, normalizeAccount, normalizeTransaction, sanitizeErrorMessage. assertEncryptionReady is invoked before any secret operation.
  • Per-user sync (bankSyncService.js): upserts financial_accounts rows keyed by (data_source_id, provider_account_id); upserts transactions keyed by (data_source_id, provider_transaction_id). Honors the monitored flag (v0.64) so the user can exclude a credit-card account from bill matching. Applies merchant rules, spending-category rules, merchant-store matches, then autoMatchForUser in sequence.
  • Config (bankSyncConfigService.js): getBankSyncConfig(userId), SYNC_DAYS_EFFECTIVE, SYNC_DAYS_DEFAULT. Per-user default-lookback window.
  • Worker (bankSyncWorker.js): scheduled background sync, started from server.js after app.listen. bankSyncConfigService admin endpoints live at /api/admin/bank-sync-config.

services/billMerchantRuleService.js

  • Persistent merchant→bill auto-match rules (table created in v0.67).
  • applyMerchantRules(transactions, userId) — returns transactions with matched_bill_id and match_status='matched' filled in.
  • auto_attribute_late (v0.83) marks rules whose bills always post after month-end, so amount-suggestion math skips the “looks like last months bill” signal.

services/merchantStoreMatchService.js

  • 5k merchant/store matching pack seeded by migration v1.05.
  • applyMerchantStoreMatches(transactions, userId) — broad merchant-to-store normalization for spending categorization.

services/advisoryFilterService.js

  • 5k advisory non-bill patterns + bill-like override terms seeded by migration v0.68.
  • Used to suppress “this looks like a bill but isnt” false positives in tracker suggestions.

services/subscriptionService.js + services/amountSuggestionService.js

  • subscriptionService.js — manages the users subscription bills, recommendations, declined hints (v0.66), feedback (v0.97), and the top-200 catalog seeding. Exposes normalizeMerchant used by migration v0.90 to re-normalize stored rules.
  • amountSuggestionService.jscomputeAmountSuggestion (per-bill) and computeAmountSuggestionsBatch (all-bills, used by the Tracker P1 fix to kill the N+1).
  • The batched suggestion is byte-identical to the per-bill function; pinned by tests/amountSuggestionService.test.js.

services/snowballService.js + services/calendarFeedService.js + services/driftService.js + services/statusRuntime.js

  • snowballService.js — Debt snowball/avalanche calculations, Ramsey mode support, order updates, plan lifecycle math.
  • calendarFeedService.js — generates iCalendar (.ics) text for upcoming bills, gated by calendar_tokens (v1.00).
  • driftService.js — detects when an autopay/posted amount drifts from the bills expected amount; surfaces the “drift_snoozed_until” field (v0.71).
  • statusRuntime.jsrecordError(err, req), runtime worker state, health JSON consumed by /api/status.

services/spendingService.js + services/userSettings.js + services/userDataService.js + services/eraseUserData.js (in utils/tests) + services/billsService.js + services/aprService.js

  • spendingService.js — spending categories, rules (v0.87), budgets, default category seeding, income breakdown, applySpendingCategoryRules used by the bank-sync pipeline.
  • userSettings.jsgetUserSettings(userId) returns a typed settings object; consumed by the bank-sync pipeline.
  • userDataService.js — user-scoped read helpers (e.g. getCategoriesForUser).
  • billsService.js — bill CRUD helpers, soft-delete + RecentlyDeletedBills restore support.
  • aprService.js — annual-percentage-rate / monthly interest accrual used by v0.93 (interest_accrued_month, interest_delta).

services/paymentAccountingService.js + services/paymentValidation.js

  • paymentAccountingService.js — source-of-truth for “manual vs. bank” payments. The bank-backed predicate, the accounting-active SQL fragment, and the core invariant: a bank payment overrides a provisional manual payment so it isnt double-counted, restores the balance, and the manual payment counts again with its balance re-applied when the override is removed. Pinned by tests/paymentAccountingService.test.js.
  • paymentValidation.jsPAYMENT_SOURCES = ['manual', 'file_import', 'provider_sync', 'transaction_match'] (auto_match was normalized to provider_sync in v0.82), validatePaymentSource(), validatePaymentInput().

services/auditService.js

Writes audit_log rows for security-sensitive events such as login, logout, password changes, role changes, CSRF failures, user seed flag resets, migration runs, migration rollback attempts, WebAuthn/TOTP events, bank-sync admin changes, and 2FA enable/disable.

db/database.js does not import logAudit at module load time. It uses a lazy getLogAudit() helper so migration code can write audit rows without creating a circular dependency: auditService imports database.js, and database.js needs audit logging during migrations. If the lazy require fails, the helper degrades to a no-op audit function while console logging still records migration progress/errors.


5. API Reference

All routes are prefixed with /api unless stated otherwise. Most mutating endpoints require CSRF token header x-csrf-token. Auth is cookie-based (bt_session). User routes are scoped to the authenticated user unless noted. GET /api/health is the unauthenticated liveness probe and is the only public non-meta endpoint.

Response conventions:

  • Success: JSON object/array unless endpoint downloads a file.
  • Validation: 400 {error, code?, field?}.
  • Unauthenticated: 401.
  • Forbidden: 403.
  • Missing resource: 404.
  • Conflict: 409.
  • Rate-limited: 429.

5.1 Auth (/api/auth)

  • POST /auth/login{username, password}. Sets bt_session. Rate-limited (skipped if zero users). Skips CSRF (no session yet).
  • POST /auth/logout — clears bt_session.
  • POST /auth/logout-all — invalidates every session for the user.
  • GET /auth/me — public user object (single-user mode supplies user without session).
  • GET /auth/mode — auth mode, local-login flag, OIDC public info, single-user status.
  • POST /auth/restore-multi-user-mode — restores multi-user mode where allowed.
  • POST /auth/acknowledge-privacy — updates first-login/privacy acknowledgement flags.
  • POST /auth/acknowledge-version — stamps users.last_seen_version.
  • POST /auth/change-password{current_password, new_password}. Password-limited, CSRF-skip. Rotates session id, invalidates others.
  • GET /auth/csrf-token — returns {token} mirroring the cookie; SPA caches in memory.
  • GET /auth/has-users — whether non-default users exist.
  • GET /auth/users — admin-only safe user list.
  • POST /auth/users — admin-only create.
  • GET /auth/login-history — recent login/failed-login history for the current user (encrypted columns decrypted in-process).
  • GET /auth/webauthn/status{enabled, credentials:[{id, name, createdAt, lastUsedAt}]}.
  • TOTP (/api/auth/totp):
    • POST /auth/totp/challenge{username, password}{challenge_id} (5-min TTL).
    • GET /auth/totp/setup — admin-aware: returns {secret, uri, qr_data_url} for a new enrolment.
    • POST /auth/totp/enable{secret, token} → verifies, encrypts, stores, returns recovery codes.
    • POST /auth/totp/disable — clears secret + recovery codes.
    • GET /auth/totp/status{enabled, hasSecret, recoveryCodesRemaining}.

5.2 OIDC Auth (/api/auth/oidc)

OIDC rate limiter applies.

  • GET /auth/oidc/login?redirect_to=/path — public when OIDC active; builds PKCE state and redirects.
  • GET /auth/oidc/callback?code=&state= — validates state/nonce, exchanges code, provisions/fetches user, creates session, redirects to saved path or app root. Error redirects use query errors such as access_denied or authentication_failed.

5.3 Admin (/api/admin)

Mounted under /api/admin; all require requireAuth + requireAdmin + adminActionLimiter. Backup subroutes also use backupOperationLimiter.

  • GET /admin/has-users{has_users:boolean}.
  • GET /admin/users — safe users ordered by default-admin / role / username.
  • POST /admin/users{username, password}.
  • PUT /admin/users/:id/password{password}. Invalidates target sessions; requires password change.
  • PUT /admin/users/:id/role{role:"admin"|"user"}. Cannot change own role; cannot remove last admin.
  • PUT /admin/users/:id/active{active:boolean}. Cannot deactivate self.
  • PUT /admin/users/:id/username — change a users username (admin only).
  • DELETE /admin/users/:id — transaction deletes import sessions/history, sessions, and the user.
  • Backups: GET/POST /admin/backups, GET /admin/backups/settings, PUT /admin/backups/settings, POST /admin/backups/run-scheduled-now, POST /admin/backups/import (raw SQLite, max 100 MB, optional X-Checksum-Sha256), GET /admin/backups/:id/download, POST /admin/backups/:id/restore, DELETE /admin/backups/:id.
  • Cleanup: GET/PUT /admin/cleanup, POST /admin/cleanup/run.
  • Auth mode: GET /admin/auth-mode, POST /admin/auth-mode/oidc-test, PUT /admin/auth-mode.
  • Bank sync config: GET /admin/bank-sync-config, PUT /admin/bank-sync-config (default-lookback days, monitored-account default, etc.).
  • Migrations: POST /admin/migrations/rollback with {version}. Maps NOT_APPLIED → 404, ROLLBACK_NOT_SUPPORTED → 422, other failures → 500.

5.4 Bills (/api/bills)

User/admin tracker access. Standard bill fields plus cycle_type, cycle_day, is_subscription, subscription_type, catalog_id, autopay_verified_at, inactive_reason, drift_snoozed_until, sort_order, interest_accrued_month, current_balance, minimum_payment, snowball_order, snowball_include, snowball_exempt, auto_mark_paid, deleted_at.

Endpoints (subset): GET /bills?inactive=true, GET /bills/:id, POST /bills, PUT /bills/:id, DELETE /bills/:id (hard delete; cascades payments, monthly state, history ranges, merchant rules; returns warning), GET/PUT /bills/:id/monthly-state?year=&month=, GET /bills/:id/payments, POST /bills/:id/toggle-paid, GET/POST/PUT/DELETE /bills/:id/history-ranges, GET /bills/recently-deleted (admin), POST /bills/:id/restore.

5.5 Payments (/api/payments)

User/admin tracker access. Soft delete via deleted_at. payment_source{manual, file_import, provider_sync, transaction_match}. Hard-dedupe unique index on transaction_id (v0.61).

Endpoints: GET /payments?bill_id=&year=&month=, GET /payments/:id, POST /payments, POST /payments/quick, POST /payments/bulk (max 50), PUT /payments/:id, DELETE /payments/:id, POST /payments/:id/restore.

5.6 Categories (/api/categories)

User/admin tracker access. Categories have a spending_enabled flag (v0.88) and a sort_order (v0.75). Categories can be grouped via category_groups (v1.06).

Endpoints: GET /categories (seeds defaults, includes spending_enabled + counts), POST /categories, PUT /categories/:id, DELETE /categories/:id, plus category-group endpoints under /api/categories/groups (list/create/update/delete/reorder).

5.7 Tracker and Calendar (/api/tracker, /api/calendar)

  • GET /tracker?year=&month= — returns year, month, tracker rows, totals, starting-amount info, previous-month paid total, three-month averages/trends, drift / autopay / amount-suggestion objects, safe-to-spend (cashflow timeline), generated_at.
  • GET /tracker/upcoming?days=30 — upcoming active bills.
  • GET /tracker/health — dedicated health endpoint for the home page (subset of /tracker).
  • GET /calendar?year=&month= — month days with payment entries, bills/due-status entries, totals.
  • GET /calendar/feed.ics?token=... — public iCalendar feed of upcoming bills, gated by calendar_tokens (v1.00).

5.8 Summary and Starting Amounts (/api/summary, /api/monthly-starting-amounts)

  • GET /summary?year=&month= — returns {year, month, income, expenses, starting_amounts, previous_month, summary, chart, bank_tracking, generated_at}.
  • PUT /summary/income{year, month, amount, label?}.
  • GET /monthly-starting-amounts?year=&month= / PUT /monthly-starting-amounts — bucket response.

5.9 Snowball (/api/snowball)

  • GET /snowball — current user's debt bills pre-sorted by snowball_order.
  • GET /snowball/settings / PATCH /snowball/settings — extra payment, ramsey mode, ready-current, ready-emergency-fund.
  • GET /snowball/projection{snowball, avalanche, minimum_only, comparison} with enriched debt arrays (APR snapshots, interest-accrual month).
  • PATCH /snowball/order[{id, snowball_order}] batch update.
  • Plans (lifecycle, v0.73): POST /snowball/plans, GET /snowball/plans, GET /snowball/plans/active, PATCH /snowball/plans/:id, POST /snowball/plans/:id/pause|resume|complete|abandon.

5.10 Spending (/api/spending)

  • GET /spending/summary?year=&month= — monthly spending summary, by category / by bill.
  • GET /spending/transactions — paginated, filterable.
  • PATCH /spending/transactions/:id/category — manual reassignment.
  • GET /spending/budgets / PUT /spending/budgets / POST /spending/budgets/copy — monthly budgets with month-rollover.
  • GET /spending/category-rules / POST /spending/category-rules / DELETE /spending/category-rules/:id — automation rules seeded in v0.87.
  • GET /spending/income — per-user monthly income.

5.11 Subscriptions (/api/subscriptions)

  • GET /subscriptions — users subscription bills.
  • GET /subscriptions/recommendations — bank-derived candidates (declined hints suppressed).
  • POST /subscriptions/recommendations/decline — record a decline.
  • POST /subscriptions/recommendations/match-bill — link a recommendation to an existing bill.
  • POST /subscriptions/recommendations/create — create a new bill from a recommendation.
  • GET /subscriptions/transaction-matches — candidate transactions for the users subscriptions.
  • GET /subscriptions/catalog — top-200 catalog with bank descriptors.
  • PUT /subscriptions/:id/catalog-link / POST /subscriptions/catalog/:catalogId/descriptors / DELETE /subscriptions/catalog/descriptors/:id — manage custom bank descriptors.
  • PATCH /subscriptions/:id — update subscription fields (price, cycle, descriptor, etc.).

5.12 Analytics (/api/analytics)

  • GET /analytics/summary?year=&month=&months=&category_id=&bill_id=&include_inactive=&include_skipped= — monthly spending, expected vs actual, category totals, bill totals, filters, generated_at.

5.13 Settings (/api/settings)

  • GET /settings / PUT /settings — user-visible settings.
  • POST /settings/seed-demo-data — seed demo data.

5.14 Notifications (/api/notifications)

Mounted under /api/notifications; requireAuth at mount.

  • GET/PUT /notifications/admin — SMTP + push (ntfy / Gotify / Discord / Telegram) global settings.
  • POST /notifications/test{to}.
  • GET/PUT /notifications/me — per-user notification email, push channels, lead times.

5.15 Profile (/api/profile)

  • GET /profile — safe profile, notification settings, export URLs, import-history URL.
  • PATCH /profile{display_name}.
  • GET/PATCH /profile/settings — per-user notification prefs.
  • POST /profile/change-password — rotates session, invalidates others.
  • GET /profile/exports / GET /profile/import-history.

5.16 User Demo Data (/api/user)

  • POST /user/seed-demo-data, POST /user/clear-demo-data (rate-limited; deletes is_seeded=1 bills/categories for current user).

5.17 Import (/api/import, /api/imports)

requireAuth + requireUser + importLimiter at mount. Both prefixes are accepted.

  • POST /import/spreadsheet/preview (XLSX, max 10 MB, optional parse_all_sheets, year, month).
  • POST /import/spreadsheet/apply.
  • POST /import/user-db/preview (SQLite, max 50 MB) / apply.
  • POST /import/csv/preview / POST /import/csv/commit.
  • POST /import/ofx/preview / POST /import/ofx/commit (OFX/QFX bank statements).
  • GET /import/history.

5.18 Data Sources (/api/data-sources)

  • GET /data-sources?type=&status=typemanual, file_import, provider_sync; statusactive, inactive, error. Returns sources with account_count, transaction_count, sync info.

5.19 Transactions (/api/transactions)

  • GET /transactions?limit=&offset=&match_status=&ignored=&source_type=&start_date=&end_date=&q=&data_source_id=&account_id=&matched_bill_id=. Free-text search across description/payee/memo/category.
  • POST /transactions/manual — manual entry, returns created row with source_type='manual'.
  • PUT /transactions/:id — partial update.
  • DELETE /transactions/:id — soft-unmatch then hard delete.
  • POST /transactions/:id/match / unmatch / ignore / unignore.
  • The pending flag (v1.01) marks SimpleFIN pending transactions separately from posted ones.

5.20 Match Suggestions (/api/matches)

  • GET /matches/suggestions?transaction_id=&bill_id=&limit=&offset=.
  • POST /matches/:id/reject — records rejection so the pair is never re-suggested.

5.21 Export (/api/export)

  • GET /export?year=YYYY&format=csv|xlsx — payment/bill history for the year.
  • GET /export/user-excel — user workbook.
  • GET /export/user-db — portable SQLite with metadata.

5.22 Status, About, Version, Privacy, Health

  • GET /status — admin-only: app version, uptime, runtime worker state, DB health, SMTP/push config status, backup status, current-month tracker health, recent errors.
  • GET /about — public; {version, about}.
  • GET /about-admin — admin-only, file allowlist FUTURE.md / DEVELOPMENT_LOG.md.
  • GET /version — public; package version + latest structured notes from HISTORY.md.
  • GET /version/history — raw history text.
  • GET /version/update-status — async, checks upstream for new releases (uses services/updateCheckService).
  • GET /privacy — public privacy text.
  • GET /health — public liveness probe {ok:true}.

6. Database Reference

SQLite uses WAL mode and foreign keys. Base schema is in db/schema.sql; db/database.js applies migrations to reach the current schema.

Tables and columns

users

  • id INTEGER PRIMARY KEY
  • username TEXT NOT NULL UNIQUE COLLATE NOCASE
  • password_hash TEXT NOT NULL
  • role TEXT NOT NULL DEFAULT 'user' (admin or user)
  • must_change_password INTEGER NOT NULL DEFAULT 0
  • first_login INTEGER NOT NULL DEFAULT 1
  • created_at TEXT DEFAULT datetime('now')
  • updated_at TEXT DEFAULT datetime('now')
  • notification_email TEXT
  • notifications_enabled INTEGER NOT NULL DEFAULT 0
  • notify_3d INTEGER NOT NULL DEFAULT 1
  • notify_1d INTEGER NOT NULL DEFAULT 1
  • notify_due INTEGER NOT NULL DEFAULT 1
  • notify_overdue INTEGER NOT NULL DEFAULT 1
  • display_name TEXT
  • last_password_change_at TEXT
  • auth_provider TEXT NOT NULL DEFAULT 'local'
  • external_subject TEXT
  • email TEXT
  • last_login_at TEXT
  • active INTEGER NOT NULL DEFAULT 1
  • is_default_admin INTEGER NOT NULL DEFAULT 0
  • snowball_extra_payment REAL (v0.48)
  • last_seen_version TEXT (v0.50) — release-notes notification stamp
  • webauthn_user_id TEXT (v0.92); webauthn_enabled INTEGER NOT NULL DEFAULT 0 (v0.92)
  • totp_enabled INTEGER NOT NULL DEFAULT 0 (v0.86); totp_secret (encrypted, v0.86); totp_recovery_codes (JSON array of hashes, v0.86)
  • Push notification columns (v0.80): ntfy / Gotify / Discord / Telegram (topic + URL + enabled flag each)
  • geolocation_enabled INTEGER NOT NULL DEFAULT 0 (v1.02; was a global admin setting)

sessions

  • id TEXT PRIMARY KEYsession token hash, not the raw cookie value (v0.94). The cookie carries a random opaque ID; the DB stores its SHA-256 so a leaked DB does not yield usable session tokens.
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • expires_at TEXT NOT NULL
  • created_at TEXT DEFAULT datetime('now') (v0.43)

webauthn_credentials (v0.92)

  • id INTEGER PRIMARY KEY AUTOINCREMENT
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • credential_id TEXT NOT NULL UNIQUE
  • public_key TEXT NOT NULL
  • sign_count INTEGER NOT NULL DEFAULT 0
  • transports TEXT (JSON)
  • backup_eligible INTEGER NOT NULL DEFAULT 0
  • backup_state INTEGER NOT NULL DEFAULT 0
  • credential_name TEXT
  • aaguid TEXT
  • last_used_at TEXT
  • created_at TEXT NOT NULL DEFAULT (datetime('now'))

webauthn_challenges (v0.92)

  • id TEXT PRIMARY KEY
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • challenge_type TEXT NOT NULL CHECK ('registration' OR 'authentication')
  • challenge TEXT NOT NULL
  • expires_at TEXT NOT NULL (15-min TTL)

totp_challenges (v0.86)

  • id TEXT PRIMARY KEY
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • challenge TEXT NOT NULL
  • expires_at TEXT NOT NULL (5-min TTL)

categories

  • id INTEGER PRIMARY KEY
  • user_id INTEGER REFERENCES users(id) ON DELETE CASCADE
  • name TEXT NOT NULL
  • created_at TEXT DEFAULT datetime('now')
  • updated_at TEXT DEFAULT datetime('now')
  • is_seeded INTEGER NOT NULL DEFAULT 0
  • deleted_at TEXT (v0.54)
  • sort_order INTEGER (v0.75) — persistent order
  • spending_enabled INTEGER NOT NULL DEFAULT 0 (v0.88) — separates bill vs spending categories
  • group_id INTEGER REFERENCES category_groups(id) ON DELETE SET NULL (v1.06)

category_groups (v1.06)

  • id INTEGER PRIMARY KEY AUTOINCREMENT
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • name TEXT NOT NULL
  • sort_order INTEGER
  • created_at, updated_at
  • Unique: (user_id, name)

bills

  • id INTEGER PRIMARY KEY
  • user_id INTEGER REFERENCES users(id) ON DELETE CASCADE
  • name TEXT NOT NULL
  • category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL
  • due_day INTEGER NOT NULL CHECK 1-31
  • override_due_date TEXT
  • bucket TEXT CHECK ('1st','15th')
  • expected_amount REAL NOT NULL DEFAULT 0 (dollars; converted to integer cents by v1.03)
  • interest_rate REAL CHECK null or 0-100
  • billing_cycle TEXT DEFAULT 'monthly' CHECK ('monthly','quarterly','annually','irregular') (canonicalized in v0.76)
  • autopay_enabled INTEGER NOT NULL DEFAULT 0
  • autodraft_status TEXT NOT NULL DEFAULT 'none' CHECK ('none','pending','assumed_paid','confirmed')
  • website TEXT
  • username TEXT
  • account_info TEXT
  • has_2fa INTEGER NOT NULL DEFAULT 0
  • active INTEGER NOT NULL DEFAULT 1
  • notes TEXT
  • created_at TEXT DEFAULT datetime('now')
  • updated_at TEXT DEFAULT datetime('now')
  • history_visibility TEXT NOT NULL DEFAULT 'default'
  • is_seeded INTEGER NOT NULL DEFAULT 0
  • cycle_type TEXT NOT NULL DEFAULT 'monthly'
  • cycle_day TEXT
  • current_balance REAL (dollars; converted to integer cents by v1.03)
  • minimum_payment REAL (dollars; converted to integer cents by v1.03)
  • snowball_order INTEGER
  • snowball_include INTEGER NOT NULL DEFAULT 0
  • snowball_exempt INTEGER NOT NULL DEFAULT 0
  • auto_mark_paid INTEGER NOT NULL DEFAULT 0
  • deleted_at TEXT
  • is_subscription INTEGER NOT NULL DEFAULT 0, subscription_type TEXT (v0.63)
  • interest_accrued_month TEXT (v0.93) — calendar month of last applied interest
  • drift_snoozed_until TEXT (v0.71) — suppresses drift notifications until this date
  • sort_order INTEGER (v0.72) — persistent tracker order
  • catalog_id INTEGER REFERENCES subscription_catalog(id) ON DELETE SET NULL (v0.96)
  • autopay_verified_at TEXT, inactive_reason TEXT (v0.99) — autopay trust / lifecycle fields

payments

  • id INTEGER PRIMARY KEY
  • bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE
  • amount REAL NOT NULL (dollars; converted to integer cents by v1.03)
  • paid_date TEXT NOT NULL
  • method TEXT
  • notes TEXT
  • balance_delta REAL (dollars; converted to integer cents by v1.03; v0.49)
  • payment_source TEXT NOT NULL DEFAULT 'manual' (v0.59; auto_match normalized to provider_sync in v0.82)
  • transaction_id INTEGER (v0.59; unique-per-active index added in v0.61)
  • interest_delta REAL (dollars; converted to integer cents by v1.03; v0.93)
  • accounting_excluded INTEGER NOT NULL DEFAULT 0 (v0.98) — bank-override metadata for provisional manual payments
  • created_at TEXT DEFAULT datetime('now')
  • updated_at TEXT DEFAULT datetime('now')
  • deleted_at TEXT

monthly_bill_state

  • id INTEGER PRIMARY KEY
  • bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE
  • year INTEGER NOT NULL CHECK 2000-2100
  • month INTEGER NOT NULL CHECK 1-12
  • actual_amount REAL (dollars; converted to integer cents by v1.03)
  • notes TEXT
  • is_skipped INTEGER NOT NULL DEFAULT 0
  • snoozed_until TEXT (v0.70) — overdue command-center snooze
  • created_at TEXT DEFAULT datetime('now')
  • updated_at TEXT DEFAULT datetime('now')
  • Unique: (bill_id, year, month)

monthly_income

  • id INTEGER PRIMARY KEY
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • year INTEGER NOT NULL
  • month INTEGER NOT NULL
  • label TEXT NOT NULL DEFAULT 'Salary'
  • amount REAL NOT NULL DEFAULT 0 (dollars; converted to integer cents by v1.03)
  • created_at TEXT DEFAULT datetime('now')
  • updated_at TEXT DEFAULT datetime('now')
  • Unique: (user_id, year, month)

monthly_starting_amounts

  • id INTEGER PRIMARY KEY
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • year INTEGER NOT NULL
  • month INTEGER NOT NULL
  • first_amount REAL NOT NULL DEFAULT 0
  • fifteenth_amount REAL NOT NULL DEFAULT 0
  • other_amount REAL NOT NULL DEFAULT 0
  • notes TEXT
  • created_at TEXT DEFAULT datetime('now')
  • updated_at TEXT DEFAULT datetime('now')
  • Unique: (user_id, year, month)

bill_history_ranges

  • id INTEGER PRIMARY KEY
  • bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE
  • start_year INTEGER NOT NULL
  • start_month INTEGER NOT NULL
  • end_year INTEGER
  • end_month INTEGER
  • label TEXT
  • description TEXT NOT NULL
  • applied_at TEXT NOT NULL DEFAULT datetime('now')

data_sources

  • id INTEGER PRIMARY KEY AUTOINCREMENT
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • type TEXT NOT NULL (manual, file_import, provider_sync)
  • provider TEXT
  • name TEXT NOT NULL
  • status TEXT NOT NULL DEFAULT 'active' (active, inactive, error)
  • config_json TEXT
  • encrypted_secret TEXT
  • last_sync_at TEXT
  • last_error TEXT
  • created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
  • updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
  • Unique partial index: (user_id, type, provider) WHERE type='manual' AND provider='manual'

financial_accounts

  • id INTEGER PRIMARY KEY AUTOINCREMENT
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • data_source_id INTEGER REFERENCES data_sources(id) ON DELETE SET NULL
  • provider_account_id TEXT
  • name TEXT NOT NULL
  • org_name TEXT
  • account_type TEXT
  • currency TEXT
  • balance INTEGER
  • available_balance INTEGER
  • raw_data TEXT
  • created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
  • updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
  • Unique: (data_source_id, provider_account_id)

transactions

  • id INTEGER PRIMARY KEY AUTOINCREMENT
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • data_source_id INTEGER REFERENCES data_sources(id) ON DELETE SET NULL
  • account_id INTEGER REFERENCES financial_accounts(id) ON DELETE SET NULL
  • provider_transaction_id TEXT
  • source_type TEXT NOT NULL (manual, file_import, provider_sync)
  • transaction_type TEXT
  • posted_date TEXT
  • transacted_at TEXT
  • amount INTEGER NOT NULL
  • currency TEXT
  • description TEXT
  • payee TEXT
  • memo TEXT
  • category TEXT
  • raw_data TEXT
  • matched_bill_id INTEGER REFERENCES bills(id) ON DELETE SET NULL
  • match_status TEXT NOT NULL DEFAULT 'unmatched' (unmatched, matched, ignored)
  • ignored INTEGER NOT NULL DEFAULT 0
  • spending_category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL (v0.87)
  • pending INTEGER NOT NULL DEFAULT 0 (v1.01) — SimpleFIN pending transactions
  • created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
  • updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
  • Unique partial index: (data_source_id, provider_transaction_id) WHERE provider_transaction_id IS NOT NULL
  • Indexes: (user_id, posted_date, transacted_at), (user_id, match_status, ignored), (account_id), (matched_bill_id)

Indexes

Important indexes include:

  • idx_bills_active(active)
  • idx_bills_user_active(user_id, active)
  • idx_bills_user_name(user_id, name)
  • idx_bills_user_deleted(user_id, deleted_at) (v0.91)
  • idx_bills_user_sort(user_id, active, sort_order, due_day) (v0.72)
  • idx_categories_user_name(user_id, name)
  • idx_categories_user_name_unique(user_id, name COLLATE NOCASE)
  • idx_categories_user_deleted(user_id, deleted_at) (v0.91)
  • idx_categories_user_sort(user_id, sort_order, name) (v0.75)
  • idx_payments_bill_id(bill_id)
  • idx_payments_paid_date(paid_date)
  • idx_payments_bill_date_del(bill_id, paid_date, deleted_at)
  • idx_payments_deleted(deleted_at)
  • idx_payments_method(method)
  • idx_payments_transaction_active — unique-per-active on transaction_id (v0.61)
  • idx_sessions_user_id(user_id)
  • idx_sessions_expires(expires_at)
  • idx_monthly_bill_state_lookup(bill_id, year, month)
  • idx_monthly_income_user_month(user_id, year, month)
  • idx_monthly_starting_amounts_user(user_id)
  • idx_monthly_starting_amounts_user_month(user_id, year, month)
  • idx_notifications_lookup(bill_id, user_id, year, month)
  • idx_import_sessions_user(user_id), idx_import_sessions_expires(expires_at)
  • idx_import_history_user(user_id), idx_import_history_imported_at(imported_at)
  • idx_oidc_states_expires(expires_at)
  • idx_bill_history_ranges_bill(bill_id)
  • idx_audit_log_user(user_id, created_at), idx_audit_log_action(action, created_at)
  • idx_data_sources_user_type(user_id, type, status)
  • idx_data_sources_user_manual(user_id, type, provider) WHERE type='manual' AND provider='manual'
  • idx_financial_accounts_user_source(user_id, data_source_id)
  • idx_transactions_user_date(user_id, posted_date, transacted_at)
  • idx_transactions_user_match(user_id, match_status, ignored)
  • idx_transactions_account(account_id)
  • idx_transactions_matched_bill(matched_bill_id)
  • idx_transactions_provider_dedupe(data_source_id, provider_transaction_id) WHERE provider_transaction_id IS NOT NULL
  • idx_bill_merchant_rules_user_bill(user_id, bill_id) (v0.81)

import_sessions

  • id TEXT PRIMARY KEY
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • created_at TEXT NOT NULL
  • expires_at TEXT NOT NULL
  • preview_json TEXT NOT NULL

import_history

  • id INTEGER PRIMARY KEY AUTOINCREMENT
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • imported_at TEXT NOT NULL
  • source_filename TEXT
  • file_type TEXT DEFAULT 'csv_transactions'
  • rows_parsed INTEGER DEFAULT 0
  • rows_created INTEGER DEFAULT 0
  • rows_updated INTEGER DEFAULT 0
  • rows_skipped INTEGER DEFAULT 0
  • rows_errored INTEGER DEFAULT 0
  • options_json TEXT
  • summary_json TEXT
  • created_at TEXT DEFAULT (datetime('now'))

autopay_suggestion_dismissals

  • id INTEGER PRIMARY KEY AUTOINCREMENT
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE
  • year INTEGER NOT NULL CHECK(year BETWEEN 2000 AND 2100)
  • month INTEGER NOT NULL CHECK(month BETWEEN 1 AND 12)
  • dismissed_at TEXT NOT NULL DEFAULT (datetime('now'))
  • Unique: (user_id, bill_id, year, month)

bill_templates

  • id INTEGER PRIMARY KEY AUTOINCREMENT
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • name TEXT NOT NULL
  • data TEXT NOT NULL
  • created_at TEXT DEFAULT (datetime('now'))
  • updated_at TEXT DEFAULT (datetime('now'))
  • Unique: (user_id, name)

match_suggestion_rejections

  • id INTEGER PRIMARY KEY AUTOINCREMENT
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • transaction_id INTEGER NOT NULL REFERENCES transactions(id) ON DELETE CASCADE
  • bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE
  • rejected_at TEXT NOT NULL DEFAULT (datetime('now'))
  • Unique: (user_id, transaction_id, bill_id)

user_login_history

  • id INTEGER PRIMARY KEY AUTOINCREMENT
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • logged_in_at TEXT NOT NULL DEFAULT (datetime('now'))
  • ip_address TEXT (encrypted at rest, v0.84)
  • user_agent TEXT (encrypted at rest, v0.84)
  • browser TEXT
  • os TEXT
  • device_type TEXT
  • device_fingerprint TEXT
  • location TEXT (v0.84) — IP-geolocation city/region/country when geolocation opt-in is on
  • success INTEGER NOT NULL DEFAULT 1 (v0.85) — failed attempts when 0
  • session_fingerprint TEXT (v0.85) — for current-session detection

subscription_catalog (v0.65, expanded v0.69, v0.95)

  • id INTEGER PRIMARY KEY AUTOINCREMENT
  • name TEXT NOT NULL
  • category TEXT
  • subcategory TEXT (v0.95)
  • starting_monthly_usd REAL (v0.95)
  • bank_descriptors TEXT (JSON array; populated by v0.95 from a 2026 researched dataset)
  • created_at, updated_at

declined_subscription_hints (v0.66)

  • id INTEGER PRIMARY KEY AUTOINCREMENT
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • descriptor TEXT NOT NULL
  • source TEXT
  • declined_at TEXT NOT NULL DEFAULT (datetime('now'))
  • Unique: (user_id, descriptor, source)

bill_merchant_rules (v0.67)

  • id INTEGER PRIMARY KEY AUTOINCREMENT
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE
  • normalized_merchant TEXT NOT NULL
  • match_mode TEXT NOT NULL DEFAULT 'contains' (e.g. contains, equals, regex)
  • priority INTEGER NOT NULL DEFAULT 0
  • auto_attribute_late INTEGER NOT NULL DEFAULT 0 (v0.83)
  • created_at, updated_at
  • Composite index (user_id, bill_id) for fast EXISTS lookups (v0.81)

user_catalog_descriptors (v0.96)

  • id INTEGER PRIMARY KEY AUTOINCREMENT
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • catalog_id INTEGER NOT NULL REFERENCES subscription_catalog(id) ON DELETE CASCADE
  • descriptor TEXT NOT NULL
  • created_at TEXT NOT NULL DEFAULT (datetime('now'))

subscription_recommendation_feedback (v0.97)

  • id INTEGER PRIMARY KEY AUTOINCREMENT
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • recommendation_key TEXT NOT NULL
  • verdict TEXT NOT NULL (accept | decline | mute)
  • created_at TEXT NOT NULL DEFAULT (datetime('now'))

snowball_plans (v0.73)

  • id INTEGER PRIMARY KEY AUTOINCREMENT
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • name TEXT NOT NULL
  • strategy TEXT NOT NULL (snowball | avalanche | minimum_only)
  • extra_payment REAL NOT NULL DEFAULT 0
  • status TEXT NOT NULL DEFAULT 'active' (active | paused | completed | abandoned)
  • created_at, updated_at, completed_at
  • Lifecycle transitions are recorded with audit + plan-row updates; pause/resume flip status.

calendar_tokens (v1.00)

  • id INTEGER PRIMARY KEY AUTOINCREMENT
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • token TEXT NOT NULL UNIQUE (random opaque)
  • label TEXT
  • created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
  • last_used_at TEXT
  • revoked_at TEXT

spending_rules / spending_budgets (v0.87)

  • spending_rules: per-user regex/keyword rules mapping a transaction pattern to a spending_category_id. Auto-applied during bank sync by applySpendingCategoryRules.
  • spending_budgets: per-user monthly budget rows; month-rollover uses the copy endpoint.

merchant_store_matches (seeded by v1.05)

  • Static 5k merchant/store matching pack; used by applyMerchantStoreMatches during bank sync to normalize merchant strings.

advisory_non_bill_filters (seeded by v0.68)

  • 5k advisory non-bill patterns + bill-like override terms.

Migration system

db/database.js:

  • Reads db/schema.sql in initSchema().
  • Creates schema_migrations.
  • Detects and reconciles legacy databases via db/migrations/legacyReconcileMigrations.js.
  • Applies ordered migrations from db/migrations/versionedMigrations.js only when not already recorded.
  • Validates dependency chains (dependsOn: ['v0.71']) before applying dependent migrations.
  • Uses a column whitelist for dynamic ALTER TABLE statements.
  • Wraps versioned migrations in transactions, with special handling for v0.40 because it uses PRAGMA-driven table rebuild work.
  • Supports rollback SQL for selected migrations through ROLLBACK_SQL_MAP and rollbackMigration(version).

Current migration set (v0.2v1.06):

  • v0.2 payments soft delete.
  • v0.3 tracker payment compound index.
  • v0.4 monthly bill state.
  • v0.13 user profile columns.
  • v0.14 bill history visibility.
  • v0.14.4 bill interest rate.
  • v0.15 import sessions/history.
  • v0.17 OIDC/external identity columns and state table.
  • v0.18.1 monthly income.
  • v0.18.2 monthly starting amounts.
  • v0.18.3 other starting amount bucket.
  • v0.38 per-user import audit history.
  • v0.40 ownership for bills/categories.
  • v0.41 seeded demo-data flags.
  • v0.42 bill history ranges.
  • v0.43 session created_at.
  • v0.44 performance indexes.
  • v0.45 audit log.
  • v0.46 bill cycle_type and cycle_day.
  • v0.47 bills: current_balance, minimum_payment, snowball_order, snowball_include, snowball_exempt, auto_mark_paid, deleted_at columns.
  • v0.48 users: snowball_extra_payment column.
  • v0.49 payments: balance_delta column for debt payoff tracking.
  • v0.50 users: last_seen_version for release-notes notifications.
  • v0.51 user_login_history table.
  • v0.52v0.53 ad-hoc columns (legacy reconcile).
  • v0.54 bills/categories: soft-delete columns (deleted_at).
  • v0.55 autopay: auto_mark_paid and suggestion dismissals table.
  • v0.56 bills: saved bill templates table.
  • v0.57 match_suggestion_rejections table (replaced by v0.62).
  • v0.58 import_sessions and import_history tables.
  • v0.59 payments: payment_source and transaction_id columns.
  • v0.60 data_sources, financial_accounts, transactions tables.
  • v0.61 payments: one active payment per linked transaction (unique index on transaction_id).
  • v0.62 match_suggestion_rejections table (canonical).
  • v0.63 bills: subscription metadata fields (is_subscription, subscription_type).
  • v0.64 financial_accounts: monitored flag for bill matching; transactions: partial unique index on (data_source_id, provider_transaction_id) and supporting indexes.
  • v0.65 subscription_catalog: top-200 known subscription services.
  • v0.66 declined_subscription_hints: per-user dismissed recommendation store.
  • v0.67 bill_merchant_rules: persistent merchant→bill auto-match rules.
  • v0.68 advisory_non_bill_filters: 5k advisory patterns + bill-like override terms.
  • v0.69 subscription_catalog v2: 90 new services + category fixes.
  • v0.70 monthly_bill_state: snoozed_until for overdue command center.
  • v0.71 bills: drift_snoozed_until; users: notify_amount_change.
  • v0.72 bills: persistent tracker sort order.
  • v0.73 snowball_plans table for plan lifecycle + history.
  • v0.74 subscription_catalog: Claude.ai Anthropic matching.
  • v0.75 categories: persistent sort order.
  • v0.76 bills: canonical billing schedule cleanup.
  • v0.77 encrypt SMTP password at rest.
  • v0.78 re-encrypt secrets from SHA-256 to HKDF key derivation.
  • v0.79 encrypt OIDC client secret at rest.
  • v0.80 users: push notification columns (ntfy / Gotify / Discord / Telegram).
  • v0.81 bill_merchant_rules: composite index on (user_id, bill_id).
  • v0.82 payments: normalise auto_match source to provider_sync.
  • v0.83 bill_merchant_rules: auto_attribute_late flag.
  • v0.84 user_login_history: encrypt ip/useragent at rest + add location + keep 10 records.
  • v0.85 user_login_history: failed-attempt tracking + session_fingerprint.
  • v0.86 users: TOTP/authenticator 2FA columns + totp_challenges table.
  • v0.87 spending: category assignment on transactions + rules + budgets + default categories.
  • v0.88 categories: spending_enabled flag to separate bill vs spending categories.
  • v0.89 categories: seed spending defaults for users who had existing categories before v0.87.
  • v0.90 re-normalize merchant rules after & fix; ensure rejection expiry column.
  • v0.91 performance: composite indexes on user_id+deleted_at for categories, bills, payments.
  • v0.92 auth: WebAuthn/FIDO2 security key support — webauthn_credentials + webauthn_challenges tables.
  • v0.93 bills: interest_accrued_month; payments: interest_delta; transactions: stable provider key + dedupe index.
  • v0.94 security: session token hashing + geolocation opt-in setting (seeded into settings).
  • v0.95 subscription_catalog: bank descriptors + pricing from 2026 researched dataset.
  • v0.96 bills: catalog_id FK; user_catalog_descriptors for custom bank descriptors.
  • v0.97 subscription recommendation feedback: per-user learning signals.
  • v0.98 payments: accounting_excluded for bank-override metadata.
  • v0.99 bills: autopay trust indicators (autopay_verified_at, inactive_reason) + lifecycle fields; payments: autopay failure flag.
  • v1.00 calendar feed subscription tokens (calendar_tokens).
  • v1.01 transactions: pending flag for SimpleFIN pending transactions.
  • v1.02 users: per-user geolocation opt-in (was global admin setting).
  • v1.03 money columns: dollars (REAL) → integer cents for bills.expected_amount/current_balance/minimum_payment, payments.amount/balance_delta/interest_delta, monthly_bill_state.actual_amount, monthly_income.amount, plus relevant index updates.
  • v1.04 bill_templates.data JSON: money fields dollars → integer cents (column-only v1.03 does not touch the JSON blob; serializeTemplateData reads cents).
  • v1.05 merchant_store_matches: 5k merchant/store matching pack for bank transaction categorization.
  • v1.06 category_groups: organize spending categories into named groups.
  • Unversioned user notification columns are also reconciled by legacyReconcileMigrations.js.

Migration logging is both console-based and audit-backed:

  • runMigrations() logs start, dependency status, transaction begin/commit, per-migration elapsed time, skips for already-applied migrations, failures with rollback messages, and total elapsed time.
  • Audit events use the lazy getLogAudit() helper to avoid the auditService -> database.js -> auditService circular dependency.
  • Audit actions include migration.start, migration.complete, and migration.failure.
  • Rollback paths audit migration.rollback and migration.rollback.failure.

Rollback support is defined by ROLLBACK_SQL_MAP in db/database.js and currently covers v0.44 through v1.04 (the most recent migrations v1.05 and v1.06 are not yet rollbackable):

  • v0.44 — drops selected performance indexes: idx_bills_user_name, idx_payments_method, idx_monthly_starting_amounts_user, and idx_import_history_imported_at.
  • v0.45 — drops idx_audit_log_user, idx_audit_log_action, and the audit_log table.
  • v0.46 — drops bills.cycle_day and bills.cycle_type.
  • v0.47 — drops current_balance, minimum_payment, snowball_order, snowball_include, snowball_exempt, auto_mark_paid, deleted_at columns from bills.
  • v0.48 — drops snowball_extra_payment column from users.
  • v0.49 — drops balance_delta column from payments.
  • v0.50 — drops last_seen_version column from users.
  • v0.51 — drops user_login_history table.
  • v0.54 — removes soft-delete columns (deleted_at) from bills and categories.
  • v0.55 — drops autopay suggestion dismissals table.
  • v0.56 — drops bill_templates table.
  • v0.57 — drops match_suggestion_rejections table.
  • v0.58 — drops import_sessions and import_history tables.
  • v0.59 — drops payment_source and transaction_id columns from payments.
  • v0.60 — drops data_sources, financial_accounts, and transactions tables.
  • v0.61 — drops unique index on transaction_id from payments.
  • v0.62 — drops match_suggestion_rejections table.
  • v0.63 — drops partial unique index on data_sources.
  • v0.64 — drops partial unique index and indexes on transactions.
  • v0.72 — drops bills.sort_order and idx_bills_user_sort.
  • v0.75 — drops categories.sort_order and idx_categories_user_sort.
  • v0.76 — drops bills.billing_cycle (canonical cleanup).
  • v0.77v0.79 — revert SMTP/OIDC secret encryption (re-store plaintext).
  • v0.81 — drops idx_bill_merchant_rules_user_bill.
  • v0.82 — no-op rollback (data migration).
  • v0.83 — drops bill_merchant_rules.auto_attribute_late.
  • v0.84v0.85 — revert user_login_history encryption and drop success/session_fingerprint/location.
  • v0.86 — drops users.totp_* columns and totp_challenges.
  • v0.87 — drops transactions.spending_category_id, spending_rules, spending_budgets, related defaults.
  • v0.88 — drops categories.spending_enabled.
  • v0.89 — data-only (seeding); no schema rollback.
  • v0.90 — data-only (re-normalization); no schema rollback.
  • v0.91 — drops idx_categories_user_deleted, idx_bills_user_deleted, idx_payments_user_deleted.
  • v0.92 — drops webauthn_credentials, webauthn_challenges, and the webauthn_* columns on users.
  • v0.93 — drops bills.interest_accrued_month, payments.interest_delta, transactions dedupe index + indexes.
  • v0.94 — reverts session token hashing (drops the hash; cookie value is now the raw ID again); removes geolocation_enabled setting row.
  • v0.95 — drops subscription_catalog.bank_descriptors / subcategory / starting_monthly_usd.
  • v0.96 — drops bills.catalog_id and user_catalog_descriptors table.
  • v0.97 — drops subscription_recommendation_feedback table.
  • v0.98 — drops payments.accounting_excluded.
  • v0.99 — drops bills.autopay_verified_at/inactive_reason (and any payments autopay-failure column added in the same migration).
  • v1.00 — drops calendar_tokens table.
  • v1.01 — drops transactions.pending.
  • v1.02 — drops users.geolocation_enabled and re-seeds the global admin setting key.
  • v1.03 — reverts the integer-cents conversion (back to dollar REALs) for the listed columns.
  • v1.04 — reverts bill_templates.data money fields back to dollars.

rollbackMigration(version) requires an initialized database, verifies the version exists in schema_migrations, looks up rollback SQL in ROLLBACK_SQL_MAP, executes all rollback statements inside a transaction, deletes the migration record, logs elapsed time, audits success, and returns {success:true, version, description, elapsed_ms}. If the migration is not recorded, it throws NOT_APPLIED. If no rollback definition exists, it throws ROLLBACK_NOT_SUPPORTED. Execution failures roll back the transaction and are audited as migration.rollback.failure.

The admin API exposes rollback through POST /api/admin/migrations/rollback. The route requires admin auth through the /api/admin mount. It maps NOT_APPLIED to HTTP 404, ROLLBACK_NOT_SUPPORTED to HTTP 422, and unexpected rollback failures to HTTP 500.

The lazy audit helper in db/database.js is:

let _logAudit = null;
function getLogAudit() {
  if (!_logAudit) {
    try { _logAudit = require('../services/auditService').logAudit; } catch { _logAudit = () => {}; }
  }
  return _logAudit;
}

Use this pattern for database-layer audit calls instead of a top-level require('../services/auditService').


7. Frontend Reference

Frontend stack

  • React ^19.2.7 (with babel-plugin-react-compiler for automatic memoization)
  • TypeScript ^6.0.3 with strict: true + noUncheckedIndexedAccess: true; allowJs: true, checkJs: false for the gradual JS → TS migration
  • Vite ^5.4.10 with @vitejs/plugin-react and vite-plugin-pwa (autoUpdate service worker, manifest, runtime caching for tracker/bills/calendar/summary/analytics/snowball/categories)
  • React Router ^6.26.2
  • TanStack Query ^5.100.9 + @tanstack/react-query-devtools + @tanstack/react-virtual for the virtualized Subscriptions list
  • Tailwind CSS ^3.4.14 (content glob: ./client/**/*.{js,jsx,ts,tsx} — must include ts,tsx or .tsx-only classes generate no CSS)
  • shadcn/ui component primitives, backed by Radix UI where applicable
  • Sonner toast notifications via sonner
  • framer-motion ^12.40.0 for transitions
  • react-markdown, remark-gfm, rehype-sanitize for markdown rendering
  • vitest ^4.1.8 + @testing-library/react for client unit tests (server tests stay on node --test)

client/main.tsx

Creates the React root and wraps App with global providers. Theme is provided by client/contexts/ThemeContext.tsx.

client/App.tsx

  • Creates a TanStack QueryClient with:
    • Global QueryCache.onError — toasts a sonner error when a background refetch fails while stale data is on screen. Pages render inline errors on the initial load so the toast only fires for refetches.
    • staleTime: 2 * 60 * 1000, retry: 1, refetchOnWindowFocus: false.
  • Uses lazy loading and Suspense with PageLoader for most pages.
  • Wraps route elements in ErrorBoundary; pages further wrap in PageTransition.
  • Exposes ReactQueryDevtools.
  • Provides skip link for keyboard users.
  • Mounts the CommandPalette and ReleaseNotesDialog globally.
  • RequireAuth ({children, role}) behavior:
    • Shows loading while auth state is undefined.
    • Single-user mode bypasses the auth gate for role: 'user'.
    • Redirects unauthenticated users to /login (preserves state.from).
    • Default admin cannot access user routes — redirected to /admin.
    • Role mismatches redirect to /admin (for admin role) or / (for user role).
  • AdminShell wraps admin routes so the user-shell nav does not appear inside admin.

Routes (all rendered through Routes with ErrorBoundary, lazy + Suspense for non-critical pages):

  • /loginLoginPage (public; supports local + OIDC + TOTP challenge)
  • /aboutAboutPage (public)
  • /privacyPrivacyPage (public)
  • /release-notesReleaseNotesPage (public)
  • /adminAdminPage (admin only)
  • /admin/about → admin shell + AboutPage admin (admin only)
  • /admin/roadmap → admin shell + RoadmapPage (admin only)
  • /admin/status → admin shell + StatusPage (admin only)
  • /status → redirects admin to /admin/status
  • / (index under user layout) → TrackerPage
  • /calendarCalendarPage
  • /summarySummaryPage
  • /billsBillsPage
  • /subscriptionsSubscriptionsPage (virtualized list, optimistic reorder, drag-reorder)
  • /subscriptions/catalogSubscriptionCatalogPage (admin view of the catalog)
  • /categoriesCategoriesPage
  • /healthHealthPage (bill + payment health snapshot)
  • /analyticsAnalyticsPage
  • /settingsSettingsPage
  • /snowballSnowballPage (projection + plans)
  • /payoffPayoffPage (payoff scenarios)
  • /spendingSpendingPage (categorized transactions, rules, budgets, income)
  • /bank-transactionsBankTransactionsPage (the bank ledger, Cents-based)
  • /dataDataPage (export, CSV/OFX/XLSX/SQLite imports, bank sync, transaction matching)
  • /profileProfilePage
  • *NotFoundPage

API client

client/api.ts (TypeScript; converted from api.js in the TypeScript migration):

  • _fetch<T>(method, path, body, opts) calls /api${path} with JSON headers and credentials: include.
  • Reads CSRF token from the bt_csrf_token cookie or fetches once from /api/auth/csrf-token and caches the token in memory; sends x-csrf-token on POST/PUT/PATCH/DELETE.
  • Non-OK responses throw an Error whose status/data/details/code match the server's structured errorFormatter.
  • ApiError interface carries those fields; TogglePaidResult and PaymentRecord provide typed payment responses; QueryParams types the query-string builder.
  • Exposes grouped functions for auth, admin, notifications, profile, tracker, calendar, summary, bills, payments, categories, settings, analytics, status, version/about, import, export, data-sources, transactions, matches, snowball, subscriptions, spending, monthlyStartingAmounts, csvImport, matchSuggestions.
  • File download/upload endpoints use raw fetch because responses/bodies are blobs or octet streams.
  • All 16 import sites that referenced @/api.js were normalized to @/api so Vite/TS resolve the .ts.

Domain types — client/types.ts

  • Branded Dollars and Cents money types (from client/lib/money.ts).
  • Bill, Payment, Category, TrackerResponse (with summary / bank_tracking / cashflow / rows).
  • DriftBill, AutopaySuggestion, AmountSuggestion, AutopayStats.
  • BankTransaction (integer cents on the wire).
  • TimelineBill for the Safe-to-Spend SVG timeline.
  • Money fields are typed as Dollars for bill/payment/tracker/summary amounts and as Cents for raw bank transactions — so the unit mixup bug class is unrepresentable in typed code.
  • A money.type-test.ts never-imported guard uses @ts-expect-error to assert each unit mixup is a real compile error, so tsc --noEmit fails loudly if the branding ever regresses.

Auth state

client/hooks/useAuth.tsx:

  • AuthContext typed (a createContext(null) would have made useContext return never).
  • Maintains user, singleUserMode, loading.
  • Calls api.authMode() and api.me() on startup.
  • Exposes logout(), logoutAll(), refresh().
  • singleUserMode is sourced from /auth/mode; RequireAuth consults it.

Query hooks

client/hooks/useQueries.ts (TypeScript; converted from useQueries.js):

  • useTracker(year, month), useTrackerHealth(year, month), usePrefetchTracker() — warms the adjacent-month cache on hover/focus.
  • useBills(), useCategories(), useSnowball*, useSpending*, useSummary, useBankLedger, useCategoriesWithSpending, useSubscriptions, useSubscriptionCatalog, useSubscriptionRecommendations, useMatchSuggestions, useTransactions, useTransactionsCategory, useTransactionCategories, useBankDataSources, useMatchBillDialog, useBankAccountDataSources, useBankSyncConfig, useAdminUserList, useAdminOIDCTest, useAdminBackups, useAdminCleanup, useAdminMigrations, useAdminAuthMode, useAdminNotifications, useReleaseNotes, useLoginHistory, useSnowballPlans, useWebAuthnStatus, useTOTPStatus, useUpcomingBills.
  • All hooks typed (params + QueryParams-typed query builders) and exported from api.ts.
  • React Query v5: caches use gcTime (not cacheTime, which the v5 rename silently ignored). All mutation hooks invalidate / set query data via setQueryData<T>(key, updater).
  • Mutations: useQuickPay (shared across desktop + mobile tracker rows; isPending replaces manual busy flags; tracker/badge caches invalidate on settle), useTogglePaid, useMarkPaid, useMatch, useUnmatch, useIgnore, useUnignore, useCategorizeTransaction, useBillCreate, useBillUpdate, useBillDelete, useBillReorder, useBudgetPut, useBudgetCopy, useCategoryRuleCreate, useCategoryRuleDelete, usePlanCreate, usePlanUpdate, usePlanPause, usePlanResume, usePlanComplete, usePlanAbandon, useSubscriptionLink, useSubscriptionDecline, useSubscriptionCreate, useSubscriptionUpdate, useImportCsv, useImportOfx, useBankSyncNow, etc.
  • useAutoSave<T>(value, options) is a generic debounced-save hook with an AutoSaveStatus union (idle | pending | saving | saved | error).
  • usePaymentActions.ts — shared quick-pay/toggle-paid mutation hooks; typed payloads with Dollars on money amounts.
  • useSearchPanelPreference.ts — typed [boolean, setter] tuple.

Pages

  • LoginPage.tsx — local login, OIDC login button, TOTP challenge, WebAuthn assertion, recovery code entry.
  • TrackerPage.tsx — monthly tracker, payment interactions, upcoming bills, starting-amount awareness, safe-to-spend, drift/autopay/amount-suggestion badges, bulk/session logout, month-nav prefetch.
  • CalendarPage.tsx — calendar grid backed by /calendar.
  • SummaryPage.tsx — monthly plan, income, starting amounts, expenses, chart, bank-tracking overlay, skip-override.
  • BillsPage.tsx — bill CRUD, categories, monthly state/history range controls, soft-delete + recently-deleted restore, merchant rules manager, bill historical-import dialog.
  • CategoriesPage.tsx — category list/create/update/delete, group/category split, sort order, spending flag.
  • AnalyticsPage.tsx — analytics summary filters and charts.
  • SettingsPage.tsx — user settings and demo data seed.
  • DataPage.tsx — export, spreadsheet import, user DB import, import history, CSV transaction import with preview + commit flow (ImportTransactionCsvSection), OFX import, bank sync.
  • ProfilePage.tsx — display name, notification preferences, password change, TOTP setup, WebAuthn enrollment, login history, export/import-history links.
  • AdminPage.tsx — users, auth mode/OIDC, WebAuthn, TOTP, backups, cleanup, notifications, migrations, admin settings, bank-sync config.
  • StatusPage.tsx — admin system status.
  • AboutPage.tsx — public or admin markdown/about view; admin mode uses /about-admin; markdown is sanitized.
  • RoadmapPage.tsx — admin roadmap.
  • ReleaseNotesPage.tsx — release history display; the ReleaseNotesDialog is shown to the user on login when their last_seen_version is older than the current package version.
  • PrivacyPage.tsx — public privacy text.
  • SubscriptionsPage.tsx (1814 lines) — useOptimistic<Subscription[]> reducer, virtualizer's FlatItem discriminated union, drag-reorder typed via the shared DragProps/MoveControls.
  • SubscriptionCatalogPage.tsx — admin view of the top-200 catalog with bank descriptors.
  • SnowballPage.tsx — projection, settings, plans lifecycle.
  • PayoffPage.tsx — payoff scenarios.
  • SpendingPage.tsx — categorized transactions, rules, monthly budgets with month-rollover, income.
  • BankTransactionsPage.tsx — the Cents-based bank ledger, with a local Tx structurally compatible with BankTransaction so it still satisfies MatchBillDialog.
  • HealthPage.tsx — bill + payment health snapshot.
  • NotFoundPage.tsx — 404 fallback.

Components

  • Layout: Layout, Sidebar, BrandBlock, NavPill, CommandPalette, PageTransition.
  • Admin: full components/admin/ directory — users, auth methods, OIDC, WebAuthn, TOTP, bank-sync, backup manager, cleanup, notifications, migrations, settings cards.
  • Bill modal: full components/bill-modal/ directory plus BillModal.tsx (1092-line add/edit form, reached from 9 entry points).
  • Tracker: full components/tracker/ directory (18 files — the home page) where fmt(row.expected_amount) type-checks against Dollars.
  • Transactions: components/transactions/ directory.
  • Snowball: components/snowball/ directory (plan list, projection, settings).
  • Data: full components/data/ directory (14 files) — the Data page's SimpleFIN connect/sync workbench, transaction matching, and CSV / OFX / XLSX-spreadsheet / SQLite import flows, with Cents-branded bank-transaction amounts flowing through.
  • Domain UI: BillModal, BillsTableInner, MobileBillRow, MobileTrackerRow, StatusBadge, SummaryCard, MarkdownText, ReleaseNotesDialog, IncomeBreakdownModal, BillHistoricalImportDialog, BillRulesManager, BillMerchantRules, RecentlyDeletedBillsDialog, CalendarFeedManager, SearchFilterPanel, SubscriptionCatalogSection.
  • Reliability: ErrorBoundary (typed class component), PageLoader, PageTransition.
  • UI primitives (components/ui/): alert-dialog, badge, button, card, checkbox, collapsible, confirm-dialog, dialog, dropdown-menu, input, input-dialog, label, save-status, select, separator, skeleton, sonner, switch, table, tabs, theme-toggle, tooltip — all .tsx.

client/lib/

  • money.ts — branded Cents/Dollars types, asCents/asDollars/centsToDollars/dollarsToCents, formatters. A formatUSD(dollars) and formatCentsUSD(cents) split mirrors the server's formatUSD/formatCentsUSD split. Inputs are coerced defensively so null / '' / undefined / NaN never render as $NaN.
  • utils.tscn, fmt, date/format helpers. fmt inherits formatUSD's branded-dollars input.
  • trackerUtils.ts — row/status/sort helpers, TrackerRow domain interface, TrackerStatus union.
  • billDrafts.tsSourceBill / Template / Category shapes for makeBillDraft.
  • billingSchedule.tsSchedule union and helpers.
  • cashflowUtils.ts — typed SVG timeline geometry for the Safe-to-Spend card.
  • trackerTableColumns.ts — typed column descriptors.
  • reorder.ts — generic moveInArray<T>.
  • version.ts — typed release-notes shapes; the Vite-injected __APP_VERSION__ is declared inline.

Frontend build & lint

  • npm run buildvite build (output: dist/, emptyOutDir: true).
  • npm run typechecktsc --noEmit -p tsconfig.json. Wired into npm run ci.
  • npm run linteslint client (flat config; react-hooks/rules-of-hooks, react-hooks/exhaustive-deps, react-refresh/only-export-components, typescript-eslint parser for .ts/.tsx).
  • npm run test — server node --test tests/*.test.js.
  • npm run test:clientvitest run (client unit tests).
  • npm run test:all — both.
  • npm run test:e2e — Playwright (chromium desktop + mobile); prepare-db.js reseeds the test DB.
  • npm run check:servernode --check on every server JS file.
  • npm run checkcheck:server + build.
  • npm run cilint + typecheck + check:server + test:all + build.

Frontend security notes

  • CSRF header is sent on POST/PUT/PATCH/DELETE; the token is fetched once from /api/auth/csrf-token and cached in memory.
  • Auth is cookie/session based; no tokens are stored in localStorage. Session token hashing (v0.94) means a leaked DB does not yield usable session IDs.
  • Admin routes are client-guarded and server-guarded; the default admin is redirected away from user routes.
  • Markdown rendering uses rehype-sanitize; CSP headers in middleware/securityHeaders.js cover the built assets.
  • Error boundaries prevent route crashes from taking down the whole SPA.
  • PWA: vite-plugin-pwa registers a service worker (autoUpdate) with manifest + icons. Runtime caching uses NetworkFirst for /api/(tracker|bills|calendar|summary|analytics|snowball|categories) with a 5-second network timeout, 30-entry max, 5-minute max-age.

8. Infrastructure and Deployment

package.json

Version: 0.40.0. "type": "commonjs".

Scripts:

  • npm run dev:apinode --watch server.js.
  • npm run dev:ui — Vite dev server.
  • npm run devconcurrently runs API and UI.
  • npm run buildvite build (output: dist/, emptyOutDir: true).
  • npm run check:servernode --check every server JS file under server.js, db, middleware, routes, services, utils.
  • npm run linteslint client (flat config; react-hooks + react-refresh + typescript-eslint).
  • npm run typechecktsc --noEmit -p tsconfig.json.
  • npm run checkcheck:server + build.
  • npm run testnode --test tests/*.test.js (server).
  • npm run test:clientvitest run (client).
  • npm run test:all — both.
  • npm run test:e2e / test:e2e:ui / test:e2e:update / test:e2e:probe — Playwright (chromium desktop + mobile + axe), with e2e/setup/prepare-db.js reseeding the test DB.
  • npm run smoke:prodbash scripts/prod-smoke.sh.
  • npm run cilint + typecheck + check:server + test:all + build. Runs in Forgejo Actions on every push and PR (.forgejo/workflows/ci.yml, container node:22-bookworm).
  • npm startnode server.js.

Key runtime dependencies:

  • Express 4, cookie-parser, cors, express-rate-limit.
  • better-sqlite3 12.
  • bcryptjs.
  • openid-client 5.
  • @simplewebauthn/server 13, @simplewebauthn/browser 13.
  • otplib 13, qrcode 1.
  • nodemailer 8.
  • node-cron 4.
  • React 19, React DOM 19, React Router 6, TanStack Query 5, TanStack Virtual 3, framer-motion 12.
  • shadcn/ui component primitives, Radix UI primitives, lucide-react, Tailwind utilities, Sonner toasts.
  • xlsx for spreadsheet import/export.

Key dev dependencies:

  • typescript 6, @types/react 19, @types/react-dom 19, typescript-eslint 8.
  • eslint 9, @eslint/js, eslint-plugin-react-hooks 5, eslint-plugin-react-refresh 0.4, globals 17.
  • vite 5, @vitejs/plugin-react 4, vite-plugin-pwa 1, babel-plugin-react-compiler 1.
  • vitest 4, @testing-library/react 16, @axe-core/playwright 4, @playwright/test 1.
  • tailwindcss 3, autoprefixer 10, postcss 8.

Dockerfile

Multi-stage build:

  1. Builder: node:22-alpine, installs build deps python3 make g++, runs npm install, copies source, runs npm run build.
  2. Runtime: node:22-alpine, installs bash nano su-exec, creates non-root bill user, copies built app, creates /data/db, /data/backups, /app/backups, sets ownership and restrictive permissions.

Runtime environment:

  • NODE_ENV=production
  • PORT=3000
  • DB_PATH=/data/db/bills.db
  • BACKUP_PATH=/data/backups

Exposes port 3000, declares volume /data, entrypoint docker-entrypoint.sh, command node server.js.

docker-compose.yml

Service: bill-tracker.

  • Image: dream.scheller.ltd/null/billtracker:latest.
  • Container name: bill-tracker.
  • Ports: host 3030 to container 3000.
  • Volume: /portainer/hosting/bill-tracker/data:/data.
  • Restart: unless-stopped.
  • Environment includes INIT_ADMIN_USER, INIT_ADMIN_PASS, and CSRF cookie settings (CSRF_HTTP_ONLY: "false", CSRF_SAME_SITE: "strict", CSRF_SECURE: "true", CSRF_COOKIE_NAME: "bt_csrf_token").

Important deployment note: the compose file currently sets CSRF_SECURE: "true"; for plain HTTP development this prevents CSRF cookies from being sent by browsers. Use HTTPS or override to false only in local/dev.

CI (.forgejo/workflows/ci.yml)

Forgejo Actions runs on every push and PR: actions/checkout@v4 on node:22-bookworm (matches Dockerfile runtime so better-sqlite3 prebuilds resolve identically), then npm ci and npm run ci. The workflow is the only place a build is fully re-verified end-to-end (lint + typecheck + server tests + client tests + build).

Vite (vite.config.mjs)

  • @vitejs/plugin-react with babel-plugin-react-compiler enabled (React 19 auto-memoization). The codebase is rules-of-hooks clean (enforced by eslint-plugin-react-hooks).
  • vite-plugin-pwa with registerType: 'autoUpdate', manifest, icons in client/public/img/, runtime caching for /api/(tracker|bills|calendar|summary|analytics|snowball|categories) via NetworkFirst (5s network timeout, 30-entry max, 5-min max-age).
  • __APP_VERSION__ is injected at build time from package.json; the SPA reads it through client/lib/version.ts.
  • Manual chunk splitting: vendor-react (react / react-dom / react-router-dom), vendor-radix (dialog / select / dropdown-menu / tabs / tooltip / alert-dialog), vendor-motion (framer-motion), vendor-query (@tanstack/react-query + @tanstack/react-virtual).
  • resolve.alias['@']client/, matches the Vite alias and the tsconfig.json paths mapping.
  • server.port: 5173, proxies /api to http://localhost:${API_PORT || PORT || 3000}.
  • test.environment: 'node'; tests opt into jsdom via @vitest-environment per-file.
  • test.include: ['client/**/*.test.{js,jsx}']; note: client tests on .ts are currently run via the tests/*.test.js server suite (which still exercises the same behaviors). The Vitest include is restricted to .js/.jsx to keep current test coverage green during the gradual TS migration.

TypeScript (tsconfig.json)

  • target: ES2022, lib: [ES2023, DOM, DOM.Iterable].
  • module: ESNext, moduleResolution: bundler, jsx: react-jsx.
  • paths: { "@/*": ["./client/*"] }, resolveJsonModule, esModuleInterop, isolatedModules, skipLibCheck, noEmit.
  • allowJs: true, checkJs: false — legacy .js/.jsx keep resolving and running untouched; only .ts/.tsx are type-checked. Convert file-by-file.
  • strict: true, noUncheckedIndexedAccess: true.
  • types: ["vite/client"] — provides import.meta.env and __APP_VERSION__ typings.
  • include: ["client/**/*"], exclude: ["node_modules", "dist", "client/**/*.test.*"].

ESLint (eslint.config.mjs)

Flat config scoped to the React client. The point is enforcement of the two rules that catch real React bugs — rules-of-hooks (conditional hooks) and exhaustive-deps (stale closures) — which nothing previously checked. Server code (CommonJS Node) is out of scope here; npm run check:server covers it.

  • JS/JSX block (client/**/*.{js,jsx}): js.configs.recommended + react-hooks + react-refresh. react-hooks/rules-of-hooks: error, react-hooks/exhaustive-deps: warn, react-refresh/only-export-components: warn (with allowConstantExport: true).
  • TS/TSX block (client/**/*.{ts,tsx}): same react rules via the typescript-eslint parser. no-undef and no-unused-vars off (TypeScript handles them); @typescript-eslint/no-unused-vars: warn with ^[A-Z_] ignore pattern.
  • no-empty: warn with allowEmptyCatch: true.
  • Ignores: dist/**, node_modules/**, coverage/**, client/**/*.test.*.

9. Auth and Security Flows

Local login

  1. Client calls GET /auth/mode to determine local/OIDC visibility.
  2. Client submits POST /auth/login.
  3. Server checks local_login_enabled, validates credentials through bcrypt (cost 12), rejects inactive users, cleans expired sessions, creates a 7-day session, hashes the session token for storage (v0.94), and sets the bt_session cookie with the raw opaque ID.
  4. Server sets bt_session cookie using cookieOpts(req).
  5. Client calls GET /auth/me to populate auth state.

TOTP 2FA

  1. User enables TOTP via GET /auth/totp/setup → scans QR code → POST /auth/totp/enable with {secret, token}. Server encrypts the secret with services/encryptionService and stores 8 recovery codes (hashed).
  2. On subsequent logins, after a successful POST /auth/login, the server returns { totp_required: true, challenge_id }. The SPA prompts for the 6-digit code and submits it (or a recovery code) with the challenge id.
  3. POST /auth/totp/challenge accepts {username, password} and returns a 5-minute challenge_id; the SPA submits it with the token on the next request.
  4. disableTotp(userId) clears the encrypted secret and recovery codes.

WebAuthn 2FA

  1. User enrolls a security key: GET /auth/webauthn/status (or the admin equivalent) returns current credentials. The SPA calls an internal helper that triggers a registration ceremony; the server stores the credential in webauthn_credentials (AAGUID, transports, backup flags, friendly name).
  2. On login, the SPA requests an assertion via webauthn_login flow; createAuthenticationChallenge returns options + a 15-minute challenge_id; verifyAuthentication updates the sign count and audits webauthn.login.
  3. WEBAUTHN_RP_ID (env, default localhost) and WEBAUTHN_ORIGIN (env, default http://localhost:${PORT}) must match the browser-visible hostname, or registration/assertion will fail.

OIDC login

  1. Client navigates to /api/auth/oidc/login.
  2. Server verifies active OIDC config (the OIDC client secret is decrypted from settings.oidc_client_secret, encrypted at rest by v0.79), creates PKCE state, redirects to provider.
  3. Provider returns to /api/auth/oidc/callback.
  4. Server validates state/nonce, exchanges code, maps/provisions user, creates local session cookie (token hashed in sessions.id), redirects back into SPA.

Password change

  1. Current password and matching new password are required.
  2. New password must be at least 8 chars.
  3. Server updates hash, clears must_change_password, sets last_password_change_at.
  4. Other sessions are invalidated and current session is rotated when possible.

Login history + device fingerprint

  • Every login (successful or failed) writes a user_login_history row.
  • ip_address and user_agent are encrypted at rest (v0.84).
  • location (IP-geolocation city/region/country) is captured only when the user has users.geolocation_enabled = 1 (v1.02).
  • success (v0.85) tracks failed-attempt history separately.
  • session_fingerprint lets the SPA detect a different device for the same session and trigger a re-auth challenge.

Authorization

  • All user data routes enforce owner scope by req.user.id in SQL.
  • Admin-only routes require requireAdmin on server.
  • Default admin cannot use tracker routes.
  • Role changes invalidate target sessions.
  • Deactivation invalidates target sessions.

Secret encryption at rest

  • services/encryptionService.js provides encryptSecret / decryptSecret / assertEncryptionReady using a master key derived with HKDF (v0.78; the prior SHA-256 derivation was migrated in-place).
  • Encrypted at rest: oidc_client_secret (v0.79), SMTP password (v0.77), data_sources.encrypted_secret (SimpleFIN access URLs), user_login_history.ip_address / user_agent (v0.84), users.totp_secret (v0.86).
  • reEncryptWithEnvKey() is invoked by the migration runner when an admin sets ENCRYPTION_KEY after install; called from the runtime error handler when a legacy ciphertext is detected.

Backup safety

  • Managed filename regex and path checks prevent traversal.
  • Uploads are written to temp paths first, validated by SQLite integrity_check + optional SHA-256 checksum, then moved.
  • Restore creates a pre-restore backup.

Import safety

  • Spreadsheet import accepts only XLSX and validates size, sheets, rows, cells, headers, and decisions.
  • User DB import validates SQLite magic, size, required metadata/tables, and maps all data to current user ownership.
  • CSV import preview/apply is split; ambiguous rows are surfaced in errors for the user to resolve.
  • OFX/QFX import is rate-limited via the import limiter; previews have a 24-hour TTL.

Bank sync safety

  • SimpleFIN access URLs are encrypted at rest (data_sources.encrypted_secret).
  • assertEncryptionReady is called before any secret operation.
  • The monitored flag (v0.64) lets users exclude a credit-card account from bill matching.
  • Transactions are deduplicated by provider_transaction_id (unique partial index).
  • Account balance updates run in transactions; partial failures roll back.

10. Operational Notes

Startup behavior

  • DB path is DB_PATH or db/bills.db.
  • DB open logging prints only path.basename(DB_PATH) instead of the full database path, so startup logs identify the file without exposing the full filesystem location.
  • SQLite WAL and foreign keys are enabled.
  • Schema and migrations run automatically (db/migrations/versionedMigrations.js + db/migrations/legacyReconcileMigrations.js).
  • Default categories/settings are seeded (incl. spending defaults for new users in v0.87).
  • Expired sessions are purged at startup; a periodic SESSION_CLEANUP_INTERVAL_MS cleanup interval is scheduled.
  • bankSyncWorker.start() is started; backupScheduler is started; workers/dailyWorker.start() runs the daily jobs.
  • First-run flow: if userCount === 0 after migrations, setup/firstRun.run(db) is invoked.

Environment-seeded regular users use INIT_REGULAR_USER and INIT_REGULAR_PASS. New seeded users are inserted with first_login = 0 and must_change_password = 0. Existing seeded regular users have their password hash updated and both flags reset to 0, then the server audits seed.flag_reset with the username, reset flags, and source: "server-seed". This lets ENV-managed users skip first-login/privacy/password-change gates after seed refreshes.

Environment variables

Common variables used by current code:

  • PORT — server port (default 3000)
  • BIND_HOST — server bind (default 0.0.0.0)
  • DB_PATH
  • BACKUP_PATH
  • INIT_ADMIN_USER
  • INIT_ADMIN_PASS
  • INIT_REGULAR_USER
  • INIT_REGULAR_PASS
  • SESSION_CLEANUP_INTERVAL_MS
  • TRUST_PROXYtrue / 1 / numeric / loopback / CIDR; required behind nginx/Traefik
  • CORS_ORIGIN — comma-separated allowlist
  • COOKIE_SECURE
  • HTTPS
  • CSRF_HTTP_ONLY
  • CSRF_SAME_SITE
  • CSRF_SECURE
  • CSRF_COOKIE_NAME
  • OIDC_ISSUER_URL
  • OIDC_CLIENT_ID
  • OIDC_CLIENT_SECRET — encrypted at rest (v0.79); the settings-table copy is the source of truth
  • OIDC_REDIRECT_URI
  • OIDC_TOKEN_AUTH_METHOD
  • WEBAUTHN_RP_ID — default localhost
  • WEBAUTHN_ORIGIN — default http://localhost:${PORT}
  • ENCRYPTION_KEY — master key for at-rest encryption (HKDF, v0.78)
  • API_PORT — Vite dev-server proxy target (defaults to PORT || 3000)
  • DATA_IMPORT_ENABLED — gates CSV import (defaults to true)

Most notification, OIDC, backup, cleanup, and auth-mode settings are also stored in the settings table and managed from Admin UI.

Known code characteristics to preserve

  • Use transactions for multi-step destructive or bulk DB changes.
  • Keep user-owned SQL scoped by req.user.id.
  • Keep admin lockout protection before changing login methods.
  • Do not expose password_hash, session IDs (raw or hashed), OIDC client secret, encrypted secret blobs, or internal backup paths in API responses.
  • Keep import preview/apply separated so users can resolve ambiguous spreadsheet / CSV / OFX / user-DB data before DB writes.
  • Branded Dollars / Cents types in client/lib/money.ts are the source of truth for client money handling. A bare number at a fmt() / formatUSD / formatCentsUSD boundary is a compile error in typed code. The money.type-test.ts guard uses @ts-expect-error so a regression in the branding fails tsc --noEmit.
  • Tailwind's content glob must include ts,tsx (./client/**/*.{js,jsx,ts,tsx}). Without it, classes used only in .tsx files generate no CSS and produce a runtime-only failure that the e2e probe (not typecheck/build/tests) is the only gate that catches.
  • DB path support: db/database.js uses path.basename(DB_PATH) in logging to anonymize the DB path while still providing useful diagnostic information. Absolute and relative paths are both supported.
  • React Compiler (babel-plugin-react-compiler) requires a rules-of-hooks-clean codebase; do not introduce conditional hooks.
  • React Query v5: use gcTime, not cacheTime (silently ignored in v5).

11. Verification Checklist Used for This Reference

Reviewed current code sources:

  • server.js (including TRUST_PROXY handling, health probe, route mount order, post-listen worker startup).
  • All 26 route files under routes/.
  • All 41 service files under services/ (incl. bankSync*, simplefinService, ofxImportService, merchantStoreMatchService, advisoryFilterService, totpService, webauthnService, loginFingerprint, driftService, aprService, paymentAccountingService, encryptionService, calendarFeedService).
  • All middleware files (csrf, errorFormatter, rateLimiter, requireAuth, securityHeaders).
  • db/schema.sql, db/database.js, db/migrations/versionedMigrations.js, db/migrations/legacyReconcileMigrations.js, db/subscriptionCatalogSeed.js.
  • Actual initialized SQLite schema via better-sqlite3 introspection (PRAGMA table_info, PRAGMA index_list).
  • client/main.tsx, client/App.tsx, client/api.ts, client/types.ts, client/contexts/ThemeContext.tsx.
  • client/hooks/useAuth.tsx, client/hooks/useQueries.ts, client/hooks/usePaymentActions.ts, client/hooks/useAutoSave.ts, client/hooks/useSearchPanelPreference.ts.
  • Page inventory under client/pages/ (24 files) and component inventory under client/components/ (incl. admin/, bill-modal/, tracker/, transactions/, snowball/, data/, layout/, ui/).
  • client/lib/ (money.ts, utils.ts, trackerUtils.ts, billDrafts.ts, billingSchedule.ts, cashflowUtils.ts, trackerTableColumns.ts, reorder.ts, version.ts).
  • package.json (version 0.40.0; "type": "commonjs"), tsconfig.json, eslint.config.mjs, vite.config.mjs.
  • Dockerfile (node:22-alpine), docker-compose.yml, docker-entrypoint.sh, .forgejo/workflows/ci.yml.

End-to-end checks run against the codebase:

  • find client -name '*.jsx' returns 0 — the JSX → TSX migration is complete.
  • find client -name '*.ts' and find client -name '*.tsx' together cover every previously .jsx file in components/, pages/, hooks/, contexts/, plus the API client and lib/.
  • grep on package.json for React 19, TypeScript 6, Vite 5, TanStack Query 5, @simplewebauthn/* 13, otplib 13.
  • npm run typecheck was green at the time of the v0.41.0 entry in HISTORY.md (0 errors); 48 client unit tests pass; 17/17 e2e probe (every page renders, all API paths respond).
  • node --check on every server JS file under server.js, db, middleware, routes, services, utils (npm run check:server).

The previous manual (v0.28.1) contained stale route/page descriptions and missed major features added between v0.28 and v0.40 (subscriptions, spending, snowball plans, SimpleFIN, WebAuthn, TOTP, push notifications, category groups, calendar feed, money-cents migration in progress, React 18 → 19 + TypeScript migration, branded money types). This version replaces that with a current-state engineering reference.