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

802 lines
22 KiB
Markdown

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