Repository navigation
Conversation
BlockNote 0.56 ships a version history that runs on the Yjs 13 binding we use (YjsVersioningExtension + VersioningSidebar). Its storage interface takes raw Yjs updates, which is what yjs_document_snapshots already stores. VersionHistory.tsx and the HTML preview/restore path go. Restore now happens on the server against the live document: - The current content is saved first as "Vor der Wiederherstellung", read from Hocuspocus so unsaved typing is included. - The version is applied as a forward edit. The live fragment's children are replaced by deep clones of the version's, which keeps block ids and comment marks. Applying the old state alone reverts nothing, because Yjs merges it. - Hocuspocus broadcasts the change to every connected client. - The old overwrite of yjs_document_updates with the old state is gone. Tabs from before this change still get the safe half: the before- restore snapshot, then their own client-side apply. New endpoints: GET /snapshots/:version/content (raw update) and PUT /snapshots/:version (rename). The list also returns created_by, for author avatars. There is no delete, because a snapshot can be the base a document loads from. BlockNote's version view forks the doc like the AI review does. useDocAIReviewState and usePendingDocAI now tell the two apart, so the "Übernehmen" bar can no longer merge a version preview into the live document. Closes #4624
Contributor
📝 Docs-Freshness-Check
|
Movm
added this pull request to stack #4633
October 16, 2026 00:15
The snapshot endpoints were a plain Express router with hand-written row types and their own access checks. They now have docSnapshotsContract (Zod schemas in @gruenerator/contracts) and a ts-rest router. The web side calls them through getContractsClient() and narrows each answer by status instead of catching exceptions. - Write access now uses checkDocumentWriteAccess, the mirror of the Hocuspocus connection check. The old snapshot-only check left out editors who come through an editor share link or a group share with write. They could edit the document but could not save or restore versions. Restore writes around Hocuspocus auth via its internal API, so it has to apply exactly the Hocuspocus rule. - Read access uses checkDocumentAccess like the rest of /api/docs. - Row types derive from the Drizzle table (YjsDocumentSnapshotRow). - Timestamps go out as ISO strings, and the next version number is taken in the same INSERT statement. - The routes keep authenticatedReadLimiter, which they used to inherit from docsRouter. No change on the wire for existing callers: same paths and bodies. Tabs that still restore without `apply` keep working.
The list returned every snapshot of a document in one response. It now pages newest first with a version keyset (limit 50, max 100) and returns next_cursor, which BlockNote's sidebar follows on scroll. A page reads on until the entry after it has begun, so an auto-save group at a page end is counted in full. The query walks the (document_id, version) unique index.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #4628 (BlockNote 0.56 upgrade). Review that one first. This PR targets its branch, so the
Closes #4624lives in the commit message (GitHub ignores body keywords while the base is notmaster).Replaces our own docs version history UI with BlockNote's (TypeCellOS/BlockNote#3090), backed by the snapshots we already store. Restore moves to the server and works on the live document.
What changes for users
DiffVersioningExtensionfrom the Yjs 14 path, see Track: BlockNote Yjs 14 binding (@blocknote/core/y) — stay on /yjs until the stack is stable #4629.How restore works now
POST /docs/:id/snapshots/:version/restorewith{ apply: 'live' }:GET /internal/doc/:id/yjs, falls back to Postgres there), so unsaved typing is included, and stores it as "Vor der Wiederherstellung".buildRestore(services/docs/snapshotRestore.ts) replaces the fragment's children with deep clones of the version's in one transaction. That is a forward edit, and it keeps block ids, attributes and text formats, comment marks included. Applying the old state alone reverts nothing. Yjs merges it, and the first test proves that trap.POST /internal/doc/:id/yjs. Hocuspocus applies it and broadcasts it to every connected client.The old server overwrite of
yjs_document_updateswith the old state is gone (#4624 part 2). Tabs loaded before this deploy still call the endpoint withoutapply. They get step 1 only and then apply the version themselves as before. No data path depends on them.Version preview vs. AI review
BlockNote's version view forks the doc exactly like the AI review. Without a gate, our review bar would have offered "Übernehmen" on a version preview, which merges the preview into the live document.
isAIReviewFork(forked and the versioning store not inversionsmode) now drivesuseDocAIReviewState,usePendingDocAIandisDocAIForked. Both stores change in the same synchronousopen()call, so the hooks never see half a transition.Verification
packages/docs: 72/72 tests (new:snapshotVersionStorage.vitest.ts),apps/api:docSnapshotsContractRouter.vitest.ts9/9.apps/api:snapshotRestore.vitest.ts4/4 (revert after later edits, ids and formats kept, comment threads untouched, a collaborator converges) plustrashReaders.vitest.ts.tscclean for docs/web/api,check:internalgreen.Contract (second commit)
The snapshot endpoints are now
docSnapshotsContract(Zod schemas in@gruenerator/contracts, ts-rest routerdocSnapshotsContractRouter.ts). The web side calls them throughgetContractsClient().docSnapshots. Paths and bodies stay the same, so old tabs keep working.checkDocumentWriteAccess, the mirror of the Hocuspocus connection check. The old snapshot-only check left out editors who come through an editor share link or a group share with write: they could edit the document but could not save or restore versions. Restore writes around Hocuspocus auth through its internal API, so it has to apply exactly that rule.authenticatedReadLimiterthey used to inherit fromdocsRouter.docSnapshotsContractRouter.vitest.ts(7 tests) pins the order (backup before apply), no restore without a readable live state, the legacy path (backup only, noyjs_document_updateswrite), and 403/404/401 before anything is touched.Paging (third commit)
The list used to return every snapshot in one response. It now pages newest first on a version keyset (
limit, default 50, max 100;cursor) and returnsnext_cursor, which BlockNote's sidebar follows on scroll or "load more". BlockNote merges pages by id.yjs_document_snapshots_document_id_version_keybackwards (Index Scan Backward,version <= $2), so no new index is needed. A NULL cursor reads the first page.docSnapshotsContractRouter.vitest.tsis now 9 tests. The boundary test fails against a naive one-query-per-page version (red-probed).snapshotVersionStorage.vitest.tspasses the cursor through and ends the history onnext_cursor: null.next_cursorand see the newest 50 entries. Restore and preview work by version, so nothing depends on the full list.