Realtime
Realtime
Move messages from your backend to open browser tabs, and know who is connected and what they missed.
Lessly Realtime moves messages between your backend and your users’ open browser tabs. Your backend publishes to a named channel; every client currently subscribed to that channel receives the message within the same request. On top of that it keeps a roster of who is on a channel, and a replayable history of what was sent there.
You work with your own concepts: namespaces, channels, capability tokens and grants. Holding the connections open, delivering to every subscriber and reconnecting a client that drops off are Realtime’s job.
When to reach for it
Reach for Realtime when something that happens on your server has to show up in a browser without the browser asking for it: a chat message, a document another person is editing, an order changing state, a long job reporting progress, a dashboard that must stay current. Reach for presence when you need to show who else is here, and for history when a client that reconnects must not miss what it was away for.
It is not the right tool for work that has no live audience. If nothing is watching, publish nothing.
The four ideas
- Channel — a named stream of messages, such as
chat:room-1. Its name is 3 to 128 characters ofa-z,A-Z,0-9,.,_,-and:, and the first colon is required. You never create a channel: publishing to a name or subscribing to it is enough. - Namespace — the segment before the first colon.
chat:room-1andchat:room-2both live inchat. It is 1 to 64 characters ofa-z,A-Z,0-9,.,_and-, and cannot contain a colon. It carries the policy for every channel under it. - Capability — one channel name paired with a set of operations. There are exactly four:
subscribereceives messages,publishsends one from a connected client,presenceenters and reads the roster,historyreplays what was sent earlier. A token carries a list of capabilities, and that list is the entire authority of the connection holding it. - Grant — the durable form of a capability, tying a subject to a channel pattern and a set of operations until you revoke it.
A namespace you have not registered allows nothing. Channels in it are silently dropped wherever capabilities are worked out, so subscribing to
chat:room-1beforechatexists never succeeds. Register the namespaces your channels use before you send anything through them.
What a namespace decides
A namespace is the unit of policy. Registering it decides what any channel under it is allowed to do.
| Setting | Values | Default | What it decides |
|---|---|---|---|
visibility | public, authorized | authorized | public gives every authenticated identity subscribe on namespace:* without a grant |
presence | boolean | false | Whether a channel keeps a roster |
clientEvents | boolean | false | Whether connected clients may publish directly |
history | none, last-message, window | none | What is retained and replayable |
historyWindowSeconds | 1 … 2592000 | — | Retention window; required when history is window |
encryptionRequired | boolean | false | Whether payloads must be end-to-end encrypted |
identifiedOnly | boolean | false | Whether anonymous callers get any capability at all |
subscribeProxyUrl | HTTPS URL or null | null | A callback of yours consulted per channel while a token is minted |
archive | off, { days: N } | off | Durable retention of archived history entries, up to 3650 days |
Namespaces are registered from the platform, not from the public HTTP surface. Over MCP that is realtime_namespace_create and realtime_namespace_update; over REST it is on the Realtime API reference.
How policy narrows a capability
Nothing that reaches a token is ever wider than the namespace allows. The same rule applies whether the capability comes from a grant or from what your backend declares when it mints a token:
presencesurvives only if the namespace haspresenceenabled.publishsurvives only if the namespace hasclientEventsenabled.historysurvives only if the namespace’shistoryis notnone.subscribeis not gated by these flags. Who may subscribe is decided by the caller side: a grant, or the channel your backend names when it mints.
Operations that do not survive are dropped, not refused. A request for ['subscribe', 'presence'] on a namespace without presence yields a capability of ['subscribe']. If everything asked for is stripped, the mint fails with 422 rather than handing back a token that would connect and then be refused on every action.
Two more settings act before that narrowing. identifiedOnly yields nothing at all to an anonymous caller. subscribeProxyUrl, if set, is consulted once per concrete channel while the token is minted, and it is fail-closed: a channel your callback does not allow produces no capability.
Grants
A grant ties three things together, and stands until you revoke it.
| Field | Meaning |
|---|---|
subject | The identity the grant applies to, or * for every identity in the product |
pattern | A channel pattern such as chat:* |
ops | The operations granted on every channel the pattern matches |
In a pattern, * stands for exactly one segment, and the number of segments must match: chat:* matches chat:room-1 but not chat:room-1:typing, and orders:*:* matches orders:acme:42. The namespace segment is always concrete — you cannot write *:room-1 — and a pattern carries at most eight segments in total.
Where grants apply, and where they do not, is the practical distinction:
| Caller | Needs a grant | How its capabilities are decided |
|---|---|---|
| A platform identity | Yes | Grants are resolved when a token is minted for that identity, together with the public visibility rule |
| One of your own end users | No | Your backend states which concrete channels and operations the token should carry, and namespace policy narrows that declaration. No wildcards |
Grants are managed from the platform: over MCP, realtime_grant_create, realtime_grant_list and realtime_grant_revoke.
What you build against
| Surface | Use it for |
|---|---|
@lessly/realtime — the server SDK | Your backend: publish, read history, mint tokens for your users |
@lessly/realtime-client — the browser client | The tab: connect, subscribe, take part in presence, reconnect |
| The public HTTP routes | A backend that does not use the SDK, reached at https://public.lessly.com/{productId}/realtime with your API key in the X-Api-Key header |
Managing namespaces, grants, API keys and webhooks is not part of that public surface — you do those from the platform.
Next steps
- Send your first realtime message: one path from nothing to a message arriving in a browser tab.
- Authenticate your backend and your users: the product API key you hold and the short-lived token each user gets.
- Show who is on a channel: turn on the roster and read it.
- Replay what a client missed: retain messages and recover after a reconnect.
- Look up a limit or an error: every quota, status code and close code in one place.