By default every player is anonymous: a new player in each browser tab, with nothing kept across devices. When saves, leaderboards or bans should follow a person, sign them in. If your game already signs players in with Firebase, Supabase, Clerk or Auth0, it takes one setting and one line, with no server of your own.
Three ways to know who a player is
| Way | For | Same player on every device | You run |
|---|---|---|---|
| Anonymous (default) | Jams, quick play, testing | No: a new player per browser tab | Nothing |
| Your sign-in provider | Games that already sign players in with Firebase, Supabase, Clerk, Auth0… | Yes | Nothing: GameRelay checks the token |
| Your own backend | Games with their own accounts and server | Yes | A server holding your secret key |
Anonymous players
GameRelay.connect({ publicKey }) gives each tab its own player id (p_…), kept across reloads of that tab. Two tabs are two players, which makes testing easy. Saves (relay.storage) and leaderboard entries belong to that tab's player, so a player on a new device starts over. That's fine for a jam game or quick play.
Sign in with your provider
Your sign-in already gives the game an ID token: a short-lived, signed statement from your provider that says who the player is. Pass it to connect, and GameRelay checks its signature against the provider's published keys, its issuer and audience, and its expiry. The player then gets the same GameRelay player on every device.
- In the dashboard, open your game's Settings → Player sign-in, pick your provider and fill in what it asks for (below). An AI assistant with the account MCP server can do it with
update_instance. - Pass the token as
idToken:
const relay = await GameRelay.connect({
publicKey: 'gr_pub_…',
playerName: 'Guest', // used when the sign-in has no username
idToken: () => auth.currentUser?.getIdToken(), // your provider's line, from the list below
});
if (relay.signedIn) {
// relay.playerId is the same on every device (see Player ids below).
}idToken is called before every connect and reconnect, so return a fresh token; each provider's call below refreshes it for you. When nobody is signed in, return null and the player joins anonymously, so signing in can stay optional. To let only signed-in players in, turn off Anonymous players on the same page; then connect() rejects with sign_in_required for anyone who isn't, including your provider's own guest accounts (Firebase and Supabase anonymous sign-in), so your game can show its sign-in. Players already in a game when you turn it off stay until they leave. With anonymous players on, those guests join with relay.signedIn false. If you turn sign-in off again, games that still pass idToken keep working: everyone joins anonymously.
What to set for each provider
Firebase. In Player sign-in: Firebase, and your project id. In your game:
idToken: () => auth.currentUser?.getIdToken()Supabase. In Player sign-in: Supabase, and your project URL (https://<ref>.supabase.co). In your game:
idToken: async () => (await supabase.auth.getSession()).data.session?.access_tokenThe project must use asymmetric JWT signing keys (Project settings → JWT signing keys); projects on the old shared secret publish no keys to check against.
Clerk. In Player sign-in: Clerk, and your Frontend API URL (Clerk dashboard → API keys). In your game:
idToken: () => window.Clerk.session?.getToken()Clerk's session tokens last a minute, which is fine: the SDK asks for a fresh one before each connect.
Auth0 (and any OIDC provider). In Player sign-in: OIDC, the issuer exactly as in its tokens (Auth0: https://<your domain>/, with the slash), and your client id as the audience. In your game:
// auth0-spa-js keeps handing back its ID token after it expires (about 10 hours in),
// so renew it when it's within a minute of expiring.
async function freshIdToken() {
if (!(await auth0.isAuthenticated())) return null;
let claims = await auth0.getIdTokenClaims();
if (!claims || claims.exp * 1000 < Date.now() + 60_000) {
await auth0.getTokenSilently({ cacheMode: 'off' });
claims = await auth0.getIdTokenClaims();
}
return claims?.__raw ?? null;
}
// in connect:
idToken: freshIdTokenGoogle, Cognito, Keycloak and Okta work the same way: GameRelay reads the keys from the issuer's discovery document. Check that your provider's SDK hands back a fresh ID token, not just a stored one.
Firebase has its own walkthrough, including hosting on itch.io: Multiplayer with Firebase sign-in.
Player ids
With Firebase, Supabase and Clerk, relay.playerId is the player's user id there (the token's sub): a Firebase uid, a Supabase user UUID, a Clerk user_… id. Because it's the same id a backend would use, a game that later adds its own server can mint tokens for the same players.
With OIDC, it's h_ and a hash of the user id, the same every time. Every player in a room sees the others' ids, and an OIDC user id can follow a person across apps: Google's is the same in every app they sign in to. If your backend needs the provider's id, add sub to Copied claims: it then comes with the player's webhook events, and other players never see it. Any other id that doesn't fit GameRelay's (more than 64 characters, or characters like |) is hashed the same way.
Names, and keeping real names private
The name other players see is, in order:
- The claim you set as Name claim, if you set one (
username,user_metadata.handle). - The provider's username, if it has one (
preferred_username; with Supabase,user_metadata.preferred_usernameoruser_metadata.user_name, which GitHub sign-ins have). An email address there is skipped: Microsoft Entra puts the user's email inpreferred_username. - The
playerNameyour game passes toconnect.
GameRelay never shows the token's full name unless you set the name claim to name. For a player signed in with Google, that's their real name, shown to everyone in the room, which you rarely want in a game and never want in a game for children. Every name goes through your game's name moderation, and players can still rename themselves with relay.setName() for the session; the next connect brings back the name above.
Telling roles apart on your backend
Claims you list under Copied claims, such as a Firebase custom claim role, are kept as the player's custom claims and sent with their webhook events, as a backend's claims are. Other players' games don't receive them; your game can read its own player's claims from its sign-in SDK.
Testing
A signed-in player is one player everywhere, so the same account in two tabs is one player: the second tab takes over and the first gets relay.on('replaced'). Test multiplayer with two accounts, or with one tab signed in and one anonymous.
Sign in with your own backend
If your game has its own accounts and a server, the server mints tokens with your game's secret key and the game fetches them through getToken. Never put the secret key in the game itself.
// your server: check who the player is with your own sign-in, then
const res = await fetch('https://gamerelay.io/v1/auth/token', {
method: 'POST',
headers: { authorization: `Bearer ${process.env.GAMERELAY_SECRET_KEY}`, 'content-type': 'application/json' },
body: JSON.stringify({ playerId: user.id, playerName: user.username, claims: { role: user.role } }),
});
// send the whole answer back to the game, e.g. as the JSON of /my/gamerelay-token
// your game
const relay = await GameRelay.connect({
getToken: (signal) => fetch('/my/gamerelay-token', { signal }).then((r) => r.json()),
});The REST docs have every field. This is also the way to go when you want to decide a player's name or avatar yourself, or check something before letting a player in.
Which one should I use?
- No accounts in your game: stay anonymous until you need more.
- You sign players in with a provider, or want to add sign-in without writing a server: use your provider.
- You have your own accounts and a server: use your backend. You can also use your provider in one client and your backend in another (with Firebase, Supabase and Clerk they give the same player ids), but one
connecttakesidTokenorgetToken, not both.
Security notes
- An ID token proves who the player is to games that trust your project, until it expires (an hour for Firebase and Supabase, a minute for Clerk). GameRelay accepts only RS256 and ES256 tokens, fetches the keys only from your provider's published key URL (for OIDC, the one its discovery document names), and checks the issuer and audience exactly.
- Set Allowed origins too, so other sites can't use your public key.
- Using an AI assistant? The "Signed-in players" section of llms.txt covers all of this, so it writes the one-line version.
Try it
GameRelay has a free plan, no card needed. Get a public key, then follow the quickstart, or point your AI coding agent at llms.txt.