Home API Documentation Articles Buy PLUS Releases Contacts

← Back to Articles

Setting up WebSocket channels

An HTTP request is one client asking one question and getting one answer. Anything where several clients need to hear about each other — a chat, a game, a dashboard that updates itself, a phone that wants to know when the desktop changed something — does not fit that shape, and normally means either polling or a server of your own.

A channel is a WebSocket room the server hosts for you. Clients connect to /api/channels/{id}/connect and then either relay messages to one another (echo mode) or share a single JSON document the server keeps in sync (state mode). There is nothing to write on the server side: you enable the feature, define a channel, and the clients talk.

Availability — channels are available from shttps v3.4.0, and the whole feature is off by default. While it is off, every path under /api/channels answers 403 Channels are disabled.

This article covers enabling channels, defining them, and who is allowed to do what. The message protocol spoken over an open connection is a separate article: the channel protocol.

Nothing is stored

Channels live in memory for as long as the server runs. A state channel's document is not saved anywhere: restart the server and every channel starts again from its configured initial state, with no participants and its counter back at zero. A predefined channel is rebuilt on each start, which closes any socket still attached to the previous incarnation with close code 1001.

That makes channels a good fit for live coordination and a poor one for anything you would be upset to lose. When a value has to survive a restart, write it to the database and use a channel to tell the other clients that you did.

Turning it on

Everything to do with channels is configured in the app — on Android and on desktop alike — in the WebSocket channels section:

  • Enable WebSocket channels (channels_enabled) — the master switch.
  • Predefined channels — the channels that exist for as long as the server runs, whether or not anybody is connected.
  • Let clients create channels (allow_dynamic_channel_creation) — whether clients may add their own at runtime.
  • Limits — the caps that apply to every channel.

There is no web page for channel administration. Channels are set up in the app and used through the API; the built-in web interface (file browser, database browser, text editor) has no channels screen. A browser-based client of your own talks to /api/channels/… directly, exactly as any other client does.

Predefined channels

A predefined channel is described once in the configuration and recreated on every server start. It is never removed for being empty, and it is the only kind of channel whose every setting you can choose.

On desktop and on the command-line server the same channels can be written by hand into the configuration file, under predefined_channels:

"predefined_channels": [
  {
    "id": "lobby",
    "mode": "ECHO",
    "guestsAllowed": true,
    "notifyPresence": true,
    "deletable": false,
    "binaryAllowed": false
  },
  {
    "id": "game",
    "mode": "STATE",
    "initialState": {"players": {}},
    "maxParticipants": 8,
    "messageRateLimitPerSecond": 0,
    "guestsAllowed": false,
    "notifyPresence": true,
    "deletable": true,
    "binaryAllowed": true
  }
]
FieldDefaultMeaning
idrequiredThe channel's address. Letters, digits, dot, dash and underscore, up to 64 characters.
modeECHOECHO or STATE. Written in upper case here, although the API reports it in lower case; both spellings are accepted wherever it is read.
initialState{}State channels only: the document the channel starts from, every time the server starts.
maxParticipantsserver-wideConnections allowed at once. Omit it to use the server-wide limit.
messageRateLimitPerSecondserver-wideIncoming messages per second, per connection. 0 means this channel is not throttled at all; omit the field to follow the server-wide limit.
guestsAllowedtrueWhen false, only signed-in users may connect, even where guest access is configured for the rest of the server.
notifyPresencetrueWhether join and leave events are sent — Announce joins and leaves in the app.
deletablefalseWhether DELETE /api/channels/{id} may remove it.
binaryAllowedfalseWhether raw binary frames are relayed alongside ordinary messages.
password_hashnoneWritten by the app when you set a password. It is a hash — there is no way to put a plain password here, so set it in the app or leave the field out.

A malformed entry, an unusable id or a duplicate id is skipped with a warning rather than stopping the server: one bad channel definition must not be the reason the server does not come up.

persistent and deletable are different things. The API reports a predefined channel as "persistent": true, which only means it comes back on every server start. If its definition also says deletable, a client holding the right really can delete it — and it reappears the next time the server starts. Neither flag implies the other.

Channels clients create themselves

Let clients create channels is off by default. Switched on, a client holding the Create channels right may POST /api/channels and get a channel back:

