steid

@jamesgill /

steid/plans/current.md
6.2 KBCode·Blame·Raw
ab7fea9chore: plans setup1mo
1# Current
2
3> Keep this file short. One active step, one ordered backlog. Completed work moves to
4> [progress.md]progress.md. If this file starts reading like a changelog, it has
5> drifted — that's exactly what went wrong last time.
6
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
7## Active: Milestone 1 — Identity, thin
ab7fea9chore: plans setup1mo
8
c1a35b0feat: claim the instance on first run instead of bootstrapping from config1mo
9**Goal:** the app knows who you are. An unclaimed installation is claimed through
10`/setup`, and the resulting owner can log in and out. Enough identity to hang a profile
11page off, and no more.
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
12
13**Explicitly out of scope** — these are Milestone 7: multi-user registration,
14invite codes, `RegistrationPolicy`, organisation management UI, roles beyond owner.
ab7fea9chore: plans setup1mo
15
16### Steps
17
c1a35b0feat: claim the instance on first run instead of bootstrapping from config1mo
18- [x] Domain: typed IDs, `Email`, `PasswordHash`, `User`, `Organization`,
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
19 `Membership`, `Role`, `Actor`, `DomainError`
c1a35b0feat: claim the instance on first run instead of bootstrapping from config1mo
20- [x] Domain: repository traits — `UserRepository`, `OrgRepository`,
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
21 `MembershipRepository`
c1a35b0feat: claim the instance on first run instead of bootstrapping from config1mo
22- [x] Infrastructure: in-memory implementations (these are what make use cases
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
23 testable without a database)
c1a35b0feat: claim the instance on first run instead of bootstrapping from config1mo
24- [x] Application: `PasswordHasher` port + Argon2 adapter, stub hasher for tests
25- [x] Domain: `SetupToken` — one-time claim secret, constant-time comparison
26- [x] Application: `claim_instance` use case — token-gated, creates org → user →
27 owner membership, returns the owner signed in
28- [x] Application: `login` use case — verifies credentials, returns an `Actor`
88583f2docs: bring tracking docs up to date with milestone 11mo
29- [x] Infrastructure: migrations + SQLite implementations
30- [x] Infrastructure: `sessions` table + session storage
31- [x] Boot: mint and print a `SetupToken` when unclaimed; register it in app context
32- [x] Web: `/setup` claim page; every other route redirects there while unclaimed
33- [x] Web: login page, logout, `current_actor(cx)` helper
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
34- [ ] `/api/me` — first `/api` route, proves the use case layer has two consumers
ab7fea9chore: plans setup1mo
35
36### Done when
37
c1a35b0feat: claim the instance on first run instead of bootstrapping from config1mo
38A fresh database prints a setup token at boot; `/setup` with that token creates the
39owner and signs them in; logging out and back in works; `/api/me` returns that
40identity.
d7b99d9docs: record milestone 0 progress and routing findings1mo
41
88583f2docs: bring tracking docs up to date with milestone 11mo
42Everything but `/api/me` is done and verified in a browser.
43
44### Resolved
45
46- **`__Host-` cookies need a secure context — and it bit.** The claim succeeded, the
47 server recorded sessions, and every page still rendered signed out, because the
48 browser silently discarded a `Secure` cookie served over plain HTTP. Nothing errored
49 on either side. Fixed with `InsecureCookieTokenStore` behind
50 `STEID_INSECURE_COOKIES`, off by default — see [runbook.md]runbook.md#configuration.
51- **Claim TOCTOU** is now covered by a test that drives a real claim through the SQLite
52 repos and asserts `unique(orgs.name)` / `unique(users.email)` refuse the second.
53
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
54### Watch for
d7b99d9docs: record milestone 0 progress and routing findings1mo
55
c1a35b0feat: claim the instance on first run instead of bootstrapping from config1mo
56- **CSRF.** `SameSite=Lax` blocks cross-site POSTs, which covers the common case.
57 Whether forms also want tokens is an open decision, not a default to pick quietly.
88583f2docs: bring tracking docs up to date with milestone 11mo
58 Still undecided.
59- **No rate limiting anywhere.** `/login` and `/setup` accept unlimited attempts. The
60 setup token has 256 bits so brute force is not the worry; password guessing is.
61- **Form errors are invisible.** A wrong token or password redirects back with no
62 message — deliberate, so failures can't be used to probe, but indistinguishable from
63 a broken form. Flash messages are the fix and don't exist yet.
64- **Session sweeping is never called.** `sweep_expired` exists and is tested but
65 nothing invokes it, so expired rows accumulate. Expiry is enforced on read, so this
66 is tidiness rather than a security hole.
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
67- **Foreign key ordering.** The org must be saved before the user — attempt #2 had to
88583f2docs: bring tracking docs up to date with milestone 11mo
68 fix this in two places. Enforced now: `foreign_keys(true)` plus a test.
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
69- Never log or `Debug`-print a password. `PasswordHash` is opaque on purpose.
ab7fea9chore: plans setup1mo
70
71## Backlog
72
73Ordered. Pull from the top.
74
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
751. **Milestone 2 — Profile page.** `/{owner}` becomes the real home page, replacing
76 the Milestone 0 placeholder. The frame the rest of the product hangs in.
772. **Milestone 3 — Writing.** Posts, markdown rendering, `/{owner}/{slug}`.
78 *Open question: is writing actually the first portfolio feature, or is it
79 projects/showcases?*
803. **Milestone 4 — Repo model.** `Repository` entity, `Visibility`, `create_repo`,
81 bare repo on disk at `{data_dir}/{org}/{repo}.git`. Watch the DB-plus-filesystem
82 atomicity problem — see [architecture.md]architecture.md#db-plus-filesystem-writes.
834. **Milestone 5 — Git over HTTP.** `git http-backend` subprocess, PATs over HTTP
84 Basic. See [0001]decisions/0001-git-over-http-not-ssh.md.
ab7fea9chore: plans setup1mo
85
86## Open questions
87
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
88- **Topcoat is early** (v0.5.0, first released 2026-07-22, breaking changes expected
89 by its own authors). Expect churn that isn't feature work.
90- Body size limits will reject large pushes at Milestone 5 — `topcoat-router` has a
91 `body_limit` layer that needs raising on the git routes. Recorded here because it
92 will surface as a confusing failure rather than a clear one.
93- Topcoat ships Tailwind without Node, which reopens the design system attempt #1
94 dropped purely to avoid an npm build step — see [ui.md]ui.md.
95
96## Routing findings (Milestone 0)
97
98- **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat`
99 silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local
100 stable is now 1.97.1.
101- `Router::builder().discover()` collects `#[page]`-annotated items **at link time**,
102 so pages can live in any module. Layering is our choice, not the framework's.
103- `module_router!` derives each URL from the module tree rather than a path string.
104 Still deferred — Steid's URL space is parameterised at the root (`/{owner}`,
105 `/{owner}/{repo}`), which means `path_param!` declarations inside route modules.
106 Worth designing at Milestone 2 when the profile page makes it concrete.
107- Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not
108 injected as handler arguments. Parses are memoized per request.
109- Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's
110 `NotFoundError` to render a branded 404.
111- `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` is gone.
112- `Body` is a boxed `http_body::Body` used for both requests and responses, with
113 `into_data_stream()` to read and `Body::new()` to wrap a stream — pack data can
114 stream both directions without buffering. This is what makes Milestone 5 viable.