Skip to content

Add IndexedDB offline persistence for document editing#22125

Draft
akabiru wants to merge 1 commit intodevfrom
implementation/72665-remove-test-mode-for-local-document-fallback-require-hocuspocus-provider
Draft

Add IndexedDB offline persistence for document editing#22125
akabiru wants to merge 1 commit intodevfrom
implementation/72665-remove-test-mode-for-local-document-fallback-require-hocuspocus-provider

Conversation

@akabiru
Copy link
Copy Markdown
Member

@akabiru akabiru commented Feb 27, 2026

Ticket

What are you trying to accomplish?

Add offline editing support for collaborative documents using IndexedDB as a local persistence layer. When a user loses connection to the Hocuspocus server, their edits are stored locally in IndexedDB and automatically synced when the connection is restored.

This PR builds on top of #22564 (collaboration refinements) and adds only the IndexedDB-specific functionality:

  • y-indexeddb dependency for Y.Doc persistence in the browser
  • IndexeddbPersistence setup in the Stimulus controller with timeout and graceful fallback
  • Cached document detection via Y.js state vector to distinguish soft offline (editable with cache) from blocking offline (no cache, editor hidden)
  • IndexedDB cache clearing on authentication errors
  • hasCachedDocument prop chain through LiveCollaborationManager, block-note-element, OpBlockNoteContainer, and useCollaboration hook
  • blockingOffline return value from useCollaboration for conditional editor visibility
  • Feature specs for soft offline mode and offline-edit-then-reconnect sync

Screenshots

bn-offline-mode.mp4

Note

This is not the final design — updates will be done separately

What approach did you choose and why?

Uses y-indexeddb to persist the Y.Doc state in the browser's IndexedDB. On page load, the controller waits for IndexedDB sync (with a 10s timeout via Promise.race) before initializing the HocuspocusProvider, so the local state is merged with the server via Y.js CRDTs.

The soft vs blocking offline distinction prevents a fresh empty Y.Doc (no prior cache) from being synced as authoritative content on reconnect — which would overwrite real document content on the server.

Known limitation: IndexedDB data is not encrypted at rest. This is a security concern being evaluated separately and may require a composition wrapper with AES-256-GCM encryption before shipping to production.

Merge checklist

  • Added/updated tests
  • Added/updated documentation in Lookbook (patterns, previews, etc)
  • Tested major browsers (Chrome, Firefox, Edge, ...)

@akabiru akabiru changed the title Ihordubas99/feature/{72373,72665} Add offline editing with IndexedDB sync support and remove test-mode for local document fallback feature/72373 Add offline editing with IndexedDB sync support and remove test-mode for local document fallback Feb 27, 2026
@akabiru akabiru self-assigned this Feb 27, 2026
@akabiru akabiru force-pushed the implementation/72665-remove-test-mode-for-local-document-fallback-require-hocuspocus-provider branch from 24aff7d to 89b12aa Compare February 27, 2026 13:22
@akabiru akabiru marked this pull request as ready for review February 27, 2026 13:44
@akabiru akabiru requested a review from a team February 27, 2026 13:44
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 27, 2026

Deploying openproject with PullPreview

Field Value
Latest commit 3d85760
Job deploy
Status ✅ Deploy successful
Preview URL https://pr-22125-72665-remove-test-m-ip-49-12-2-188.my.opf.run:443

View logs

@akabiru
Copy link
Copy Markdown
Member Author

akabiru commented Feb 27, 2026

@claude review this pr

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances offline editing support for collaborative documents by integrating IndexedDB persistence and removing the test-mode local document fallback. The changes require HocuspocusProvider for all scenarios, treating disconnections and timeouts as transitions to offline mode rather than hard connection errors. The editor remains usable during server unavailability with automatic recovery when the connection is restored.

