Skip to content

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

Creating 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/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.
401 public_key_requiredNo key reached the edge. Check that your backend is sending Authorization: Bearer <key>.
401 public_key_invalidThe 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_scopeThe 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_unavailableThe edge is starting up and fails closed. Retry with backoff; your key is fine.
Calls return 404The namespace is not registered for your product.
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