PatoLab’s report editor supports simultaneous multi-user editing of pathology report sections. This is powered by a standalone Node.js process that runs alongside the Laravel application and manages the real-time synchronization layer. The architecture combines TipTap (rich text editor) and Yjs (CRDT-based conflict-free document model) on the browser side with Hocuspocus v4 as the central WebSocket authority, which in turn communicates back to Laravel through a webhook API to persist document state.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/lerichardv/patolab-platform/llms.txt
Use this file to discover all available pages before exploring further.
Architecture Overview
base64) and a rendered HTML string.
The collaboration server lives in the editor-collaboration-server/ directory at the root of the repository. It has its own package.json and must be started as a separate process from the Laravel application.
Setup
Install Node.js dependencies
package.json):| Package | Version | Purpose |
|---|---|---|
@hocuspocus/server | ^4.1.0 | WebSocket collaboration engine |
@hocuspocus/transformer | ^4.1.0 | Yjs ↔ TipTap JSON conversion |
@tiptap/starter-kit | ^3.26.0 | Core TipTap extensions |
yjs | ^13.6.31 | CRDT document model |
express | ^5.2.1 | HTTP server for auxiliary endpoints |
crossws | ^0.4.5 | WebSocket adapter bridging Express and Hocuspocus |
dotenv | ^17.4.2 | Environment variable loading |
Configure environment variables
Create a Then, in the root Laravel
.env file inside editor-collaboration-server/ (or copy from .env.example if present). At minimum, set the webhook URL pointing to your Laravel app:editor-collaboration-server/.env
.env, set the collaboration server URL for the frontend:.env
Room Naming Convention
Hocuspocus rooms are identified by a document name string. PatoLab uses the following pattern:{field} is one of the following report section identifiers:
| Field key | Database column (HTML) | Database column (Yjs state) |
|---|---|---|
macroscopy | macroscopy_html | yjs_macroscopy_state |
microscopy | microscopy_html | yjs_microscopy_state |
diagnosis | diagnosis_html | yjs_diagnosis_state |
clinical_details | clinical_details_html | yjs_clinical_details_state |
comments_notes | comments_notes_html | yjs_comments_notes_state |
protocols | protocols_html | yjs_protocols_state |
legend | legend_html | yjs_legend_state |
report_date | report_date | yjs_report_date_state |
sections_order | sections_order | yjs_sections_order_state |
status | (read-only, from specimen.status) | — |
save-status | (transient UI state) | — |
report-42-macroscopy corresponds to the macroscopy section of specimen_reports row with id = 42.
Laravel Webhook: /api/collaboration
The collaboration server sends POST requests to /api/collaboration (handled by ReportEditorController::handleWebhook) for three lifecycle events. The document name is parsed using the regex report-(\d+)-(.+) to extract the reportId and field.
onConnect — Client connects to a room
Fired when a browser client establishes a WebSocket connection. Laravel responds with the saved binary Yjs state for that document field, base64-encoded. The server uses this to immediately restore the document to its last persisted state without a round-trip database query for each individual keystroke.
create — Room is first initialized
Called by the onLoadDocument hook when no binary state exists in the onConnect response (i.e., the document has never been edited collaboratively, or the Yjs state was cleared). Laravel returns the raw HTML content from the database, which the server then converts to a Yjs document via TiptapTransformer.toYdoc() to seed the initial collaborative state.
onChange — Document content changes (debounced autosave)
Fired after a 1-second debounce following any edit. The server sends both the full binary Yjs state (base64-encoded) and the rendered HTML string. Laravel stores the binary state for future onConnect hydration and the HTML for display, PDF generation, and read-only views.
How the Server Works
The server is structured as a singleindex.js file (editor-collaboration-server/index.js) that wires together three layers:
1. Hocuspocus Engine
A Hocuspocus instance is initialized with a custom webhook extension object (customWebhookExtension) that implements onConnect, onLoadDocument, and onChange hooks. These hooks call the Laravel /api/collaboration endpoint as described above.
2. Express HTTP Server
An Express app handles auxiliary HTTP routes that the React frontend calls directly:
POST /api/dictate-chunk— receives audio chunks and transcribes them via the Grok STT API for voice dictationPOST /api/fix-grammar— sends report text to the Grok API for medical Spanish grammar correction and optionally injects the result back into the live Yjs documentPOST /api/refresh-insumos— triggers a refresh of the supply/inventory document roomGET /health— returns server status
upgrade event is intercepted by crossws, which routes WebSocket handshakes to Hocuspocus. This allows Hocuspocus and Express to share the same TCP port (1234) — standard HTTP requests go to Express while WebSocket upgrade requests go to Hocuspocus.
editor-collaboration-server/index.js
StarterKit(withundoRedodisabled — Yjs manages undo/redo history)CustomImage(extendedImagewith alignment and resize attributes)TableKit(resizable tables)TextAlign(heading, paragraph, image)Highlight(multicolor)ImageGrid(custom node for multi-column image layouts)
Production Deployment
For production, the collaboration server must run continuously and restart automatically on failure. The project’s
README.md documents a Supervisor configuration as the recommended approach. PM2 is an equally viable alternative if it is already part of your Node.js operations stack.Option A: PM2
Option B: Supervisor (recommended in project README)
Create/etc/supervisor/conf.d/patolab-collaboration.conf:
Update
directory to match the absolute path of your editor-collaboration-server/ folder, and ensure command points to the correct node binary (which node). The user must have read/write access to that directory.Nginx Proxy (WebSocket)
If Nginx sits in front of your application, you must configure it to forward WebSocket upgrade requests to the collaboration server port:VITE_COLLABORATION_SERVER_URL to point to your proxied path instead of the direct port.