Skip to content

Hold a linked device's history on the server, not Gryt's relay (GRYT-2055) - #366

Merged
sivert-io merged 3 commits into
mainfrom
claude/GRYT-2055-link-history
Nov 9, 2026
Merged

sivert-io merged 3 commits into
mainfrom
claude/GRYT-2055-link-history

Conversation

@grytchat

@grytchat grytchat commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Part one of GRYT-2055: the server half of moving a linked device's message history off Gryt's own pairing relay. Today that history goes through id.gryt.chat, up to 256 MiB a link, so the cost grows with every user. After this, the old device leaves the sealed chunks on a Gryt server both devices are on, and the new one reads them back. Nothing calls these endpoints until the core and app PRs land.

What's new

  • src/routes/linkHistory.ts:
    • PUT /api/link-history/:transfer/:n (raw bytes), GET /api/link-history/:transfer/:n, DELETE /api/link-history/:transfer.
    • requireBearerToken runs first, then a 401 if there's no server user. The raw-body parser comes after both.
    • A transfer belongs to the server user who first wrote to it. Anyone else gets 404, the same answer as no transfer. The new device has the same identity, so it's the same server user.
    • Limits: 2 MiB a chunk, 256 MiB and two transfers per user, 4096 slots, one hour. Uploads turned off on the server means 503.
    • Chunks go through the existing storage API into a link-history folder under DATA_DIR, separate from uploads. The whole folder is wiped on start, because the in-memory index of transfers doesn't survive a restart.
  • /info returns linkHistory: true when uploads are on, so the old device can pick a server that can hold history.
  • Mounted behind the chat-upload rate limit (300 a minute), since one link is up to about 128 chunk writes.

Owners can turn it off (second commit)

  • A new server_config.link_history column, on by default, set through the settings patch (linkHistory) the same way as videoProfiles. Off means /api/link-history answers 403 link_history_off and /info says linkHistory: false, so the old device picks another server or moves no history.
  • This touches src/db (the column, its migration and its mapping), which is a review-required path. Test: src/db/sqlite/linkHistorySetting.test.ts.
  • Turning it off doesn't delete transfers already in progress. They expire within the hour.
  • The switch in Server settings comes in the client PR.

Check

  • /info is open to everyone. linkHistory tells a stranger whether uploads are on. A failed upload tells them that anyway, but it is one more field.
  • Owner is serverUserId. If two devices of one person could ever end up as different server users on the same server, the new device would get a 404 and no history. That fails safe. It wouldn't leak anything.
  • The transfer id comes from the client (22–64 URL-safe characters). It's only a name inside that user's own space, so it doesn't need to be secret. The chunks are sealed with the key exchanged during linking.
  • The wipe on start uses rm on DATA_DIR/link-history directly rather than going through src/storage, which has no "delete a folder". Storage is filesystem-only since 1.11.0.

Tested

  • 5 new tests: ownership, the size and count limits, a chunk replaced in place counting once, bad ids and slots, expiry deleting the files, and auth running before every route.
  • yarn test (2173 pass), yarn build, eslint on the changed files, comment length.

Next PRs: core gets a server-backed chunk store and drops the relay for history. Then the client and the phone use it. Then the relay's chunk limit goes to zero and the endpoints are removed.

🤖 Generated with Claude Code

…(GRYT-2055)

Linking sent the old message history through Gryt's own pairing relay, up to
256 MiB a link. That cost grows with every user. The old device can now leave
the sealed chunks on a server both devices are on, and the new device reads
them back. It's the same user, because it has the same identity.

PUT/GET /api/link-history/:transfer/:n and DELETE /api/link-history/:transfer,
behind requireBearerToken. A transfer is visible only to the user who started
it. Each user may hold 256 MiB in at most two transfers, a chunk is at most
2 MiB, and a transfer is deleted after an hour, when it's removed, or on a
restart. The server can't read the chunks. /info says linkHistory: true where
uploads are on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A server_config column, link_history, on by default, set through the same
settings patch as the other server switches. When it's off, /api/link-history
answers 403 link_history_off and /info says linkHistory: false. The old device
then picks another server, or moves no history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@grytchat
grytchat requested a review from sivert-io as a code owner November 9, 2026 21:53
The new device drops each chunk once it has it, so the server's disk empties
as the transfer goes, and that chunk's bytes stop counting against the user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sivert-io
sivert-io merged commit 0057041 into main Nov 9, 2026
5 checks passed
@sivert-io
sivert-io deleted the claude/GRYT-2055-link-history branch November 9, 2026 22:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants