2026-05-09 13:03:36 -05:00
# Engineering Reference Manual — Bill Tracker
2026-05-10 11:49:05 -05:00
**Status:** Current code reference
**Last Updated:** 2026-05-10
**Version:** 0.23.1
**Primary stack:** Node.js + Express, React + Vite, SQLite via `better-sqlite3`
2026-05-10 10:44:39 -05:00
2026-05-10 11:49:05 -05:00
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.
2026-05-09 13:03:36 -05:00
---
2026-05-10 11:49:05 -05:00
## 1. System Overview
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Bill Tracker is a self-hosted bill management application. It supports:
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- 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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Runtime flow:
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
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.
2026-05-09 13:03:36 -05:00
---
2026-05-10 11:49:05 -05:00
## 2. Project Layout
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
---
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
## 3. Backend Entry Point and Middleware
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### `server.js`
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Server defaults:
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- 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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Global middleware order:
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
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
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### Authentication middleware
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
`middleware/requireAuth.js` exports:
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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'` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### CSRF middleware
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
`middleware/csrf.js` :
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- 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` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### Rate limits
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Defined in `middleware/rateLimiter.js` :
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- 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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Rate-limit responses are JSON: `{ "error": "..." }` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### Security headers
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
`middleware/securityHeaders.js` sets CSP with a per-request nonce plus common hardening headers. Static SPA scripts/styles must comply with the generated CSP.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### Error formatting
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
`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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
---
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
## 4. Services
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### `services/authService.js`
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- 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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
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` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
`/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}` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### `services/oidcService.js`
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Handles Authentik/OIDC login with `openid-client` :
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- 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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### `services/backupService.js`
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- 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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### `services/backupScheduler.js`
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- 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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### `services/cleanupService.js`
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Cleanup tasks:
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- Expired import sessions.
- Stale temp SQLite export files in OS temp dir.
- Orphan `.partial` /`.upload` backup files.
- Optional old import-history pruning.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Settings are stored in `settings` ; run results are stored as JSON.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### `services/notificationService.js`
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- 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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### `services/spreadsheetImportService.js`
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- 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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### `services/userDbImportService.js`
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- 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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### `services/statusService.js`
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Shared tracker/calendar logic:
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### `services/auditService.js`
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
`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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
---
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
## 5. API Reference
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Response conventions:
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- 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` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### 5.1 Auth
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Mounted under `/api/auth` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `POST /auth/logout`
- Auth: required.
- Body: none.
- Response: clears cookie; `{success:true}` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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}` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /auth/me`
- Auth: required unless single-user mode supplies user.
- Response: public user object.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /auth/mode`
- Public.
- Response: auth mode, local-login flag, OIDC public info, single-user status.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `POST /auth/restore-multi-user-mode`
- Auth: required.
- Body: none.
- Response: restores multi-user mode where allowed.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `POST /auth/acknowledge-privacy`
- Auth: required.
- Body: none.
- Response: updates first-login/privacy acknowledgement flags.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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}` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /auth/has-users`
- Response: whether non-default users exist.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /auth/users`
- Auth: admin.
- Response: safe user list.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `POST /auth/users`
- Auth: admin.
- Body: `{username, password}` .
- Validation: username min 3, password min 8, unique username.
- Response: created safe user.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
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` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### 5.2 OIDC Auth
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Mounted under `/api/auth/oidc` ; OIDC rate limiter applies.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /auth/oidc/login?redirect_to=/path`
- Public when OIDC active.
- Creates PKCE state and redirects to provider authorization URL.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### 5.3 Admin
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Mounted under `/api/admin` ; all require `requireAuth + requireAdmin + adminActionLimiter` at the mount level. Backup subroutes also use `backupOperationLimiter` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /admin/has-users`
- Response: `{has_users:boolean}` for users other than current admin.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /admin/users`
- Response: safe users ordered by default-admin, role, username.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `POST /admin/users`
- Body: `{username, password}` .
- Validation: username min 3, password min 8, unique.
- Response 201: created user.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `PUT /admin/users/:id/password`
- Body: `{password}` .
- Validation: password min 8; user exists.
- Response: `{success:true}` ; invalidates target sessions and requires password change.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `PUT /admin/users/:id/active`
- Body: `{active:boolean}` .
- Validation: user exists; cannot deactivate self.
- Response: updated safe user; deactivation invalidates sessions.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `DELETE /admin/users/:id`
- Validation: user exists; cannot delete self.
- Response: `{success:true, deleted_user_id}` ; transaction deletes import sessions/history, sessions, and user.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `POST /admin/backups`
- Body: none.
- Response 201: backup metadata `{id, filename, size_bytes, checksum, ...}` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /admin/backups`
- Response: `{backups:[metadata...]}` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /admin/backups/settings`
- Response: backup schedule status/settings.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `POST /admin/backups/run-scheduled-now`
- Body: none.
- Response 201: scheduled backup result.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /admin/backups/:id/download`
- Response: file download. ID must be a managed backup filename.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `POST /admin/backups/:id/restore`
- Response: `{restored_from, pre_restore_backup}` ; validates and restores managed backup.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `DELETE /admin/backups/:id`
- Response: `{deleted:true, id, deleted_at}` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /admin/cleanup`
- Response: cleanup settings and last result.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `POST /admin/cleanup/run`
- Response: cleanup run result by task.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /admin/auth-mode`
- Response: local/single-user/OIDC settings and lockout warnings. Client secret is not returned.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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}` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### 5.4 Bills
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Mounted under `/api/bills` ; auth: user/admin tracker access.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Validation shared by create/update:
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Endpoints:
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /bills?inactive=true`
- Response: current user's bills; inactive excluded unless `inactive=true` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /bills/:id`
- Response: one owned bill or 404.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `POST /bills`
- Body: bill fields.
- Response 201: created bill.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `PUT /bills/:id`
- Body: partial bill fields.
- Response: updated bill.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `DELETE /bills/:id`
- Hard delete; cascades payments, monthly state, history ranges.
- Response: `{success:true, deleted_bill_id, deleted_bill_name, warning}` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /bills/:id/monthly-state?year=&month=`
- Validation: year 2000-2100; month 1-12.
- Response: `{bill_id, year, month, actual_amount, notes, is_skipped}` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /bills/:id/payments?page=1&limit=20`
- Validation: limit capped at 100.
- Response: `{bill_id, bill_name, total, page, limit, pages, payments}` .
2026-05-09 18:25:25 -05:00
2026-05-10 11:49:05 -05:00
- `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?}` .
2026-05-09 18:25:25 -05:00
2026-05-10 11:49:05 -05:00
- `GET /bills/:id/history-ranges`
- Response: `{bill_id, history_visibility, ranges}` .
2026-05-09 18:25:25 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 18:25:25 -05:00
2026-05-10 11:49:05 -05:00
- `PUT /bills/:id/history-ranges/:rangeId`
- Body: partial range fields.
- Response: updated range.
2026-05-09 18:25:25 -05:00
2026-05-10 11:49:05 -05:00
- `DELETE /bills/:id/history-ranges/:rangeId`
- Response: `{success:true}` .
2026-05-09 18:25:25 -05:00
2026-05-10 11:49:05 -05:00
### 5.5 Payments
2026-05-09 18:25:25 -05:00
2026-05-10 11:49:05 -05:00
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` .
2026-05-09 18:25:25 -05:00
2026-05-10 11:49:05 -05:00
- `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` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /payments/:id`
- Response: live payment or 404.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `POST /payments`
- Body: `{bill_id, amount, paid_date, method?, notes?}` .
- Validation: bill exists and owned; amount > 0; required fields present.
- Response 201: created payment.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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:[...]}` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `PUT /payments/:id`
- Body: partial `{amount, paid_date, method, notes}` .
- Response: updated payment. Current code preserves existing fields when omitted.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `DELETE /payments/:id`
- Response: `{success:true}` after setting `deleted_at` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `POST /payments/:id/restore`
- Response: restored payment with `deleted_at:null` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### 5.6 Categories
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Mounted under `/api/categories` ; auth: user/admin tracker access.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /categories`
- Seeds default categories for user if needed.
- Response: categories with bill counts/payment counts and bill summaries.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `POST /categories`
- Body: `{name}` .
- Validation: non-empty name; unique per user case-insensitive.
- Response 201: created category.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `PUT /categories/:id`
- Body: `{name}` .
- Validation: category belongs to user; non-empty unique name.
- Response: updated category.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `DELETE /categories/:id`
- Validation: category belongs to user.
- Behavior: transaction nulls category on owned bills, then deletes category.
- Response: deletion summary.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### 5.7 Tracker and Calendar
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /tracker/upcoming?days=30`
- Auth: user/admin tracker access.
- Response: upcoming active bills in the requested horizon.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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}` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### 5.8 Summary and Starting Amounts
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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}` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /monthly-starting-amounts?year=&month=`
- Response: `{year, month, first_amount, fifteenth_amount, other_amount, combined_amount, paid deductions, remaining values, notes}` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### 5.9 Analytics
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### 5.10 Settings
2026-05-09 16:25:12 -05:00
2026-05-10 11:49:05 -05:00
- `GET /settings`
- Auth: user/admin tracker access.
- Response: user-visible settings from the allowed settings key list.
2026-05-09 16:25:12 -05:00
2026-05-10 11:49:05 -05:00
- `PUT /settings`
- Auth: user/admin tracker access.
- Body: key/value object for allowed user setting keys.
- Response: updated settings object.
2026-05-09 16:25:12 -05:00
2026-05-10 11:49:05 -05:00
- `POST /settings/seed-demo-data`
- Auth: user/admin tracker access.
- Response: demo seed result.
2026-05-09 16:25:12 -05:00
2026-05-10 11:49:05 -05:00
### 5.11 Notifications
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Mounted under `/api/notifications` . Server mount requires `requireAuth` ; route-level guards further restrict.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /notifications/admin`
- Auth: admin.
- Response: global SMTP/notification settings.
2026-05-09 16:25:12 -05:00
2026-05-10 11:49:05 -05:00
- `PUT /notifications/admin`
- Auth: admin.
- Body: allowed global SMTP and notification settings.
- Response: saved settings.
2026-05-09 16:25:12 -05:00
2026-05-10 11:49:05 -05:00
- `POST /notifications/test`
- Auth: admin.
- Body: `{to}` .
- Validation: recipient required.
- Response: send result or SMTP error.
2026-05-09 16:25:12 -05:00
2026-05-10 11:49:05 -05:00
- `GET /notifications/me`
- Auth: user/admin tracker access.
- Response: current user's notification email and toggle settings.
2026-05-09 16:25:12 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 16:25:12 -05:00
2026-05-10 11:49:05 -05:00
### 5.12 Profile
2026-05-09 16:25:12 -05:00
2026-05-10 11:49:05 -05:00
Mounted under `/api/profile` ; auth: user/admin tracker access; password limiter applies at mount.
2026-05-09 16:25:12 -05:00
2026-05-10 11:49:05 -05:00
- `GET /profile`
- Response: safe profile, notification settings, export URLs, import-history URL.
2026-05-09 16:25:12 -05:00
2026-05-10 11:49:05 -05:00
- `PATCH /profile`
- Body: `{display_name}` .
- Validation: string, max 100 chars.
- Response: `{success:true, profile}` .
2026-05-09 16:25:12 -05:00
2026-05-10 11:49:05 -05:00
- `GET /profile/settings`
- Response: user notification preferences only.
2026-05-09 16:25:12 -05:00
2026-05-10 11:49:05 -05:00
- `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}` .
2026-05-09 16:25:12 -05:00
2026-05-10 11:49:05 -05:00
- `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}` .
2026-05-09 16:25:12 -05:00
2026-05-10 11:49:05 -05:00
- `GET /profile/exports`
- Response: metadata for `user_db` and `user_excel` export URLs.
2026-05-09 16:25:12 -05:00
2026-05-10 11:49:05 -05:00
- `GET /profile/import-history`
- Response: `{history:[...]}` .
2026-05-09 16:25:12 -05:00
2026-05-10 11:49:05 -05:00
### 5.13 User Demo Data
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Mounted under `/api/user` ; auth: user/admin tracker access.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `POST /user/seed-demo-data`
- Response: seed result for current user.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### 5.14 Import
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Mounted under `/api/import` ; auth: user/admin tracker access; import limiter applies.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 15:17:40 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 15:17:40 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 15:17:40 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 15:17:40 -05:00
2026-05-10 11:49:05 -05:00
- `GET /import/history`
- Response: current user's import history.
2026-05-09 15:17:40 -05:00
2026-05-10 11:49:05 -05:00
### 5.15 Export
2026-05-09 15:17:40 -05:00
2026-05-10 11:49:05 -05:00
Mounted under `/api/export` ; auth: user/admin tracker access; export limiter applies.
2026-05-09 15:17:40 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 15:17:40 -05:00
2026-05-10 11:49:05 -05:00
- `GET /export/user-excel`
- Response: Excel workbook with user categories, bills, payments, monthly state, monthly starting amounts, and notes/metadata.
2026-05-09 15:17:40 -05:00
2026-05-10 11:49:05 -05:00
- `GET /export/user-db`
- Response: portable SQLite file with export metadata and user-owned categories, bills, payments, monthly state, monthly starting amounts, and notes.
2026-05-09 15:17:40 -05:00
2026-05-10 11:49:05 -05:00
### 5.16 Status
2026-05-09 15:17:40 -05:00
2026-05-10 11:49:05 -05:00
- `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.
2026-05-09 15:17:40 -05:00
2026-05-10 11:49:05 -05:00
### 5.17 About and Version
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /about`
- Public.
- Response: package version and public project/about metadata.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `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` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /version`
- Public.
- Response: current package version and latest structured notes from `HISTORY.md` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `GET /version/history`
- Public.
- Response: package version and raw history text, or error if unavailable.
2026-05-09 13:03:36 -05:00
2026-05-09 19:47:00 -05:00
---
2026-05-10 11:49:05 -05:00
## 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:
2026-05-09 19:47:00 -05:00
```javascript
2026-05-10 11:49:05 -05:00
let _logAudit = null;
function getLogAudit() {
if (!_logAudit) {
try { _logAudit = require('../services/auditService').logAudit; } catch { _logAudit = () => {}; }
2026-05-09 19:47:00 -05:00
}
2026-05-10 11:49:05 -05:00
return _logAudit;
2026-05-09 19:47:00 -05:00
}
```
2026-05-10 11:49:05 -05:00
Use this pattern for database-layer audit calls instead of a top-level `require('../services/auditService')` .
2026-05-09 13:03:36 -05:00
---
2026-05-10 11:49:05 -05:00
## 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:
- `/login` → `LoginPage` public.
- `/about` → `AboutPage` public.
- `/release-notes` → `ReleaseNotesPage` public.
- `/admin` → `AdminPage` , 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` .
- `/calendar` → `CalendarPage` .
- `/summary` → `SummaryPage` .
- `/bills` → `BillsPage` .
- `/categories` → `CategoriesPage` .
- `/analytics` → `AnalyticsPage` .
- `/settings` → `SettingsPage` .
- `/data` → `DataPage` .
- `/profile` → `ProfilePage` .
- `*` 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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### Components
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- 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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### Frontend security notes
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- 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.
2026-05-09 13:03:36 -05:00
---
2026-05-10 11:49:05 -05:00
## 8. Infrastructure and Deployment
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### `package.json`
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Version: `0.23.1` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Scripts:
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `npm run dev:api` — `node --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 start` — `node server.js` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Key runtime dependencies:
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- 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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### Dockerfile
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Multi-stage build:
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Runtime environment:
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- `NODE_ENV=production`
- `PORT=3000`
- `DB_PATH=/data/db/bills.db`
- `BACKUP_PATH=/data/backups`
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Exposes port 3000, declares volume `/data` , entrypoint `docker-entrypoint.sh` , command `node server.js` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### `docker-compose.yml`
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
Service: `bill-tracker` .
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- 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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
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.
2026-05-09 13:03:36 -05:00
---
2026-05-10 11:49:05 -05:00
## 9. Auth and Security Flows
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### Local login
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### OIDC login
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### Password change
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### Authorization
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- 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.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### Backup safety
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- Managed filename regex and path checks prevent traversal.
- Uploads are written to temp paths first, validated, then moved.
- Restore creates a pre-restore backup.
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
### Import safety
2026-05-09 13:03:36 -05:00
2026-05-10 11:49:05 -05:00
- 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.
2026-05-09 13:03:36 -05:00
---
2026-05-10 11:49:05 -05:00
## 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.
2026-05-10 09:45:39 -05:00
---
2026-05-10 11:49:05 -05:00
## 11. Verification Checklist Used for This Reference
2026-05-10 09:45:39 -05:00
2026-05-10 11:49:05 -05:00
Reviewed current code sources:
2026-05-10 09:45:39 -05:00
2026-05-10 11:49:05 -05:00
- `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`
2026-05-10 09:45:39 -05:00
2026-05-10 11:49:05 -05:00
The previous manual contained large historical update sections and stale route/page descriptions. This version replaces those with a current-state engineering reference.