Address boards by a stable key instead of their database id #213
No reviewers
Labels
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/Ponente!213
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-197-stable-board-keys"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #197.
Boards were addressed by database id (
/board/3). That's one Postgres sequence's answer: meaningless once a board is a record (#198), and it let anybody count a deployment's boards from the address bar. A board now carries a TID, the record key atproto uses. It's minted when the board is made, and every address names it.What changes
Board.Key(Services/BoardKey.cs): 13 characters of base32-sortable, following the TID spec (zero is2222222222222, the first character is limited to234567abcdefghij). New boards get a random clock id; boards that predate keys get one from their ownCreatedAt, with the id as the clock.ShareLink, the board, calendar and settings routes,NavHeader, the home tiles, the for-you strip, search results, link previews, the move-card dialog's share link, andCalendarScopefor new subscriptions. The id stays the primary key, and services, the notifier and the palette still take ints./board/3,/board/3/settings/tags,/calendar/board/3,/admin/columns/3and?card=all resolve. For a board the reader may see, the page moves the address bar on to the key (BoardAddress.MoveToKey), as an HTTP redirect during prerender. For a board they may not see, the URL stays put, so a redirect never hands out a key. The two.icsendpoints read an old id unchanged rather than redirecting, because calendar apps are never told about a redirect.Migration
AddBoardKeysadds a nullable, uniquely indexed column, with no SQL backfill.DatabaseInitializer.EnsureBoardKeysAsyncfills missing keys on every boot. That keeps one implementation of what a key is, and it means the previous release, which inserts boards without a key, can still be rolled back to. A later release can make the columnNOT NULL.Also here
Boardsid sequence answers exactly. That also covers a first boot that failed partway through.Tests
BoardKeyTestscovers the format, sorting, legacy ids and the clock.ShareLinkTestscovers keys, legacy ids andWithBoardKey.CalendarScopeTestscovers keys and ids, mixed and normalised.LastPageTestscovers settings paths by key.BOARD_KEYregex). There's a new test walking every old address shape, andaccess.specpins that a refused old link stays put.🤖 Generated with Claude Code
🤖 Reviewing this PR — the review will link the full session transcript.
✅ Verdict: approve — no blocking issues found.
Solid, well-reasoned change. The parts that could have gone wrong are all right:
BoardKey.Frommasks µs to 53 bits, shifts left 10, ORs a 10-bit clock, and emits 13 base32-sortable chars with the first char carrying four bits — zero is2222222222222, andBoardKeyTestspins exact values including the one-microsecond step.IsWellFormedmirrors the spec's first-character limit;IsLegacyId(≤9 digits, invariant parse, positive) can never be confused with a 13-char key.MoveToKeyonly navigates when the board resolvedFound; denied/missing boards keep the id in the address bar, andaccess.spec.jspins exactly that. The.icsendpoints read ids without redirecting, as calendar apps require.ResolveManyAsynchandles mixed key/id lists, normalises007, and dedups a board named both ways.?card=interplay is handled. WhenMoveToKeyreturns true,OpenSharedCardis skipped for that parameter set and the keyed navigation's parameter set opens it via the early-return branch — the comment describes exactly what the code does, andWithBoardKeypreserves the query.EnsureBoardKeysAsyncon every boot before anything serves. The newSeedDefaultBoardAsyncrule is correct:pg_get_serial_sequencedoes return the implicit sequence for EF's identity columns (verified against the PostgreSQL docs),pg_sequence_last_valueis NULL until the first insert, and theAddBoardsmigration inserts without an explicit id so it advances the sequence too; the|| AnyAsyncguard covers explicit-id inserts.data-board-id→data-board-keywith no JS reading the old attribute;SearchDocument.BoardKey/BoardNameKeyrename avoids the name collision andPassesFiltersuses the right one.Minor notes, none blocking:
BoardKey.New()'s random clock id collides 1/1024 per same-microsecond pair; the unique index turns that into a failedCreateAsync(generic error). Documented as deliberate and negligible.board.Id & 0x3FFas the clock, so two boards created in the same microsecond with ids 1024 apart would collide — the unique index catches it as a boot error. Astronomically unlikely; the doc comment's "still differ" claim has this one congruence exception.ForYouService'sboard?.Key ?? ""yields a broken/board/?card=Nlink if the board vanishes between the two passes, where the old id link at least reached the empty state. Vanishingly rare edge.:introute constraint means/board/abcnow renders the "missing" empty state (200) rather than a 404 — consistent with the design's "Missing without a query" and with the existing behaviour for absent ids, just noting the change.⏱ 20m 38s · 15 steps · $0.1475 · tokens: 660.6k in / 61.5k out (+587.1k cache-read) · Go: 5h 16% · wk 6% · mo 36%
📜 Full session transcript
d3ad6b98cbf48f8620b7🤖 Reviewing this PR — the review will link the full session transcript.
@opencode-bot /review — the review started at 20:19 UTC for the rebased head (
f48f862) never posted a verdict.🤖 Reviewing this PR — the review will link the full session transcript.
✅ Verdict: approve — no blocking issues found.
Re-review of the updated head: everything I verified last pass still holds, and the new commits only tighten naming (
ShareTarget.BoardRef) and test coverage — no new issues found.Still correct:
BoardKey.Frommasks µs to 53 bits, shifts 10, ORs a 10-bit clock; 13 base32-sortable chars, first char limited to the first sixteen letters; zero is2222222222222.BoardKeyTestspins exact values including the one-microsecond step and rejects sign/separator/overflow id shapes.MoveToKeynavigates only for a resolvedFoundboard; denied/missing keep the id in the address bar (pinned byaccess.spec.js), and the.icsendpoints read ids without redirecting.ResolveManyAsynchandles mixed key/id lists, normalises007, and dedups a board named both ways.?card=interplay is right: whenMoveToKeyreturns true the shared-card open is left to the keyed navigation's parameter set, andWithBoardKeypreserves the rest of the path and query.EnsureBoardKeysAsyncon every boot before anything serves;SeedDefaultBoardAsync's "never had a board" rule viapg_sequence_last_valueis correct for EF identity columns.Remaining minor notes, all documented in-code and none blocking:
BoardKey.New()'s random clock id collides 1/1024 per same-microsecond pair (unique index turns it into a failed create); the backfill'sid & 0x3FFclock has a congruence exception to the "still differ" claim;ForYouService'sboard?.Key ?? ""yields a broken link if the board vanishes between passes; dropping the:introute constraint means/board/abcrenders the missing empty state (200) rather than a 404, consistent with the design.⏱ 1m 27s · 13 steps · $0.0434 · tokens: 192.9k in / 2.1k out (+447.5k cache-read) · Go: 5h 26% · wk 17% · mo 41%
📜 Full session transcript