# Send your first realtime message

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`](/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 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:

```bash
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](/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. |
| `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](/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.
