...
Days
Hours
Minutes
Seconds
x
Skip to content

Ship the Collaborative backend in 10 minutes

Real-time relay, comments, suggestions, version history, and async save for Froala's Collaborative plugin. Copy one Node app, deploy to the host you already use, and paste the URL into your editor.

One backend, three hosts. All three run the identical app. Only the hosting differs.
3
One-click hosts
4
Modules included
1
SQLite file
~10 min
Start to finish
What you deploy

Four modules, one HTTP server

Node + Express with the wysiwyg-editor-node-sdk. The WebSocket relay and all REST endpoints share one port and one collab.db SQLite file.
Core · The relay

Collaborative

A transparent WebSocket relay. It broadcasts every update verbatim to peers in the same document, while Yjs handles merging in the browser. This is the piece that has to stay warm.
Included freews(s)://…/{docId}Collaborative.attachToServer(server)
Every Froala plan includes it. CKEditor and Tiptap sell the same depth as a paid add-on.
/collab/{docId}

CollabPersistence

Track-change suggestions and inline comments, anchored to editor positions and stored in SQLite.
/collab/{docId}/versions

VersionControl

Append-only named or automatic snapshots of full document content, with titles and change notes.
/collab/{docId}/content

AsyncSave

Single-row upsert of the latest editor HTML, the offline fallback, used before a sync URL is set.
Step 1

01. Create the backend

Three files. Drop them in a folder, then push to a git repo your host can read.
server.js
const path = require('path');
const fs = require('fs');
const http = require('http');
const express = require('express');
const cors = require('cors');

const FroalaEditor = require('wysiwyg-editor-node-sdk');
const { Collaborative, CollabPersistence, VersionControl, AsyncSave } = FroalaEditor;

const PORT = parseInt(process.env.PORT, 10) || 3000;
// DATA_DIR must point at a persistent volume on your host (see step 2).
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data');
const DB_PATH = path.join(DATA_DIR, 'collab.db');
const CORS_ORIGINS = (process.env.CORS_ORIGINS || '*').trim();

fs.mkdirSync(DATA_DIR, { recursive: true });

const app = express();
app.set('trust proxy', true);
app.use(cors(CORS_ORIGINS === '*' ? { origin: true }
  : { origin: CORS_ORIGINS.split(',').map(o => o.trim()).filter(Boolean) }));
app.use(express.json({ limit: '5mb' }));

// All REST routes are namespaced under /collab/:docId by the SDK.
CollabPersistence.attachRoutes(app, { dbPath: DB_PATH });
VersionControl.attachRoutes(app, { dbPath: DB_PATH });
AsyncSave.attachRoutes(app, { dbPath: DB_PATH });

// Health check — reports live relay stats for your host's probe.
app.get('/health', (_req, res) => res.json({ status: 'ok', ...Collaborative.getStats() }));

// One HTTP server carries both Express (REST) and the WebSocket relay.
const server = http.createServer(app);
Collaborative.attachToServer(server);
server.listen(PORT, () => console.log('Collaborative backend on :' + PORT));
package.json
{
  "name": "froala-collaborative-backend",
  "version": "1.0.0",
  "private": true,
  "main": "server.js",
  "engines": { "node": ">=20" },
  "scripts": { "start": "node server.js" },
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.19.2",
    "wysiwyg-editor-node-sdk": "^5.3.0"
  }
}
.env.example
# Port — most hosts inject this automatically; leave unset in production.
PORT=3000
# Directory for collab.db. MUST be a persistent volume on your host.
DATA_DIR=./data
# Comma-separated origins your editor page is served from ("*" for a first test).
CORS_ORIGINS=*
Step 2

02. Deploy to your host

Same backend on all three. Pick the account you already have, copy the config, and follow the steps.
Why Railway. Keeps one container warm with WebSockets open and lets you attach a volume from the dashboard. Deploy straight from a repo.
railway.json
{
  "$schema": "https://railway.com/railway.schema.json",
  "build": { "builder": "NIXPACKS", "buildCommand": "npm install" },
  "deploy": {
    "startCommand": "npm start",
    "healthcheckPath": "/health",
    "restartPolicyType": "ON_FAILURE",
    "numReplicas": 1
  }
}
  1. New project from your repo

    On railway.com: New Project → Deploy from GitHub repo. In Settings → Source, set Root Directory to your backend folder and point the config path at railway.json.

  2. Attach a volume

    Settings → Volumes → New Volume, mount path /data. The volume keeps your data through every redeploy.

  3. Set variables

    DATA_DIR=/data and CORS_ORIGINS. Leave PORT unset, since Railway injects it.

  4. Deploy & get the URL

    Settings → Networking → Generate Domain. That domain is your backend.

