BillTracker/docs/Engineering_Reference_Manua...

72 KiB

Engineering Reference Manual — Bill Tracker

Status: Current code reference Last Updated: 2026-05-16 Version: 0.28.1 Primary stack: Node.js + Express, React + Vite, Tailwind CSS + shadcn/ui, Sonner, SQLite via better-sqlite3

This manual reflects the current application code in server.js, routes/, services/, middleware/, db/, client/, package.json, Dockerfile, and docker-compose.yml. It is written as a current-state reference, not a changelog.


1. System Overview

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

  • Local username/password authentication, optional single-user mode, and optional Authentik/OIDC login.
  • User-scoped bills, categories, payments, monthly bill overrides, monthly income, and starting cash buckets.
  • Admin user management, backup/restore, cleanup, auth-mode/OIDC configuration, status checks, and migration rollback.
  • Spreadsheet and user-SQLite import workflows.
  • CSV, Excel, and user-SQLite export workflows.
  • SMTP-based bill due notifications.
  • React SPA frontend with protected user/admin routes and CSRF-protected JSON APIs.

Runtime flow:

  1. server.js initializes SQLite through db/database.js, runs schema/migrations, seeds defaults/admin user, cleans expired sessions, then starts Express.
  2. Express applies security headers, JSON body parsing, cookies, CSRF token provisioning, route-level CSRF/auth/rate-limit middleware, static dist/ serving, and JSON error formatting.
  3. Route files under routes/ validate input, enforce ownership through req.user.id, and use service modules for business logic.
  4. React under client/ calls /api/* through client/api.js with credentials: include and CSRF headers on mutating methods.

2. Project Layout

  • server.js — Express entry point and route mounting.
  • routes/ — HTTP API handlers:
    • auth.js — login, logout, password change, OIDC callback.
    • bills.js — bills CRUD, auto-mark paid, history.
    • payments.js — payments CRUD, status matching, snowball handling.
    • categories.js — category CRUD, tree support.
    • tracker.js — monthly tracker data, bucket resolution, cycle handling.
    • summary.js — summary stats, starting amounts.
    • analytics.js — expense reports, category breakdown.
    • settings.js — user settings, admin config, notification settings.
    • notifications.js — notification management.
    • profile.js — user profile, demo data.
    • import.js — CSV, Excel, user-SQLite import workflows.
    • export.js — CSV, Excel, SQLite export.
    • status.js — system status, health.
    • dataSources.js — new — data sources CRUD with sync status.
    • transactions.js — new — transaction CRUD, match/ignore/commit actions.
    • matches.js — new — match suggestions, rejection tracking.
  • services/ — business logic modules.
  • middleware/ — auth guards, CSRF, rate limits, security headers, error formatting.
  • db/schema.sql — base SQLite schema.
  • db/database.js — DB connection, migrations, defaults, settings, rollback support.
  • workers/dailyWorker.js — scheduled notification/cleanup worker entry.
  • client/ — React SPA.
  • dist/ — generated Vite build output served by Express.
  • Dockerfile, docker-compose.yml, docker-entrypoint.sh — container deployment.

3. Backend Entry Point and Middleware

server.js

Server defaults:

  • Port: PORT || 3000.
  • Static frontend: dist/.
  • Optional CORS: enabled when CORS_ORIGIN is set; credentials allowed.
  • 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.

Global middleware order:

  1. securityHeaders
  2. optional cors
  3. express.json()
  4. cookieParser()
  5. csrfTokenProvider
  6. mounted API routers with route-level rate-limit/auth/CSRF middleware
  7. static legacy/, redirect /login.html to /login, static dist/
  8. SPA fallback GET * serving dist/index.html after ensuring a CSRF token cookie
  9. errorFormatter
  10. 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.js can read the token cookie and send x-csrf-token; do not enable httpOnly CSRF cookies unless token delivery changes.
  • 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.
  • 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.

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.
  • 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/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.
  • 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/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.
  • 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.

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/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.

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/paymentValidation.js

Payment validation helpers for source tracking and matching:

  • Validates payment_source values (manual, file_import, provider_sync).
  • Supports transaction linking via transaction_id when available.

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, and migration rollback attempts.

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. User routes are scoped to the authenticated user unless noted.

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

Mounted under /api/auth.

  • POST /auth/login

    • Body: {username, password}.
    • Validation: both required; local login must be enabled.
    • Response: sets bt_session; {user}.
  • POST /auth/logout

    • Auth: required.
    • Body: none.
    • Response: clears cookie; {success:true}.
  • POST /auth/logout-all

    • Auth: required; CSRF skip is set before the router mount, matching other auth/session mutation routes.
    • Body: none.
    • Behavior: deletes every session for the current user by calling invalidateOtherSessions(userId, null), also deletes the current cookie session, audits logout.all, clears bt_session, and returns {success:true}.
  • GET /auth/me

    • Auth: required unless single-user mode supplies user.
    • Response: public user object.
  • GET /auth/mode

    • Public.
    • Response: auth mode, local-login flag, OIDC public info, single-user status.
  • POST /auth/restore-multi-user-mode

    • Auth: required.
    • Body: none.
    • Response: restores multi-user mode where allowed.
  • POST /auth/acknowledge-privacy

    • Auth: required.
    • Body: none.
    • Response: updates first-login/privacy acknowledgement flags.
  • POST /auth/change-password

    • Auth: required; password limiter; CSRF skip is set before the router mount.
    • Body: {current_password, new_password}.
    • Validation: current password required unless must_change_password is set; new password min 8.
    • Behavior: updates password hash, clears must_change_password, updates last_password_change_at, invalidates all other sessions, rotates the current session ID when a valid bt_session exists, sets the new cookie, audits password.change, and returns {success:true}.
  • GET /auth/has-users

    • Response: whether non-default users exist.
  • GET /auth/users

    • Auth: admin.
    • Response: safe user list.
  • POST /auth/users

    • Auth: admin.
    • Body: {username, password}.
    • Validation: username min 3, password min 8, unique username.
    • Response: created safe user.

5.2 OIDC Auth

Mounted under /api/auth/oidc; OIDC rate limiter applies.

  • GET /auth/oidc/login?redirect_to=/path

    • Public when OIDC active.
    • Creates PKCE state and redirects to provider authorization URL.
  • GET /auth/oidc/callback?code=&state=

    • Public callback.
    • 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

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

  • GET /admin/has-users

    • Response: {has_users:boolean} for users other than current admin.
  • GET /admin/users

    • Response: safe users ordered by default-admin, role, username.
  • POST /admin/users

    • Body: {username, password}.
    • Validation: username min 3, password min 8, unique.
    • Response 201: created user.
  • PUT /admin/users/:id/password

    • Body: {password}.
    • Validation: password min 8; user exists.
    • Response: {success:true}; invalidates target sessions and requires password change.
  • PUT /admin/users/:id/role

    • Body: {role:"admin"|"user"}.
    • Validation: cannot change own role; cannot remove last admin.
    • Response: updated safe user; invalidates target sessions; audits role change.
  • PUT /admin/users/:id/active

    • Body: {active:boolean}.
    • Validation: user exists; cannot deactivate self.
    • Response: updated safe user; deactivation invalidates sessions.
  • DELETE /admin/users/:id

    • Validation: user exists; cannot delete self.
    • Response: {success:true, deleted_user_id}; transaction deletes import sessions/history, sessions, and user.
  • POST /admin/backups

    • Body: none.
    • Response 201: backup metadata {id, filename, size_bytes, checksum, ...}.
  • GET /admin/backups

    • Response: {backups:[metadata...]}.
  • GET /admin/backups/settings

    • Response: backup schedule status/settings.
  • PUT /admin/backups/settings

    • Body: {enabled, frequency:"daily"|"weekly", time:"HH:MM", retention_count}.
    • Validation: frequency, valid time, retention 1-365.
    • Response: saved schedule status.
  • POST /admin/backups/run-scheduled-now

    • Body: none.
    • Response 201: scheduled backup result.
  • POST /admin/backups/import

    • Content-Type: application/octet-stream, application/x-sqlite3, or application/vnd.sqlite3.
    • Body: raw SQLite backup, max 100 MB.
    • Optional checksum: X-Checksum-Sha256 header or ?checksum=.
    • Response 201: imported backup metadata.
  • GET /admin/backups/:id/download

    • Response: file download. ID must be a managed backup filename.
  • POST /admin/backups/:id/restore

    • Response: {restored_from, pre_restore_backup}; validates and restores managed backup.
  • DELETE /admin/backups/:id

    • Response: {deleted:true, id, deleted_at}.
  • GET /admin/cleanup

    • Response: cleanup settings and last result.
  • PUT /admin/cleanup

    • Body: any of {import_sessions_enabled, temp_exports_enabled, temp_export_max_age_hours, backup_partials_enabled, import_history_enabled, import_history_max_age_days}.
    • Validation: booleans; temp export age 1-72 hours; import history age 30-3650 days.
    • Response: updated cleanup status.
  • POST /admin/cleanup/run

    • Response: cleanup run result by task.
  • GET /admin/auth-mode

    • Response: local/single-user/OIDC settings and lockout warnings. Client secret is not returned.
  • POST /admin/auth-mode/oidc-test

    • Body: submitted or saved OIDC config fields.
    • Response: {ok:true,...} or 400 with test error. Never returns secret/token material.
  • PUT /admin/auth-mode

    • Body: legacy {auth_mode, default_user_id} plus OIDC/local settings such as local_login_enabled, oidc_login_enabled, oidc_issuer_url, oidc_client_id, oidc_client_secret, oidc_redirect_uri, oidc_scopes, oidc_admin_group, oidc_auto_provision.
    • Validation: cannot disable all login methods; cannot disable local login until OIDC is configured, enabled, and has an admin group; cannot enable incomplete OIDC.
    • Response: {success:true, ...authModeStatus}.
  • POST /admin/migrations/rollback

    • Body: {version:"v0.44"|"v0.45"|"v0.46"}.
    • Validation: version required; migration must be present in schema_migrations; ROLLBACK_SQL_MAP must define rollback SQL for that version.
    • Behavior: calls rollbackMigration(version) from db/database.js, audits success as migration.rollback, and returns {success:true, version, description, elapsed_ms}.
    • Error mapping: NOT_APPLIED becomes HTTP 404 with {error}; ROLLBACK_NOT_SUPPORTED becomes HTTP 422 with {error}; other rollback failures become HTTP 500 with {error:"Rollback failed", details} and are audited as migration.rollback.failure.

5.4 Bills

Mounted under /api/bills; auth: user/admin tracker access.

Bill object fields include id, user_id, name, category_id, category_name, due_day, override_due_date, bucket, expected_amount, interest_rate, billing_cycle, autopay_enabled, autodraft_status, website, username, account_info, has_2fa, active, notes, history_visibility, is_seeded, cycle_type, cycle_day, timestamps, and has_history_ranges on list/detail queries.

Validation shared by create/update:

  • name required for create.
  • due_day: integer 1-31.
  • expected_amount: numeric, defaults to 0.
  • interest_rate: null/empty or number 0-100.
  • category_id: must belong to current user.
  • history_visibility: default, all, ranges, or none.
  • cycle_type: monthly, weekly, biweekly, quarterly, annual.
  • cycle_day: monthly 1-31; weekly/biweekly day name; quarterly/annual text up to 50 chars.

Endpoints:

  • GET /bills?inactive=true

    • Response: current user's bills; inactive excluded unless inactive=true.
  • GET /bills/:id

    • Response: one owned bill or 404.
  • POST /bills

    • Body: bill fields.
    • Response 201: created bill.
  • PUT /bills/:id

    • Body: partial bill fields.
    • Response: updated bill.
  • DELETE /bills/:id

    • Hard delete; cascades payments, monthly state, history ranges.
    • Response: {success:true, deleted_bill_id, deleted_bill_name, warning}.
  • GET /bills/:id/monthly-state?year=&month=

    • Validation: year 2000-2100; month 1-12.
    • Response: {bill_id, year, month, actual_amount, notes, is_skipped}.
  • PUT /bills/:id/monthly-state

    • Body: {year, month, actual_amount, notes, is_skipped}.
    • Validation: year/month; actual_amount null or non-negative.
    • Response: saved monthly state with timestamps.
  • GET /bills/:id/payments?page=1&limit=20

    • Validation: limit capped at 100.
    • Response: {bill_id, bill_name, total, page, limit, pages, payments}.
  • POST /bills/:id/toggle-paid

    • Body optional: {amount, paid_date, method, notes}.
    • If latest live payment exists, soft-deletes it. Otherwise creates a payment for amount or bill expected amount.
    • Response: {success, isPaid, action, payment?}.
  • GET /bills/:id/history-ranges

    • Response: {bill_id, history_visibility, ranges}.
  • POST /bills/:id/history-ranges

    • Body: {start_year, start_month, end_year?, end_month?, label?}.
    • Validation: years 2000-2100, months 1-12, end both present/absent and not before start.
    • Response 201: created range.
  • PUT /bills/:id/history-ranges/:rangeId

    • Body: partial range fields.
    • Response: updated range.
  • DELETE /bills/:id/history-ranges/:rangeId

    • Response: {success:true}.

5.5 Payments

Mounted under /api/payments; auth: user/admin tracker access. All queries are user-owned through joined bill ownership. Delete is soft delete via deleted_at.

  • GET /payments?bill_id=&year=&month=

    • Validation: year and month must be supplied together; year 2000-2100; month 1-12.
    • Response: live payments ordered descending by paid_date.
  • GET /payments/:id

    • Response: live payment or 404.
  • POST /payments

    • Body: {bill_id, amount, paid_date, method?, notes?, payment_source?}.
    • Validation: bill exists and owned; amount > 0; required fields present; payment_source one of manual, file_import, provider_sync, transaction_match.
    • Response 201: created payment.
  • POST /payments/quick

    • Body: {bill_id, amount?, paid_date?, method?, notes?, payment_source?}.
    • Defaults amount to bill expected amount and date to today; confirms autodraft status for autopay bills; defaults payment_source to manual.
    • Response 201: created payment.
  • POST /payments/bulk

    • Body: {payments:[{bill_id, amount, paid_date, method?, notes?, payment_source?}]}.
    • Validation: array required; max 50; bill_id integer; paid_date YYYY-MM-DD; amount finite >= 0; payment_source must be valid.
    • Duplicate live payments by user/bill/date/amount are skipped.
    • Response 201: {created:[...], skipped:[...], errors:[...]}.
  • PUT /payments/:id

    • Body: partial {amount, paid_date, method, notes, payment_source}.
    • Response: updated payment. Current code preserves existing fields when omitted.
  • DELETE /payments/:id

    • Response: {success:true} after setting deleted_at.
  • POST /payments/:id/restore

    • Response: restored payment with deleted_at:null.

5.6 Categories

Mounted under /api/categories; auth: user/admin tracker access.

  • GET /categories

    • Seeds default categories for user if needed.
    • Response: categories with bill counts/payment counts and bill summaries.
  • POST /categories

    • Body: {name}.
    • Validation: non-empty name; unique per user case-insensitive.
    • Response 201: created category.
  • PUT /categories/:id

    • Body: {name}.
    • Validation: category belongs to user; non-empty unique name.
    • Response: updated category.
  • DELETE /categories/:id

    • Validation: category belongs to user.
    • Behavior: transaction nulls category on owned bills, then deletes category.
    • Response: deletion summary.

5.7 Tracker and Calendar

  • GET /tracker?year=&month=

    • Auth: user/admin tracker access.
    • Defaults to current year/month.
    • Response includes year, month, tracker rows, totals, starting amount info, previous month paid total, three-month averages/trends, and generated timestamp.
    • Row fields come from buildTrackerRow plus monthly override state and previous-month payment data.
  • GET /tracker/upcoming?days=30

    • Auth: user/admin tracker access.
    • Response: upcoming active bills in the requested horizon.
  • GET /calendar?year=&month=

    • Auth: user/admin tracker access.
    • Defaults current year/month.
    • Response: month days with payment entries, bills/due-status entries, and totals {expectedTotal, paidTotal, remainingTotal, paidPercent}.

5.8 Summary and Starting Amounts

  • GET /summary?year=&month=

    • Auth: user/admin tracker access.
    • Validation: valid year/month.
    • Response: {year, month, income, expenses, starting_amounts, previous_month, summary, chart, generated_at}.
  • PUT /summary/income

    • Body: {year, month, amount, label?}.
    • Validation: valid year/month; amount 0-1,000,000,000; label trimmed to 80 chars.
    • Response: {year, month, income} after upsert into monthly_income.
  • GET /monthly-starting-amounts?year=&month=

    • Response: {year, month, first_amount, fifteenth_amount, other_amount, combined_amount, paid deductions, remaining values, notes}.
  • PUT /monthly-starting-amounts

    • Body: {year, month, first_amount, fifteenth_amount, other_amount, notes?}.
    • Validation: valid year/month; numeric amounts.
    • Response: recomputed starting-amount response after upsert.

5.8.1 Snowball

Mounted under /api/snowball; auth: user/admin tracker access.

  • GET /snowball

    • Response: current user's debt bills (snowball_include or debt-like categories), pre-sorted by snowball_order.
  • GET /snowball/settings

    • Response: {extra_payment, ramsey_mode, ready_current_on_bills, ready_emergency_fund}.
  • PATCH /snowball/settings

    • Body: {extra_payment?, ramsey_mode?, ready_current_on_bills?, ready_emergency_fund?}.
    • Response: saved settings and computed response.
  • GET /snowball/projection

    • Response: {snowball, avalanche, minimum_only, comparison} with enriched debt arrays including APR snapshots.
  • PATCH /snowball/order

    • Body: [{id, snowball_order}].
    • Response: {success:true} after batch update.

5.9 Analytics

  • GET /analytics/summary?year=&month=&months=&category_id=&bill_id=&include_inactive=true&include_skipped=false
    • Auth: user/admin tracker access.
    • Validation: year/month valid; months clamped by route validation; IDs parsed as integers.
    • Response includes monthly spending, expected vs actual, category totals, bill totals, filters, and generated timestamp.

5.10 Settings

  • GET /settings

    • Auth: user/admin tracker access.
    • Response: user-visible settings from the allowed settings key list.
  • PUT /settings

    • Auth: user/admin tracker access.
    • Body: key/value object for allowed user setting keys.
    • Response: updated settings object.
  • POST /settings/seed-demo-data

    • Auth: user/admin tracker access.
    • Response: demo seed result.

5.11 Notifications

Mounted under /api/notifications. Server mount requires requireAuth; route-level guards further restrict.

  • GET /notifications/admin

    • Auth: admin.
    • Response: global SMTP/notification settings.
  • PUT /notifications/admin

    • Auth: admin.
    • Body: allowed global SMTP and notification settings.
    • Response: saved settings.
  • POST /notifications/test

    • Auth: admin.
    • Body: {to}.
    • Validation: recipient required.
    • Response: send result or SMTP error.
  • GET /notifications/me

    • Auth: user/admin tracker access.
    • Response: current user's notification email and toggle settings.
  • PUT /notifications/me

    • Auth: user/admin tracker access.
    • Body: {notification_email, notifications_enabled, notify_3d, notify_1d, notify_due, notify_overdue}.
    • Response: saved user notification settings.

5.12 Profile

Mounted under /api/profile; auth: user/admin tracker access; password limiter applies at mount.

  • GET /profile

    • Response: safe profile, notification settings, export URLs, import-history URL.
  • PATCH /profile

    • Body: {display_name}.
    • Validation: string, max 100 chars.
    • Response: {success:true, profile}.
  • GET /profile/settings

    • Response: user notification preferences only.
  • PATCH /profile/settings

    • Body: {notification_email|email, notifications_enabled, notify_3d, notify_1d, notify_due, notify_overdue}.
    • Validation: email value string/null, max 255 chars.
    • Response: {success:true}.
  • POST /profile/change-password

    • Body: {current_password, new_password, confirm_new_password}.
    • Validation: all required; confirmation matches; new password min 8; current password must verify.
    • Response: rotates current session, invalidates others, {success:true}.
  • GET /profile/exports

    • Response: metadata for user_db and user_excel export URLs.
  • GET /profile/import-history

    • Response: {history:[...]}.

5.13 User Demo Data

Mounted under /api/user; auth: user/admin tracker access.

  • POST /user/seed-demo-data

    • Response: seed result for current user.
  • POST /user/clear-demo-data

    • Rate-limited by demo-data limiter.
    • Behavior: deletes is_seeded=1 bills/categories for current user and records import history.
    • Response: deletion counts.

5.14 Import

Mounted under /api/import; auth: user/admin tracker access; import limiter applies.

  • POST /import/spreadsheet/preview?parse_all_sheets=true&year=&month=

    • Content-Type: application/octet-stream.
    • Headers: optional X-Filename.
    • Body: XLSX file buffer, max 10 MB.
    • Response: preview session, parsed rows, categories/bills/payment candidates, ambiguous decisions, errors.
  • POST /import/spreadsheet/apply

    • Body: {import_session_id, decisions, options}.
    • Validation: session exists, not expired, belongs to user.
    • Response: created/updated/skipped/error counts and import history summary.
  • POST /import/user-db/preview

    • Content-Type: application/octet-stream.
    • Headers: optional X-Filename.
    • Body: user SQLite export file, max 50 MB.
    • Response: preview session and sanitized import plan.
  • POST /import/user-db/apply

    • Body: {import_session_id, options}.
    • Validation: session exists, not expired, belongs to user.
    • Response: apply result with ID mappings/counts.
  • GET /import/history

    • Response: current user's import history.

5.15 Data Sources

Mounted under /api/data-sources; auth: user/admin tracker access.

  • GET /data-sources?type=&status=
    • Query params: type (manual, file_import, provider_sync), status (active, inactive, error).
    • Response: array of data sources with source_label, source_type_label, account_count, transaction_count, safe fields (encrypted_secret excluded), timestamps, sync info.

5.16 Transactions

Mounted under /api/transactions; auth: user/admin tracker access.

  • GET /transactions?limit=&offset=&match_status=&ignored=&source_type=&start_date=&end_date=&q=&data_source_id=&account_id=&matched_bill_id=

    • Response: paginated list of transactions with embedded data_source and account details, source_label, source_type_label.
    • Filter: match_status (unmatched, matched, ignored), ignored (boolean), source_type, date range, free-text search (q) across description/payee/memo/category.
  • POST /transactions/manual

    • Body: {account_id?, transaction_type?, posted_date, transacted_at?, amount, currency?, description?, payee?, memo?, category?, matched_bill_id?, match_status?, ignored?}.
    • Response 201: created transaction with manual source_type.
  • PUT /transactions/:id

    • Body: partial transaction fields.
    • Validation: match_state changes use dedicated endpoints.
    • Response: updated transaction.
  • DELETE /transactions/:id

    • Behavior: soft-deletes via unmatch then hard delete.
    • Response: {success:true, deleted:true, id}.
  • POST /transactions/:id/match

    • Body: {billId}.
    • Response: {success:true, matched:true, transaction}.
  • POST /transactions/:id/unmatch

    • Response: {success:true, unmatched:true, transaction}.
  • POST /transactions/:id/ignore

    • Response: {transaction} with match_status='ignored', ignored=1.
  • POST /transactions/:id/unignore

    • Response: {transaction} restored to unmatched state.

5.17 CSV Import

Mounted under /api/import; auth: user/admin tracker access; import limiter applies. Gated by DATA_IMPORT_ENABLED env var (defaults to true).

  • POST /import/csv/preview

    • Content-Type: text/csv.
    • Body: raw CSV.
    • Response: {import_session_id, headers, sampleRows, rowCount, suggestedMapping, errors, fields}.
  • POST /import/csv/commit

    • Body: {import_session_id, mapping, options?}.
    • Response: {imported, skipped, failed, details}.
  • GET /import/history

    • Response: current user's import history.

5.18 Match Suggestions

Mounted under /api/matches; auth: user/admin tracker access.

  • GET /matches/suggestions?transaction_id=&bill_id=&limit=&offset=

    • Response: {suggestions:[{id, transaction, bill, score, match_status, created_at}]}.
  • POST /matches/:id/reject

    • Response: {success:true} after recording rejection.

5.19 Export

Mounted under /api/export; auth: user/admin tracker access; export limiter applies.

  • GET /export?year=YYYY&format=csv|xlsx

    • Response: file download of payment/bill history for the requested year. CSV includes date, bill, category, expected, paid, method, notes, actual amount, monthly notes. XLSX includes enriched rows.
  • GET /export/user-excel

    • Response: Excel workbook with user categories, bills, payments, monthly state, monthly starting amounts, and notes/metadata.
  • GET /export/user-db

    • Response: portable SQLite file with export metadata and user-owned categories, bills, payments, monthly state, monthly starting amounts, and notes.

5.20 Status

  • GET /status
    • Auth: admin.
    • Response: app version, uptime, runtime worker state, DB health/counts/path/size, SMTP configuration status, backup status/schedule, current-month tracker health, recent errors.

5.21 About and Version

  • GET /about

    • Public.
    • Response: package version and public project/about metadata.
  • GET /about-admin

    • Auth: admin; admin action limiter; CSRF middleware.
    • Response: package version plus sanitized/redacted FUTURE.md and DEVELOPMENT_LOG.md content.
    • File allowlist only: FUTURE.md, DEVELOPMENT_LOG.md.
  • GET /version

    • Public.
    • Response: current package version and latest structured notes from HISTORY.md.
  • GET /version/history

    • Public.
    • Response: package version and raw history text, or error if unavailable.

5.22 Services

Key service modules:

  • paymentValidation.js — Payment input validation with PAYMENT_SOURCES = ['manual', 'file_import', 'provider_sync', 'transaction_match'] enum, validatePaymentSource(), and validatePaymentInput().

  • csvTransactionImportService.js — CSV parsing, field mapping, SHA-256 deduplication, import session management with preview/commit workflow.

  • transactionService.js — Transaction helpers: ensureManualDataSource(), decorateDataSource(), decorateTransaction().

  • transactionMatchService.js — Match/unmatch transactions to bills: matchTransactionToBill(), unmatchTransaction(), ignoreTransaction(), unignoreTransaction().

  • matchSuggestionService.js — Match suggestion discovery: listMatchSuggestions(), rejectMatchSuggestion(), suggestionCounts().

  • snowballService.js — Debt snowball/avalanche calculations, Ramsey mode support, order updates.

  • dataSourcesService.js — Data source CRUD with ensureManualDataSource() for user-scoped manual sources.

  • monthlyStartingAmountsService.js — Starting cash bucket tracking: first/fifteenth/other bucket amounts, payments, remaining values.

  • auditService.js — Audit logging via logAudit(); lazy-loaded in database.js to avoid circular dependency.

  • emailService.js — Email dispatch with SMTP configuration, templating, retry logic.

  • exportService.js — Export helpers: CSV, XLSX, SQLite user DB export with metadata.

  • csvTransactionImportService.js — CSV parsing, field mapping, SHA-256 deduplication, import session management with preview/commit workflow.

  • trackerService.js — Tracker calculations, cycle detection (weekly/biweekly/quarterly/annual), debt snowball support.

  • statusService.js — Health status, cycle validation, autopay simulation, budget projections.

  • analyticsService.js — Analytics queries: spending, categories, bills, filters.

  • notificationService.js — Bill notifications: due_3d, due_1d, due_today, overdue.

  • authService.js — Auth helpers: login, JWT, password hashing, session management, OIDC integration.

  • userService.js — User CRUD, profile updates, demo data seeding, role changes.

  • settingsService.js — Settings CRUD, allowed keys validation, SMTP/billing/export settings.

  • backupService.js — SQLite backup, retention, schedule.

  • cronService.js — Scheduled tasks: backup, cleanup, auto-mark paid, cycle updates.

5.23 Import and Sync Workflow

Data ingestion follows a layered architecture:

  1. Data Sources (data_sources table)

    • manual: User-created source (one per user, type='manual', provider='manual')
    • file_import: CSV/XLSX imports (provider='csv_transactions', 'spreadsheet')
    • provider_sync: External institution sync (e.g., 'plaid', 'mint')
    • Fields: type, provider, name, status ('active', 'inactive', 'error'), config_json, encrypted_secret, last_sync_at, last_error
  2. Financial Accounts (financial_accounts table)

    • Linked to data_sources via data_source_id
    • One data source can have many accounts
    • Fields: provider_account_id, name, org_name, account_type, currency, balance, available_balance, raw_data
  3. Transactions (transactions table)

    • Linked to data_sources and financial_accounts
    • Source type: manual, file_import, provider_sync
    • Match states: unmatched, matched, ignored
    • Optional provider_transaction_id for deduplication
    • Fields: amount (cents), transaction_type, posted_date, transacted_at, description, payee, memo, category, raw_data, matched_bill_id, match_status, ignored
  4. CSV Import Flow

    • User uploads CSV → /api/import/csv/preview
    • Preview parses headers, suggests field mapping
    • /api/import/csv/commit writes to transactions with source_type='file_import'
    • Import history tracked in import_history with counts
  5. Transaction Matching

    • Manual transactions (source_type='manual') can be matched to bills
    • Match suggestions discovered via matchSuggestionService
    • Users can reject suggestions to avoid重复 suggestions
  6. Provider Sync (future)

    • External sync jobs write to data_sources with type='provider_sync'
    • Financial accounts created per institution account
    • Transactions imported from provider
    • Match suggestions offered for unmatched transactions
  7. Payments (bills → payments)

    • Payments link to transactions via transaction_id for auto-draft
    • payment_source indicates origin: manual, file_import, provider_sync, transaction_match
    • Balance delta tracked for debt payoff
  8. Import Sessions (import_sessions table)

    • Temporary storage for CSV/XLSX previews
    • 1-hour TTL, auto-cleaned
    • Fields: preview_json, expires_at
  9. Import History (import_history table)

    • Audit trail of all imports
    • Fields: imported_at, source_filename, file_type, rows_parsed/created/updated/skipped/errored, options_json, summary_json

5.24 Validation and Services

Key validation and service patterns:

  • paymentValidation.js

    • PAYMENT_SOURCES = ['manual', 'file_import', 'provider_sync', 'transaction_match'] enum
    • validatePaymentSource(value) returns error if not in list
    • validatePaymentInput(body) validates amount, paid_date, bill ownership, payment_source
    • Invalid source returns 400 with VALIDATION_ERROR code
  • CSV Import Service

    • Field suggestion via header analysis
    • SHA-256 hash for deduplication
    • Import session management with preview/commit split
    • Error collection per row with detailed messages
  • Transaction Service

    • ensureManualDataSource(db, userId) creates one manual data source per user
    • decorateTransaction(row) adds source_label, source_type_label
    • decorateDataSource(row) adds source_label, source_type_label, account_count, transaction_count
  • Snowball Service

    • Computes snowball/avalanche orderings
    • Ramsey mode: minimum payments only vs. full extra payment
    • snowball_include bills sorted by snowball_order
    • snowball_exempt bills excluded from ordering
  • Match Suggestion Service

    • listMatchSuggestions(userId, transactionId, billId, limit, offset)
    • rejectMatchSuggestion(userId, transactionId, billId)
    • suggestionCounts(userId)
    • Rejects stored to prevent repeated suggestions
  • Monthly Starting Amounts Service

    • Bucketed amounts: first_amount, fifteenth_amount, other_amount
    • Payment tracking from each bucket
    • Remaining values computed on read
  • Tracker Service

    • Cycle type detection: monthly, weekly, biweekly, quarterly, annually
    • Cycle day mapping for non-standard cycles
    • Auto-mark paid logic for autopay bills
  • Status Service

    • Cycle validation warnings
    • Autopay simulation
    • Budget projections

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

sessions

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

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

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
  • interest_rate REAL CHECK null or 0-100
  • billing_cycle TEXT DEFAULT 'monthly' CHECK ('monthly','quarterly','annually','irregular')
  • 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
  • minimum_payment REAL
  • 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

payments

  • id INTEGER PRIMARY KEY
  • bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE
  • amount REAL NOT NULL
  • paid_date TEXT NOT NULL
  • method TEXT
  • notes TEXT
  • balance_delta REAL
  • payment_source TEXT NOT NULL DEFAULT 'manual'
  • transaction_id INTEGER
  • 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
  • notes TEXT
  • is_skipped INTEGER NOT NULL DEFAULT 0
  • 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
  • created_at TEXT DEFAULT datetime('now')
  • updated_at TEXT DEFAULT datetime('now')
  • Unique intended/current logic: (user_id, year, month) via migration/index.

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 intended/current logic: (user_id, year, month) via migration/index.

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
  • 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_categories_user_name(user_id, name)
  • idx_categories_user_name_unique(user_id, name COLLATE NOCASE)
  • 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_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)

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
  • user_agent TEXT
  • browser TEXT
  • os TEXT
  • device_type TEXT
  • device_fingerprint TEXT
  • 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

Migration system

db/database.js:

  • Reads db/schema.sql in initSchema().
  • Creates schema_migrations.
  • Detects and reconciles legacy databases.
  • Applies ordered migrations only when not already recorded.
  • Validates dependency chains 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.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.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.
  • 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.
  • v0.63 data_sources: partial unique index on (user_id, type, provider) WHERE type='manual' AND provider='manual'.
  • v0.64 transactions: partial unique index on (data_source_id, provider_transaction_id) WHERE provider_transaction_id IS NOT NULL; indexes on (user_id, posted_date), (user_id, match_status, ignored), account_id, matched_bill_id.
  • Unversioned user notification columns are also reconciled.

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:

  • 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.

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 ^18.3.1
  • Vite ^5.4.10
  • React Router ^6.26.2
  • TanStack Query ^5.100.9
  • Tailwind CSS ^3.4.14
  • shadcn/ui component primitives, backed by Radix UI where applicable
  • Sonner/shadcn toast notifications via sonner
  • react-markdown, remark-gfm, rehype-sanitize for markdown rendering

client/main.jsx

Creates the React root and wraps App with global providers including auth/theme where defined.

client/App.jsx

  • Creates a TanStack QueryClient with staleTime 2 minutes, retry 1, no refetch on focus.
  • Uses lazy loading and Suspense with PageLoader for most pages.
  • Wraps route elements in ErrorBoundary.
  • Exposes ReactQueryDevtools.
  • Provides skip link for keyboard users.

Routes:

  • /loginLoginPage public.
  • /aboutAboutPage public.
  • /release-notesReleaseNotesPage public.
  • /adminAdminPage, admin only.
  • /admin/about → admin shell + AboutPage admin, admin only.
  • /admin/roadmap → admin shell + AboutPage admin, admin only.
  • /admin/status → admin shell + StatusPage, admin only.
  • /status → redirects admin to /admin/status.
  • / → authenticated user layout index, TrackerPage.
  • /calendarCalendarPage.
  • /summarySummaryPage.
  • /billsBillsPage.
  • /categoriesCategoriesPage.
  • /analyticsAnalyticsPage.
  • /settingsSettingsPage.
  • /dataDataPage.
  • /profileProfilePage.
  • * under user layout → redirect /.

RequireAuth behavior:

  • Shows loading while auth state is undefined.
  • Redirects unauthenticated users to /login.
  • Allows admins to access user routes except default admin is redirected to /admin.
  • Allows single-user mode only for user routes.
  • Redirects role mismatches to /admin or /.

API client

client/api.js:

  • _fetch(method, path, body) calls /api${path} with JSON headers and credentials: include.
  • Mutating methods read bt_csrf_token cookie and send x-csrf-token.
  • Non-OK responses throw Error with status, data, details, and code.
  • Exposes grouped functions for auth, admin, notifications, profile, tracker, calendar, summary, bills, payments, categories, settings, analytics, status, version/about, import, export, data-sources, transactions, and matches.
  • File download/upload endpoints use raw fetch because responses/bodies are blobs or octet streams.

Client API v0.28.1 additions

  • api.dataSources(type, status)/api/data-sources
  • api.transactions(filters)/api/transactions
  • api.transactions.create(payload)/api/transactions/manual
  • api.transactions.update(id, payload)/api/transactions/:id
  • api.transactions.delete(id)/api/transactions/:id
  • api.transactions.match(id, {billId})/api/transactions/:id/match
  • api.transactions.unmatch(id)/api/transactions/:id/unmatch
  • api.transactions.ignore(id)/api/transactions/:id/ignore
  • api.transactions.unignore(id)/api/transactions/:id/unignore
  • api.csvImport.preview(file, options)/api/import/csv/preview
  • api.csvImport.commit(importSessionId, mapping, options)/api/import/csv/commit
  • api.import.history()/api/import/history
  • api.matchSuggestions.list(filters)/api/matches/suggestions
  • api.matchSuggestions.reject(id)/api/matches/:id/reject
  • api.snowball.get()/api/snowball
  • api.snowball.settings.get()/api/snowball/settings
  • api.snowball.settings.patch(payload)/api/snowball/settings
  • api.snowball.projection.get()/api/snowball/projection
  • api.snowball.order.patch(bills)/api/snowball/order
  • api.monthlyStartingAmounts.get(year, month)/api/monthly-starting-amounts
  • api.monthlyStartingAmounts.update(payload)/api/monthly-starting-amounts

Auth state

Auth state

client/hooks/useAuth.jsx:

  • Maintains user, singleUserMode, and loading state.
  • Calls api.authMode() and api.me() on startup.
  • Exposes logout() and refresh().

Query hooks

client/hooks/useQueries.js:

  • useTracker(year, month)api.tracker(year, month).
  • useBills()api.allBills().
  • useCategories()api.categories().

These use TanStack Query keys and cache server data for common pages.

Pages

  • LoginPage.jsx — local login plus OIDC login availability based on /auth/mode.
  • TrackerPage.jsx — monthly tracker, payment interactions, upcoming bills, starting amount awareness, bulk/session logout action.
  • CalendarPage.jsx — calendar grid backed by /calendar.
  • SummaryPage.jsx — monthly plan, income, starting amounts, expenses, chart data.
  • BillsPage.jsx — bill CRUD, categories, monthly state/history range controls.
  • CategoriesPage.jsx — category list/create/update/delete and related bill info.
  • AnalyticsPage.jsx — analytics summary filters and charts.
  • SettingsPage.jsx — user/app settings and demo data seed.
  • DataPage.jsx — export, spreadsheet import, user DB import, import history, CSV transaction import with preview and commit flow (ImportTransactionCsvSection).
  • ProfilePage.jsx — display name, notification preferences, password change, export/import-history links.
  • AdminPage.jsx — users, auth mode/OIDC, backups, cleanup, notifications, migrations, admin settings.
  • StatusPage.jsx — admin system status.
  • AboutPage.jsx — public or admin markdown/about view; admin mode uses /about-admin; markdown is sanitized.
  • ReleaseNotesPage.jsx — release history display.

Components

  • Layout: Layout, Sidebar, BrandBlock, NavPill.
  • Domain UI: AdminDashboard, BillModal, BillsTableInner, MobileBillRow, MobileTrackerRow, StatusBadge, SummaryCard, MarkdownText, ReleaseNotesDialog.
  • Reliability: ErrorBoundary, PageLoader.
  • UI primitives: alert dialog, badge, button, card, checkbox, confirm dialog, dialog, dropdown menu, input dialog, input, label, select, separator, skeleton, switch, table, tabs, theme toggle, tooltip.

Frontend security notes

  • CSRF header is sent on POST/PUT/PATCH/DELETE.
  • Auth is cookie/session based; no tokens are stored in localStorage.
  • Admin routes are client-guarded and server-guarded.
  • Markdown rendering uses rehype-sanitize.
  • Error boundaries prevent route crashes from taking down the whole SPA.

8. Infrastructure and Deployment

package.json

Version: 0.28.1.

Scripts:

  • npm run dev:apinode --watch server.js.
  • npm run dev:ui — Vite dev server.
  • npm run dev — concurrently runs API and UI.
  • npm run build — Vite production build.
  • npm startnode server.js.

Key runtime dependencies:

  • Express, cookie-parser, cors, express-rate-limit.
  • better-sqlite3.
  • bcryptjs.
  • openid-client.
  • nodemailer.
  • node-cron.
  • React, React DOM, React Router, TanStack Query.
  • shadcn/ui component primitives, Radix UI primitives, lucide-react, Tailwind utilities, Sonner toasts.
  • xlsx for spreadsheet import/export.

Dockerfile

Multi-stage build:

  1. Builder: node:18-alpine, installs build deps python3 make g++, runs npm install, copies source, runs npm run build.
  2. Runtime: node:18-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.

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.


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, rejects inactive users, cleans expired sessions, creates a 7-day session.
  4. Server sets bt_session cookie using cookieOpts(req).
  5. Client calls GET /auth/me to populate auth state.

OIDC login

  1. Client navigates to /api/auth/oidc/login.
  2. Server verifies active OIDC config, 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, 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.

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.

Backup safety

  • Managed filename regex and path checks prevent traversal.
  • Uploads are written to temp paths first, validated, 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.

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.
  • Default categories/settings are seeded.
  • Expired sessions are purged at startup.
  • A periodic expired-session cleanup interval is scheduled.
  • Backup scheduler and daily worker are started where server code imports/starts them.

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
  • DB_PATH
  • BACKUP_PATH
  • INIT_ADMIN_USER
  • INIT_ADMIN_PASS
  • INIT_REGULAR_USER
  • INIT_REGULAR_PASS
  • SESSION_CLEANUP_INTERVAL_MS
  • CORS_ORIGIN
  • COOKIE_SECURE
  • HTTPS
  • CSRF_HTTP_ONLY
  • CSRF_SAME_SITE
  • CSRF_SECURE
  • CSRF_COOKIE_NAME
  • OIDC_ISSUER_URL
  • OIDC_CLIENT_ID
  • OIDC_CLIENT_SECRET
  • OIDC_REDIRECT_URI
  • OIDC_TOKEN_AUTH_METHOD

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, OIDC client secret, or internal backup paths in API responses.
  • Keep import preview/apply separated so users can resolve ambiguous spreadsheet data before DB writes.
  • Prefer soft delete for payments; bill deletion is intentionally hard delete and returns an explicit warning.
  • 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.

11. Verification Checklist Used for This Reference

Reviewed current code sources:

  • server.js
  • all route files under routes/
  • service files under services/
  • middleware files under middleware/
  • db/schema.sql and db/database.js
  • actual initialized SQLite schema via better-sqlite3 introspection
  • client/App.jsx, client/api.js, client/hooks/useAuth.jsx, client/hooks/useQueries.js
  • page/component inventory under client/pages/ and client/components/
  • package.json
  • Dockerfile
  • docker-compose.yml

The previous manual contained large historical update sections and stale route/page descriptions. This version replaces those with a current-state engineering reference.