Skip to main content
The @veltdev/apryse-crdt-react and @veltdev/apryse-crdt libraries enable real-time collaborative annotations, XFDF state, form fields, selections, and cursors in Apryse WebViewer. The collaboration engine is built on Yjs and the Velt SDK.

Prerequisites

  • Node.js (v14 or higher)
  • An Apryse WebViewer license and deployed WebViewer assets
  • A Velt account with an API key (sign up)
  • React (v18 or higher) when using the React wrapper
  • Optional: TypeScript for type safety

Setup

Step 1: Install Dependencies

The React package provides lifecycle hooks and provider components. The base package owns annotation records, XFDF import/export, the Velt CRDT Store, awareness, and version management. The collaboration package does not ship Apryse workers or UI assets. Deploy WebViewer’s lib directory, configure its path, and supply a valid Apryse license for your environment. If your dependency graph exposes Yjs directly, ensure the final bundle resolves only one compatible yjs instance.

Step 2: Load Apryse WebViewer

The application owns WebViewer creation. Create it once and keep the completed instance stable until collaboration has been destroyed.
Dispose WebViewer only after the collaboration manager has been destroyed, as shown in Cleanup and the complete example.

Step 3: Setup Velt

React applications initialize Velt at the app root while WebViewer loads. Other Frameworks integrations initialize Velt after the viewer is available. In both cases, authenticate the user and set a stable document before creating collaboration. Follow the Velt Setup Docs for the production token endpoint used by authProvider.

Step 4: Initialize Collaboration

Use ApryseCrdtProvider to share one hook result across a component tree. It manages collaboration around an existing WebViewer instance; it does not create or render WebViewer.
ApryseCrdtViewer and ApryseCollaborationProvider are aliases of this provider.

Step 5: Synchronize Annotations and XFDF

Apryse annotation changes are captured automatically. Use XFDF helpers for imports, exports, and explicit snapshots:
sharedState contains the normalized annotation records currently stored in the CRDT map. Deleted annotations remain represented as tombstone records so concurrent changes merge consistently. The manager listens to Apryse’s public annotationChanged event for add, modify, and delete actions. It exports the affected annotation as XFDF and stores a normalized record containing its ID, action, page, author, timestamp, and XFDF. Remote records are combined and imported back into Apryse while imported-event echoes are suppressed. When the annotation manager exposes fieldChanged, the manager writes a full XFDF snapshot so form values and field metadata remain together. The adapter does not synchronize arbitrary PDF binary mutations.
Apryse collaboration synchronizes annotations and XFDF state. The underlying PDF file remains an Apryse document input and is not uploaded through this CRDT adapter.

Step 6: Status Monitoring (Optional)

The provider also supports onStatusChange, onSyncedChange, onAnnotationsChange, and onUpdateData callbacks.

Step 7: Version Management (Optional)

saveVersion() flushes the current annotations before creating the checkpoint. Restoring a version updates the shared annotation map and reapplies the resulting XFDF to Apryse for every collaborator.

Step 8: Force Reset Initial Content (Optional)

initialContent and initialXfdf accept XFDF for a brand-new shared annotation document:
Without forceResetInitialContent, existing persisted annotation records win on reconnect and the XFDF seed is used only for a new, empty collaborative document. initialContent is an alias for XFDF input; it is not plain PDF text or PDF bytes.
forceResetInitialContent replaces the existing shared annotation state. Enable it only for deliberate reset or fixture workflows.

Step 9: Publish Remote Cursors (Optional)

Apryse page coordinates are awareness state rather than persisted annotations. Publish them from your viewer interaction layer:
Render custom cursor overlays from the reactive remoteCursors array:
Your Apryse integration is responsible for converting screen coordinates into page coordinates and positioning the overlay in the current viewport. Mount remote cursor elements in an Apryse page overlay that moves with the active page. Apply the current zoom, scroll, and page transform when converting the stored page-space coordinates to CSS pixels:
These class names are application-owned. The source demo uses .cursor-layer, .apryse-remote-cursor, and .apryse-remote-cursor-label; either naming scheme works as long as the renderer and CSS agree. Awareness is ephemeral: cursor and selection records disappear when a peer disconnects and are not included in XFDF or saved versions. Throttle pointer updates in high-frequency interactions.

Step 10: CRDT Event Subscription (Optional)

Subscribe through the manager for normalized Apryse update events:
For application-wide Velt CRDT events:

Step 11: Custom Encryption (Optional)

See also: setEncryptionProvider() · VeltEncryptionProvider

Step 12: Error Handling (Optional)

Public wrapper and manager methods catch integration errors and return safe fallback values.

Step 13: Access Yjs Internals (Advanced)

