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>
7.6 KiB
HSA Receipt Tracker — Implementation Plan (v1)
Companion to spec.md. This is the how and the order; spec.md is the what.
Strategy: red/green (write a failing test, make it pass) wherever the logic is pure
and deterministic. Where behaviour depends on the live world (the real OIDC
round-trip against Authelia, a camera, a browser), we verify manually instead and
say so explicitly.
Decisions locked (from consultation)
| Topic | Decision |
|---|---|
| Stack | Go — single static binary, systemd in LXC, SQLite |
| App domain | hsa.maisym.com |
| Auth issuer | auth.maisym.com |
| OIDC client | Confidential + PKCE |
| Auth group | hsa-users (gates access; not in group → 403) |
| Session | Encrypted, signed cookie (stateless, no server-side store) |
| Edit | Designed-for, not built in v1 |
| Export | /export/db only, with include-blobs yes/no toggle |
| Files | PDF + images, accepted as-is; client compression is phase 2 |
| View list/detail | Out of scope for v1 |
| Auth testing | Unit-test pure pieces; verify live round-trip manually |
Build order (milestones)
- M0 — Auth flow only. App authenticates against live Authelia and shows a "you're logged in" result, including the group gate. This is the first thing JM wants to see running. Detailed below.
- M1 — Storage layer. SQLite schema + receipts data access (insert, soft-delete, fetch). Dual-write (filesystem + BLOB) in one transaction.
- M2 — Upload flow. Mobile-first capture/upload form (amount, date, category), PDF+image handling, validation, store both copies, confirmation page.
- M3 — Export.
GET /export/dbwith include-blobs toggle, via point-in-time snapshot (never the live DB file). - Phase 2 / later. Client-side image compression; edit functionality.
Only M0 is planned in detail below — we plan the next milestone when we get there, so the plan stays honest.
Milestone 0 — Auth flow
Goal / acceptance: Open hsa.maisym.com in a phone/browser → redirected to
auth.maisym.com → log in → land back on a page that reads roughly:
Logged in as
jeanmi.tremblay@gmail.com· groups:[hsa-users]· access: GRANTED
…and a user who is not in hsa-users gets a 403. That single screen proves
the entire chain: OIDC+PKCE round-trip, identity claims, group claim, and the
authorization gate.
Step 0.1 — Prerequisites (setup, not tested)
- Install Go toolchain (not currently present on this machine).
git initthe repo.go mod init— module path TBD (see Open Questions). Default proposal:maisym.com/hsa.- Project skeleton:
cmd/hsa/main.go,internal/auth/,internal/web/,internal/config/.
Step 0.2 — JM's Authelia-side config (your action; app can't do this)
Enumerated here so the app and Authelia agree on every value:
- Create group
hsa-users; add both users. - Register a confidential OIDC client in
identity_providers.oidc.clients:client_id: e.g.hsa-trackerclient_secret: random, stored hashed (viaauthelia crypto hash generate)redirect_uris:https://hsa.maisym.com/callback(+ a dev redirect, e.g.http://localhost:8080/callback— see Open Questions)scopes:openid,profile,email,groupsresponse_types:code;grant_types:authorization_code- PKCE: require
S256 - token endpoint auth method:
client_secret_basic(confirm at execution)
- Reload Authelia.
Step 0.3 — App config (env vars)
ISSUER_URL, CLIENT_ID, CLIENT_SECRET, REDIRECT_URL, REQUIRED_GROUP
(=hsa-users), SESSION_KEY (cookie encryption key), LISTEN_ADDR. Loaded and
validated at startup (fail fast if any missing).
Step 0.4 — Red/green units (pure logic)
Each is a failing test first, then the implementation:
- PKCE —
GenerateVerifier()(43–128 URL-safe chars) andChallengeS256(verifier). Test with the RFC 7636 Appendix B known vector (fixed verifier → known challenge) so the encoding is provably correct. - State / nonce — sufficient length, two calls differ (CSRF + replay defense).
- Session codec —
Encode(session)/Decode(cookie)round-trips; a tampered value is rejected; a value signed with the wrong key is rejected. - Authorization decision —
IsAuthorized(groups, required) bool. Table tests: in-group → true; empty groups → false; other-group-only → false. This is the 403 rule, tested in isolation. - Authorize-URL builder — construct
oauth2.Configwith fixed endpoints (no network/discovery) and assertAuthCodeURLcarriesstate,code_challenge,code_challenge_method=S256, and the right scopes.
Step 0.5 — Handlers tested via httptest (no live Authelia)
GET /healthz→ 200.GET /(protected) behindRequireAuth: forged valid session cookie → 200 and the page shows identity + groups; no/invalid cookie → 302 to/login.GET /logout→ clears cookie, 302.
Step 0.6 — Live round-trip (manual verification — the M0 acceptance)
Not unit-testable (needs real Authelia + a human clicking login):
GET /login→ builds PKCE+state, stashes verifier/state in a short-lived cookie, 302 to Authelia's authorize endpoint.GET /callback→ validatestate, exchangecode(with PKCE verifier), verify the ID token, extract claims, runIsAuthorized, set the session cookie, redirect to/. (The pure sub-parts — claim extraction, group gate — are already red/green from 0.4.)
Manual test script:
- Run the app with real config (locally with the dev redirect, or deployed in the LXC behind Caddy).
- Browser → app → redirected to
auth.maisym.com→ log in → returned to success page showing identity +[hsa-users]+ GRANTED. - Negative path: an account not in
hsa-users→ 403.
M0 done =
All red/green units green, httptest handler tests green, and the manual
round-trip (both positive and 403 paths) confirmed against live Authelia.
Open questions (revisit before/at execution — not guessing)
- Module path / repo name —
maisym.com/hsa? a GitHub path? Will there be a GitHub remote, or local-only for now? - Dev redirect URI — do you want to test M0 locally first (needs a
http://localhost:8080/callbackredirect added to the Authelia client), or deploy-to-LXC-first and test only athttps://hsa.maisym.com? - Token endpoint auth method —
client_secret_basicvs_post. Pick when we wire it; basic is the default assumption. - Infra ownership — Caddy vhost + systemd unit: you handle, or you want me to draft the unit file / Caddyfile snippet as part of M0?
Deferred to later milestones (noted so we don't design against them)
- SQLite driver choice — lean
modernc.org/sqlite(pure Go, keeps the static binary, no cgo). Decide at M1. - Edit (v1: schema/storage must not preclude it — on a future edit, update FS file
and BLOB in one transaction, keep
file_pathUUID stable). - Client-side image compression (phase 2): re-encode camera images to JPEG/WebP at a quality factor before upload; PDFs pass through untouched.