Files
galaxy-game/user
2026-04-17 18:39:16 +02:00
..
2026-04-10 19:05:02 +02:00
2026-04-17 18:39:16 +02:00
2026-04-10 19:05:02 +02:00
2026-04-17 18:39:16 +02:00
2026-04-17 18:39:16 +02:00
2026-04-10 19:05:02 +02:00
2026-04-10 19:05:02 +02:00
2026-04-10 19:05:02 +02:00
2026-04-17 18:39:16 +02:00
2026-04-10 19:05:02 +02:00

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:

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