Player identity

Until now irtio had two ways to know who a player is. A key join gives you a client id that lasts as long as the resume token does. A JWT join gives you a durable id, and costs you a server and a signing secret to run.

There is now a third, and it costs you one option:

const room = await joinRoom(schema, { identity: true });

ctx.playerId in your room code is now a stable id for that player, and it stays the same across a reconnect, across the room going to sleep and waking up, and across the player closing the tab and coming back tomorrow. No email, no password, no account, no sign-in screen.

What the player gets

The first time a browser runs that line, irtio mints an identity and the browser stores it. That is the whole ceremony: no interruption, nothing to fill in, nothing to remember.

Because it lives in the browser, it has the limits of a thing that lives in a browser. Clearing site data is a new player. A private window is a new player. A different browser, or a different device, is a different player. That is exactly what an account would fix, and an account is not this — being able to claim an identity with an email so it follows you between devices is a separate feature that has not shipped.

Say so in your UI if progress matters. “Your scores are saved on this device” is honest; “your account” is not.

What your room sees

An irt: player id:

onJoin(state, ctx) {
  // ctx.playerId is something like 'irt:9tW2rQ...' — stable for this player, in this project
  void ctx.room.kv.get(ctx.playerId, 'progress');
}

That id is per project. The same human playing two of your games has two different ids, and the two games cannot tell they are the same person. This is deliberate and it is not configurable: an identity that games could compare notes on would be a tracking identifier, and irtio is not going to hand one out.

Within a project it is stable, which is what makes it useful: it is the right key for player storage and it is what leaderboards rank.

How it stays out of your game

This is the part worth reading even if you never think about it again, because it is the reason the feature is shaped the way it is.

Your room code runs on our infrastructure, but it is your code, and so is every other customer’s. A platform-wide player credential that travelled into a game’s server would mean any game could collect credentials that work in every other game. So it does not travel there:

  • The identity — the long-lived secret in the player’s browser — is only ever sent to irt.io over HTTPS. It never touches a room socket.
  • Before connecting, the client trades it for an assertion: a short-lived token that names one project, expires in minutes, and carries a per-project pseudonym rather than a platform id.
  • The assertion is signed with a private key that exists only in the control plane. Game servers hold the public half, so they can check an assertion and cannot mint one.

The upshot for you: nothing you can read inside a room identifies a player anywhere else, and nothing a compromised game server holds lets it impersonate a player — in your game or in anyone’s.

Using it with a token you already have

identity and token are mutually exclusive: a join asserts one identity, and passing both would be asking the server to pick. If you already run your own auth, keep using your JWT — you have something better than an anonymous id.

Under the hood

If you are not using @irtio/client, the two calls are:

POST /v1/identity            {"project":"<id>"}                  -> {"identity":"…","id":"pid_…"}
POST /v1/identity/assertion  {"identity":"…","project":"<id>"}   -> {"assertion":"…","playerId":"irt:…"}

Mint once and store the result; exchange before every connect, including reconnects, because an assertion that outlives its exchange is refused with E_TOKEN_EXPIRED. Both endpoints are rate limited — see the limits page — and the mint one tightly, because a browser should call it exactly once, ever.