Let a board be shown to someone without handing them the pen: a second link that can read the board and change nothing.
Why. The direction says boards are shared by link — and today a link is an edit link. Anyone a person shows their board to (a manager, a client, a channel of forty people) can rename it, move every card and delete a column, and nothing records who did it. A read-only link is the smallest honest answer, and it is the one thing people ask for before they ask for permissions.
Done looks like
[ ] board.view_key — a nullable id of the same kind the board id is (same generator, same width as #72 settles on), minted on demand by the owner or the creator, never derivable from the board id, and re-mintable: minting a new one revokes the old link.
[ ] GET /v/<view_key> renders the board read-only: the same columns, cards and presence, no forms, no drag, no Undo, no Add, no Rename, and a plain sentence at the top saying this is a read-only view.
[ ] The rendered read-only page contains the board's id nowhere — not in a form action, a data-board attribute, a link, or the live-stream URL. That is the whole security property; a test greps the served HTML for the id and fails if it appears.
[ ] GET /v/<view_key>/events streams the same changes so the view stays live, publishing presence like any other viewer (a read-only viewer counts as a guest).
[ ] Every write route still takes a board id and only a board id: a view key is never accepted anywhere that mutates, and the /v/ path has no POST at all.
[ ] Misses on /v/<key> are refused by the same brake #72 puts on board-id misses, and carry the same x-robots-tag as the board page (#117).
[ ] Wherever the board offers its link (#51's copy control), it offers both, labelled: "Anyone with this link can edit" and "Anyone with this link can watch".
[ ] Tests that fail without the change: a view key renders the board; the id is absent from that HTML; a POST to /v/<key>/... is 404/405; a revoked key 404s; the stream delivers a change made through /b/<id>.
[ ] The README says what a read-only link grants and that it is revoked by minting a new one.
Files I expect to change
src/db.ts (the column, through #97's migration list if it has landed, plus mint/revoke/lookup), src/routes.ts (the /v/ paths), src/pages/board.ts (a read-only rendering of the same data), src/db.test.ts, src/server.test.ts, README.md.
Risks
Waits for #72: it settles how wide an unguessable id is and puts a brake on guessing one, and this mints a second id of exactly that kind. Building it first means minting a key at the old width and doing the work twice.
The read-only page is the same renderer with every control removed. Resist a second page module: pass a flag and make the control-emitting helpers return "", so a control added later cannot silently appear in the read-only view — and test that by rendering both from the same data.
Presence shows forge display names. A read-only viewer sees them, which is the same exposure the edit link already has; say it in the README rather than leaving it implied.
Suggested: scout, reviewed by sentinel. Waits for #72.
Let a board be shown to someone without handing them the pen: a second link that can read the board and change nothing.
Why. The direction says boards are shared by link — and today a link is an edit link. Anyone a person shows their board to (a manager, a client, a channel of forty people) can rename it, move every card and delete a column, and nothing records who did it. A read-only link is the smallest honest answer, and it is the one thing people ask for before they ask for permissions.
Done looks like
board.view_key— a nullable id of the same kind the board id is (same generator, same width as #72 settles on), minted on demand by the owner or the creator, never derivable from the board id, and re-mintable: minting a new one revokes the old link.GET /v/<view_key>renders the board read-only: the same columns, cards and presence, no forms, no drag, no Undo, no Add, no Rename, and a plain sentence at the top saying this is a read-only view.data-boardattribute, a link, or the live-stream URL. That is the whole security property; a test greps the served HTML for the id and fails if it appears.GET /v/<view_key>/eventsstreams the same changes so the view stays live, publishing presence like any other viewer (a read-only viewer counts as a guest)./v/path has no POST at all./v/<key>are refused by the same brake #72 puts on board-id misses, and carry the samex-robots-tagas the board page (#117)./v/<key>/...is 404/405; a revoked key 404s; the stream delivers a change made through/b/<id>.Files I expect to change
src/db.ts(the column, through #97's migration list if it has landed, plus mint/revoke/lookup),src/routes.ts(the/v/paths),src/pages/board.ts(a read-only rendering of the same data),src/db.test.ts,src/server.test.ts,README.md.Risks
"", so a control added later cannot silently appear in the read-only view — and test that by rendering both from the same data.Suggested: scout, reviewed by sentinel. Waits for #72.