# 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` - `AdminUsers` - lookups by `user_id`, exact-after-trim `email`, and exact `race_name` - deterministic filtered listing - explicit entitlement, sanction, and limit commands The public authenticated gateway boundary currently exposes exactly three self-service message types: - `user.account.get` - `user.profile.update` - `user.settings.update` Externally these commands use authenticated gRPC plus FlatBuffers payloads. Internally gateway calls: - `GET /api/v1/internal/users/{user_id}/account` - `POST /api/v1/internal/users/{user_id}/profile` - `POST /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`, or `blocked` - `ensure-by-email` - returns `created`, `existing`, or `blocked` - `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_context` is required. - Its frozen shape is: - `preferred_language` - `time_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 Service` forwards the preferred-language candidate derived from public `Accept-Language` - unsupported or missing public language input falls back to `en` - `Auth / Session Service` forwards the public confirm `time_zone` - the create-only registration context remains unchanged for existing users Auth-facing blocking semantics: - `blocked` means the auth flow must not create or return a usable session for that subject. - `send-email-code` may still remain success-shaped at the auth edge, but `User Service` remains 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_language` - `time_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_country` change - 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 - `UpdateMySettings` - changes only `preferred_language` and `time_zone` - rejects unsupported or unknown fields - active `profile_update_block` sanction blocks both profile and settings writes with `409 conflict` ## Validation Rules ### E-mail - 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_block` - `private_game_create_block` - `private_game_manage_block` - `game_join_block` - `profile_update_block` Supported user-specific limit codes: - `max_owned_private_games` - `max_pending_public_applications` - `max_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=false` rather than `404` - 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_login` - `can_create_private_game` - `can_manage_private_game` - `can_join_game` - `can_update_profile` ## declared_country Ownership Split Ownership is intentionally split: - `User Service` - stores only the current effective `declared_country` value - `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_token` is 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.changed` - `user.settings.changed` - `user.entitlement.changed` - `user.sanction.changed` - `user.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 `initialized` for auth-driven creation and `updated` for later self-service writes - entitlement events use: - `initialized` - `granted` - `extended` - `revoked` - `expired_repaired` - sanction events use: - `applied` - `removed` - limit events use: - `set` - `removed` ## Error Model The trusted internal REST contract uses strict JSON error envelopes: ```json { "error": { "code": "invalid_request", "message": "request is invalid" } } ``` Stable error codes: - `invalid_request` - `conflict` - `subject_not_found` - `internal_error` - `service_unavailable` Gateway mirrors these business errors on the authenticated `user.*` boundary as: - gateway `result_code` - FlatBuffers error payload carrying the same `code` and `message` Transport failures, timeouts, and upstream `503` remain transport-level gateway `UNAVAILABLE`, not business results. ## References - [Internal REST contract](openapi.yaml) - [Service docs index](docs/README.md) - [System architecture](../ARCHITECTURE.md)