Realtime
Send your first realtime message
Register a namespace, mint a token, subscribe a browser tab, and publish to it from your backend.
One path from nothing to a message arriving in a browser tab. It takes a namespace, an API key, a few lines in your backend and a few in your frontend. The example channel is chat:room-1.
1. Register the namespace
Channels only work inside a registered namespace, so register chat from the platform before anything else. For this walkthrough the defaults are enough: visibility: authorized and no presence, client events or history.
MCP. realtime_namespace_create, with the name chat.
REST. On the Realtime API reference.
A namespace that does not exist allows nothing, and the mint in step 3 would fail with
422.
2. Create a product API key
Create a key for your backend from the platform. The full rtk_… secret is shown once, at creation — copy it into your backend’s secret store now.
MCP. organization_public-keys_create. The key is the product’s public key with its scope narrowed to Realtime — see authentication.
REST. Under /governance/api/v1/products/:productId/public-keys, on the Organization API reference.
LESSLY_REALTIME_API_KEY=rtk_xxxxxx…
LESSLY_PRODUCT_ID=your-product-idKeep this key on the server. It is never sent to a browser. See authentication.
3. Install the server SDK and mint a token
npm install @lessly/realtimeConstruct the client once, at startup:
// server/realtime.ts
import { Realtime } from '@lessly/realtime';
export const realtime = new Realtime({
apiKey: process.env.LESSLY_REALTIME_API_KEY!,
productId: process.env.LESSLY_PRODUCT_ID!,
});Then add one endpoint of your own that mints a token for the signed-in user. Your session decides who that is; Realtime takes the subject from you. This calls POST /tokens/issue on the public routes:
// server/routes/realtime-token.ts
import { realtime } from '../realtime.js';
export async function handleTokenRequest(req, res) {
const user = await requireSignedInUser(req); // your own session
const { token, gatewayUrl, expiresAt } = await realtime.tokens.issue({
subject: user.id,
channels: [{ name: 'chat:room-1', ops: ['subscribe'] }],
ttlSeconds: 900,
});
res.json({ token, gatewayUrl, expiresAt });
}Only subscribe is asked for here. The namespace registered in step 1 has client events off, so a publish operation would be stripped and the browser would receive a token that cannot publish. Publishing in this walkthrough is your backend’s job.
4. Install the browser client and subscribe
npm install @lessly/realtime-clientGive the client a token provider pointing at the endpoint from step 3, then subscribe. connect returns immediately and starts connecting in the background:
// app/chat.ts
import { connect } from '@lessly/realtime-client';
const client = connect({
tokenProvider: async () => {
const res = await fetch('/api/realtime/token', { credentials: 'include' });
return res.json(); // { token, gatewayUrl }
},
});
client.onStateChange((state) => {
console.log('realtime:', state); // connecting → connected
});
client.subscribe('chat:room-1', (message) => {
console.log(message.channel, message.data);
});You can call subscribe before the connection is up. The client remembers the channel and subscribes as soon as it is connected — and re-subscribes for you after a reconnect.
5. Publish from your backend
With the tab open and subscribed, publish from anywhere in your backend. This calls POST /messages:
import { realtime } from './realtime.js';
const result = await realtime.messages.publish('chat:room-1', {
from: 'alice',
text: 'hello',
});
// { channel: 'chat:room-1', published: true }The browser’s handler fires with the payload you published:
chat:room-1 { from: 'alice', text: 'hello' }That is the whole loop. publish returns offset and epoch as well when the namespace retains history, which is what lets a reconnecting client replay what it missed.
When something does not arrive
| Symptom | What it means |
|---|---|
The mint returns 422 | Every capability you declared was stripped. The namespace is not registered, or its policy does not allow the operations you asked for. |
Calls return 404 | The API key belongs to a different product than the one in the URL. |
Calls return 401 | The key is wrong, revoked, or from a different environment. Keys are per environment and do not carry across. |
| The connection opens but nothing arrives | Check that the channel name in subscribe is exactly the one you publish to, and that the token was minted with subscribe on it. |
Next steps
- Replay what a client missed: turn on history so a reconnecting tab catches up.
- Show who is on a channel: turn on presence and render the roster.
- Authenticate your backend and your users: shorten the token TTL and let the token provider refresh it.
- Connect a browser tab: watch
onStateChangeto tell your users when they are live and when they are catching up.