curl -X POST http://your-server/api/channels \
     -H "Content-Type: application/json" \
     -d '{"id": "lobby-42", "mode": "state", "initialState": {"players": {}}, "maxParticipants": 8}'
{"id":"lobby-42","mode":"state","persistent":false,"deletable":true,
 "guestsAllowed":true,"passwordProtected":false,"maxParticipants":8,
 "binaryAllowed":false,"participants":0,"seq":0,
 "lastActivityAt":1765900000000,"wsUrl":"/api/channels/lobby-42/connect"}

Every field of the body is optional — an empty body asks for an echo channel with all defaults — and omitting id has the server generate one. wsUrl is a path, not a full URL: the server does not know which scheme or host name you reached it by, so prefix it yourself.

Two things cannot be chosen this way: such a channel is always deletable and always announces joins and leaves. And it is temporary — once it has been empty for longer than the idle timeout, the server removes it. Predefined channels are never collected this way, however long they sit empty.

Limits

The Limits dialog holds the caps that apply to every channel at once. The app shows some of them in friendlier units than the configuration file stores.

SettingConfig keyDefaultWhat it bounds
Max dynamic channelsmax_dynamic_channels20How many channels clients may have created at once. Predefined ones do not count.
Max participantsmax_participants_per_channel50Connections per channel, unless the channel sets its own. A connection over the limit is refused with 409.
Idle timeout (minutes in the app)channel_idle_timeout_ms600000 (10 min)How long an empty client-created channel survives. 0 keeps such channels forever.
Message rate limitchannel_message_rate_limit_per_second20Incoming messages per second per connection, unless the channel sets its own. 0 turns throttling off.
Max message size (KiB in the app)channel_max_message_bytes1048576 (1 MiB)The largest message or frame accepted on any channel.

The size cap is one number for the whole server, not one per channel. Every channel is served by the same WebSocket endpoint and therefore by the same reader, and the limit is applied before the reader knows which channel a frame belongs to. Raising it so that one channel can carry 8 MiB blobs lets every connection on every channel buffer 8 MiB too — which is the memory a single talking client can make the server hold. A message over the limit closes that connection with the standard code 1009.

Changing these values in the app restarts the server, which disconnects anybody currently connected.

One more limit has no setting of its own: the server holds a bounded queue of pending messages for each connection, so that everybody receives a channel's events in the same order without one slow reader holding up the senders. The bound follows the maximum message size, so a connection may fall a message or two behind without trouble. A client that stops reading altogether while the channel keeps talking eventually exhausts it and is disconnected with close code 4408; it can reconnect and take a fresh snapshot. Dropping such a connection is deliberate — quietly skipping messages for it would leave a state-channel client disagreeing with the server with no way to notice.

Channel permissions

When authorization is enabled, six permissions decide what a user may do with channels. They are set per user or per role on the users screen, under Channel permissions.

PermissionName in the APIWhat it allows
Connect to channelsCONNECTOpen a socket to a channel, and read a channel's settings with GET /api/channels/{id}. On its own it is listen-only, in both modes.
Send messagesPOSTEverything a participant sends: the send command, all five state commands, binary frames, and publishing over HTTP.
Create channelsCREATEPOST /api/channels.
Delete channelsDELETEDELETE /api/channels/{id}, for a channel that is deletable.
List channelsLIST_CHANNELSGET /api/channels — seeing that the other channels exist at all.
List participantsLIST_PARTICIPANTSSeeing who else is connected: the REST endpoint, the participant list a new connection is greeted with, and the join/leave events.

A read-only participant is a supported way to use a channel rather than a broken one. With CONNECT but not POST, a client still receives everything: in an echo channel every message, and in a state channel the snapshot on connect and every subsequent patch. It just cannot send.

Two things that will otherwise cost you an afternoon.

1. Users that existed before you upgraded have no channel permissions at all. The sensible defaults — connect and send for a named user, connect only for the guest — apply to newly created users. Everyone already in your user list starts with nothing until you grant it.

2. A role's channel permissions replace the user's own, they are not added to them. If a user has a role, the role's permissions are the ones that count and the user's own are ignored — the same way the other permission groups behave. See Users & Auth.

Permissions are read once, when a connection is opened; POST and LIST_PARTICIPANTS are not consulted again for each frame. Granting or revoking one therefore takes effect on the user's next connection, not the current one.

Who may connect

