Send your first realtime message
Publish a message from your backend and receive it in a browser.
Connect a browser tab to chat:room-1, then send it a message from your backend. You will register the channel’s namespace, create a backend key and issue a token that lets the browser subscribe. The examples use one chat application; replace the product ID, key and user lookup with your own.
Prerequisites
- A Lepsto product with Owner or Admin access for namespace and key creation.
- A backend where you can add a token endpoint, with a way to authenticate your users.
- A frontend where you can open a browser connection.
- Node 20 or later and npm for
@lepsto/sdk.
1. Register the namespace
Open Realtime → Namespaces and select Create namespace. Name it chat and keep the defaults: visibility: authorized, with presence, client events and history disabled.
With MCP, call realtime_namespace_create with name: chat.
For the REST equivalent, see the Realtime API reference.
Register the namespace before proceeding. Without it, token issuance in step 3 returns
422because no requested capability is allowed.
2. Create a public key
Open Product → Settings → Public Access and select Create public key. Enter a name, choose Selected toolkits, and select Realtime. This grants the key access to all keyed Realtime routes. Copy the lpk_… secret into your backend’s secret store before closing the dialog; it is shown only once. Set your product ID alongside it:
LEPSTO_REALTIME_API_KEY=lpk_0123456789abcdef…
LEPSTO_PRODUCT_ID=your-product-idWait about 30 seconds for the new key to reach the edge. A 401 during that interval does not establish that the key is invalid.
If you already use an rtk_… key, keep that value. Imported legacy keys continue to work and appear with an Imported badge.
Keep the public key on your backend. The browser will receive a separate delegated token. See authentication.
3. Install the SDK and mint a token
Install the package in your application:
npm install @lepsto/sdkThis tutorial uses @lepsto/sdk/realtime: its HTTP client accepts your product public key and product ID, while its socket client accepts a delegated token. The separate @lepsto/sdk/realtime/server import supports management operations with a bearer token and explicit base URL.
Create the HTTP client once when your backend starts:
// server/realtime.ts
import { createRealtime } from '@lepsto/sdk/realtime';
export const realtime = createRealtime({
productId: process.env.LEPSTO_PRODUCT_ID!,
publicKey: process.env.LEPSTO_REALTIME_API_KEY!,
});Add a token endpoint at /api/realtime/token. Replace requireSignedInUser with your application’s session lookup and ensure the user may access chat:room-1. The SDK sends this request to POST /tokens/issue:
// 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 });
}The token requests only subscribe. With client events disabled, namespace policy removes any requested publish capability. Your backend will publish the message in step 5.
4. Subscribe from the browser
In the browser, call connectRealtime with a provider that fetches the token from your endpoint, then subscribe to the channel. Both clients are in the same package; no additional install is needed. The socket starts connecting in the background:
// app/chat.ts
import { connectRealtime } from '@lepsto/sdk/realtime';
const socket = connectRealtime({
tokenProvider: async () => {
const res = await fetch('/api/realtime/token', { credentials: 'include' });
return res.json(); // { token, gatewayUrl }
},
});
socket.onStateChange((state) => {
console.log('realtime:', state); // connecting → connected
});
socket.subscribe('chat:room-1', (message) => {
console.log(message.channel, message.data);
});The socket factory is connectRealtime, and its class is RealtimeSocket. RealtimeClient names the HTTP client’s type; connect() is a method on the socket instance.
You can subscribe before the socket opens. It remembers the channel, subscribes when connected and resubscribes after a reconnect.
5. Publish from your backend
Wait for the tab to connect and subscribe, then run this on your backend. The SDK calls POST /messages:
import { realtime } from './realtime.js';
const result = await realtime.messages.publish({
channel: 'chat:room-1',
data: { from: 'alice', text: 'hello' },
});
// { channel: 'chat:room-1', published: true }Check the browser console. The subscription handler should print the payload:
chat:room-1 { from: 'alice', text: 'hello' }You have now sent a backend message to a subscribed tab. With history enabled, the publish response also contains offset and epoch, which clients use to recover missed messages. This tutorial leaves history off.
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. |
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. |
A call rejects with status: 0 and code: 'public_key_required' | The request never left your process: the operation needs a key and the client was constructed without one. Pass publicKey to createRealtime. |
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.