The manager owns these Yjs and provider objects. Do not construct a second document, provider, or awareness instance, and prefer the XFDF/annotation methods over direct shared-type mutation. Internally, Apryse uses a map Store with contentKey: 'apryse' and source: 'apryse'. getXmlFragment() is a compatibility view for generic XML-based harnesses, and direct inserts into it are translated into Apryse XFDF annotation records. Application code should normally use getMap(), getAnnotationsMap(), and the XFDF helpers.

Step 14: Cleanup

The hook and provider destroy the collaboration manager automatically. Call destroy() before disposing an application-owned WebViewer when you need to guarantee teardown order:

Notes

  • Annotation scope: The adapter synchronizes annotation/XFDF state, not PDF file bytes.
  • Stable instance: Create WebViewer once per mount and pass the same instance to the hook.
  • Data model: Individual annotation records live in a Yjs map, with XFDF snapshots used for Apryse import/export.
  • Cursor ownership: The adapter publishes cursor awareness, while the application maps Apryse page coordinates to overlay DOM.
  • Unique editorId: Collaborators must share the same Velt document context and editorId.
  • React readiness: The hook checks Velt initialization, an authenticated user, a non-empty editor ID, and an Apryse instance. It does not explicitly wait for document context.
  • Viewer ownership: The application loads PDFs, owns WebViewer assets and licensing, and disposes the viewer after collaboration is destroyed.
  • Form fields: When Apryse emits fieldChanged, the manager captures a full XFDF snapshot to retain field metadata and values.
  • Initial XFDF: Seed data is applied once unless force reset is explicitly enabled.
  • Security and performance: XFDF can contain user-generated content. Sanitize custom renderers, throttle cursor updates, tune debounceMs, and test realistic annotation volumes.
  • Production authentication: Use VeltProvider with a server-backed authProvider; never expose privileged server tokens in browser code.

Testing and Debugging

To test collaboration:
  1. Open the same PDF, Velt document, and Apryse editorId as two different users.
  2. Add, modify, and delete annotations in both clients.
  3. Verify XFDF imports, remote cursor awareness, version restore, and persistence after reload.
  4. Test concurrent changes to separate annotations and to the same annotation.
Common issues:
  • Viewer not loading: Confirm /webviewer/lib contains the deployed Apryse assets and the license is valid.
  • Manager remains loading: Confirm the document context was set first, Velt is initialized, a user is authenticated, the editor ID is non-empty, and instance is non-null. Document context is a prerequisite, not an explicit hook wait condition.
  • Annotations do not sync: Confirm both clients use the same Velt document and editorId.
  • Cursor position is incorrect: Convert pointer and selection positions to Apryse page coordinates before calling updateCursor().
  • Existing annotations reset: Disable forceResetInitialContent during normal loads.
Use the VeltCrdtStoreMap debugging interface to inspect annotation records and provider state.

Complete Example

Complete App.tsx
Complete styles.css

How It Works

  1. The application loads Apryse assets, initializes WebViewer, and retains the completed instance.
  2. React uses an authenticated VeltProvider; other frameworks initialize one Velt client, authenticate the user, and set the document before creating collaboration.
  3. The React hook checks Velt initialization, an authenticated user, a non-empty editor ID, and an Apryse instance. It does not explicitly wait for document context.
  4. The base manager stores normalized annotation records in a Velt-backed Yjs map. Apryse annotationChanged events create XFDF records and deletion tombstones; fieldChanged schedules a full XFDF snapshot.
  5. Remote records are rebuilt as XFDF and imported into the viewer with echo suppression.
  6. Awareness carries transient page coordinates, selections, and cursor identity, while versions save and restore the persistent annotation state.

APIs

React: useApryseCrdt()

React: ApryseCrdtProvider

Accepts the hook configuration plus callbacks for manager readiness, status, sync, cursors, annotations, update events, and versions. Children can be a React node or render function.

Other Frameworks: createCollaboration()

Create WebViewer first, then initialize and authenticate Velt, set the document, and call this factory with the completed instance.

CollaborationManager Methods

Exported Types and Helpers

  • UseApryseCrdtConfig, ApryseCrdtHookResult, ApryseCrdtProviderProps
  • ApryseWebViewerInstanceLike, ApryseAnnotationManagerLike, ApryseDocumentViewerLike
  • ApryseAnnotationRecord, ApryseSharedState, ApryseCursorData, ApryseCursorState, ApryseRemoteCursor
  • CrdtUpdateDataEvent, SyncStatus, Unsubscribe, Version
  • normalizeXfdf(), buildXfdf(), createAnnotationRecord(), recordsFromState()
  • getAnnotationId(), hasSharedAnnotations(), SNAPSHOT_RECORD_ID