Files
galaxy-game/user/README.md
T
2026-04-09 09:00:06 +02:00

22 KiB

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

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:

{
  "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

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-<shortid> 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.