# 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_country` changes. - 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 Gateway` - `Auth / Session Service` - `Game Lobby` - `Geo Profile Service` - `Admin Service` later - `Billing Service` later - 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_name` - `preferred_language` - `time_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_session` or revoke state - full payment history - game ownership, game membership, or game-level bans - `declared_country` history or approval workflow - per-request country observations ## High-Level Architecture ```mermaid 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_name` - `preferred_language` - `time_zone` - current effective `declared_country` - creation timestamp - last update timestamp Important rules: - `email` is the primary login/contact identifier for end users. - `email` is 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 Service` mutations. - `declared_country` is 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 remains `email`. - 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 `I` versus `1`, `O` versus `0`, and `B` versus `8` - 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_language` uses BCP 47 language tags. - `time_zone` uses 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_language` is supplied to `User Service` through auth create-only registration context - the value is derived by `Edge Gateway` from local geoip country plus local country-to-language mapping - when that lookup cannot determine a language, gateway falls back to `en` - `time_zone` is supplied by the client in public `confirm-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: - `free` - `paid_monthly` - `paid_yearly` - `paid_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_id` - `plan_code` - `source` - actor identity or actor type - `reason_code` - `starts_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_block` - `private_game_create_block` - `private_game_manage_block` - `game_join_block` - `profile_update_block` Each sanction record stores: - `user_id` - `sanction_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_games` - `max_active_private_games` - `max_pending_public_applications` - `max_pending_private_join_requests` - `max_pending_private_invites_sent` - `max_active_game_memberships` Each limit record stores: - `user_id` - `limit_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_id` when 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 Service` owns regular user identity and current effective account state. - `Auth / Session Service` owns login challenge and session lifecycle state. - `Game Lobby` owns game membership and game-specific moderation. - `Geo Profile Service` owns geo workflow and `declared_country` history. - `Admin Service` later owns administrator identity and UI orchestration. In particular: - no service other than `Geo Profile Service` should mutate `declared_country` - no service other than `User Service` should 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: - `UpdateMyProfile` - `UpdateMySettings` ### GetMyAccount `GetMyAccount` returns one read-optimized account aggregate for the currently authenticated regular user. The aggregate should include at minimum: - `user_id` - `email` - `race_name` - `preferred_language` - `time_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_country` cannot be changed here - `race_name` must pass global uniqueness policy before commit - active `profile_update_block` sanction rejects the mutation ### UpdateMySettings `UpdateMySettings` updates self-service settings only. Editable fields in v1: - `preferred_language` - `time_zone` Rules: - values are validated as BCP 47 and IANA formats - active `profile_update_block` sanction 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-email` - `GET /api/v1/internal/users/{user_id}/exists` - `POST /api/v1/internal/users/ensure-by-email` - `POST /api/v1/internal/users/{user_id}/block` - `POST /api/v1/internal/user-blocks/by-email` Auth-facing behavior: - resolve by e-mail returns `existing`, `creatable`, or `blocked` - ensure by e-mail returns `existing`, `created`, or `blocked` - 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: ```json { "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_country` value in `User 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 ```mermaid 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_name` in `player-` form - `preferred_language` from gateway-derived registration context - `time_zone` from client-provided registration context - `free` entitlement - 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.changed` - `user.settings.changed` - `user.entitlement.changed` - `user.sanction.changed` - `user.limit.changed` - `user.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_name` - `preferred_language` - `time_zone` - current `declared_country` - `created_at` - `updated_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_id` - `plan_code` - `source` - actor metadata - `reason_code` - `starts_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_id` - `sanction_code` - scope - `reason_code` - actor metadata - `applied_at` - optional `expires_at` - current status metadata ### Limit Record Required logical fields: - `user_id` - `limit_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 Service` must 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 Service` must not create hidden partial country history ## Minimal Initial API Surface The minimum useful v1 API surface is: - gateway-routed authenticated: - `GetMyAccount` - `UpdateMyProfile` - `UpdateMySettings` - 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` - 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 Gateway` public `confirm-email-code` contract must add required `time_zone`. - `Auth / Session Service` public OpenAPI must mirror the same `time_zone` addition. - `Auth / Session Service -> User Service` ensure-by-email contract must add create-only registration context with `preferred_language` and `time_zone`. - a shared `pkg/geoip` package must be introduced for `Edge Gateway` and `Geo Profile Service` - `gateway/README.md` should later document the local geoip dependency used for initial language derivation - `geoprofile/README.md` should later document the shared `pkg/geoip` dependency 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_name` uniqueness is stricter than plain case-insensitive comparison to reduce impersonation risk. - `User Service` stores only the latest effective `declared_country` while 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.