- `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>
6.6 KiB
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
@tablerow 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
@tableclass: 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
Sessionmiddleware whosebeforereads the signed cookie, loads the row, checks both timeouts, and attaches identity. Decide whether it setsreq.principalor writes intoreq.ctx—principalis 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.shrather 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 linkcheckgreen.
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:
- 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 storesTextand 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. - 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. - Does
Sessionsetreq.principalorreq.ctx?principalis the framework's declared home for identity and auth middleware already writes it, so two writers of one field need a documented precedence.