Changes:

  • Adds IndexedDB synchronization via y-indexeddb package to persist document state locally
  • Removes test-mode local document fallback, making HocuspocusProvider required
  • Updates feature specs to test offline mode behavior and collaboration notices with proper hocuspocus context

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
frontend/package.json Adds y-indexeddb dependency for local persistence
frontend/package-lock.json Lock file update for y-indexeddb dependency
frontend/src/stimulus/controllers/dynamic/documents/init-yjs-provider.controller.ts Implements IndexedDB persistence initialization and cleanup
frontend/src/react/hooks/useCollaboration.ts Replaces connectionError with offlineMode state, removes local document sync
frontend/src/react/OpBlockNoteContainer.tsx Removes inputField parameter and test-mode document fallback
frontend/src/elements/block-note-element.ts Requires HocuspocusProvider, removes collaboration-disabled branching
lib/primer/open_project/forms/block_note_editor.rb Uses Setting.real_time_text_collaboration_enabled? for collaboration check
modules/documents/config/locales/en.yml Updates connection error message to reflect offline mode
modules/documents/app/components/documents/show_edit_view/connection_error_notice_component.html.erb Adds test selector for offline mode notice
modules/documents/app/components/documents/show_edit_view/collaboration_disabled_notice_component.html.erb Adds test selector for collaboration disabled notice
modules/documents/spec/features/real_time_collaboration_spec.rb Adds offline mode test, restructures hocuspocus context
modules/documents/spec/features/documents/project/show_edit_document_spec.rb Removes allow_any_instance stub
modules/documents/spec/features/block_note_editor_spec.rb Removes allow_any_instance stub, adds collaboration disabled test
modules/documents/spec/features/attachment_upload_spec.rb Removes allow_any_instance stub
Files not reviewed (1)
  • frontend/package-lock.json: Language not supported

Comment thread modules/documents/config/locales/en.yml Outdated
Comment thread modules/documents/spec/features/real_time_collaboration_spec.rb
Comment thread frontend/src/react/hooks/useCollaboration.ts Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • frontend/package-lock.json: Language not supported

Comment thread frontend/src/elements/block-note-element.ts Outdated
);

