10 KiB
User Service
galaxy/user owns regular-user platform identity and account state.
The service is internal-only. Its source-of-truth transport is trusted
REST/JSON. Edge Gateway exposes selected self-service operations externally
through authenticated gRPC with FlatBuffers payloads and transcodes those
requests to this service's internal REST API.
Scope
User Service is the source of truth for:
- opaque regular-user identifiers in
user-*form - exact-after-trim login e-mail addresses
- current race name and editable self-service settings
- current entitlement snapshot
- active sanctions and active user-specific limits
- current effective
declared_country
User Service is not the source of truth for:
- system-administrator identity
- device sessions, challenges, or client public keys
- declared-country review workflow or history
- edge authentication, request signing, or replay protection
Administrative reads and writes against regular-user state do not make this
service the owner of administrator identity. Admin identity belongs to the
future Admin Service.
Trusted Surfaces
The internal REST surface is split into five stable groups:
AuthIntegration- resolve-by-email
- exists-by-user-id
- ensure-by-email
- block-by-user-id
- block-by-email
MyAccount- get account aggregate
- update profile
- update settings
LobbyIntegration- read synchronous eligibility snapshot
GeoIntegration- synchronize current effective
declared_country
- synchronize current effective
AdminUsers- lookups by
user_id, exact-after-trimemail, and exactrace_name - deterministic filtered listing
- explicit entitlement, sanction, and limit commands
- lookups by
The public authenticated gateway boundary currently exposes exactly three self-service message types:
user.account.getuser.profile.updateuser.settings.update
Externally these commands use authenticated gRPC plus FlatBuffers payloads. Internally gateway calls:
GET /api/v1/internal/users/{user_id}/accountPOST /api/v1/internal/users/{user_id}/profilePOST /api/v1/internal/users/{user_id}/settings
Gateway must derive user_id from authenticated session context only. The
client payload never carries user identity for this boundary.
Identity And Lookup Rules
- User IDs are opaque stable identifiers generated by
User Service. - New users receive generated default race names in
player-*form until the user replaces them. - E-mail semantics are exact-after-trim.
- The service trims surrounding whitespace.
- The service does not lowercase, canonicalize, or alias-normalize e-mail values.
- Exact lookup by e-mail uses the trimmed stored value.
- Race-name lookup is exact by stored value.
- Race-name uniqueness is not exact-string-only.
- Stored casing is preserved.
- Uniqueness is enforced by a canonical reservation key.
- The canonical policy is case-insensitive and includes the frozen anti-fraud confusable-pair rules used by the race-name policy adapter.
Auth-Facing Contract
Auth / Session Service depends on the following synchronous user-owned
decisions:
resolve-by-email- returns
creatable,existing, orblocked
- returns
ensure-by-email- returns
created,existing, orblocked
- returns
exists-by-user-id- supports trusted session revoke and block flows
- block operations
- support trusted auth-driven user or e-mail blocking flows
ensure-by-email rules:
registration_contextis required.- Its frozen shape is:
preferred_languagetime_zone
- The registration context is create-only.
- New users store the supplied values after semantic validation.
- Existing users ignore the registration context completely.
- Existing users must not have settings overwritten by a later auth flow.
- The current rollout source of truth is:
Auth / Session Serviceforwards the preferred-language candidate derived from publicAccept-Language- unsupported or missing public language input falls back to
en Auth / Session Serviceforwards the public confirmtime_zone- the create-only registration context remains unchanged for existing users
Auth-facing blocking semantics:
blockedmeans the auth flow must not create or return a usable session for that subject.send-email-codemay still remain success-shaped at the auth edge, butUser Serviceremains the source of truth for the blocked decision.
Self-Service Account Contract
Self-service reads and writes operate on one shared account aggregate:
- profile:
race_name
- settings:
preferred_languagetime_zone
- derived current state:
- entitlement snapshot
- active sanctions
- active limits
- current
declared_country
Self-service writes return the refreshed full account aggregate.
Forbidden self-service mutations:
- e-mail change
- direct
declared_countrychange - direct entitlement mutation
- direct sanction mutation
- direct limit mutation
Current write rules:
UpdateMyProfile- changes only
race_name - rejects unsupported or unknown fields
- returns the current aggregate unchanged on no-op rename
- changes only
UpdateMySettings- changes only
preferred_languageandtime_zone - rejects unsupported or unknown fields
- changes only
- active
profile_update_blocksanction blocks both profile and settings writes with409 conflict
Validation Rules
- trim surrounding whitespace
- validate as structurally valid e-mail
- keep the trimmed exact value
- do not lowercase or canonicalize
Race Name
- validate non-empty user-facing name
- preserve accepted casing in storage and reads
- enforce uniqueness through canonical reservation
- reject conflicts as
409 conflict
preferred_language
- validate as BCP 47 language tag
- store canonical BCP 47 tag form
- current auth-driven create path temporarily uses
"en"from authsession
time_zone
- validate as IANA time-zone name
- store trimmed value
- do not apply additional alias canonicalization
Entitlements
User Service owns the current effective entitlement snapshot.
Rules:
- every new user starts with the frozen free entitlement baseline
- explicit admin or later billing commands may:
- grant
- extend
- revoke
- finite paid entitlements are repaired lazily on read when expiry has passed
- downstream services read current entitlement from
User Service, not from billing or any write-side source
The shared account aggregate and lobby eligibility snapshot always expose the current effective entitlement after lazy expiry repair.
Sanctions And Limits
Sanctions and user-specific limits are explicit command-driven state.
Supported sanction codes:
login_blockprivate_game_create_blockprivate_game_manage_blockgame_join_blockprofile_update_block
Supported user-specific limit codes:
max_owned_private_gamesmax_pending_public_applicationsmax_active_game_memberships
Rules:
- active views expose only currently supported codes
- retired legacy limit codes may remain in stored history but are not part of the active read or write contract
- sanctions and limits are projected into:
- the self-service account aggregate
- admin reads
- lobby eligibility snapshots
Lobby Eligibility Semantics
Game Lobby depends on a synchronous read-optimized eligibility snapshot.
Rules:
- unknown users return
exists=falserather than404 - entitlement state is current and expiry-repaired
- active sanctions are filtered to the lobby-relevant subset
- effective limits are derived from:
- the frozen free or paid default catalog
- plus any active user-specific override
Current markers:
can_logincan_create_private_gamecan_manage_private_gamecan_join_gamecan_update_profile
declared_country Ownership Split
Ownership is intentionally split:
User Service- stores only the current effective
declared_countryvalue
- stores only the current effective
Geo Profile Service- owns review workflow
- owns decision history
- owns version history and retry state
User Service accepts only trusted sync commands from Geo Profile Service
for the latest approved effective value.
Sync rules:
- accepted values are uppercase ISO 3166-1 alpha-2 country codes
- syncing the already stored value is a no-op
- a successful change updates the current account record and emits a domain event
Admin Read And List Semantics
Trusted admin reads operate on regular-user state only.
Lookups:
- by
user_id - by exact-after-trim
email - by exact
race_name
Listing rules:
- deterministic order:
created_at desc- then
user_id desc
- all supplied filters combine with logical
AND page_tokenis opaque and bound to the normalized filter set that produced it- malformed or filter-mismatched tokens return
400 invalid_request
Listing filters include:
- paid/free state
- paid expiry bounds
- current
declared_country - active sanction code
- active limit code
- derived eligibility markers
Domain Events
User Service publishes auxiliary post-commit domain events to the shared
Redis stream configured for domain events.
Frozen event types:
user.profile.changeduser.settings.changeduser.entitlement.changeduser.sanction.changeduser.limit.changed
The current effective declared-country sync remains externally observable as
user.declared_country.changed.
Event rules:
- events are post-commit only
- event envelopes carry
user_id, mutation source, occurrence timestamp, and optional trace correlation - event payloads expose the latest committed state relevant to the operation
- profile and settings events use
initializedfor auth-driven creation andupdatedfor later self-service writes - entitlement events use:
initializedgrantedextendedrevokedexpired_repaired
- sanction events use:
appliedremoved
- limit events use:
setremoved
Error Model
The trusted internal REST contract uses strict JSON error envelopes:
{
"error": {
"code": "invalid_request",
"message": "request is invalid"
}
}
Stable error codes:
invalid_requestconflictsubject_not_foundinternal_errorservice_unavailable
Gateway mirrors these business errors on the authenticated user.* boundary
as:
- gateway
result_code - FlatBuffers error payload carrying the same
codeandmessage
Transport failures, timeouts, and upstream 503 remain transport-level
gateway UNAVAILABLE, not business results.