| 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 | |
| 7 | ## Active: Milestone 2 — Profile page |
| 8 | |
| 9 | **Goal:** `/{handle}` becomes the real profile page, replacing the Milestone 0 |
| 10 | placeholder. This is the frame the rest of the product hangs in — repos, writing, and |
| 11 | projects all appear on it later. |
| 12 | |
| 13 | **Explicitly out of scope:** repos and posts don't exist yet, so the page shows |
| 14 | identity plus empty sections. Editing display name / bio is Milestone 2 only if it |
| 15 | stays small. |
| 16 | |
| 17 | ### Steps |
| 18 | |
| 19 | - [x] Settle URL shape — handles at the root, routes grouped under prefixes, see |
| 20 | [0004](decisions/0004-root-handles-grouped-routes.md) |
| 21 | - [x] Reserved-handle denylist in `OrgName::new`; auth routes moved under `/auth/` |
| 22 | - [ ] Settle routing mechanics: `module_router!` vs explicit `#[page]` paths, and |
| 23 | `path_param!` for `{handle}` — deferred from Milestone 0 |
| 24 | - [ ] `/{handle}` — public page, resolves the org by handle, 404 when unknown |
| 25 | - [ ] `/` redirects to the owner's profile once claimed |
| 26 | - [ ] Owner-only affordances visible when the viewer is the owner |
| 27 | - [ ] `/api/users/{handle}` — the API surface for the same read model |
| 28 | |
| 29 | ### Done when |
| 30 | |
| 31 | `/{handle}` renders for a signed-out visitor, shows extra affordances to the |
| 32 | owner, and an unknown handle 404s rather than erroring. |
| 33 | |
| 34 | ### Watch for |
| 35 | |
| 36 | - **Handle lookups are case-insensitive** in storage (`collate nocase`) and lowercased |
| 37 | by `OrgName::new`. A URL with different casing must resolve, not 404. |
| 38 | - **Reserve handles early.** Adding to the denylist later is a breaking change for |
| 39 | whoever holds that handle. It is free while unclaimed, so reserve an area before it |
| 40 | exists — see [0004](decisions/0004-root-handles-grouped-routes.md). |
| 41 | - **A profile is public.** It is the first page rendering for anonymous visitors by |
| 42 | design, so anything private must be gated explicitly rather than by assuming a |
| 43 | session exists. |
| 44 | |
| 45 | ### Carried over — small, unblocked, worth doing alongside |
| 46 | |
| 47 | - **Flash messages.** A wrong password or setup token bounces with no explanation. |
| 48 | Deliberate on the security side, but indistinguishable from a broken form. Every |
| 49 | form added from here inherits the problem. |
| 50 | - **No rate limiting** on `/login` or `/setup`. |
| 51 | - **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is |
| 52 | enforced on read, so this is tidiness, not a hole. |
| 53 | - **CSRF.** `SameSite=Lax` covers the common case; whether forms also want tokens is |
| 54 | still undecided. |
| 55 | - **Styling.** Everything is unstyled HTML. Topcoat ships Tailwind without Node, and |
| 56 | `steid-backup/AGENTS/UI.md` has a full OKLCH system to mine. Cheaper at two pages |
| 57 | than at ten — see [ui.md](ui.md). |
| 58 | |
| 59 | ## Backlog |
| 60 | |
| 61 | Ordered. Pull from the top. |
| 62 | |
| 63 | 1. **Milestone 3 — Writing.** Posts, markdown rendering, `/{handle}/posts/{slug}`. |
| 64 | *Open question: is writing actually the first portfolio feature, or is it |
| 65 | projects/showcases?* |
| 66 | 2. **Milestone 4 — Repo model.** `Repository` entity, `Visibility`, `create_repo`, |
| 67 | bare repo on disk at `{data_dir}/{org}/{repo}.git`. Watch the DB-plus-filesystem |
| 68 | atomicity problem — see [architecture.md](architecture.md#db-plus-filesystem-writes). |
| 69 | 3. **Milestone 5 — Git over HTTP.** `git http-backend` subprocess, PATs over HTTP |
| 70 | Basic. See [0001](decisions/0001-git-over-http-not-ssh.md). |
| 71 | |
| 72 | ## Open questions |
| 73 | |
| 74 | - **Topcoat is early** (v0.5.0, first released 2026-07-22, breaking changes expected |
| 75 | by its own authors). Expect churn that isn't feature work. |
| 76 | - Body size limits will reject large pushes at Milestone 5 — `topcoat-router` has a |
| 77 | `body_limit` layer that needs raising on the git routes. Recorded here because it |
| 78 | will surface as a confusing failure rather than a clear one. |
| 79 | - Topcoat ships Tailwind without Node, which reopens the design system attempt #1 |
| 80 | dropped purely to avoid an npm build step — see [ui.md](ui.md). |
| 81 | |
| 82 | ## Routing findings (Milestone 0) |
| 83 | |
| 84 | - **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat` |
| 85 | silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local |
| 86 | stable is now 1.97.1. |
| 87 | - `Router::builder().discover()` collects `#[page]`-annotated items **at link time**, |
| 88 | so pages can live in any module. Layering is our choice, not the framework's. |
| 89 | - `module_router!` derives each URL from the module tree rather than a path string. |
| 90 | Still deferred. Application routes now group cleanly (`auth/login`, `api/me`), but |
| 91 | handles sit at the root ([0004](decisions/0004-root-handles-grouped-routes.md)), so a |
| 92 | parameterised root segment still has to coexist with static ones. Worth checking how |
| 93 | `module_router!` handles that before committing to it. |
| 94 | - Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not |
| 95 | injected as handler arguments. Parses are memoized per request. |
| 96 | - Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's |
| 97 | `NotFoundError` to render a branded 404. |
| 98 | - `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` is gone. |
| 99 | - `Body` is a boxed `http_body::Body` used for both requests and responses, with |
| 100 | `into_data_stream()` to read and `Body::new()` to wrap a stream — pack data can |
| 101 | stream both directions without buffering. This is what makes Milestone 5 viable. |