User Service
Context and Purpose
User Service is the internal source of truth for regular Galaxy Plus platform
users.
The service exists to solve six closely related problems:
- Own the durable platform user account identified by
user_id. - Store the current editable self-service profile and settings of a user.
- Materialize the current effective entitlement state used by the rest of the platform.
- Store user-specific sanctions and limit overrides that affect access decisions.
- Expose synchronous trusted APIs needed by
Auth / Session Service,Game Lobby,Geo Profile Service, and future administrative tooling. - Publish auxiliary user-domain change events without turning events into the source of truth.
The service is intentionally the owner of regular user identity only.
System-administrator identity is outside this service and belongs to the later
Admin Service architecture.
Explicit Non-Goals
The following are intentionally out of scope for this service:
- Authentication challenges, device sessions, or request-signing state.
- System-administrator identity or administrator role management.
- Ownership of game membership, invites, roster, or per-game moderation.
- Automatic billing computation or payment-provider integration in v1.
- History of
declared_countrychanges. - Geo-IP lookup or country-review workflow logic.
- Direct public unauthenticated exposure.
- Using async events as the authoritative representation of user state.
Place in the Existing Microservice System
User Service operates inside the trusted internal platform and integrates
with:
Edge GatewayAuth / Session ServiceGame LobbyGeo Profile ServiceAdmin ServicelaterBilling Servicelater- internal event bus
Edge Gateway routes authenticated user-facing account operations to this
service.
Auth / Session Service uses this service to resolve, create, and block users
during the public e-mail-code login flow.
Game Lobby uses this service for synchronous eligibility checks that depend
on current entitlement, sanctions, and limit state.
Geo Profile Service remains the owner of country-change workflow and history,
but synchronizes the latest effective declared_country into this service.
Admin Service will later use the trusted internal read and mutation APIs
defined here. Administrator accounts themselves still do not belong to
User Service.
Billing Service is future-only in v1 and will later feed entitlement
outcomes through the trusted entitlement mutation path defined here.
The event bus is an auxiliary propagation channel and not the source of truth.
Responsibility Boundaries
User Service owns:
- regular platform
user_id - login/contact e-mail stored on the user account
race_namepreferred_languagetime_zone- current effective
declared_country - current effective entitlement snapshot
- entitlement history records
- active and historical user sanctions
- active and historical user-specific limit overrides
- blocked e-mail subjects that may exist before any user record exists
- synchronous trusted reads used for auth, lobby, geo, and admin workflows
- auxiliary user-domain events
User Service does not own:
- system-admin accounts
device_sessionor revoke state- full payment history
- game ownership, game membership, or game-level bans
declared_countryhistory or approval workflow- per-request country observations
High-Level Architecture
flowchart LR
Gateway[Edge Gateway]
Auth[Auth / Session Service]
Lobby[Game Lobby]
Geo[Geo Profile Service]
Admin[Admin Service]
Billing[Billing Service]
User[User Service]
Redis[Redis]
Bus[Event Bus]
Gateway --> User
Auth --> User
Lobby --> User
Geo --> User
Admin --> User
Billing --> User
User --> Redis
User --> Bus
Semantic Model
The service works with several core concepts.
User Account
The user account is the canonical regular-user aggregate.
Required logical fields:
user_id- normalized
email race_namepreferred_languagetime_zone- current effective
declared_country - creation timestamp
- last update timestamp
Important rules:
emailis the primary login/contact identifier for end users.emailis not directly editable through self-service profile updates.- future e-mail change is a separate confirm-based workflow and is not part of
v1
User Servicemutations. declared_countryis readable in account views but writable only through the trusted geo sync path.
race_name
race_name is the user-facing account name.
Properties:
- It is globally unique.
- It is not an identity key. Internal identity remains
user_id, and end-user login identity remainsemail. - It is stored and returned in the original user-provided casing.
- Uniqueness is enforced through a dedicated policy boundary rather than by naive string equality.
The uniqueness policy must at minimum:
- compare case-insensitively
- reject common confusable substitutions used for impersonation, such as
Iversus1,Oversus0, andBversus8 - remain replaceable behind a dedicated interface because a future shared name catalog service is expected
preferred_language and time_zone
preferred_language and time_zone are explicit user settings, not inferred
runtime facts.
Properties:
preferred_languageuses BCP 47 language tags.time_zoneuses IANA time zone names.- both values exist on every created user in v1
- both values are editable later through self-service settings mutation
Initial creation rules:
preferred_languageis supplied toUser Servicethrough auth create-only registration context- the value is derived by
Edge Gatewayfrom local geoip country plus local country-to-language mapping - when that lookup cannot determine a language, gateway falls back to
en time_zoneis supplied by the client in publicconfirm-email-code
User Service does not perform its own geo lookup for this purpose.
declared_country
declared_country is the latest effective user-declared country.
Properties:
- It uses ISO 3166-1 alpha-2.
- It is the read-optimized current value only.
- It is owned for storage by
User Service. - It is owned for mutation workflow and history by
Geo Profile Service.
This split is intentional:
- reads of current account state go to
User Service - reads of review workflow and country history go to
Geo Profile Service
Entitlement
Entitlement describes the paid/free access state of a user account.
The plan catalog fixed for v1 is:
freepaid_monthlypaid_yearlypaid_lifetime
The service stores both:
- immutable or append-only entitlement period history records
- a materialized current effective entitlement snapshot for synchronous reads
The current effective snapshot is not computed on every request. It is updated when trusted entitlement mutations succeed.
Period history records store:
user_idplan_codesource- actor identity or actor type
reason_codestarts_at- optional
ends_at - creation timestamp
Current effective snapshot stores at minimum:
user_id- current
plan_code - effective paid/free state
- effective period bounds when applicable
- source metadata needed by operations and admin reads
- last recomputation timestamp
In v1, entitlement mutations come from explicit trusted admin/internal
commands. Later, Billing Service uses the same mutation path.
Sanctions
Sanctions are negative policy records that may deny or restrict access regardless of entitlement state.
The initial sanction set for v1 is:
login_blockprivate_game_create_blockprivate_game_manage_blockgame_join_blockprofile_update_block
Each sanction record stores:
user_idsanction_code- scope
reason_code- actor identity or actor type
applied_at- optional
expires_at - optional removal metadata if later removed
Sanctions are typed records rather than inline booleans so the service can keep auditability and deterministic active-state evaluation.
User-Specific Limits
User-specific limits are count-based override records that shape access and eligibility decisions.
The initial limit set for v1 is:
max_owned_private_gamesmax_active_private_gamesmax_pending_public_applicationsmax_pending_private_join_requestsmax_pending_private_invites_sentmax_active_game_memberships
Each limit record stores:
user_idlimit_code- numeric value
reason_code- actor identity or actor type
applied_at- optional
expires_at - optional removal metadata if later removed
Limit rules:
- limits are count-based only in v1
- limits are user-specific overrides, not the global default catalog itself
- effective eligibility combines entitlement-derived defaults with active user-specific overrides
Blocked E-Mail Subject
User Service must support blocking an e-mail subject before any user account
exists.
This requires a separate blocked-email-subject model.
Required logical fields:
- normalized
email reason_code- block timestamp
- optional actor metadata when available
- optional expiry or removal metadata if policy later requires it
- optional resolved
user_idwhen the e-mail already belongs to an existing user
This model exists to support Auth / Session Service flows such as
BlockByEmail and ResolveByEmail before user creation.
Data Ownership Rules
The ownership split is intentional and must remain stable.
User Serviceowns regular user identity and current effective account state.Auth / Session Serviceowns login challenge and session lifecycle state.Game Lobbyowns game membership and game-specific moderation.Geo Profile Serviceowns geo workflow anddeclared_countryhistory.Admin Servicelater owns administrator identity and UI orchestration.
In particular:
- no service other than
Geo Profile Serviceshould mutatedeclared_country - no service other than
User Serviceshould create or edit regular user profile/settings records - no other service should maintain its own source of truth for current entitlements
User-Facing Interface Model
User-facing traffic reaches User Service only through authenticated gateway
routing.
The v1 aggregate query is:
GetMyAccount
The v1 self-service mutations are:
UpdateMyProfileUpdateMySettings
GetMyAccount
GetMyAccount returns one read-optimized account aggregate for the currently
authenticated regular user.
The aggregate should include at minimum:
user_idemailrace_namepreferred_languagetime_zone- current
declared_country - current entitlement snapshot
- active sanctions
- active effective limits
- account timestamps needed by clients
declared_country is read-only in this aggregate.
UpdateMyProfile
UpdateMyProfile updates self-service profile fields only.
Editable fields in v1:
race_name
Rules:
- e-mail cannot be changed here
declared_countrycannot be changed hererace_namemust pass global uniqueness policy before commit- active
profile_update_blocksanction rejects the mutation
UpdateMySettings
UpdateMySettings updates self-service settings only.
Editable fields in v1:
preferred_languagetime_zone
Rules:
- values are validated as BCP 47 and IANA formats
- active
profile_update_blocksanction rejects the mutation
Trusted Internal API Model
All service-to-service integration in v1 is documented as trusted JSON REST.
Auth-Facing Contract
The auth-facing contract is already reserved by Auth / Session Service and
must remain stable.
Frozen endpoints:
POST /api/v1/internal/user-resolutions/by-emailGET /api/v1/internal/users/{user_id}/existsPOST /api/v1/internal/users/ensure-by-emailPOST /api/v1/internal/users/{user_id}/blockPOST /api/v1/internal/user-blocks/by-email
Auth-facing behavior:
- resolve by e-mail returns
existing,creatable, orblocked - ensure by e-mail returns
existing,created, orblocked - block by user id and block by e-mail are idempotent
- blocked e-mail subjects are respected even when no user exists yet
EnsureUserByEmail is extended for v1 user creation context.
Recommended request shape:
{
"email": "pilot@example.com",
"registration_context": {
"preferred_language": "en",
"time_zone": "Europe/Berlin"
}
}
Rules for registration_context:
- it is used only when the user is created
- it is ignored for an existing user
- it must not overwrite settings of an existing user
- it is required by the future auth contract because first successful confirm may create the user
Lobby-Facing Eligibility Snapshot
Game Lobby needs one synchronous query by user_id.
Purpose:
- determine whether the user currently may create or join game flows
- obtain effective quotas relevant to lobby decisions
The response should include at minimum:
- whether the user exists
- current entitlement snapshot
- active sanctions relevant to lobby actions
- effective limit values relevant to lobby actions
- derived booleans such as whether private-game creation is currently allowed
This query is intentionally one read-optimized snapshot rather than multiple smaller cross-service round trips.
Geo-Facing Declared Country Sync
Geo Profile Service needs one explicit trusted command to synchronize the
current effective declared_country.
Required behavior:
- update only the current
declared_countryvalue inUser Service - not create or manage country history here
- fail explicitly on unknown
user_id - remain synchronous so geo workflow can decide whether its own version record becomes effective
Admin/Internal Reads
The trusted admin/internal read surface must support:
- exact lookup by
user_id - exact lookup by normalized
email - exact lookup by exact
race_name - paginated listing with filters
The v1 listing filters must support at minimum:
- paid/free state
- paid expiry window
- current
declared_country - active sanction code
- active limit code
- relevant eligibility markers
Listing must use deterministic pagination and stable ordering.
Recommended default ordering is newest first by created_at, with user_id
used as the deterministic tiebreaker.
Admin/Internal Mutations
Trusted mutations remain explicit-command based.
The minimum command vocabulary in v1 is:
- grant paid access
- extend paid access
- revoke paid access
- apply sanction
- remove sanction
- set limit
- remove limit
- sync declared country
These are intentionally explicit commands rather than one generic patch API. The service should preserve reason and actor metadata on every trusted administrative mutation.
New User Creation Flow
sequenceDiagram
participant Client
participant Gateway
participant Auth as Auth / Session Service
participant User as User Service
Client->>Gateway: confirm-email-code(code, client_public_key, time_zone)
Gateway->>Gateway: local geoip lookup and country-to-language mapping
Gateway->>Auth: confirm-email-code(..., time_zone)
Auth->>User: ensure-by-email(email, registration_context)
alt user already exists
User-->>Auth: existing user_id
else new user
User->>User: create user with generated race_name
User->>User: initialize language, time zone, free entitlement
User-->>Auth: created user_id
end
Auth-->>Gateway: device_session_id
Gateway-->>Client: device_session_id
New-user defaults:
- generated
race_nameinplayer-<shortid>form preferred_languagefrom gateway-derived registration contexttime_zonefrom client-provided registration contextfreeentitlement- no active sanctions
- no custom limit overrides
Interface Between Entitlement, Sanction, and Limit Evaluation
The service must keep these three layers separate.
- entitlement provides the base paid/free access state
- sanctions can deny actions regardless of entitlement
- user-specific limits can narrow or override numeric quotas
This means:
- a paid user may still be denied private-game creation by sanction
- a non-blocked user may still be quota-limited by effective count limits
- lobby checks should consume one effective snapshot rather than reimplementing this evaluation itself
Events
Events are auxiliary notifications only. They are not the source of truth.
The service should emit per-domain-area events for:
- profile changes
- settings changes
- entitlement changes
- sanction changes
- limit changes
- declared-country changes
Recommended event classes:
user.profile.changeduser.settings.changeduser.entitlement.changeduser.sanction.changeduser.limit.changeduser.declared_country.changed
Each event should include at minimum:
user_id- event timestamp
- mutation source
- correlation or request metadata when available
- enough event-specific detail to identify the changed domain area
Loss of an event must not lose the authoritative business state.
Data Entities
This section defines the core logical entities. These are domain entities, not mandatory final physical Redis key names.
User Account Record
Required logical fields:
user_id- normalized
email race_namepreferred_languagetime_zone- current
declared_country created_atupdated_at
race_name Reservation
Required logical fields:
- canonical uniqueness key produced by the race-name policy
- referenced
user_id - original stored
race_name - reservation timestamp
This entity exists so uniqueness policy stays replaceable and explicit.
Blocked E-Mail Subject Entity
Required logical fields:
- normalized
email reason_code- block status
blocked_at- optional resolved
user_id
Entitlement Period Record
Required logical fields:
user_idplan_codesource- actor metadata
reason_codestarts_at- optional
ends_at - record creation timestamp
Current Entitlement Snapshot
Required logical fields:
user_id- effective
plan_code - paid/free state
- effective period bounds
- source metadata
- snapshot update timestamp
Sanction Record
Required logical fields:
user_idsanction_code- scope
reason_code- actor metadata
applied_at- optional
expires_at - current status metadata
Limit Record
Required logical fields:
user_idlimit_code- numeric value
reason_code- actor metadata
applied_at- optional
expires_at - current status metadata
Failure and Degradation Model
The service is synchronous for critical reads and mutations.
Auth Dependency Failure
If User Service is unavailable during auth flows:
Auth / Session Servicemust fail the affected operation explicitly- no user should be created partially without source-of-truth persistence
Event Publication Failure
If event publication fails:
- the source-of-truth mutation still remains committed
- failure is logged and metered
- downstream consumers recover from direct reads if needed
race_name Uniqueness Backend Failure
If the dedicated race-name uniqueness policy backend fails:
- self-service profile update must fail closed
- new-user creation must fail explicitly rather than create ambiguous names
Geo Sync Failure
If Geo Profile Service cannot synchronize declared_country into
User Service:
- geo must treat the change as not yet effective
User Servicemust not create hidden partial country history
Minimal Initial API Surface
The minimum useful v1 API surface is:
- gateway-routed authenticated:
GetMyAccountUpdateMyProfileUpdateMySettings
- trusted internal auth:
- resolve by e-mail
- ensure by e-mail
- exists by user id
- block by user id
- block by e-mail
- trusted internal lobby:
- get user eligibility snapshot
- trusted internal geo:
- sync current
declared_country
- sync current
- trusted internal admin:
- exact user reads
- filtered user listing
- entitlement mutations
- sanction mutations
- limit mutations
Cross-Service Follow-Up Dependencies
The service design here depends on later follow-up work in other modules.
Required follow-up items:
Edge Gatewaypublicconfirm-email-codecontract must add requiredtime_zone.Auth / Session Servicepublic OpenAPI must mirror the sametime_zoneaddition.Auth / Session Service -> User Serviceensure-by-email contract must add create-only registration context withpreferred_languageandtime_zone.- a shared
pkg/geoippackage must be introduced forEdge GatewayandGeo Profile Service gateway/README.mdshould later document the local geoip dependency used for initial language derivationgeoprofile/README.mdshould later document the sharedpkg/geoipdependency explicitly alongside its own local geo lookup
Design Trade-Offs Accepted by This Architecture
- Current entitlement is materialized for fast reads instead of computed from history on each request.
- User-specific limits are count-based only in v1 to keep evaluation simple.
race_nameuniqueness is stricter than plain case-insensitive comparison to reduce impersonation risk.User Servicestores only the latest effectivedeclared_countrywhile geo owns the workflow and version history.- Explicit trusted commands are preferred over generic patch semantics so administrative changes remain auditable and predictable.
Implementation Readiness Statement
This service specification is intended to be implementation-ready for a first production-capable internal version.
The main remaining work is not product ambiguity inside User Service, but
follow-up cross-service contract changes in gateway, authsession, and the
future shared pkg/geoip package.