# Send your first realtime message

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`](/reference/mcp-tools/realtime_namespace_create), with the name `chat`.

**REST.** On the [Realtime API reference](/reference/openapi/realtime).

> 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`](/reference/mcp-tools/organization_public-keys_create). The key is the product's public key with its scope narrowed to Realtime — see [authentication](/ship/realtime/authentication).

**REST.** Under `/governance/api/v1/products/:productId/public-keys`, on the [Organization API reference](/reference/openapi/organization).

```bash
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](/ship/realtime/authentication).

## 3. Install the server SDK and mint a token

```bash
npm install @lessly/realtime
```

Construct the client once, at startup:

```ts
// server/realtime.ts

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:

```ts
// server/routes/realtime-token.ts

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

```bash
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:

```ts
// app/chat.ts

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`:

```ts

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:

```text
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](/ship/realtime/history): turn on history so a reconnecting tab catches up.
- [Show who is on a channel](/ship/realtime/presence): turn on presence and render the roster.
- [Authenticate your backend and your users](/ship/realtime/authentication): shorten the token TTL and let the token provider refresh it.
- [Connect a browser tab](/ship/realtime/browser-client): watch `onStateChange` to tell your users when they are live and when they are catching up.