Why Render. A Blueprint provisions everything from one file, and a disk pins the service to a single instance automatically, exactly what single-writer SQLite needs.
render.yaml
services:
  - type: web
    name: froala-collaborative
    runtime: node
    rootDir: backend
    plan: starter          # a disk requires a paid instance type
    buildCommand: npm install
    startCommand: npm start
    healthCheckPath: /health
    numInstances: 1        # single writer — do not raise
    envVars:
      - key: DATA_DIR
        value: /var/data
      - key: CORS_ORIGINS
        value: "*"
    disk:
      name: collab-data
      mountPath: /var/data
      sizeGB: 1
  1. New Blueprint Instance

    On render.com: New → Blueprint, connect your repo. Render reads render.yaml and shows the service with its disk.

  2. Confirm the disk

    Verify a disk named collab-data at /var/data is listed. A paid instance type is required, since it mounts the disk and stays awake between requests.

  3. Apply

    Render builds and deploys. Adjust CORS_ORIGINS in the dashboard when you go past testing.

  4. Grab the URL

    Open https://froala-collaborative-XXXX.onrender.com.

Why Fly.io. A persistent VM with a mounted volume and no scale-to-zero, driven entirely from the CLI. Two files, run from your repo root.
fly.toml
app = "froala-collaborative"
primary_region = "sea"

[env]
  DATA_DIR = "/data"
  PORT = "3000"
  CORS_ORIGINS = "*"

[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = false   # keep the relay warm
  auto_start_machines = false
  min_machines_running = 1     # single instance only

  [[http_service.checks]]
    method = "GET"
    path = "/health"
    interval = "30s"
    timeout = "5s"

[[mounts]]
  source = "collab_data"
  destination = "/data"

[[vm]]
  size = "shared-cpu-1x"
  memory = "512mb"
Dockerfile
FROM node:20-slim
WORKDIR /app
RUN apt-get update \
  && apt-get install -y --no-install-recommends python3 make g++ \
  && rm -rf /var/lib/apt/lists/*
COPY package*.json ./
RUN npm install --omit=dev
COPY . ./
ENV NODE_ENV=production DATA_DIR=/data PORT=3000
EXPOSE 3000
CMD ["npm", "start"]
terminal
brew install flyctl          # or: curl -L https://fly.io/install.sh | sh
fly auth signup

fly launch --no-deploy       # accept app name + region
fly volumes create collab_data --size 1
fly deploy
fly open
  1. Install flyctl & sign in

    Run the first two commands above.

  2. Launch, then make the volume

    --no-deploy first so you can create collab_data before the first boot.

  3. Deploy

    fly deploy builds the image and boots one machine with the volume at /data.

  4. Open it

    Your URL is https://<app-name>.fly.dev.

Step 3

03. Point your editor at it

Use the secure wss:// scheme (all three hosts serve HTTPS). docId is any string that names a shared document.
your editor page
new FroalaEditor('#editor', {
  docId: 'my-doc-2024',
  realTimeConfig: {
    // your deployed backend, plus the docId
    syncUrl: 'wss://your-app.up.railway.app/' + 'my-doc-2024'
  }
});

Where the REST endpoints live

Comments, suggestions, versions, and async save all sit under https://your-app.up.railway.app/collab/{docId}/… on the same domain as the relay. One service, one URL to configure.
Every option on this page is documented in full in the Collaborative plugin docs.
Before you ship

Three things that apply to every host

All three are core to a stable deploy. Get them right and your data stays intact and your connections stay live in production.

Keep WebSockets open

This is a long-lived connection relay. Run it on an always-on instance so connections stay live, with scale-to-zero and short idle timeouts turned off.

SQLite needs a volume

Mount persistent storage at DATA_DIR. With it, comments, suggestions, and version history survive every redeploy.

Single instance only

SQLite is single-writer. Every config here defaults to one instance. Keep this service on that single instance.

Your editor is ten minutes from real-time

Copy the backend, deploy it to Railway, Render, or Fly.io, and paste the URL into your collabConfig.
Seraphinite AcceleratorOptimized by Seraphinite Accelerator
Turns on site high speed to be attractive for people and search engines.