Skip to content

Region Invite Onramp

Status: 🚧 Partial — region OWNER mint/list/revoke is shipped (RegionInvitePanel + gameserver regional-governance routes). Gameserver POST /auth/register accepts optional invite_code (auth.py). Player-visible signup residual on tip 46bce720: JoinInviteLanding.tsx ABSENT; RegisterForm.tsx has no invite field; AuthContext.register / registerWithOAuth do not send invite_code. Open PR #746 codes /join + RegisterForm invite_code but is Soft-HOLD behind Max GO (board #901/#546 GATED; do not treat as tip-landable or as verified LEG-DEC-52). Do not read the owner-mint path as end-to-end invite signup live. (re-verified 2026-08-21 vs Sectorwars2102 46bce720.)

This is the invite-link path of the region-onramp problem: without it, a new player only ever lands in the operator-run Terran Space starter region. An invite code lets a region owner pull new players directly into their own region instead.

Lifecycle

  1. Mint — region owner calls POST /{region_id}/invites (owner-only, ownership re-checked server-side every call). Returns a high-entropy code (secrets.token_urlsafe(16)). max_uses defaults to 1 (one-time link); expires_at defaults to now + 7 days — there is no infinitely-reusable invite.
  2. List / manageGET /{region_id}/invites (owner-scoped, newest first, includes the code) and POST /{region_id}/invites/{invite_id}/revoke (owner-scoped, idempotent — revoking an already-revoked invite is a success, not an error).
  3. Redeem at signup — gameserver password signup accepts optional invite_code on POST /auth/register and can lock/validate (lock_and_validate_invite) inside the signup transaction. UI residual: the player-client registration form does not collect or POST that field; OAuth registerWithOAuth does not thread it. Until the form is wired, a new player cannot supply a code through the shipped signup UI even though the GS field exists.
  4. Redemption auditRegionInviteRedemption row written with hashed IP + device fingerprint (never raw), feeding the future ADR-0056 multi-account clustering.

Redeemability rules

An invite is redeemable only if all hold at redeem time (re-checked under a row lock, not just at mint time): - status == 'active' - uses < max_uses - now() < expires_at - the target region still exists - the minting owner still owns the region (re-checked — an ownership transfer after minting invalidates outstanding invites for the new redeem, not retroactively)

Status transitions: activeexhausted (uses hit max_uses) / revoked (owner-initiated) / expired (TTL passed). All auth-free — minting/listing/revoking never touches account creation; only the signup-path redemption (Max-gated, WO-IL6) actually creates a player and grants citizenship.

Schema

RegionInvite (region_invites): id, code (unique, indexed), region_id (FK → regions.id, CASCADE), created_by (FK → users.id, SET NULL), max_uses (default 1), uses (default 0), expires_at (mandatory, no default), status (active/exhausted/revoked/expired, string-enum-in-string), created_at, revoked_at. DB-level check constraints enforce the status vocabulary and uses <= max_uses.

RegionInviteRedemption (region_invite_redemptions): id, invite_id (FK, CASCADE), redeemed_by_player_id (FK → players.id, SET NULL, nullable — set after the player row exists in the same transaction), redeemed_at, ip_hash, device_fingerprint_hash (both hashed-only, never raw).

Source: services/gameserver/src/models/region_invite.py.

Endpoints

Route Auth Purpose
POST /{region_id}/invites Region owner Mint a new invite code
GET /{region_id}/invites Region owner List invites minted for this region
POST /{region_id}/invites/{invite_id}/revoke Region owner Revoke (idempotent)
POST /auth/register (invite_code optional field) Anonymous (signup) GS redeem at password signup — player RegisterForm does not send this yet
OAuth callback path (invite_code threaded through) Anonymous (signup) GS can redeem — player OAuth register does not thread invite_code yet

Source: services/gameserver/src/api/routes/regional_governance.py (mint/list/revoke), services/gameserver/src/api/routes/auth.py + services/gameserver/src/auth/oauth.py (redemption at signup).

Player-facing affordances

  • Region owner invite managementRegionInvitePanel.tsx mounted from RegionOwnerControls.tsx; mint / list / revoke against POST/GET /{region_id}/invites (origin/feat 46bce720; join-URL builder only — not a /join landing). Tests: RegionInvitePanel.test.tsx.
  • 🚧 Signup invite-code redemption — gameserver POST /auth/register accepts optional invite_code (auth.py + lock_and_validate_invite); player-client on tip 46bce720 has no invite pathJoinInviteLanding.tsx ABSENT; RegisterForm.tsx / AuthContext.register / registerWithOAuth do not thread invite_code (git grep empty). PR #746 Soft-HOLD behind Max GO (GATED #901/#546; LEG-DEC-52 unverified this seat) — not tip-landable. Do not read end-to-end invite-link signup as player-live.

Source map

Concern Target path
Owner invite panel services/player-client/src/components/governance/RegionInvitePanel.tsx
Owner controls mount services/player-client/src/components/governance/RegionOwnerControls.tsx
Invite panel tests services/player-client/src/components/governance/__tests__/RegionInvitePanel.test.tsx
/join landing (tip-absent) JoinInviteLanding.tsxABSENT on tip 46bce720; in-flight Soft-HOLD PR #746 behind Max GO (not tip-landable)
Mint / list / revoke routes services/gameserver/src/api/routes/regional_governance.py
Signup redeem field services/gameserver/src/api/routes/auth.py (invite_code on register) — PC RegisterForm / AuthContext still zero threading on tip
Invite models services/gameserver/src/models/region_invite.py

Design provenance

Design brief: audit/design-briefs/invite-link-onramp.md. Note: the model's own docstring cites a DECISIONS.md:473-475 line reference that no longer resolves against the current DECISIONS.md (396 lines total as of this writing) — likely stale after a prior file restructure. Flagging rather than propagating the dead citation into this page; the design brief itself is the reliable source.

  • ADR-0056 — multi-account detection, the eventual consumer of the hashed IP/fingerprint columns.
  • ../DATA_MODELS/migrations.md — migration table entry.