hsa-app/spec.md
Jean-Michel Tremblay 8b7252dad4 Initial commit: HSA receipt tracker
Go app for capturing and archiving HSA-eligible receipts: OIDC/PKCE auth
against Authelia, SQLite storage with dual-write (filesystem + DB blob),
mobile-first upload, and DB export.

Adds AI receipt classification: a config.json catalog of people and
categories (seeded into the DB on startup), a prompt builder that derives
name-order/initial variants from the data (with same-surname ambiguity
handling), and an Anthropic tool-use client behind POST /classify. Tests
run against a mock endpoint; a live integration test is env-gated to the
cheapest model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:40:12 -04:00

4.1 KiB

HSA Receipt Tracker — Requirements (v1) Purpose Capture and archive HSA-eligible receipts for future reimbursement and tax substantiation. No parsing, no OCR, no reporting. Users

Two users, both with full access (shared visibility). Authentication via Authelia OIDC. Authorization via membership in an Authelia group (e.g. hsa-users). Not in the group → 403. No other roles or gradations.

Core flow

User opens app on phone (mobile-first UI; camera access matters). Takes a photo of a receipt, or selects an existing image / PDF from device. Form prompts for: amount, date, category. Submit → image stored to disk, metadata row inserted into DB. Confirmation page, with options to view list or add another.

Data model receipts table:

id (UUID) uploaded_by (Authelia username or email) uploaded_at (server timestamp) receipt_date (user-supplied date on the receipt) amount_cents (integer — never store money as float) category (enum) file_path (relative path on disk — the filesystem copy) image_data (BLOB — the receipt file bytes, also stored in the DB itself) file_size_bytes (integer — convenience for listings/exports) original_filename (preserved for reference) mime_type deleted_at (nullable — soft delete)

Categories (fixed list, hardcoded for v1):

Medical Dental Vision Pharmacy Other

Storage

Receipt images/PDFs are stored in BOTH places on upload:

  1. Filesystem at a configurable path, filename randomized (UUID) on save (file_path) — used as the primary path for serving.
  2. As a BLOB inside the SQLite database (image_data column) — so the single .db file is a complete, self-contained dataset (metadata + files).

Rationale: the filesystem copy keeps serving simple/efficient; the DB blob makes backup and export trivial ("hand over one file" gets everything, even without the files directory). Written once at upload; no edit in v1, so the two copies never diverge. Acceptable cost because scale is tiny (two users, small files). original_filename and mime_type kept as metadata for download/serving. Backups remain JM's responsibility outside the app.

Auth integration

OIDC with PKCE against https://auth.jmopines.com. Session cookie after successful callback. /login, /callback, /logout, /healthz are public; everything else requires a valid session. Group claim (hsa-users) gates access; otherwise 403.

Deployment

Runs as a systemd service in an LXC. Caddy reverse proxy at https://hsa.jmopines.com (TBD: maisym.com vs jmopines.com). SQLite DB + filesystem storage. No external dependencies (no Redis, no Postgres, no S3).

Operations

Soft delete supported (set deleted_at, hide from default list views). No edit functionality in v1 — fix mistakes by deleting and re-adding.

Database export

Authenticated users (hsa-users group) can download the data for offline use.

Two endpoints:

GET /export/db — downloads a consistent copy of the SQLite database file. Because images are stored as BLOBs in the DB, this single file IS the complete dataset (metadata + all receipt images). This is the primary export.

  • Must NOT serve the live DB file directly (avoids locking/corruption against the running app). Use SQLite's online backup API (or VACUUM INTO a temp file) to produce a point-in-time snapshot, then stream that.
  • Content-Type: application/octet-stream; filename like hsa-export-YYYY-MM-DD.db.

GET /export/archive (optional convenience) — downloads a zip with the image files extracted to normal files (named by original_filename) plus a CSV/JSON of the metadata, for someone who wants the pictures as browseable files rather than inside a DB.

  • Streamed zip to avoid buffering large archives in memory.
  • filename like hsa-export-YYYY-MM-DD.zip.

Notes:

  • Amounts remain integer cents in the export; consumers divide by 100 for dollars.
  • Read-only operation; no app state is mutated.

Out of scope for v1

OCR / image content parsing Reports, totals, dashboards CSV / tax-software export Reimbursement tracking (paid vs pending status) In-place editing of existing receipts Multi-tenancy or per-user data isolation Notification / reminders