Three independent layers, in this order. All three must be satisfied.

  1. The server's authorization. With authorization switched off, anybody can connect and every participant is anonymous — there are no users, so no permissions are consulted. With it on, /api/channels/… goes through exactly the same authentication as the rest of the API, and an unauthenticated request is refused with 403.
  2. Guest access. Where a guest user is configured, a visitor arrives as the guest and gets the guest's permissions — unless the channel sets Allow guests to false, which closes it to signed-in users only.
  3. The channel password. Orthogonal to both. When a channel has one it is always required, of everybody: a fully authenticated user holding every permission still has to know that particular channel's password.

The password travels either as a query parameter or as a header:

ws://your-server/api/channels/game/connect?password=room-pin-1234

X-Channel-Password: room-pin-1234

When both are supplied, the header wins. The query form exists because a browser's WebSocket cannot set headers on a handshake — only the URL — so without it the endpoint would be unusable from an ordinary web page. Anything that is not a browser should prefer the header, which keeps the password out of URLs and logs.

The password guards connecting and publishing. It is not asked for by GET /api/channels/{id} or GET /api/channels/{id}/participants — those are guarded by their permissions instead.

Which origins may connect

A WebSocket handshake is not subject to CORS in the browser. There is no preflight, and the browser does not check Access-Control-Allow-Origin before letting the connection through — a page on any origin can open a socket to any server, and the user's cookies for that server go with it. That is why this endpoint decides for itself who may connect.

The decision uses the same CORS rules you configure for ordinary requests, enforced server-side. A handshake is upgraded when it comes from the same origin as the server, when it carries no Origin header (which is everything that is not a browser), or when its origin is covered by a CORS rule — one naming it exactly, or a * rule. Anything else is answered with 403 instead of an upgrade. The rules are read at each handshake, so a change takes effect immediately.

Note the weight a * rule carries here. For ordinary requests a browser refuses to combine Access-Control-Allow-Origin: * with credentials, so a wildcard cannot by itself expose an authenticated endpoint. No such protection exists for a WebSocket. A * rule lets any page join your channels, and if the server uses session authentication that page connects as whoever is logged in. Prefer naming the origins you actually serve.

The endpoints

EndpointNeedsDoes
GET /api/channelsList channelsEvery channel, with its settings and how many participants it has.
POST /api/channelsCreate channelsCreates one at runtime. Also needs Let clients create channels.
GET /api/channels/{id}Connect to channelsOne channel's settings.
DELETE /api/channels/{id}Delete channelsRemoves it and closes every socket with code 4410. The channel must be deletable.
GET /api/channels/{id}/participantsList participantsWho is connected right now.
GET /api/channels/{id}/connectConnect to channelsThe WebSocket handshake — see the protocol article.
POST /api/channels/{id}/messageSend messagesPublishes one message without holding a socket. Echo channels only.

Every parameter and response is in the API documentation, under Channels API.

Publishing without a socket

A cron job, a webhook or a sensor usually has one thing to say and no reason to hold a connection open for it. POST /api/channels/{id}/message drops a single message into an echo channel:

curl -X POST "http://your-server/api/channels/lobby/message" \
     -H "Content-Type: application/json" \
     -d '{"payload": {"text": "Nightly backup finished"}, "senderLabel": "backup-script"}'

{"delivered":true,"recipientCount":3,"seq":43}

The publisher registers nothing: it does not become a participant, does not appear in the participant list, and does not take a slot of maxParticipants. The connected clients receive it as an ordinary message, marked "source": "http" so they can tell it apart from a live one. senderLabel is optional and self-reported — nothing verifies it, so treat it as a hint about the source rather than an identity.

It is echo channels only. A state channel answers 409: a payload nobody would apply to the document is a mistake worth naming rather than ignoring.

This endpoint is not covered by the channel message rate limit. That limit is per open socket. An HTTP publisher is an ordinary HTTP client, so it is bounded only by the server's global rate limit — which is off by default. If you expose this endpoint to something that could misbehave, set a global rate limit, or a channel password, or both.

Keeping an eye on it

GET /api/system/status reports channels under its own scope, for a user holding the read status system right:

GET /api/system/status?scopes=channels

{"channels": {"enabled": true, "predefined": 3, "dynamic": 5, "totalParticipants": 12}}

See also

  • The channel protocol — what a client sends and receives once the socket is open: commands, events, state operations, presence, binary messages and close codes.
  • API Documentation — every parameter and response of the seven endpoints above.
  • Users & Auth — enabling authorization, guests, roles, and how role permissions relate to a user's own.
  • Database: transactions over WebSocket — the other WebSocket endpoint, which follows the same conventions for envelopes, error kinds and close codes.