Live Documents
The live-doc plugin lets every member of a channel co-edit a single shared document in real time. Documents support rich formatting, images with float modes, tables, math (LaTeX), code blocks, and collaborative cursors showing where each participant is typing.
Content is synchronized using Yjs - a conflict-free replicated data type (CRDT) - transported over a dedicated WebSocket port. Changes made while offline merge automatically when the client reconnects.
How it fits together
Section titled “How it fits together”- The client sends a
FancyLiveDocOpenplugin-data message (wire ID 141). - The server passes it to the live-doc plugin via FFI.
- The plugin mints a short-lived JWT and replies with
FancyLiveDocInvitecontaining the token and the WebSocket URL. - The server broadcasts
FancyLiveDocAnnounceto the channel so every member can join. - All participants connect to the WebSocket endpoint and exchange Yjs CRDT frames directly.
- After a configurable idle window the plugin flushes a binary snapshot to the file server for persistence across restarts.
Prerequisites
Section titled “Prerequisites”The live-doc plugin depends on the file server plugin for persistence. Without it, documents are kept in memory only and lost whenever the container restarts or the teardown grace window expires.
See File server for setup instructions.
Turn it on
Section titled “Turn it on”The plugin is configured only via a mounted INI file - the plugin.live-doc.*
keys are not mapped to MUMBLE_CONFIG_* environment variables because they
require absolute paths that vary per deployment.
-
If you do not already have a custom config file, copy the template from the image:
Terminal window docker cp mumble-server:/data/mumble_server_config.ini ./mumble-server.ini -
Add the following block to your
mumble-server.ini, adjusting paths and URLs for your deployment:; Master toggle. Default: false.plugin.live-doc.enabled=true; Required: writable directory for JWT secret and room state.plugin.live-doc.state_path=/data/live-doc-state; WebSocket bind address and port. Default: 0.0.0.0:64740.;plugin.live-doc.host=0.0.0.0;plugin.live-doc.port=64740; Public base URL for clients (use wss:// when behind a TLS proxy).;plugin.live-doc.public_url=wss://chat.example.com/live-doc; Persistence bridge. Must match file-server admin token.plugin.live-doc.file_server_url=http://127.0.0.1:64739plugin.live-doc.file_server_admin_token=your-shared-secret -
Mount the file and create the state directory:
environment:MUMBLE_CUSTOM_CONFIG_FILE: /data/mumble-server.inivolumes:- ./mumble-server.ini:/data/mumble-server.ini:ro- mumble-data:/data # live-doc-state lives inside here -
Expose the WebSocket port (default 64740) in
docker-compose.yml:ports:- "64738:64738/tcp"- "64738:64738/udp"- "64739:64739/tcp"- "64740:64740/tcp" # live-doc WebSocket -
Restart:
Terminal window docker compose up -d --force-recreate mumble-server
Configuration reference
Section titled “Configuration reference”| Key | Default | Description |
|---|---|---|
plugin.live-doc.enabled | false | Master toggle. |
plugin.live-doc.state_path | - | Required. Absolute path to a writable directory. The plugin stores its JWT signing secret here. |
plugin.live-doc.host | 0.0.0.0 | WebSocket bind address. |
plugin.live-doc.port | 64740 | WebSocket TCP port. |
plugin.live-doc.public_url | unset | URL clients use to connect (e.g. wss://chat.example.com/live-doc). Falls back to ws://<bind address>:<port> when empty. |
plugin.live-doc.max_update_bytes | 4194304 (4 MiB) | Maximum size of a single CRDT update. |
plugin.live-doc.snapshot_idle_secs | 60 | Seconds of inactivity before the in-memory document is flushed to the file server. |
plugin.live-doc.teardown_grace_secs | 30 | Seconds after the last viewer disconnects before the in-memory room is torn down. |
plugin.live-doc.file_server_url | unset | Base URL of the file server admin endpoint (e.g. http://127.0.0.1:64739). When unset, documents are never persisted. |
plugin.live-doc.file_server_admin_token | unset | Shared secret for the file server admin API. Required when file_server_url is set. |
Behind a reverse proxy
Section titled “Behind a reverse proxy”Like the file server, the live-doc WebSocket should be TLS-terminated by a reverse proxy on public deployments.
handle_path strips the matched prefix automatically:
chat.example.com { handle_path /live-doc/* { reverse_proxy localhost:64740 }}Then set plugin.live-doc.public_url=wss://chat.example.com/live-doc.
The trailing slash on both location and proxy_pass strips the prefix:
location /live-doc/ { proxy_pass http://127.0.0.1:64740/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_set_header Host $host;}Then set plugin.live-doc.public_url=wss://chat.example.com/live-doc.
Add a StripPrefix middleware so Traefik removes /live-doc before
forwarding:
http: middlewares: live-doc-strip: stripPrefix: prefixes: - /live-doc routers: live-doc: rule: "Host(`chat.example.com`) && PathPrefix(`/live-doc`)" service: live-doc entryPoints: [websecure] middlewares: - live-doc-strip services: live-doc: loadBalancer: servers: - url: "http://127.0.0.1:64740"Then set plugin.live-doc.public_url=wss://chat.example.com/live-doc.
Persistence
Section titled “Persistence”When both file_server_url and file_server_admin_token are set, the
plugin automatically:
- Flushes a binary CRDT snapshot to
PUT /admin/documents/{name}everysnapshot_idle_secsseconds after the last update. - Flushes once more when the last viewer disconnects and the teardown grace window expires.
- Loads the latest saved snapshot when the first viewer opens the document.
Documents that have never been persisted start empty for every viewer.
Pitfalls
Section titled “Pitfalls”- Port 64740 not exposed: the client will time out waiting for the
WebSocket invite reply. Check
docker compose psand confirm the port mapping. state_pathmissing or not writable: the plugin logs an error and refuses to start. The client will never receive aFancyLiveDocInvite.- File server admin token mismatch: snapshots fail silently (logged server-side). Documents open and sync normally but are lost on restart.
MUMBLE_CUSTOM_CONFIG_FILEnot set: the plugin keys are read from a mounted INI but the entrypoint-generated config does not include them. ConfirmMUMBLE_CUSTOM_CONFIG_FILEpoints at your file.
Next step
Section titled “Next step”You have walked through all of the feature deep-dives. Continue with Customize & disable features for the consolidated toggle reference.