Files
galaxy-game/user/docs/stage21-user-name-display-name.md
T
2026-04-25 23:20:55 +02:00

4.8 KiB

Stage 21 — user_name + display_name refactor

Context

The Game Lobby plan moved every in-game race_name value into the Lobby Race Name Directory. User Service stopped owning any in-game naming concept. The legacy single-valued RaceName field on UserAccount, the canonical race-name reservation store, and the RaceNamePolicy port were deleted. Two stable fields replace them:

  • user_name — immutable auto-generated player-<suffix> handle, unique platform-wide, assigned once at account creation;
  • display_name — mutable optional free-text label validated by pkg/util/string.go:ValidateTypeName, empty by default, not unique.

Key decisions

Crockford Base32 lowercase alphabet for the suffix

user_name ends with eight characters drawn from the alphabet 0123456789abcdefghjkmnpqrstvwxyz (Crockford Base32 lowercase, i, l, o, u excluded). Each player- identifier therefore has 40 bits of entropy and is free of visually ambiguous pairs.

The generator lives in user/internal/adapters/local/id_generator.go:randomSuffix; implementation reads five random bytes via crypto/rand and walks the 5-bit groups through the alphabet without using encoding/base32 so the alphabet swap stays self-contained.

Retry limit bumped to 10

authdirectory.Ensurer.ensureCreateRetryLimit moved from 8 to 10. Collisions on the 40-bit suffix are expected to be extremely rare; the extra two attempts give a comfortable margin before the service falls back to 503 service_unavailable.

Canonical reservation removed

RaceNameReservation, RaceNameCanonicalKey, ErrRaceNameConflict, and the reservation:race-name:* Redis keys are gone. Uniqueness now comes from the single lookup:user-name:<encoded> index; no canonical form is persisted in User Service. Any existing Redis dataset must be re-initialized (the codebase has no production deployment, so no migration script ships).

Confusable policy moved to Lobby

user/internal/ports/race_name_policy.go and user/internal/adapters/local/race_name_policy.go were deleted. The Unicode case-fold + digit-to-letter anti-fraud map + TR39 confusable skeleton landed under lobby/internal/domain/racename/ so Stage 09R can wire it into the new Race Name Directory. Golden fixtures (Pilot Nova vs P1lot N0va, unicode paypal vs Cyrillic lookalike) moved with the policy.

Admin surface

  • /api/v1/internal/user-lookups/by-race-name is replaced by /api/v1/internal/user-lookups/by-user-name (exact 1:1 lookup).
  • Admin listing GET /api/v1/internal/users gains user_name, display_name, and display_name_match query parameters. display_name supports exact (default) and prefix matching.

Empty display_name

An empty or whitespace-only input value on POST /profile is accepted and stored as an empty display_name. Non-empty values are validated by pkg/util/string.go:ValidateTypeName; internal whitespace, leading/trailing special characters, and unsupported characters are rejected.

Eligibility snapshot gains max_registered_race_names

lobbyeligibility.limitCatalog now covers every tariff explicitly. The defaults for max_registered_race_names are free=1, paid_monthly=2, paid_yearly=6, paid_lifetime=0 (unlimited marker). A LimitCodeMaxRegisteredRaceNames user-specific override, when active, replaces the tariff default. This feeds Stage 17A's registration quota enforcement and Stage 22's permanent_block cascading.

Domain event payload

ports.ProfileChangedEvent.RaceName was replaced by UserName and DisplayName. The Redis stream publisher emits user_name on every event and display_name only when non-empty, keeping the event type user.profile.changed stable.

Telemetry rename

Runtime.RecordRaceNameReservationConflict is now Runtime.RecordUserNameConflict; the metric name changed from user.race_name.reservation_conflicts to user.user_name.conflicts.

Gateway boundary

The cross-module FlatBuffers boundary also moved: pkg/schema/fbs/user.fbs swaps AccountView.race_name for user_name + display_name, and UpdateMyProfileRequest.race_name for display_name. pkg/transcoder/user.go, pkg/model/user, gateway downstream tests, and the integration harness mirror the change.

Files of interest

  • Domain: user/internal/domain/common/types.go, user/internal/domain/account/model.go, user/internal/domain/policy/model.go
  • Storage: user/internal/adapters/redisstate/keyspace.go, user/internal/adapters/redis/userstore/store.go
  • Services: user/internal/service/{authdirectory,selfservice,adminusers,accountview,lobbyeligibility,shared}/
  • HTTP + OpenAPI: user/internal/api/internalhttp/, user/openapi.yaml
  • Lobby seed: lobby/internal/domain/racename/{policy,types,policy_test}.go
  • Gateway boundary: pkg/schema/fbs/user.fbs, pkg/transcoder/user.go, pkg/model/user/user.go