Skip to content

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-id

Keep 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/realtime

Construct 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-client

Give 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

SymptomWhat it means
The mint returns 422Every capability you declared was stripped. The namespace is not registered, or its policy does not allow the operations you asked for.
Calls return 404The API key belongs to a different product than the one in the URL.
Calls return 401The key is wrong, revoked, or from a different environment. Keys are per environment and do not carry across.
The connection opens but nothing arrivesCheck 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

Was this page helpful?
Esc

Start typing to search the docs.

navigateselect