Files
kvitt/PLAN-M3.md
T
jacobh2 5e1be38364 Plans and start prompts for every milestone; SPEC v0.2
PLAN-<id>.md for U1, U2, M1, M2a, M2b, M2c-dev, M2c-pi, M3, M4, M5, M6,
M7a-M7f, M8, M9, M10, M11, each with the eleven fixed sections, and a
start prompt per plan in prompts/ plus prompts/README.md (dependency
order and parallel waves).

SPEC v0.2: owner answers D-001 (development-first, M2c split into
M2c-dev and M2c-pi, Gitea registry), D-002 (local backups deleted once
on Drive), D-003 (Splitwise CSV format, English and Swedish variants,
category mapping), OQ-1, OQ-2, OQ-4 answered; M1's readings of
ambiguous contract points stated explicitly in §14.3 to §14.5.
HANDOFF §2 lists the plans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:05:34 +02:00

10 KiB
Raw Blame History

PLAN-M3: Server core API

1. Goal

Implement the REST surface of the server other than sync: invites and pairing, profile and pictures, devices, accounts, categories, recurring templates, the settlement proposal, APK serving, the invite landing page, rate limiting, and the bootstrap and pair-device subcommands. Every endpoint is covered by integration tests for each error code it can return.

2. Read first

  1. SPEC §7 API outline.
  2. SPEC §14.3 OpenAPI 3.1 outline: 14.3.1 Common rules, 14.3.2 Error shape and error codes, 14.3.3 Wire objects, 14.3.4 Endpoints (E-2 to E-30, E-33), 14.3.5 Examples as contract.
  3. SPEC §4 FR-1 Server bootstrap, FR-2 Invites and pairing, FR-3 Devices and revocation, FR-4 Profiles, FR-5 Household gate, FR-6 Expense accounts, FR-7 Categories, FR-9 Recurring expenses (AC-9.1, AC-9.7, AC-9.8), FR-11 AC-11.8, FR-16 AC-16.1.
  4. SPEC §8.2 Device tokens, §8.3 Invite codes, §8.4 Authorization rules, §8.5 Transport (X-Forwarded-For), §8.6 Rate limiting and input limits, §8.7 Privacy and logging.
  5. SPEC §9.4 Settlement allocation proposal, §9.5 Recurring generation (resume rule, next_occurrence_date, due-period generation on create), §9.6 Limits and validation, §9.7 Icon and colour tokens, §9.10 Other edge cases.
  6. SPEC §10.4 Server configuration and CLI (bootstrap, pair-device), §10.6 APK hosting and release (latest.json, file name pattern).
  7. SPEC §14.1 SQLite schema v1 and server-side rules; §14.5.2 Functions (propose_allocation).
  8. SPEC §11.2 (row M3), §11.3, §14.7, §14.8.
  9. Files: contracts/openapi.yaml, contracts/examples/**, the M2a code under server/ (router, error, auth, wire, db), CLAUDE.md (BUILD AND VERIFY, ARCHITECTURE RULES, ANTI-PATTERNS).

3. Already established

  • Routes, the utoipa ApiDoc, error type, auth extractors, wire objects, DB pool and sequence exist from M2a; M3 fills in handler bodies in its own files (§11.3).
  • bootstrap: no users → cancel any pending admin_setup, create a new one, print code, link <KVITT_PUBLIC_URL>/i/<CODE>, expiry, exit 0; users exist → already initialized on stderr, exit 2 (AC-1.1 to AC-1.3, §10.4). pair-device --user <id>: device invite, exit 3 for unknown user (AC-1.6, §10.4).
  • Invite codes: 12 Crockford base32 characters, displayed XXXX-XXXX-XXXX, normalised (uppercase, strip - and spaces, O→0, I/L→1), stored as SHA-256 hex, single-use, 72 h expiry, cancellable; failures are invite_invalid without saying why (§8.3, AC-2.3).
  • Tokens: kvt_ + base64url(32 CSPRNG bytes) without padding, 47 characters, stored as SHA-256 hex; never logged (§8.2).
  • Partner invite refused with household_full at two users; a new partner invite cancels a pending one (AC-2.1). Accept returns token, user, device, household (AC-2.4, E-3).
  • Landing page GET /i/{code}: Swedish if Accept-Language prefers sv, else English; app name, "Download Kvitt" to the latest APK, "Open in Kvitt" to kvitt://invite?server=<urlencoded KVITT_PUBLIC_URL>&code=<CODE>, code in groups of 4, one sentence of instructions; no validity hint, no personal data; any 1 to 32 character code gets 200, else 404 HTML (AC-2.5, E-4).
  • Rate limits: unauthenticated 60/min per IP, authenticated 600/min per device, 429 rate_limited with Retry-After; more than 10 failed accepts per IP in 60 minutes → rate_limited for the rest of the window; in memory; X-Forwarded-For trusted only from the Caddy peer (KVITT_TRUSTED_PROXY) (§8.6, AC-2.12, §8.5, §10.4).
  • Pictures: JPEG/PNG/WebP up to 5 MiB and 8000 × 8000, EXIF orientation applied, centre-crop square, JPEG q85 at 512 and 128, metadata stripped, picture_version incremented, files pictures/<user_id>/<v>-512.jpg and -128.jpg, older versions deleted after success; E-11 headers ETag, Cache-Control: private, max-age=31536000, immutable, size 128|512 default 512 (AC-4.4, §10.4, E-9 to E-11).
  • Household gate: account create, template create and any expense or settlement mutation → household_incomplete below two users (AC-5.2).
  • Accounts: names unique among non-deleted, case-insensitive after trim (Unicode lowercase) via name_key; delete only when unreferenced (account_not_empty), soft delete; ratio change affects only later expenses (FR-6).
  • Categories: icon and colour validated against §9.7 lists; new sort_order = max + 10; name: null resets a default name, category_is_default on a non-default; delete blocked by use (category_in_use) or default key (category_is_default); no uniqueness (FR-7, E-23 to E-27).
  • Recurring templates: E-28 generates an already-due period before responding; E-29 split_mode and splits together; resume does not backfill; next_occurrence_date per §9.5 (§14.3.4, §9.5).
  • Every REST write that changes a synced row takes a new server_version in a BEGIN IMMEDIATE transaction and returns the row in wire form (§7, §14.1).
  • E-33 uses kvitt-domain::propose_allocation over non-deleted rows, exclude_settlement_id honoured (AC-11.8, E-33).
  • APK: GET /apk/latest reads apk/latest.json on each request; GET /apk/files/{file_name} only for ^kvitt-[0-9]+\.apk$ that exists, application/vnd.android.package-archive, Content-Length (§10.6, E-5, E-6).
  • last_seen_at/app_version_code update at most hourly (§8.2); devices never deleted (AC-3.5).

4. Owned paths

From SPEC §14.7, verbatim: kvitt-server: src/api/{invites,landing,me,pictures,devices,accounts,categories,recurring,proposal,apk}.rs, src/ratelimit.rs, src/cli/{bootstrap,pair_device}.rs, tests/core_*.rs (owner M3; after landing: "later server plans"). Also from §14.7: M3 "may add a dependency line to a crate's Cargo.toml (second to land resolves Cargo.lock on rebase)" and "may add one line per new module to mod.rs files they need".

Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator.

Open points the orchestrator must settle in the start prompt's notes: (a) E-28's due-period generation, E-29's resume rule and next_occurrence_date use §9.5 logic, the audit writer and the push queue, which §14.7 places in M4's src/scheduler/**, src/audit/** and src/push/** while M3 and M4 run in parallel: which plan writes the shared generation function and in which order they land; (b) E-2 GET /setup/status is listed for M3 in §11.2 but implemented by M2a (src/api/setup.rs); M3 only tests it; (c) how the rate-limit layer and body limits attach to M2a's router.rs; (d) the landing page text (one sentence of instructions, English and Swedish) is not in §13 or docs/ux/strings.csv: who writes the Swedish copy.

5. Deliverables

  • src/api/invites.rs: E-3, E-12, E-13, E-14.
  • src/api/landing.rs: E-4 HTML in sv and en.
  • src/api/me.rs: E-7, E-8.
  • src/api/pictures.rs: E-9, E-10, E-11.
  • src/api/devices.rs: E-15, E-16, E-17.
  • src/api/accounts.rs: E-18 to E-22.
  • src/api/categories.rs: E-23 to E-27.
  • src/api/recurring.rs: E-28 to E-30.
  • src/api/proposal.rs: E-33.
  • src/api/apk.rs: E-5, E-6.
  • src/ratelimit.rs: §8.6 limits and AC-2.12.
  • src/cli/bootstrap.rs, src/cli/pair_device.rs: FR-1 subcommands.
  • tests/core_*.rs: one or more integration test files per endpoint group covering success and every error code listed for each endpoint in §14.3.4, responses compared with contracts/examples/.

6. Steps

  1. Worktree. From the main checkout /Users/jhag/projects/kvitt: git worktree add .claude/worktrees/agent-m3 -b agent-m3 main, then in .claude/worktrees/agent-m3: git reset --hard main. No git-ignored files are needed; copy none. Run tools/install-git-hooks.sh.
  2. Read §2 in order. Confirm M2a has landed (server/crates/kvitt-server/src/router.rs exists on main); if not, stop and report.
  3. Versions: any crate added (for example image decoding, HTML templating, a rate limiter) at its latest stable version from crates.io / docs.rs; confirm current API usage from official docs; record each.
  4. Build the test harness first in tests/core_common.rs or equivalent (temp SQLite, injected clock, seeded two-user household helper, request helper that sends X-Kvitt-Protocol: 1).
  5. Implement in this order, each with its tests before moving on: bootstrap and pair-device; E-3 accept (all three kinds) and E-12 to E-14; E-7, E-8; E-15 to E-17; E-18 to E-22; E-23 to E-27; E-28 to E-30 (per open point a); E-33; E-9 to E-11; E-5, E-6; E-4; rate limiting (per open point c).
  6. For each endpoint, test every error code §14.3.4 lists for it, plus household_incomplete where AC-5.2 applies and forbidden for member on admin routes.
  7. Compare response bodies with contracts/examples/<E-id>-<name>/response.json by JSON value (ids and timestamps normalised as the examples allow).
  8. Check logs from one test run contain no token, code, name, description, note or amount (§8.7).
  9. Run the gate (§7). Commit on agent-m3, git rebase main (resolve Cargo.lock if M4 landed first), rerun the gate. Never merge, push or remove the worktree.

7. Gate

From SPEC §11.2, verbatim: "Server gate; integration tests per endpoint and per error code listed for it". Command, from server/: cargo fmt --check && cargo clippy -- -D warnings && cargo test. Green means: all three exit 0 in this session; test result: lines show 0 failed; the report contains a table endpoint × error code → test name with no empty cell for E-2 to E-30 and E-33.

8. HUMAN steps

From SPEC §11.2, verbatim: "none".

9. Out of scope

  • E-31, E-32 sync, audit writer and diff, push sender, scheduler, rotate-epoch, sync_mutations pruning (M4), except what open point (a) assigns.
  • Router, error type, auth extractor, wire types, migrations (M2a; contract requests only).
  • The importer (M9); infra/ (M2c-dev, M2c-pi); any Android work.
  • Semantic changes to contracts/**.

10. Report

  • Branch agent-m3, head commit hash after rebasing on main.
  • Gate output: fmt and clippy exit status, every test result: line.
  • The endpoint × error code → test table.
  • Versions of crates added, with sources.
  • How open points (a) to (d) were resolved.
  • Numbering used (decision ids, spec version) or TODO(spec) occurrences.
  • Contract requests (anything needed in M2a's files), open questions, spec gaps (with section numbers).

11. Numbering

Decision ids, spec version and schema version are assigned by the orchestrator in the start prompt; use TODO(spec) if none was given.