return Promise.race([
persistence.whenSynced.then(() => {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 (TIL) 💡

  • Promise.race()

    The Promise.race() static method takes an iterable of promises as input and returns a single Promise. This returned promise settles with the eventual state of the first promise that settles.

  • persistence.whenSynced - returns a promise
Image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice solution, TIL as well!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • frontend/package-lock.json: Language not supported

Comment thread modules/documents/spec/features/real_time_collaboration_spec.rb
Comment thread modules/documents/spec/features/real_time_collaboration_spec.rb
@akabiru akabiru changed the title feature/72373 Add offline editing with IndexedDB sync support and remove test-mode for local document fallback feature/72373 Add offline editing via IndexedDB; drop local document fallback Feb 27, 2026
Copy link
Copy Markdown
Contributor

@brunopagno brunopagno left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not test, but the code looks nice. I'm really glad we're doing this 👏

But just not to leave unsaid, hope you can sanity check if you haven't already a few things:

  • How does the indexdb sync with remote state? Is it somewhat stable?
  • How about local and remote state sync during active collaboration sessions?
  • What happens with readonly when using offline mode? And what happens when the user reconnects?
  • Is there a chance that a user might be using offline mode and not notice? I'm afraid it could cause false positives when an admin is setting up hocuspocus, they could think that things are working but it's just offline mode

Still, this is a beautiful job. Thanks for making it 🙇

Comment thread frontend/src/react/hooks/useCollaboration.ts Outdated
Comment thread frontend/src/react/hooks/useCollaboration.ts
Comment thread frontend/src/react/hooks/useCollaboration.ts
@akabiru
Copy link
Copy Markdown
Member Author

akabiru commented Mar 2, 2026

Thanks for the review @brunopagno

  • How does the indexdb sync with remote state? Is it somewhat stable?

Seems fairly stable, but will of course need some real world use. IndexedDB is a local cache only; it doesn't sync with remote directly. Instead, both IndexedDB and Hocuspocus will write into the same shared Y.Doc. On hocuspocus connection Y.js should automatically merge any local changes without conflict.

We also set up non-blocking providers- IndexedDB initialization runs in the background with a 10s timeout guard (important because y-indexeddb has no built-in error events), and if it fails the hocuspocus provider continues without offline persistence.

It's fairly small in footprint: See: https://github.com/yjs/y-indexeddb/blob/master/src/y-indexeddb.js

  • How about local and remote state sync during active collaboration sessions?

Hocuspocus handles all real-time, multi-client sync; IndexedDB is purely a local cache layer that mirrors the Y.Doc on-device. If the connection to sync server (HP) drops, changes are buffered in the Y.Doc locally (including in IndexedDB) and Hocuspocus forwards them automatically on reconnect.

  • What happens with readonly when using offline mode? And what happens when the user reconnects?

Nice catch, seems we didn't account for this! y-indexeddb does not have have readonly awareness afaict- so we need to rely on the editor. Will cover this shortly!

  • Is there a chance that a user might be using offline mode and not notice? I'm afraid it could cause false positives when an admin is setting up hocuspocus, they could think that things are working but it's just offline mode

No. The connection-error banner will be displayed. See current version below: however, the designs will soon change to this.

Screenshot 2026-03-02 at 3 58 58 PM

@akabiru akabiru marked this pull request as draft March 2, 2026 16:30
@akabiru akabiru marked this pull request as draft March 2, 2026 16:30
@akabiru
Copy link
Copy Markdown
Member Author

akabiru commented Mar 3, 2026

@brunopagno @ihordubas99 as it turns out we needed to revert the non-blocking IndexedDB <-> Hocuspocus provider setup and add local cache presence detection to close two data-loss paths:

🟠 Race condition (users with a local cache)
Making waitForIndexedDBSync non-blocking meant HocuspocusProvider could connect with an empty Y.Doc before the local cache had loaded. Awaiting IDB first ensures the provider's initial state vector already includes cached content, so no late updates are sent.

🔴 No-cache + server offline = block editor
A user opening a document for the first time while Hocuspocus is unreachable has no verified state at all. Previously they could type freely into an empty Y.Doc; on reconnect onStoreDocument would overwrite the real document for everyone. We now check hasLocalCache after waitForIndexedDBSync: if the connection times out and there is no local cache, the editor is blocked with a "server unavailable" message instead of allowing edits that could poison server state on reconnect.

Screenshot 2026-03-02 at 10 56 05 PM

@akabiru akabiru force-pushed the implementation/72665-remove-test-mode-for-local-document-fallback-require-hocuspocus-provider branch from 06292ce to a3eaf76 Compare March 3, 2026 09:13
@akabiru akabiru requested a review from brunopagno March 3, 2026 09:45
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • frontend/package-lock.json: Language not supported

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 3, 2026

@akabiru I've opened a new pull request, #22168, to work on those changes. Once the pull request is ready, I'll request review from you.

@akabiru akabiru requested review from a team, judithroth and myabc and removed request for brunopagno March 3, 2026 14:03
Copy link
Copy Markdown
Contributor

@judithroth judithroth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, I like where this is going! There are some details where we should at least discuss and make a conscious decision as I think they affect security.

I am not sure if tests got a bit more flaky or if it is just today. Maybe we should keep an eye on that.

Comment thread frontend/src/elements/block-note-element.ts Outdated
Comment thread frontend/src/react/hooks/useCollaboration.ts Outdated
Comment thread frontend/src/react/hooks/useCollaboration.ts Outdated
Comment thread frontend/src/react/hooks/useCollaboration.ts Outdated
Comment thread frontend/src/react/hooks/useCollaboration.ts
Comment thread frontend/src/react/hooks/useCollaboration.ts
Comment thread frontend/src/react/hooks/useCollaboration.ts
);

return Promise.race([
persistence.whenSynced.then(() => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice solution, TIL as well!

Comment thread modules/documents/spec/features/attachment_upload_spec.rb
return () => document.removeEventListener(PROVIDER_AUTH_ERROR_EVENT, handleProviderAuthError);
}, []);
// When offline with no local cache, block the editor entirely to prevent an
// empty Y.Doc from being synced as the authoritative document on reconnect.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the very naïve understanding, but isn't the whole point of CRDTs that such a change won't clobber the document?

Copy link
Copy Markdown
Member Author

@akabiru akabiru Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but isn't the whole point of CRDTs that such a change won't clobber the document?

This is true- but it requires that the yDOC has some initial "state vector" (akin to git initial commit/ref). And, that initial sync is done from an authoritative data source; in our case hocuspocus onLoadDocument(). As soon as the CRDT achieves this initial state sync- then subsequent conflicts/merges can be resolved. However, IF we start out with an "empty Y.Doc" instance, it would not have any state- and will hence overwrite during onStoreDocument()

See: https://tiptap.dev/docs/hocuspocus/server/hooks#onloaddocument

@akabiru akabiru changed the title feature/72373 Add offline editing via IndexedDB; drop local document fallback feature/72373 Support offline document editing via IndexedDB mitigate network connection blips Mar 10, 2026
@akabiru akabiru marked this pull request as draft March 16, 2026 10:43
akabiru added a commit that referenced this pull request Mar 28, 2026
…are error messages

Extract non-IndexedDB refinements from PR #22125 so they can ship
independently while IndexedDB offline persistence is evaluated separately.

- Gate collaboration on Setting.real_time_text_collaboration_enabled?
  instead of hardcoding it to true
- Remove the test-mode fallback that created a standalone Y.Doc without
  a provider; HocuspocusProvider is now required for document editing
- Refactor useCollaboration hooks: callback-based timeout with proactive
  cancel on sync, extracted useProviderAuthError hook, JSDoc comments
- Add read/write context-aware connection error messages (readonly users
  see "real-time updates will resume" vs writers see "changes will sync")
- Add blocked offline mode: when the server is unreachable and there is
  no local cache, hide the editor entirely to prevent an empty Y.Doc
  from being synced as authoritative content on reconnect
- Update feature specs to use real hocuspocus shared context instead of
  stubbing collaboration_enabled, add offline blocking tests
@akabiru akabiru force-pushed the implementation/72665-remove-test-mode-for-local-document-fallback-require-hocuspocus-provider branch from 3d85760 to 33717ab Compare March 28, 2026 07:53
@akabiru akabiru changed the base branch from dev to refinements/72665-collaboration-improvements March 28, 2026 07:54
@akabiru akabiru changed the title feature/72373 Support offline document editing via IndexedDB mitigate network connection blips Add IndexedDB offline persistence for document editing Mar 28, 2026
akabiru added a commit that referenced this pull request Mar 30, 2026
…are error messages

Extract non-IndexedDB refinements from PR #22125 so they can ship
independently while IndexedDB offline persistence is evaluated separately.

- Gate collaboration on Setting.real_time_text_collaboration_enabled?
  instead of hardcoding it to true
- Remove the test-mode fallback that created a standalone Y.Doc without
  a provider; HocuspocusProvider is now required for document editing
- Refactor useCollaboration hooks: callback-based timeout with proactive
  cancel on sync, extracted useProviderAuthError hook, JSDoc comments
- Add read/write context-aware connection error messages (readonly users
  see "real-time updates will resume" vs writers see "changes will sync")
- Add blocked offline mode: when the server is unreachable and there is
  no local cache, hide the editor entirely to prevent an empty Y.Doc
  from being synced as authoritative content on reconnect
- Update feature specs to use real hocuspocus shared context instead of
  stubbing collaboration_enabled, add offline blocking tests
@akabiru akabiru force-pushed the refinements/72665-collaboration-improvements branch from 85ade6d to 8272768 Compare March 30, 2026 13:44
Add y-indexeddb to persist Y.Doc state in the browser's IndexedDB,
enabling offline editing with automatic sync on reconnect.

- Add IndexeddbPersistence setup with timeout and fallback handling
  in the Stimulus controller (waitForIndexedDBSync, Promise.race)
- Detect cached documents via state vector to distinguish soft offline
  (has cache, editor editable) from blocking offline (no cache, editor
  hidden to prevent empty doc overwriting server state)
- Clear IndexedDB cache on auth errors to prevent stale data
- Add hasCachedDocument prop chain through LiveCollaborationManager,
  block-note-element, OpBlockNoteContainer, and useCollaboration hook
- Add blockingOffline return from useCollaboration for conditional
  editor visibility
- Add feature specs for soft offline mode (write and readonly) and
  offline-edit-then-reconnect sync
@akabiru akabiru force-pushed the implementation/72665-remove-test-mode-for-local-document-fallback-require-hocuspocus-provider branch from 33717ab to 6608537 Compare March 30, 2026 14:08
Base automatically changed from refinements/72665-collaboration-improvements to dev April 1, 2026 11:15
@judithroth judithroth removed their request for review April 23, 2026 07:52
@akabiru akabiru closed this Apr 23, 2026
@akabiru akabiru reopened this Apr 23, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 23, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Development

Successfully merging this pull request may close these issues.

6 participants