| | @@ -4,75 +4,84 @@ |
| 4 | 4 | > [progress.md](progress.md). If this file starts reading like a changelog, it has |
| 5 | 5 | > drifted — that's exactly what went wrong last time. |
| 6 | 6 | |
| 7 | | −## Active: Milestone 0 — Skeleton |
| 7 | +## Active: Milestone 1 — Identity, thin |
| 8 | 8 | |
| 9 | | −**Goal:** a Topcoat app that boots, serves one page, reads config from env, and opens |
| 10 | | −a SQLite pool. No domain logic yet. The point is to learn Topcoat's shape before |
| 11 | | −committing the architecture to it. |
| 9 | +**Goal:** the app knows who you are. One owner, bootstrapped from config, who can log |
| 10 | +in and out. Enough identity to hang a profile page off, and no more. |
| 11 | + |
| 12 | +**Explicitly out of scope** — these are Milestone 7: multi-user registration, |
| 13 | +invite codes, `RegistrationPolicy`, organisation management UI, roles beyond owner. |
| 12 | 14 | |
| 13 | 15 | ### Steps |
| 14 | 16 | |
| 15 | | −- [x] Add `topcoat` 0.5 + `tokio` to `Cargo.toml` |
| 16 | | −- [x] Get the getting-started hello-world page rendering |
| 17 | | −- [x] Work out how routing wants the source tree laid out — see |
| 18 | | − [routing findings](#routing-findings) below |
| 19 | | −- [x] Config from env via `envy` (`STEID_*`) — see [runbook.md](runbook.md) |
| 20 | | −- [x] SQLite pool registered as app context; page reads it via `app_context::<T>(cx)` |
| 21 | | −- [ ] Install `topcoat-cli` and confirm `topcoat dev` watch/reload works |
| 22 | | −- [ ] Decide the layering question in [architecture.md](architecture.md#open-question-topcoats-data-access-vs-clean-architecture) |
| 23 | | − — this is the one genuinely open design question and it blocks Milestone 1 |
| 17 | +- [ ] Domain: typed IDs, `Email`, `PasswordHash`, `User`, `Organization`, |
| 18 | + `Membership`, `Role`, `Actor`, `DomainError` |
| 19 | +- [ ] Domain: repository traits — `UserRepository`, `OrgRepository`, |
| 20 | + `MembershipRepository` |
| 21 | +- [ ] Infrastructure: in-memory implementations (these are what make use cases |
| 22 | + testable without a database) |
| 23 | +- [ ] Infrastructure: migrations `001`–`003`, SQLite implementations |
| 24 | +- [ ] Application: `PasswordHasher` port + Argon2 adapter, stub hasher for tests |
| 25 | +- [ ] Application: `bootstrap_owner` use case — creates org, then user, then |
| 26 | + membership, idempotent on reboot |
| 27 | +- [ ] Application: `login` use case — verifies credentials, returns an `Actor` |
| 28 | +- [ ] Web: login page, logout, session cookie, `current_actor(cx)` helper |
| 29 | +- [ ] `/api/me` — first `/api` route, proves the use case layer has two consumers |
| 24 | 30 | |
| 25 | 31 | ### Done when |
| 26 | 32 | |
| 27 | | −`cargo run` serves a page that renders a value read from SQLite, with config supplied |
| 28 | | −by env. **Met** — renders the SQLite version, and `STEID_DATABASE_URL` redirects it to |
| 29 | | −a different file. |
| 33 | +A fresh database boots into an owner account from `STEID_OWNER_*`; logging in through |
| 34 | +the web UI sets a session; `/api/me` returns that identity; logging out clears it. |
| 30 | 35 | |
| 31 | | −### Routing findings |
| 36 | +### Watch for |
| 32 | 37 | |
| 33 | | −- **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat` |
| 34 | | − silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local |
| 35 | | − stable is now 1.97.1. Worth a `rust-toolchain.toml` if this ever builds on another |
| 36 | | − machine. |
| 37 | | −- `Router::builder().discover()` collects `#[page]`-annotated items **at link time**, |
| 38 | | − so pages can live in any module. Layering is our choice, not the framework's — the |
| 39 | | − home page sits in `infrastructure/web.rs` per `architecture.md`. |
| 40 | | −- `module_router!` is Topcoat's recommended alternative, deriving each URL from the |
| 41 | | − module tree rather than a path string. **Deferred to Milestone 1.** Steid's URL space |
| 42 | | − is parameterised at the root (`/{owner}`, `/{owner}/{repo}`, `/{owner}/keys`), which |
| 43 | | − means `path_param!` declarations inside route modules; worth designing once there are |
| 44 | | − real routes rather than guessing now. |
| 45 | | −- Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not |
| 46 | | − injected as handler arguments. Parses are memoized per request. |
| 47 | | −- Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's |
| 48 | | − `NotFoundError` to render a branded 404 — that replaces attempt #2's `not_found.html`. |
| 49 | | −- `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` from attempt #2 is |
| 50 | | − gone. `runbook.md` reflects this. |
| 38 | +- **Foreign key ordering.** The org must be saved before the user — attempt #2 had to |
| 39 | + fix this in both `bootstrap_owner` and `register_user`. See |
| 40 | + [progress.md](progress.md#identity). |
| 41 | +- **Bootstrap must be idempotent.** It runs on every boot, not just the first. |
| 42 | +- Never log or `Debug`-print a password. `PasswordHash` is opaque on purpose. |
| 51 | 43 | |
| 52 | 44 | ## Backlog |
| 53 | 45 | |
| 54 | 46 | Ordered. Pull from the top. |
| 55 | 47 | |
| 56 | | −1. **Milestone 1 — Identity.** Domain model (User, Org, Membership, Actor, Role), |
| 57 | | − value objects (Email, PasswordHash), typed IDs, repository ports, Argon2 hashing, |
| 58 | | − registration policy, session cookies. Reference implementation exists and was |
| 59 | | − solid — port the domain layer, rewrite the web layer. |
| 60 | | −2. **Milestone 2 — Repo model.** `Repository` entity, `Visibility`, `create_repo` use |
| 61 | | − case, bare repo on disk at `{data_dir}/{org}/{repo}.git`. Watch the |
| 62 | | − DB-plus-filesystem atomicity problem — see |
| 63 | | − [architecture.md](architecture.md#db-plus-filesystem-writes). |
| 64 | | −3. **Milestone 3 — Git over SSH.** `GitProtocolServer` port, `GitBinary` adapter, |
| 65 | | − embedded russh. Channel-splitting is fiddly; the notes in |
| 66 | | − [progress.md](progress.md#ssh-channel-bridging) are hard-won. |
| 67 | | −4. **Milestone 4 — SSH key auth + authz.** |
| 48 | +1. **Milestone 2 — Profile page.** `/{owner}` becomes the real home page, replacing |
| 49 | + the Milestone 0 placeholder. The frame the rest of the product hangs in. |
| 50 | +2. **Milestone 3 — Writing.** Posts, markdown rendering, `/{owner}/{slug}`. |
| 51 | + *Open question: is writing actually the first portfolio feature, or is it |
| 52 | + projects/showcases?* |
| 53 | +3. **Milestone 4 — Repo model.** `Repository` entity, `Visibility`, `create_repo`, |
| 54 | + bare repo on disk at `{data_dir}/{org}/{repo}.git`. Watch the DB-plus-filesystem |
| 55 | + atomicity problem — see [architecture.md](architecture.md#db-plus-filesystem-writes). |
| 56 | +4. **Milestone 5 — Git over HTTP.** `git http-backend` subprocess, PATs over HTTP |
| 57 | + Basic. See [0001](decisions/0001-git-over-http-not-ssh.md). |
| 68 | 58 | |
| 69 | 59 | ## Open questions |
| 70 | 60 | |
| 71 | | −- **Topcoat is nine days old** (v0.5.0, first release 2026-07-22, breaking changes |
| 72 | | − expected). Pin the exact version and expect to chase it. Budget time for churn that |
| 73 | | − isn't feature work. |
| 74 | | −- Does Topcoat's asset/CSS pipeline coexist with an embedded russh server in one |
| 75 | | − binary, or does the CLI's watch-and-rebuild model push SSH into a separate process? |
| 76 | | − Unresolved — affects the embedded-SSH approach and single-binary deploy. |
| 77 | | −- Topcoat ships Tailwind without Node. That reopens the design system from attempt #1 |
| 78 | | − that was dropped purely to avoid an npm build step — see [ui.md](ui.md). |
| 61 | +- **Topcoat is early** (v0.5.0, first released 2026-07-22, breaking changes expected |
| 62 | + by its own authors). Expect churn that isn't feature work. |
| 63 | +- Body size limits will reject large pushes at Milestone 5 — `topcoat-router` has a |
| 64 | + `body_limit` layer that needs raising on the git routes. Recorded here because it |
| 65 | + will surface as a confusing failure rather than a clear one. |
| 66 | +- Topcoat ships Tailwind without Node, which reopens the design system attempt #1 |
| 67 | + dropped purely to avoid an npm build step — see [ui.md](ui.md). |
| 68 | + |
| 69 | +## Routing findings (Milestone 0) |
| 70 | + |
| 71 | +- **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat` |
| 72 | + silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local |
| 73 | + stable is now 1.97.1. |
| 74 | +- `Router::builder().discover()` collects `#[page]`-annotated items **at link time**, |
| 75 | + so pages can live in any module. Layering is our choice, not the framework's. |
| 76 | +- `module_router!` derives each URL from the module tree rather than a path string. |
| 77 | + Still deferred — Steid's URL space is parameterised at the root (`/{owner}`, |
| 78 | + `/{owner}/{repo}`), which means `path_param!` declarations inside route modules. |
| 79 | + Worth designing at Milestone 2 when the profile page makes it concrete. |
| 80 | +- Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not |
| 81 | + injected as handler arguments. Parses are memoized per request. |
| 82 | +- Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's |
| 83 | + `NotFoundError` to render a branded 404. |
| 84 | +- `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` is gone. |
| 85 | +- `Body` is a boxed `http_body::Body` used for both requests and responses, with |
| 86 | + `into_data_stream()` to read and `Body::new()` to wrap a stream — pack data can |
| 87 | + stream both directions without buffering. This is what makes Milestone 5 viable. |