BillTracker/docs/Engineering_Reference_Manua...

52 KiB

Engineering Reference Manual — Bill Tracker

Status: Current code reference Last Updated: 2026-05-10 Version: 0.23.1 Primary stack: Node.js + Express, React + Vite, 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.
  • services/ — auth, OIDC, backup, cleanup, notification, import, status, audit business logic.
  • 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 !== false, CSRF_SAME_SITE || strict, CSRF_SECURE !== false.
  • 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/statusService.js

Shared tracker/calendar logic:

  • resolveDueDate(bill, year, month) clamps due day to month length.
  • resolveBucket(bill) uses bucket or due-day threshold.
  • getCycleRange(year, month) returns first/last day of month.
  • calculateStatus(...) returns paid/autodraft/upcoming/due/overdue-style status.
  • buildTrackerRow(...) returns row data for the monthly tracker.

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}.
    • Errors: 401 invalid credentials, 403 disabled login.
  • 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.

Server also mounts routes/authLogin.js at /api/auth/login; that router defines POST /login, creating an effective compatibility path /api/auth/login/login with the same local-login behavior. The frontend uses /api/auth/login.

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?}.
    • Validation: bill exists and owned; amount > 0; required fields present.
    • Response 201: created payment.
  • POST /payments/quick

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

    • Body: {payments:[{bill_id, amount, paid_date, method?, notes?}]}.
    • Validation: array required; max 50; bill_id integer; paid_date YYYY-MM-DD; amount finite >= 0.
    • 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}.
    • 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.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 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.16 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.17 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.

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

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
  • 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
  • created_at TEXT DEFAULT datetime('now')
  • updated_at TEXT DEFAULT datetime('now')

settings

  • key TEXT PRIMARY KEY
  • value TEXT NOT NULL
  • updated_at TEXT DEFAULT datetime('now')

Used for app settings, auth mode, OIDC settings, SMTP settings, backup schedule, cleanup settings, and worker state.

notifications

  • id INTEGER PRIMARY KEY
  • bill_id INTEGER NOT NULL REFERENCES bills(id) ON DELETE CASCADE
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • year INTEGER NOT NULL
  • month INTEGER NOT NULL
  • type TEXT NOT NULL (due_3d, due_1d, due_today, overdue)
  • sent_date TEXT NOT NULL DEFAULT date('now')
  • Unique: (bill_id, user_id, year, month, type, sent_date)

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
  • user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
  • imported_at TEXT NOT NULL
  • source_filename TEXT
  • file_type TEXT DEFAULT 'xlsx'
  • sheet_name TEXT
  • rows_parsed INTEGER DEFAULT 0
  • rows_created INTEGER DEFAULT 0
  • rows_updated INTEGER DEFAULT 0
  • rows_skipped INTEGER DEFAULT 0
  • rows_ambiguous INTEGER DEFAULT 0
  • rows_errored INTEGER DEFAULT 0
  • options_json TEXT
  • summary_json TEXT

oidc_states

  • id TEXT PRIMARY KEY
  • nonce TEXT NOT NULL
  • code_verifier TEXT NOT NULL
  • redirect_to TEXT
  • created_at TEXT NOT NULL
  • expires_at TEXT NOT NULL

audit_log

  • id INTEGER PRIMARY KEY
  • user_id INTEGER
  • action TEXT NOT NULL
  • entity_type TEXT
  • entity_id INTEGER
  • details_json TEXT
  • ip_address TEXT
  • user_agent TEXT
  • created_at TEXT DEFAULT datetime('now')

schema_migrations

  • id INTEGER PRIMARY KEY
  • version TEXT NOT NULL UNIQUE
  • description TEXT NOT NULL
  • applied_at TEXT NOT NULL DEFAULT datetime('now')

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

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

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
  • Radix/shadcn-style UI primitives
  • sonner for toasts
  • 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, and export.
  • File download/upload endpoints use raw fetch because responses/bodies are blobs or octet streams.

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.
  • 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.23.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.
  • Radix UI primitives, lucide-react, Tailwind utilities.
  • 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.