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, a public 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 before anything else: in the workspace, open Realtime → Namespaces and press Create namespace. 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 public key
The key that unlocks Realtime’s public routes is issued by the platform, not by Realtime. In the workspace, open Product → Settings → Public Access and press Create public key. Give it a name and a scope that covers Realtime. The full lpk_… secret is shown once, in the dialog — copy it into your backend’s secret store now:
LESSLY_REALTIME_API_KEY=lpk_0123456789abcdef…
LESSLY_PRODUCT_ID=your-product-idCreating a key is an Owner or Admin action, and it reaches the edge within about 30 seconds — if the next step answers 401 straight away, wait out that window before suspecting the key.
Already have a key from before? Keep it. An rtk_… key still works, and shows up on this page marked Imported.
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/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. |
401 public_key_required | No key reached the edge. Check that your backend is sending Authorization: Bearer <key>. |
401 public_key_invalid | The key is not an active key of the product in the URL: unknown, revoked, mistyped, belonging to another product, or created on the other environment. Keys are per environment and do not carry across, and the response will not tell you which environment answered — check the edge origin before you suspect the key. A key created less than 30 seconds ago also lands here. |
403 public_key_scope | The key is real but its scope does not reach Realtime. Widen it with Edit scope on Product → Settings → Public Access. A key revoked in the last 30 seconds can also answer this way. |
503 public_config_unavailable | The edge is starting up and fails closed. Retry with backoff; your key is fine. |
Calls return 404 | The namespace is not registered for your product. |
| 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.