New Froala Editor v5.3.0 is here – Learn More
Froala Documentation
- Installation Guides
- Browser Support
- Languages Support
- Shortcuts
- Activation
- Examples
- Customize the Editor
- Use-cases
- Plugins
- APIs
- Development Frameworks
- Server Integrations
- Server SDKs
- Migration Guides
- Changelog
- Tutorials
- Froala Docs
- /
- Plugins
- /
- Collaborative Plugin
- /
- Backend API
Node.js · Express · SQLite · WebSocket
Collaborative Backend API Reference
Four independent modules power Froala’s collaboration plugin: a real-time WebSocket relay, plus REST + SQLite stores for suggestions & comments, version snapshots, and offline saves. Mount only what you need — or run all four on a single HTTP server and a single database file.
System at a glance
Overview
The collaborative backend is split across four modules that you compose as needed: a WebSocket relay for real-time peer sync, REST endpoints for persisting suggestions and comments, a version history store, and an async content-save endpoint for offline mode. All four can share a single HTTP server and a single SQLite file.
| Module | File | Responsibility |
|---|---|---|
| Collaborative | lib/collaborative.js | Pure WebSocket relay. Broadcasts messages between peers of the same document. |
| CollabPersistence | lib/collab_persistence.js | REST + SQLite for track-change suggestions and inline comments. |
| VersionControl | lib/version_control.js | REST + SQLite for named or auto snapshots of document content. |
| AsyncSave | lib/async_save.js | REST + SQLite for saving/loading the latest content in offline (async) mode. |
Getting started
Install
bash
npm install wysiwyg-editor-node-sdk
SQLite dependency
CollabPersistence, VersionControl, and AsyncSave require better-sqlite3. It ships as a regular dependency and installs automatically — no separate step needed. Import the modules
All collaborative modules are exported from the package’s main entry point. Destructure the ones you need:
javascript · commonjs
const FroalaEditor = require('wysiwyg-editor-node-sdk'); const Collaborative = FroalaEditor.Collaborative; const CollabPersistence = FroalaEditor.CollabPersistence; const VersionControl = FroalaEditor.VersionControl; const AsyncSave = FroalaEditor.AsyncSave;
Conventions
Every REST endpoint is namespaced under /collab/:docId/. The :docId segment identifies the collaborative document and must match the docId used by the WebSocket connection. Document IDs are arbitrary strings; URL-encode any characters outside a-z 0-9 - _.
| Transport | URL pattern |
|---|---|
| WS | ws://<host>/{docId} |
| REST | http://<host>/collab/{docId}/{resource} |
Full server example
The most common setup runs the relay and all three REST stores on one Express-wrapped HTTP server, backed by one shared SQLite file. This wires everything together end to end:
javascript · server.js
const http = require('http'); const express = require('express'); const path = require('path'); const { Collaborative, CollabPersistence, VersionControl, AsyncSave, } = require('wysiwyg-editor-node-sdk'); const app = express(); app.use(express.json()); // One SQLite file shared by all three persistence modules const dbPath = path.join(__dirname, 'data', 'collab.db'); CollabPersistence.attachRoutes(app, { dbPath }); VersionControl.attachRoutes(app, { dbPath }); AsyncSave.attachRoutes(app, { dbPath }); // Optional: expose live relay stats on the shared server app.get('/health', (req, res) => res.json(Collaborative.getStats())); // Share ONE HTTP server between REST and the WebSocket relay const server = http.createServer(app); Collaborative.attachToServer(server); server.listen(3000, () => { console.log('Collaborative backend on http://localhost:3000'); });
Why attachToServer? Use
attachToServer() (not createServer()) whenever REST endpoints and the relay must live on the same port. createServer() is for running the relay as the only service on a port. Module 01 — Collaborative
lib/collaborative.js · WebSocket relay
A pure WebSocket relay server. It groups connections by docId and broadcasts every incoming message to all other members of that document. All collaboration logic lives in the browser clients — the server never parses or validates message content. A keepalive heartbeat pings every 30 s and terminates any client that fails to pong within 10 s.
FN Collaborative.createServer(options?)
Creates a self-contained HTTP + WebSocket server. The HTTP side answers GET /health and rejects all other HTTP requests with 426 Upgrade Required. Use it when the relay is the only service on the port.
| Parameter | Type | Description | |
|---|---|---|---|
| options | object | optional | Configuration object. |
| options.port | number | optional | Port to listen on. Default: 3000. |
Returns — WebSocket.Server, the underlying ws instance.
javascript
const { Collaborative } = require('wysiwyg-editor-node-sdk'); // Starts on port 3000; clients connect to ws://localhost:3000/{docId} const wss = Collaborative.createServer({ port: 3000 });
FN Collaborative.attachToServer(httpServer)
Attaches the relay to an existing HTTP (or Express-wrapped) server. Use it when REST endpoints and the relay must share one port.
| Parameter | Type | Description | |
|---|---|---|---|
| httpServer | http.Server | required | A Node.js http.Server instance (including those created by Express). |
Returns — WebSocket.Server.
javascript · express + shared port
const http = require('http'); const express = require('express'); const { Collaborative } = require('wysiwyg-editor-node-sdk'); const app = express(); const server = http.createServer(app); Collaborative.attachToServer(server); server.listen(3000);
FN Collaborative.setupWSConnection(conn, req)
Low-level handler that wires a single connection into the document registry. Called automatically by both createServer and attachToServer — use it directly only when managing your own ws server.
| Parameter | Type | Description | |
|---|---|---|---|
| conn | WebSocket | required | The incoming connection from the connection event. |
| req | http.IncomingMessage | required | The HTTP upgrade request. The document ID is derived from req.url: the leading slash is removed and the path before the first ? is URL-decoded. Falls back to default-doc if the path is empty. |
Returns — void.
Document lifecycle A document entry is created when the first client joins and deleted when the last client disconnects. Clients that join an empty document receive no historical state — they must wait for another peer to reconnect and push a sync message.
FN Collaborative.getStats()
Returns a snapshot of server state: active document count and total open connections.
returns
{ "docs": 3, "clients": 7 } GET /health
Live document and client counts. Available on the standalone server from createServer(). With attachToServer(), add this route yourself using Collaborative.getStats().
200 OK
json
{ "docs": 3, "clients": 7 } WS ws://<host>/<docId>
Connect to a collaborative document. The path after the leading slash is the URL-decoded document ID. Every text or binary message from one client is relayed verbatim to all other clients in the same document.
| Segment | Type | Description | |
|---|---|---|---|
| docId | string | required | Identifies the document. All clients on the same docId are peers. URL-encoded; decoded by the server before grouping. An empty path defaults to default-doc. |
Relay semantics The server is a transparent relay: it does not store messages or replay history to late joiners. All CRDT/OT state, cursor sync, and protocol versioning are the client’s responsibility — Froala’s collaborative plugin uses Yjs internally.
Message size limit Any message larger than 1 MB is silently dropped and never forwarded to peers. Keep individual sync messages well below this threshold.
javascript · browser client
// Connect to document "my-doc-2024" const ws = new WebSocket('ws://localhost:3000/my-doc-2024'); ws.addEventListener('message', (event) => { // Handle incoming sync messages from other peers handleCollabMessage(event.data); }); // Send a message — relayed to all other peers in the document ws.send(encodeUpdate(myUpdate));
Module 02 — CollabPersistence
lib/collab_persistence.js · REST + SQLite
REST endpoints backed by SQLite for persisting track-change suggestions and inline comments per document. Anchor positions are stored as opaque JSON strings — the server never interprets editor-internal position data.
FN CollabPersistence.attachRoutes(app, options?)
Registers all suggestion and comment routes on an Express app and initialises the SQLite tables on first call.
| Parameter | Type | Description | |
|---|---|---|---|
| app | Express | required | An Express application instance. |
| options.dbPath | string | optional | Path to the SQLite file. Default: collab.db (relative to the process CWD). Created if it doesn’t exist. |
javascript
const path = require('path'); const { CollabPersistence } = require('wysiwyg-editor-node-sdk'); CollabPersistence.attachRoutes(app, { dbPath: path.join(__dirname, 'data', 'collab.db') });
Suggestions
Track-change suggestions created by collaborators. Each records the original and proposed text, the editor position (anchor), the author, and a lifecycle status.
GET /collab/:docId/suggestions
Returns all suggestions for a document, ordered by timestamp ascending. The replies field is not included in the list response.
| Path param | Type | Description | |
|---|---|---|---|
| docId | string | required | Document identifier. |
200 OK — array of suggestion objects
Suggestion object schema
json
{
"id": "sug-001",
"doc_id": "demo-doc",
"type": "insert", // operation type (opaque to server)
"author_id": "user-42",
"author_name": "Alice",
"timestamp": 1720000000000, // Unix milliseconds
"original_text": "Hello world",
"suggested_text": "Hello everyone",
"anchor_start": "[0,5]", // JSON-serialised position array
"anchor_end": "[0,16]", // JSON-serialised position array
"status": "pending" // "pending" | "accepted" | "rejected"
} Anchors
anchor_start and anchor_end are stored and returned as JSON strings. Run JSON.parse if you need the underlying position array. POST /collab/:docId/suggestions
Creates a new suggestion. A 409 should be treated as confirmation the suggestion was already persisted.
| Field | Type | Description | |
|---|---|---|---|
| id | string | required | Unique identifier, generated by the client. |
| type | string | required | Operation type (e.g. insert, delete, format). Opaque to the server. |
| authorId | string | optional | Stored as author_id. |
| authorName | string | optional | Stored as author_name. |
| timestamp | number | optional | Unix ms. Defaults to server time (Date.now()). |
| originalText | string | optional | Stored as original_text. |
| suggestedText | string | optional | Stored as suggested_text. |
| anchor | object | optional | Position object with start and end. Each is JSON-serialised into anchor_start / anchor_end. |
| status | string | optional | One of pending, accepted, rejected. Default: pending. |
201 Created 400 missing id or type 409 id already exists
json · request body
{
"id": "sug-001",
"type": "insert",
"authorId": "user-42",
"authorName": "Alice",
"originalText": "Hello world",
"suggestedText": "Hello everyone",
"anchor": { "start": [0, 5], "end": [0, 16] }
} json · 201 response
{ "id": "sug-001" } PATCH /collab/:docId/suggestions/:id
Updates the status and/or replies of a suggestion. At least one field must be present.
| Field | Type | Description | |
|---|---|---|---|
| status | string | optional | One of pending, accepted, rejected. An invalid value returns 400. |
| replies | array | optional | Full replacement array of reply objects, JSON-serialised before storage. Pass [] to clear. |
200 OK 400 empty body / invalid status
json · response variants
// when status is updated { "id": "sug-001", "status": "accepted" } // when only replies is updated { "id": "sug-001" }
DELETE /collab/:docId/suggestions/:id
Permanently deletes a suggestion. Returns 204 whether or not the suggestion existed (idempotent).
204 No Content
Comments
Inline comments anchored to a position in the document. Comments support a resolved flag and a replies list.
GET /collab/:docId/comments
Returns all comments for a document, ordered by timestamp ascending. The replies field is not included in the list response.
200 OK — array of comment objects
json · comment schema
{
"id": "cmt-001",
"doc_id": "demo-doc",
"author_id": "user-42",
"author_name": "Alice",
"timestamp": 1720000000000,
"text": "Should we rephrase this paragraph?",
"anchor_start": "[1,0]", // JSON-serialised position array
"anchor_end": "[1,42]",
"resolved": 0 // 0 = unresolved, 1 = resolved (integer)
} Type note
resolved is returned as an integer (0 or 1), not a boolean. anchor_start / anchor_end are JSON strings — parse them as needed. POST /collab/:docId/comments
Creates a new comment. A 409 confirms the comment was already persisted.
| Field | Type | Description | |
|---|---|---|---|
| id | string | required | Unique identifier, generated by the client. |
| authorId | string | optional | Author’s unique identifier. |
| authorName | string | optional | Author’s display name. |
| timestamp | number | optional | Unix ms. Defaults to server time. |
| text | string | optional | Comment body. Default: "". |
| anchor | object | optional | Position with start and end. Each is JSON-serialised. |
| resolved | boolean | optional | Default: false. Stored as integer 0/1. |
| replies | array | optional | Initial reply objects. Default: []. JSON-serialised before storage. |
201 Created 400 missing id 409 id already exists
json · 201 response
{ "id": "cmt-001" } PATCH /collab/:docId/comments/:id
Updates the resolved state and/or replies. At least one field must be present.
| Field | Type | Description | |
|---|---|---|---|
| resolved | boolean | optional | Truthy values stored as 1, falsy as 0. |
| replies | array | optional | Full replacement array. Pass [] to clear. |
200 OK 400 neither field present 404 comment not found
json · 200 response
{ "id": "cmt-001" } DELETE /collab/:docId/comments/:id
Permanently deletes a comment.
204 No Content
Module 03 — VersionControl
lib/version_control.js · REST + SQLite
REST endpoints backed by SQLite for storing named or auto-generated snapshots of editor content per document. Each snapshot captures the full document state at a point in time and is optionally annotated with a title and description. Versions are append-only — they cannot be edited in content, only renamed.
FN VersionControl.attachRoutes(app, options?)
Registers version-control routes on an Express app and initialises the versions table on first call.
| Parameter | Type | Description | |
|---|---|---|---|
| app | Express | required | An Express application instance. |
| options.dbPath | string | optional | SQLite file path. Default: collab.db. Can share the same file as the other modules. |
GET /collab/:docId/versions
Returns all version summaries for a document, ordered by created_at descending (newest first). The snapshot field is excluded for performance — use the single-version endpoint to retrieve it.
200 OK — array of version summaries
json · version summary (no snapshot)
{
"id": "rv-lp8k3a2-9xf2r",
"doc_id": "demo-doc",
"title": "Before major refactor", // null if unnamed
"description": null,
"created_at": 1720000000000,
"author_id": "user-42",
"author_name": "Alice",
"is_named": 1, // 1 if title is set, 0 otherwise
"source": "manual" // "manual" | "auto"
} GET /collab/:docId/versions/:id
Returns a single version including the full snapshot field.
200 OK — full version object 404 Not Found
json · full version
{
"id": "rv-lp8k3a2-9xf2r",
"doc_id": "demo-doc",
"title": "Before major refactor",
"description": null,
"snapshot": "<p>Document content here</p>",
"created_at": 1720000000000,
"author_id": "user-42",
"author_name": "Alice",
"is_named": 1,
"source": "manual"
} POST /collab/:docId/versions
Creates a new snapshot. If id is omitted, the server generates one as rv-{base36timestamp}-{random}.
| Field | Type | Description | |
|---|---|---|---|
| snapshot | string | required | Full document content (HTML string for Froala). |
| id | string | optional | Client-supplied identifier. Auto-generated if absent. |
| title | string | optional | Human-readable name. When non-empty, sets is_named = 1. |
| description | string | optional | Longer description or change notes. |
| author | object | optional | Author info. author.id → author_id, author.name → author_name. |
| source | string | optional | auto for periodic auto-saves; anything else stored as manual. Default: manual. |
Server-controlled timestamp The server always records the current time as
created_at. Any createdAt field in the request body is ignored. 201 Created —
{ id } only 400 missing snapshot 409 id already exists json · request body
{
"snapshot": "<p>Document content here</p>",
"title": "Before major refactor",
"author": { "id": "user-42", "name": "Alice" },
"source": "manual"
} PATCH /collab/:docId/versions/:id
Updates the title and/or description. Snapshot content cannot be changed. At least one field is required.
| Field | Type | Description | |
|---|---|---|---|
| title | string | null | optional | New title. Pass null to clear (sets is_named = 0). A non-empty string sets is_named = 1. |
| description | string | null | optional | New description. Pass null to clear. |
200 OK — updated full version 400 neither field present 404 Not Found
DELETE /collab/:docId/versions/:id
Permanently deletes a version snapshot.
204 No Content
Module 04 — AsyncSave
lib/async_save.js · REST + SQLite
REST endpoints for saving and loading the latest editor content per document. Used when the editor runs in async (offline) mode — i.e. when realTimeConfig.syncUrl is not set. Each document has exactly one content row; every POST upserts the current state.
FN AsyncSave.attachRoutes(app, options?)
Registers the async-save routes on an Express app and initialises the async_content table on first call.
| Parameter | Type | Description | |
|---|---|---|---|
| app | Express | required | An Express application instance. |
| options.dbPath | string | optional | SQLite file path. Default: collab.db. Can share the same file as the other modules. |
POST /collab/:docId/content
Saves (upserts) the current editor HTML. Each document has exactly one content row — repeated calls replace the previous value.
| Field | Type | Description | |
|---|---|---|---|
| body | string | required | Full editor HTML to persist. Field name matches Froala’s default saveParam. |
| authorId | string | optional | Identifier of the user who performed the save. |
| authorName | string | optional | Display name of the user who performed the save. |
Field name The Froala save plugin sends its payload under the field name
body (its default saveParam). 200 OK 400 missing body field
json · request body
{
"body": "<p>Updated document content.</p>",
"authorId": "user-42",
"authorName": "Alice"
} json · 200 response
{ "doc_id": "demo-doc", "saved_at": 1720000000000 } GET /collab/:docId/content
Returns the most recently saved content for a document.
Sanitization Content is returned verbatim. Sanitizing the HTML before rendering is the caller’s responsibility.
200 OK 404 no content saved
json · 200 response
{
"doc_id": "demo-doc",
"content": "<p>Updated document content.</p>",
"author_id": "user-42",
"author_name": "Alice",
"saved_at": 1720000000000
} json · 404 response
{ "error": "no content saved for this document" } Error format
All endpoints return errors as JSON with a single error field describing what went wrong. No envelope wrapping, no nested objects.
json · error response
{ "error": "id and type are required" }| Status | When |
|---|---|
| 400 | A required field is missing or a value is invalid (e.g. an invalid status string, or an empty body on a PATCH endpoint). |
| 404 | The requested resource does not exist. |
| 409 | A resource with the supplied id already exists (POST suggestions, comments, versions). |
| 500 | An unexpected database or server error. The error field contains the underlying SQLite message. |
Still Have Questions?
Here are some additional resources if you can’t find an answer you need.
Whats on this page hide