AUTH4D-324 Player names
Players can have a public display name for leaderboards and lobbies. auth4.dev adds a random
four-digit discriminator, so the full name, Name#1234, is unique in the project. Names are
separate per project: the same Name#1234 can exist in two projects.
Name rules
- The server trims the name and normalises it to Unicode NFC.
- 1 to 32 characters (Unicode code points) after that.
- No spaces or other white space, no control or format characters (such as zero-width spaces) and
no
#. - Uniqueness ignores case and compatibility forms:
Ace#0427andace#0427can’t both exist. - The discriminator is
0001to9999. Changing only the case of a name keeps the discriminator; any other change gets a new random free one. - A project can block words. A name that contains a blocked word anywhere, ignoring case, is
refused. It gets the same
invalid_nameerror as a name that breaks a rule, and refused attempts count against the player’s change budget, so the list can’t be read out quickly.
Player endpoints
GET /t/{tenantId}/me/name reads the signed-in player’s name. PUT on the same path sets it:
{ "name": "Ace" }
Send { "name": null } to clear it. Both answer 200 with the stored name:
{ "name": "Ace", "discriminator": "0427", "tag": "Ace#0427" }
All three fields are null until a name is set. Responses are Cache-Control: no-store.
Both need the player’s access token in Authorization: Bearer …, from any application type and any
sign-in method, guests included. The token is never a cookie, so another site can’t make the call
for the player (CSRF). A browser caller must use one of the token’s application’s registered
origins; the preflight admits GET and PUT with Authorization and Content-Type for an origin
registered on any enabled browser application of the project.
| Status | error |
When |
|---|---|---|
400 |
invalid_name |
The name breaks a rule or contains a blocked word |
400 |
invalid_request |
The body is not { "name": string | null } |
401 |
invalid_token |
No token, an expired or revoked one, one from another project, or a ban |
403 |
access_denied |
The browser origin is not registered on the token’s application |
409 |
name_unavailable |
Every discriminator for this name is taken |
429 |
rate_limited |
More than 10 attempts by this player in an hour |
503 |
temporarily_unavailable |
Storage is unavailable, or migration 0045_player_names.sql is not applied |
A change writes the audit event player.name.updated (actor and target: the player) with action
set or cleared and the application’s client_id. The name itself is not in the audit row.
Claims
- Userinfo returns
name(Name#1234) when the access token has theprofilescope and the player has a name. Without theprofilescope it is never returned. - Access tokens carry the same
nameclaim only when the access token has theprofilescope and the project turns onaccess_token_claim(below). It is off by default because server SDKs released before AUTH4D-324 refuse access tokens with unknown claims; update them first. The claim is read again at every issuance, including refresh, so a rename shows in the next token. - ID tokens never carry it.
Management
These routes live under /management/tenants/{tenantId}. Viewers can read; owners and admins can
change. Errors use the management envelope (error.code).
GET /management/tenants/{tenantId}/users/{userId}/name (user:read) returns:
{
"player_name": {
"name": "Ace",
"discriminator": "0427",
"tag": "Ace#0427",
"updated_at": "2026-10-24T12:00:00.000Z"
}
}
PUT on the same path (user:write) overrides the name:
{ "name": "Ace", "discriminator": "0001" }
name follows the name rules, or is null to clear it. discriminator is optional; without it the
player keeps their current one when the name is unchanged ignoring case, and gets a free one
otherwise. Blocked words don’t apply to an override. A change writes management.user.name_updated
with action set or cleared.
GET /management/tenants/{tenantId}/player-names (user:read) returns the project setting:
{ "player_names": { "blocked_words": ["grief"], "access_token_claim": false } }
PUT on the same path (user:write) takes either field or both. blocked_words replaces the list:
up to 200 words of 1 to 32 characters with no spaces; repeats that differ only in case are stored
once. A change writes management.player_names.updated with blocked_word_count and status
(enabled or disabled for the access-token claim). The words are not in the audit row.
| Status | error.code |
When |
|---|---|---|
400 |
validation_failed |
A malformed name, discriminator, word list or access_token_claim |
403 |
forbidden |
The project is unknown or belongs to someone else |
403 |
insufficient_role |
A viewer tries to make a change |
404 |
not_found |
The user is not in this project |
409 |
name_unavailable |
The requested Name#NNNN belongs to another player, or none is free |
409 |
project_suspended |
The project is suspended |
503 |
not_ready |
Migration 0045_player_names.sql has not been applied |
Storage
Migration 0045_player_names.sql adds users.display_name, display_name_key (the NFKC,
lower-cased comparison form), display_name_discriminator and display_name_updated_at, a unique
index on (tenant_id, display_name_key, display_name_discriminator), and the
player_name_settings table (one optional row per project). Code running before the migration
issues tokens and userinfo without a name.
SDKs
- Browser:
getPlayerName()andsetPlayerName(name | null)resolve with{ name, discriminator, tag }and reject withinvalid_name,name_unavailable,invalid_sessionorplayer_name_failed.getUser()includesnamewhen userinfo returns it. - Unity:
GetPlayerNameAsync()andUpdatePlayerNameAsync(name)return aPlayerName(Name,Discriminator,Tag,IsSet), like Unity Authentication’s methods of the same names. - Server SDKs (TypeScript, C#, Go, Python) accept the
nameaccess-token claim.
Source: docs/api-spec/auth4d-324.md