writeonce/docs/stories/porch/03-sessions.md
shoney.arickathil 1fe808b7a4 docs(stories): add readiness, retire status: refine, sweep all 47 iterations
- `readiness: ready | refine` is a SECOND axis, orthogonal to status.
  `ready` = the brainstorm is complete and the decisions are LOCKED (a spec
  approved, or the forks explicitly confirmed). `refine` = open forks remain
  and it cannot be planned yet
- `status: refine` RETIRED because it carried both meanings at once, so a held
  iteration with an approved spec (language 18, 26) was indistinguishable from
  one nobody had thought about. status is now purely where the WORK is:
  done | in-progress | pending | hold — `pending` was already the board's own
  rendering word, so nothing new was invented
- all 47 iterations classified from EVIDENCE in their own text, not by guess:
  "the four forks are SETTLED" / "spec + plan approved" / "Approved spec:" for
  ready; "Forks the spec must settle" / "no spec exists yet" for refine. Every
  shipped iteration is ready by definition. 19 done, 5 in-progress, 15
  pending, 8 hold; 27 ready, 20 refine
- two iterations moved refine -> in-progress rather than -> pending: language
  31 and 34 are absorbed into 24 and work on them is literally happening, which
  the board already showed as 🔄 while their frontmatter said otherwise. That
  disagreement is now gone
- board legend, board-views' frontmatter contract, and two new Dataview
  queries updated — the useful one being `readiness: ready AND status:
  pending`, the startable set

WHAT THE NEW AXIS IMMEDIATELY SURFACED: of 15 pending iterations, exactly ONE
is startable — databasev2 4, io_uring group-commit, whose forks were confirmed
settled 2026-08-20. Everything else pending needs a brainstorm first. That was
invisible while one key carried both meanings, and it is now on the board.

Also caught by the sweep, unrelated to readiness but found by cross-checking
frontmatter against the board: SIX duplicate rows. Every iteration moved into
databasev2 was still listed in the LANGUAGE pending table under its retired id
(23, 32, 33, 20, 21, 27) as well as its new one. Stale copies removed. And two
databasev2 rows made claims the sweep contradicts — iteration 1 was billed
"startable today" while its forks are open, and 6 still called itself the
ceiling-raiser after 2 took that role.

Docs only. linkcheck 0 broken / 0 anchors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:54:45 +02:00

6.6 KiB

track iteration status readiness
porch 3 pending refine

porch 3 — sessions: server-side state, revocable, durable

Part of Story — porch, the writeonce web framework. Source: the Fiber parity study §2. Needs iteration 2 (a random session id and a signed cookie to carry it) and inherits the store convention from iteration 1.

Goals

  • A session is a @table row keyed by a random id; the cookie carries only the id. The alternative — a signed cookie carrying the whole payload — needs no store but cannot be revoked, and revocation is not optional for a real login. This is the fork iteration 1's store convention exists to answer.
  • Both timeouts, because they answer different questions. Idle timeout bounds "how long since you did anything"; absolute timeout bounds "how long since you authenticated". Fiber ships both (IdleTimeout, AbsoluteTimeout) and a session with only the first never expires for an active attacker.
  • Durability is the differentiator. Fiber's default store is in-memory: a restart logs everyone out. porch's sessions ride the WAL, so they survive — and the gate proves it, because an unexercised durability claim is not a claim.
  • Revocation that works. Logout, and "log out everywhere for this principal" — the second is what a password change needs, and it is nearly free once sessions are rows with an owner column.

Phases

Phase A — the session table and its lifecycle

  • The @table class: random id, principal, created-at, last-seen, and whatever payload shape fork 1 settles. Secondary index on principal, because "revoke all for this user" is an equality probe and that is the only index shape the engine has.
  • Create, touch, expire and delete, with expiry evaluated on access (the lazy discipline iteration 1 established — there is still no timer).
  • Verify: rows replay across a restart; an expired row is pruned when touched.

Phase B — the middleware

  • A Session middleware whose before reads the signed cookie, loads the row, checks both timeouts, and attaches identity. Decide whether it sets req.principal or writes into req.ctx — principal is the field the framework already means for identity, so it should be that unless the session carries more than identity.
  • Rotate the id on privilege change (login especially). Session fixation is the attack that a login which reuses the pre-login id walks straight into.
  • A missing, expired, unknown or tampered cookie all end at the same place — unauthenticated — but for distinguishable reasons, because debugging a login loop without that distinction is miserable.
  • Verify: each of those four cases behaves; the id changes across login.

Phase C — login, logout, revoke-all

  • The three flows end to end in a sample, using porch's existing auth pieces (bearer_token, basic_credentials, ct_eq) for the credential check — sessions are about keeping identity, not establishing it.
  • Logout deletes the row and clears the cookie with matching attributes.
  • Revoke-all deletes by principal and is proven with two concurrent sessions.
  • Verify: after logout the old cookie is inert even though it is still well-signed — the point of server-side state.

Phase D — the gate and the ledger

  • Extend scripts/site-accept.sh rather than only the storefront: the site has an admin route that currently checks a bearer token per request, which is exactly the thing sessions replace.
  • Ledger rows, status board entry, and the standup questions answered.
  • Verify: just web-app, just site, just linkcheck green.

Acceptance Criteria

  • Given a valid session cookie, when a request arrives, then identity is attached and the row's last-seen advances.
  • Given a session whose signature is valid but whose row was deleted, when it is presented, then the request is unauthenticated — a well-formed cookie is not authority.
  • Given an idle timeout of T, when a session is unused for longer than T, then it is rejected and its row pruned on that access.
  • Given an absolute timeout of A, when a session is continuously active past A, then it is still rejected — the criterion an idle-only implementation fails.
  • Given an anonymous session id, when the user logs in, then the id is rotated and the pre-login id is inert.
  • Given active sessions and a SIGTERM plus restart, when the same cookies return, then the users are still logged in, replayed from the WAL.
  • Given two sessions for one principal, when revoke-all runs, then both are inert and other principals are untouched.
  • Given logout, when the cleared cookie is compared to the one that set it, then the attributes match, so the browser actually drops it.

Out Of Scope

  • Flash messages and a general session bag. A typed session with a known shape first; an untyped map<Text,Text> payload is a decision to defer, not a default to adopt.
  • OAuth, OIDC, SSO, "log in with X". Every one needs an outbound socket, which does not exist — language iteration 38.
  • Remember-me tokens — a second, longer-lived credential class with its own rotation story. Its own slice.
  • CSRF — iteration 4. Sessions make CSRF possible to do properly; they do not provide it.
  • Sliding-window renewal of the absolute timeout. That is not what absolute means.

Info

Forks the spec must settle:

  1. What does the session row carry beyond identity? Just principal, or a payload? A payload wants a shape, and without generics the shape is either a declared class per app or Text. The framework's cache already stores Text and that is the compromise this repo dislikes most — so leaning: identity plus declared columns, and apps that want more keep their own table keyed by session id.
  2. Idle-timeout writes on every request. Touching last-seen means a WAL write per request, which turns every read into a durable write. Options: write at a coarser granularity, or accept the cost and say so with a number from just db-bench. This is a real performance fork, not a detail.
  3. Does Session set req.principal or req.ctx? principal is the framework's declared home for identity and auth middleware already writes it, so two writers of one field need a documented precedence.