# Authenticate your backend and your users

Two credentials, two levels. Your backend holds a long-lived **product API key**. Each of your end users gets a short-lived **delegated token**, minted by your backend from that key. The key never leaves your servers; the token is the only thing a browser ever sees.

| Credential | Held by | Lifetime | Reaches |
|---|---|---|---|
| Product API key (`rtk_…`) | Your backend only | Until you revoke it | The whole public HTTP surface, for one product |
| Delegated token | One end user's browser | 60–3600 seconds | Exactly the channels and operations you named, after namespace narrowing |

## Create a product API key

A product API key is a machine secret that identifies one product. It is `rtk_` followed by 43 characters of URL-safe random data:

```text
rtk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

The key is the product's public key, the one credential that authenticates the product's public routes, and its scope is narrowed to Realtime. Keys are created, listed and revoked from the platform, using your own Lessly login. Realtime has no key of its own.

**MCP.** [`organization_public-keys_create`](/reference/mcp-tools/organization_public-keys_create) to create one, [`organization_public-keys_list`](/reference/mcp-tools/organization_public-keys_list) to see the keys and their scopes, [`organization_public-keys_revoke`](/reference/mcp-tools/organization_public-keys_revoke) to revoke one. [`organization_public-keys_update-scope`](/reference/mcp-tools/organization_public-keys_update-scope) replaces the scope of a key that is still active.

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

> **The secret is shown once, at creation, and never again.** Afterwards only a `rtk_` plus six characters prefix is retrievable — enough to tell your keys apart, useless as a credential. If you lose a key, revoke it and create another.

A product API key cannot create, list or revoke API keys — including itself.

### Where to keep it

- In your backend's secret store or environment, alongside your database password.
- Never in frontend code, a mobile app bundle, a public repository or a browser request. Anything that reaches a user's device can be read by that user.
- One key per system that needs one, so revoking a leaked key does not take down everything else. Revocation takes effect immediately.

## Send the key

Send the key in the `X-Api-Key` header on every call to the public HTTP surface, which is rooted at your product:

```http
POST /{productId}/realtime/tokens/issue HTTP/1.1
Host: public.lessly.com
X-Api-Key: rtk_xxxxxx…
Content-Type: application/json
```

The server SDK does this for you — construct it with `apiKey` and `productId` and it builds the same base URL and sends the same header:

```ts

const realtime = new Realtime({
  apiKey: process.env.LESSLY_REALTIME_API_KEY!,
  productId: process.env.LESSLY_PRODUCT_ID!,
});
```

> A key is bound to the product it was created in. Using it against a different product's path returns `404`, not `403` — the surface never confirms that another product exists.

## Mint a token for one user

A delegated token is a short-lived credential your backend mints for one of your end users, naming the exact channels that user may use and what they may do there. Your backend is the authority on who its users are; Realtime takes your word for the subject and enforces the rest.

`POST /tokens/issue`, authenticated with the API key. Through the SDK:

```ts
const { token, gatewayUrl, expiresAt } = await realtime.tokens.issue({
  subject: 'user-42',
  channels: [
    { name: 'chat:room-1', ops: ['subscribe', 'history'] },
    { name: 'chat:room-7', ops: ['subscribe'] },
  ],
  ttlSeconds: 900,
});
```

The request declares:

| Field | Rules |
|---|---|
| `subject` | Your identifier for the end user. 1 to 128 characters of `a-z`, `A-Z`, `0-9`, `.`, `_` and `-`. No colons. |
| `channels` | 1 to 32 entries. Each has a `name` — a **concrete** channel, no wildcards — and `ops`, one or more of `subscribe`, `publish`, `presence`, `history`. |
| `ttlSeconds` | Optional. 60 to 3600. Defaults to 3600. |

The response carries:

| Field | Meaning |
|---|---|
| `token` | The credential to hand to the browser |
| `gatewayUrl` | The WebSocket endpoint the browser client connects to |
| `expiresAt` | ISO-8601 timestamp at which the token stops working |

## What narrowing does to it

What you declare is a ceiling, not a guarantee. Before the token is signed, each channel is checked against its namespace policy:

- A channel whose namespace is not registered is dropped.
- Operations the namespace does not allow are dropped — `presence` without presence enabled, `publish` without client events enabled, `history` on a namespace retaining nothing.
- If everything you declared is dropped, the call fails with `422` rather than returning a token that could connect and then be refused on every action.

Narrowing only ever removes. A token can never carry more than its namespace allows, whatever your backend asks for. The full policy table is on [Realtime](/ship/realtime).

## External subjects

Your end users live in a different identity space from Lessly platform users. The subject you pass is stored in the token prefixed with `ext:`, so `user-42` becomes `ext:user-42`, and the token is marked as belonging to an external subject. The two spaces cannot collide: naming your own user after a Lessly user id borrows none of that user's authority.

That prefixed subject is what identifies the connection on a channel roster, so it is the value you will see in [presence](/ship/realtime/presence) members for tokens minted this way.

## Refresh a live connection

A token stops working at `expiresAt`. Plan for that rather than minting hour-long tokens by default: a shorter TTL means a revoked or reassigned user loses access sooner.

The browser client is built around this. You give it a **token provider** — a function that calls an endpoint on your backend and returns a fresh token and WebSocket endpoint — and it calls that function on every connection attempt:

```ts

const client = connect({
  tokenProvider: async () => {
    const res = await fetch('/api/realtime/token', { credentials: 'include' });
    return res.json(); // { token, gatewayUrl }
  },
});
```

Your endpoint decides who the caller is with your own session, then mints for that user and nobody else. A connection whose token has expired is closed by the realtime service; the client fetches a new token and reconnects straight away, with no backoff on that particular case.

To swap the token on a connection that is still open — after the user gains access to another channel, for instance — call `auth()`. With no argument it asks the token provider for a fresh one:

```ts
await client.auth();
```

The call resolves when the service accepts the new token, and rejects if it does not.

## Next steps

- [Send your first realtime message](/ship/realtime/quickstart): the whole loop, from namespace to a message in a tab.
- [Connect a browser tab](/ship/realtime/browser-client): what the client does with the token you hand it.
- [Show who is on a channel](/ship/realtime/presence): mint with `presence` and read the roster.
- [Look up a limit or an error](/ship/realtime/limits-and-errors): what `401`, `404` and `422` mean and what to do about each.
