# Player tokens

> How your backend tells IRT who a player is, with JWTs signed by the project's signing keys, and how guests work without one.

A player token tells the router who a player is and what they may join.
Your backend signs it after it has authenticated the player its own way;
IRT never sees your users' passwords or sessions.

Projects without a backend can let players join as guests instead.

## The token

A player token is a JWT signed with HS256 using one of the project's
signing keys.

| Claim | Required | Meaning |
|---|---|---|
| `sub` | yes | The player ID you choose: 1 to 128 bytes, not starting with `guest:` |
| `prj` | yes | The project's slug |
| `exp` | yes | Expiry, at most 24 hours after `iat` |
| `iat` | yes | When the token was issued |
| `room` | no | The only room this token may join |
| `meta` | no | JSON up to 1 KB, passed to the room's `onJoin` |

The player ID is how the room, webhooks and voice know the player, and it
stays the same across reconnects. A player has one connection per room:
when the same player ID connects again, the older connection is closed.

Player IDs are public. Other players in the room see them as entity
owners, as voice identities and in anything the room sends. If your
backend knows players by email address or an internal account ID, give
each player an opaque ID for `sub` instead.

Use `room` when your backend decides who goes where, so a token for one
room can't be used to join another. Use `meta` for data the room needs
on join and shouldn't trust from the client, such as a display name or a
team.

## Signing keys

Create signing keys in the dashboard, under the project's keys, with
`irt signing-keys create`, or with `POST /projects/{p}/signing-keys`.
The secret is shown once. Store it on
your backend.

To rotate, create a second key, move your backend to it, then delete the
first. Tokens signed with either key are accepted while both exist.

## Signing a token

`@irt/server` signs tokens with WebCrypto, so it runs in Node, Deno, Bun
and edge workers:

```ts
import { playerToken } from "@irt/server";

export function tokenFor(player: { id: string; name: string }, room?: string): Promise<string> {
  return playerToken({
    key: process.env.IRT_SIGNING_KEY!,
    project: "my-game",
    player: player.id,
    room,
    meta: { name: player.name },
  });
}
```

`room` and `meta` are optional. The token lasts 24 hours unless
`lifetime` sets fewer seconds; the client SDK reconnects with the same
token, so a short lifetime ends a long session at its next reconnect.
`playerToken` throws a `TypeError` for a player ID, `meta` or lifetime
the router would refuse. Any JWT library that does HS256 works too, with
the secret's UTF-8 bytes as the key.

Hand the token to your client, which passes it to the client SDK when it
connects. A token that is invalid or expired closes the connection with
close code 4001.

## Guests

When the project's `players.guests` setting is on, a client can connect with
no token. The router then creates a guest player ID, starting with
`guest:`, and returns a guest token to the client. The client SDK keeps
it in the browser's `localStorage` and sends it on every connect, so a
guest keeps the same player ID across reconnects, tabs and browser
restarts, and with it the entities it owns, its private state and its
voice identity. The token lasts 30 days and is renewed on each connect;
a guest away longer than that comes back as a new guest.

Guests suit games with no accounts. Turn `players.guests` off when every
player must come through your backend.

## Rooms without a backend

Three more settings let clients manage rooms themselves:

- `players.createRooms`: clients may create rooms and join them by a 6-character
  join code, and connecting to a room that doesn't exist creates it.
- `players.listPublic`: clients may list active public rooms, with their player
  count and `meta`. Hibernated rooms have no players and aren't listed.
  With `players.guests` off, listing needs a player token too, unless
  `players.listPublicWithoutToken` is on, which lets anyone list them.
- `players.quickJoin`, off by default: clients may join any public room with
  space whose `meta` matches, or a new public room when none has space. See
  [quickJoin](/docs/reference/client-sdk#http-helpers-for-games-without-a-backend).
  It's for filling rooms with whoever comes; matchmaking by skill, parties
  and queues is your backend's.

With `players.createRooms` off, only your backend creates rooms, through the
[HTTP API](/docs/http-api#rooms). [Games without a backend](/docs/no-backend)
shows how a client uses these settings.
