New Froala Editor v5.5.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
Browser peer Froala + Yjs Browser peer Froala + Yjs Browser peer Froala + Yjs SINGLE HTTP SERVER · PORT 3000 01 Collaborative WebSocket relay · broadcast by docId 02 CollabPersistence suggestions · comments 03 Version Control snapshots 04 AsyncSave offline content upsert collab.db SQLite 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
The four modules ship as part of the Froala Node.js SDK.
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. Body parsing: All four modules read parsed request bodies from req.body. Mount both Express body parsers before calling any attachRoutes method. The collaboration plugin sends comments, suggestions and versions as JSON, while the save plugin sends document content form-encoded, so a server with only express.json() will reject every content save with 400 Bad Request.
JavaScript
app.use(express.json({ limit: '5mb' }));
app.use(express.urlencoded({ extended: true, limit: '5mb' }));
The default 100kb limit is too small for version snapshots, which contain the full document. Raise it to match your largest realistic document.
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. In real-time mode this endpoint is still required if comment and suggestion highlights are to survive a reload. The comment and suggestion records come from commentsUrl and suggestionsUrl, but their anchors resolve against the document content, which is stored here. A document that is never saved will load its comment cards with nothing to attach them to — i.e. when collabConfig.realTime.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 using the field name
body (the default saveParam), encoded as application/x-www-form-urlencoded. This endpoint also accepts application/json if you post to it directly. Your server must have express.urlencoded() mounted to receive saves from the plugin. 400 Bad Request — if the body field is missing. A request that looks correct in the browser’s network tab but returns 400 usually means the matching body parser is not mounted, so req.body arrived empty. 200 OK 400 missing body field
json · request body
{
"body": "<p>Updated document content.</p>",
"authorId": "user-42",
"authorName": "Alice"
} Save timing
saveInterval is the minimum delay before a save is attempted, not a fixed cadence. The save plugin waits for a contentChanged event, then waits saveInterval milliseconds, then skips the request entirely if the content is unchanged since the last successful save. The observed gap between an edit and a request is therefore longer than saveInterval and varies with typing. editor.save.force() sets a flag so the next save is not skipped. It returns undefined and does not send a request. Use editor.save.save() to save immediately. For predictable persistence, save explicitly rather than relying on the interval JavaScript
events: { contentChanged: function () { const html = this.html.get(); clearTimeout(this._saveTimer); this._saveTimer = setTimeout(() => { fetch('/collab/my-doc/content', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ body: html }) }); }, 2000); } }
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