# Flows
This document collects the eight platform flows that span Game Lobby plus
its synchronous and asynchronous neighbours. Narrative descriptions of the
rules these flows enforce live in `../README.md`; the diagrams here focus on
the message order across the boundary.
## Public Game Application
```mermaid
sequenceDiagram
participant User
participant Gateway
participant Lobby as Lobby publichttp
participant UserSvc as User Service
participant Redis
participant Stream as notification:intents
User->>Gateway: lobby.application.submit(game_id, race_name)
Gateway->>Lobby: POST /api/v1/lobby/games/{id}/applications + X-User-ID
Lobby->>UserSvc: GetEligibility(user_id)
UserSvc-->>Lobby: snapshot (entitlement, sanctions)
Lobby->>Redis: persist Application(submitted) + indexes
Lobby->>Stream: lobby.application.submitted (admin recipients)
Lobby-->>Gateway: 200 ApplicationRecord
```
Approval and rejection follow the same pattern, mutating the application
status to `approved`/`rejected` and emitting
`lobby.membership.approved`/`lobby.membership.rejected` to the applicant.
## Private Game Invite
```mermaid
sequenceDiagram
participant Owner
participant Invitee
participant Lobby
participant Redis
participant Stream as notification:intents
Owner->>Lobby: lobby.invite.create(invitee_user_id)
Lobby->>Redis: persist Invite(created)
Lobby->>Stream: lobby.invite.created (recipient: invitee)
Invitee->>Lobby: lobby.invite.redeem(race_name)
Lobby->>Lobby: User Service guard for inviter and invitee
Lobby->>Redis: RND.Reserve + Membership(active) + Invite(redeemed)
Lobby->>Stream: lobby.invite.redeemed (recipient: owner)
```
The owner-facing decline and revoke transitions persist the invite status
update and produce no notification in v1.
## Enrollment Automation
```mermaid
sequenceDiagram
participant Tick as Worker tick
participant Lobby
participant Redis
participant Stream as notification:intents
Tick->>Lobby: enrollment automation cycle
Lobby->>Redis: load enrollment_open games + roster sizes
alt deadline reached or gap exhausted
Lobby->>Redis: status enrollment_open → ready_to_start (CAS)
Lobby->>Redis: pending invites → expired
Lobby->>Stream: lobby.invite.expired (per expired invite)
else still within window
Lobby-->>Tick: no-op
end
```
Manual `lobby.game.ready_to_start` from owner or admin runs the same close
pipeline synchronously without waiting for the next tick.
## Game Start (happy path)
```mermaid
sequenceDiagram
participant Actor as Owner or Admin
participant Lobby
participant Redis
participant RT as Runtime Manager
participant GM as Game Master
Actor->>Lobby: lobby.game.start
Lobby->>Redis: status ready_to_start → starting (CAS)
Lobby->>Redis: XADD runtime:start_jobs
RT->>Redis: XADD runtime:job_results (success + container metadata)
Lobby->>Redis: persist runtime_binding on game record
Lobby->>GM: POST /internal/games/{id}/register-runtime
GM-->>Lobby: 200 OK
Lobby->>Redis: status starting → running; set started_at
```
If runtime metadata persistence fails, Lobby publishes a stop-job to remove
the orphan container before flipping the game to `start_failed`.
## Game Start (GM unavailable)
```mermaid
sequenceDiagram
participant Lobby
participant Redis
participant GM as Game Master
participant Stream as notification:intents
Lobby->>GM: POST /internal/games/{id}/register-runtime
GM-->>Lobby: timeout / 5xx
Lobby->>Redis: status starting → paused (CAS)
Lobby->>Stream: lobby.runtime_paused_after_start (admin)
Note over Lobby,GM: Container stays alive; admin restarts GM
and issues lobby.game.resume.
```
## Game Finish + Capability Evaluation
```mermaid
sequenceDiagram
participant GM as Game Master
participant Stream as gm:lobby_events
participant Lobby
participant Redis
participant Intents as notification:intents
GM->>Stream: XADD runtime_snapshot_update (player_turn_stats)
Lobby->>Redis: UpdateMax for each member's stats aggregate
GM->>Stream: XADD game_finished
Lobby->>Redis: status running/paused → finished; finished_at = event_ts
Lobby->>Redis: capability evaluator runs per active membership
alt member capable
Lobby->>Redis: RND.MarkPendingRegistration(eligible_until = finished_at + 30d)
Lobby->>Intents: lobby.race_name.registration_eligible (recipient: user)
else not capable
Lobby->>Redis: RND.ReleaseReservation
Lobby->>Intents: lobby.race_name.registration_denied (optional)
end
Lobby->>Redis: ReleaseReservation for removed/blocked memberships
Lobby->>Redis: delete per-game stats aggregate
```
The evaluation guard `lobby:capability_evaluation:done:` makes a
replayed `game_finished` event a no-op.
## Race Name Registration
```mermaid
sequenceDiagram
participant User
participant Lobby
participant UserSvc as User Service
participant RND as Race Name Directory
participant Stream as notification:intents
User->>Lobby: lobby.race_name.register(race_name)
Lobby->>UserSvc: GetEligibility (sanctions, max_registered_race_names)
UserSvc-->>Lobby: snapshot
Lobby->>RND: Register(game_id, user_id, race_name)
RND-->>Lobby: ok / ErrPendingExpired / ErrQuotaExceeded
alt success
Lobby->>Stream: lobby.race_name.registered (recipient: user)
Lobby-->>User: 200 RegisteredRaceName
else precondition failure
Lobby-->>User: 422 DomainPreconditionError
end
```
Registration consumes one tariff slot keyed by `(canonical_key, user_id)`;
tariff downgrade never revokes existing registrations.
## Cascade Release on User Lifecycle Event
```mermaid
sequenceDiagram
participant US as User Service
participant Stream as user:lifecycle_events
participant Lobby
participant RT as Runtime Manager
participant Intents as notification:intents
US->>Stream: XADD permanent_blocked or deleted
Lobby->>Stream: XREAD (consumer)
Lobby->>Lobby: RND.ReleaseAllByUser
Lobby->>Lobby: memberships → blocked + lobby.membership.blocked per private game
Lobby->>Lobby: applications → rejected
Lobby->>Lobby: invites (addressed and inviter-side) → revoked
Lobby->>Lobby: owned non-terminal games → cancelled (external_block trigger)
Lobby->>RT: XADD runtime:stop_jobs for in-flight owned games
Lobby->>Intents: lobby.membership.blocked per affected membership
Lobby->>Stream: advance offset
```
Every step is idempotent at the store layer (`ErrConflict` from a CAS is
treated as «already done»); the consumer only advances the offset once the
handler returns nil.