Region Invite Onramp¶
Status: 🚧 Partial — region OWNER mint/list/revoke is shipped (
RegionInvitePanel+ gameserver regional-governance routes). GameserverPOST /auth/registeraccepts optionalinvite_code(auth.py). Player-visible signup residual on tip46bce720:JoinInviteLanding.tsxABSENT;RegisterForm.tsxhas no invite field;AuthContext.register/registerWithOAuthdo not sendinvite_code. Open PR #746 codes/join+ RegisterForminvite_codebut is Soft-HOLD behind Max GO (board#901/#546GATED; 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 Sectorwars210246bce720.)
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¶
- Mint — region owner calls
POST /{region_id}/invites(owner-only, ownership re-checked server-side every call). Returns a high-entropycode(secrets.token_urlsafe(16)).max_usesdefaults to 1 (one-time link);expires_atdefaults to now + 7 days — there is no infinitely-reusable invite. - List / manage —
GET /{region_id}/invites(owner-scoped, newest first, includes the code) andPOST /{region_id}/invites/{invite_id}/revoke(owner-scoped, idempotent — revoking an already-revoked invite is a success, not an error). - Redeem at signup — gameserver password signup accepts optional
invite_codeonPOST /auth/registerand 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; OAuthregisterWithOAuthdoes 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. - Redemption audit —
RegionInviteRedemptionrow 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: active → exhausted (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 management —
RegionInvitePanel.tsxmounted fromRegionOwnerControls.tsx; mint / list / revoke againstPOST/GET /{region_id}/invites(origin/feat46bce720; join-URL builder only — not a/joinlanding). Tests:RegionInvitePanel.test.tsx. - 🚧 Signup invite-code redemption — gameserver
POST /auth/registeraccepts optionalinvite_code(auth.py+lock_and_validate_invite); player-client on tip46bce720has no invite path —JoinInviteLanding.tsxABSENT;RegisterForm.tsx/AuthContext.register/registerWithOAuthdo not threadinvite_code(git grepempty). 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.tsx — ABSENT 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.
Related¶
ADR-0056— multi-account detection, the eventual consumer of the hashed IP/fingerprint columns.../DATA_MODELS/migrations.md— migration table entry.