Make keeping a board an act of its maker, not a race won by whoever signs in first: a link you were given should not let you take someone's board.
What it is like today.POST /b/:id/claim (src/routes.ts) asks only two questions — is the caller signed in, and is the board unowned — and db.claimBoard hands ownership to the first caller who passes, permanently ("It never changes hands after that", src/db.ts:178). A board's link is its only access key and it is meant to be shared. So the guest flow the direction describes — make a board, share the link, sign in later to keep it — loses to any signed-in person who opened the link first: they get it on their / list, the maker gets the sentence "Kept by «them»" and no way back. #54 (a board can be deleted by the people it belongs to) and #94 make that worse, not better.
#92 is what makes this fixable: it gives every change an actor key — a guest's HttpOnly cookie or a signed-in forge id. That same key is what a board's creator is.
Done looks like
[ ] board records the actor key of whoever created it, written in createBoard's existing transaction (a nullable creator_key column, added the way the schema already adds columns — or through #97's migration list if that has landed).
[ ] claimBoard takes the caller's actor key and claims only when it matches the recorded creator, or when the board has no creator recorded (every board that exists today). Anything else is the existing owned_by_other shape with its own reason.
[ ] POST /b/:id/claim answers 403 with a sentence a person can act on — "this board was started in someone else's browser; ask them to keep it" — in the same three ways every other refusal answers (JSON for the script, the re-rendered board for a plain form navigation, plain text otherwise: boardBad in src/routes.ts already does this).
[ ] The page never renders a button that cannot work: keepHtml (src/pages/board.ts) shows Keep this board only to the creator; another signed-in visitor sees "Not kept by anyone yet" and no control.
[ ] A board created by a signed-in person is owned at creation, exactly as today — this issue adds nothing to that path beyond the creator key.
[ ] Tests that fail without the change, in src/db.test.ts and src/server.test.ts: the creator claims and wins; a second actor key is refused 403 and the board stays unowned; a board row with a null creator is still claimable by the first signed-in visitor (the upgrade path).
[ ] The README says the rule in one sentence where it explains keeping a board.
Files I expect to change
src/db.ts (schema, createBoard, claimBoard), src/routes.ts (the claim handler and the create handler), src/pages/board.ts (keepHtml), src/db.test.ts, src/server.test.ts, README.md.
Risks
Do not invent a second cookie. If #92's actor key is not yet what this needs, fix it there rather than growing a parallel identity here — two ways to say who a guest is, is the bug this app must not grow.
The null-creator fallback is the whole upgrade path: get it wrong and every board that exists becomes unkeepable forever. It is a test, not a comment.
A guest who clears cookies loses their claim. That is acceptable and should be the sentence the page shows, not a silent dead end.
Suggested: scout, reviewed by sentinel. Waits for #92.
Make keeping a board an act of its maker, not a race won by whoever signs in first: a link you were given should not let you take someone's board.
What it is like today.
POST /b/:id/claim(src/routes.ts) asks only two questions — is the caller signed in, and is the board unowned — anddb.claimBoardhands ownership to the first caller who passes, permanently ("It never changes hands after that",src/db.ts:178). A board's link is its only access key and it is meant to be shared. So the guest flow the direction describes — make a board, share the link, sign in later to keep it — loses to any signed-in person who opened the link first: they get it on their/list, the maker gets the sentence "Kept by «them»" and no way back. #54 (a board can be deleted by the people it belongs to) and #94 make that worse, not better.#92 is what makes this fixable: it gives every change an actor key — a guest's
HttpOnlycookie or a signed-in forge id. That same key is what a board's creator is.Done looks like
boardrecords the actor key of whoever created it, written increateBoard's existing transaction (a nullablecreator_keycolumn, added the way the schema already adds columns — or through #97's migration list if that has landed).claimBoardtakes the caller's actor key and claims only when it matches the recorded creator, or when the board has no creator recorded (every board that exists today). Anything else is the existingowned_by_othershape with its own reason.POST /b/:id/claimanswers 403 with a sentence a person can act on — "this board was started in someone else's browser; ask them to keep it" — in the same three ways every other refusal answers (JSON for the script, the re-rendered board for a plain form navigation, plain text otherwise:boardBadinsrc/routes.tsalready does this).keepHtml(src/pages/board.ts) shows Keep this board only to the creator; another signed-in visitor sees "Not kept by anyone yet" and no control.src/db.test.tsandsrc/server.test.ts: the creator claims and wins; a second actor key is refused 403 and the board stays unowned; a board row with a null creator is still claimable by the first signed-in visitor (the upgrade path).Files I expect to change
src/db.ts(schema,createBoard,claimBoard),src/routes.ts(the claim handler and the create handler),src/pages/board.ts(keepHtml),src/db.test.ts,src/server.test.ts,README.md.Risks
Suggested: scout, reviewed by sentinel. Waits for #92.