MVP Join Contract
This page defines the canonical MVP player join flow for Uebliche-managed play:
Launcher -> Public API -> Gateway/Connect -> GameserverIt is a contract-first target for the Hide and Seek vertical slice. Existing code already covers Minecraft session authorization, Connect server registration, gateway route publication, and gateway TCP routing. The missing piece is a player-specific join ticket that binds one launcher join attempt to one target server and optional match.
Current Implementation Map
- Launcher intercepts Minecraft
joinServercalls through the local launcher bridge and delegates Mojang session authorization to the signed-in Microsoft account. - Launcher and the Connect client can already carry a selected minigame target through environment variables and the
connect:minigame_joinplugin message. - Public API already issues short-lived Minecraft auth join tokens and Connect network join tokens.
- Connect servers register with Public API, publish
minecraft-tcpgateway routes, and keep route status fresh through heartbeats. - Gateway resolves Minecraft hostnames to active Connect routes and tunnels TCP traffic to the registered
serverId. - Gameserver accepts the normal Minecraft login path, then applies server-side checks such as protocol compatibility, bans, maintenance mode, and reconnect handling.
MVP join tickets add one canonical gameplay handoff on top of those pieces.
Required IDs
| Field | Owner | Meaning |
|---|---|---|
playerId | Minecraft/Public API | Canonical Minecraft profile UUID for the player. It must match the authenticated launcher account and the in-game login profile. |
sessionId | Public API | One join attempt created by Public API. It is stable across the short launcher/connect retry window. |
serverId | Connect/Public API | Target Connect server id. Gateway routing and gameserver validation are scoped to this id. |
matchId | Game service | Optional gameplay target such as a Hide and Seek queue, lobby, or running match. Direct server joins may omit it. |
ticketId | Public API | Public identifier for the join ticket and audit record. Logs may include this id, but never the opaque ticket secret. |
Use hyphenated UUID strings for playerId, sessionId, and ticketId. serverId keeps the existing Connect server id format. matchId is an opaque game-owned string.
Launcher To Public API
Before opening the Minecraft multiplayer connection for a managed play action, the Launcher asks Public API for a join ticket.
POST /v1/connect/join-tickets
Authorization: Bearer <launcher user access token>
Content-Type: application/json{
"playerId": "00000000-0000-0000-0000-000000000000",
"serverId": "connect-server-01",
"matchId": "hide-and-seek:queue:default",
"intent": "play",
"client": {
"launcherSessionId": "11111111-1111-1111-1111-111111111111",
"minecraftProfileName": "PlayerName",
"minecraftVersion": "1.21.11",
"loader": "fabric"
}
}Launcher responsibilities:
- Request the ticket only after the active account and Minecraft profile are known.
- Connect the client to the
route.hostandroute.portreturned by Public API. - Keep the existing Mojang
joinServerbridge path for vanilla session authorization. - Pass the join handoff to the Connect client. The canonical MVP handoff is
connect:join_ticket; the olderconnect:minigame_joinpayload may remain as a temporary Hide and Seek target hint until the ticket payload is implemented. - Do not put the opaque ticket in the hostname, query string, logs, crash reports, or user-visible copy.
Public API Join Ticket Response
Public API validates the requested target, creates the join attempt, and returns the route plus an opaque signed or stored ticket.
{
"ticketId": "22222222-2222-2222-2222-222222222222",
"sessionId": "33333333-3333-3333-3333-333333333333",
"playerId": "00000000-0000-0000-0000-000000000000",
"serverId": "connect-server-01",
"matchId": "hide-and-seek:queue:default",
"route": {
"host": "hide-and-seek-eu.uebliche.gg",
"port": 25565,
"serviceId": "minecraft-tcp"
},
"ticket": "opaque-signed-or-random-token",
"expiresAt": "2026-05-23T12:05:00Z",
"expiresIn": 120,
"handoff": {
"type": "connect-plugin-message",
"channel": "connect:join_ticket"
}
}Public API responsibilities:
- Authenticate the launcher user and ensure
playerIdbelongs to that user. - Authorize access to the requested
serverIdandmatchId. - Ensure the
minecraft-tcproute is currently active and has a usable gateway. - Create a single-use join ticket with a short TTL.
- Store only a token hash or signed-token identifier, not the raw opaque ticket.
- Audit
ticketId,sessionId,playerId,serverId, andmatchId.
Gateway And Connect
Gateway remains the network router for MVP. It should not be the source of truth for player ticket validation because the Minecraft handshake has no trusted bearer-token channel.
Gateway responsibilities:
- Route by the active
minecraft-tcpConnect route forserverId. - Require the existing server tunnel authorization using
serverIdandserverSecret. - Keep the original hostname and routing metadata available to the backend tunnel.
- Reject missing or stale routes before opening the backend stream.
Connect client responsibilities:
- After the Minecraft play/configuration channel is available, send the join ticket handoff to the server on
connect:join_ticket. - Include
ticketId,sessionId, optionalmatchId, and the opaqueticket. - Retry only inside the ticket TTL and stop after successful server acknowledgement.
The existing connect:minigame_join payload can still carry launcher-selected minigame intent, but it is not an authorization primitive.
Gameserver Handoff
The gameserver validates the player ticket after normal Minecraft login and before admitting the player to protected gameplay.
Gameserver responsibilities:
- Treat the Minecraft login profile as the authoritative
playerId. - Validate the join ticket against Public API or a locally verifiable signed token.
- Require the ticket to bind the same
playerId,serverId,sessionId, and optionalmatchId. - Consume the ticket once; duplicate consumes must fail or return the already-consumed result for the same player/session.
- Attach
sessionIdandmatchIdto the player runtime context. - Keep existing protocol, ban, maintenance, and reconnect checks in front of gameplay admission.
- For protected modes such as Hide and Seek, deny match entry when the ticket is missing, expired, already used, or bound to another player/server. The server may move the player to a lobby instead of disconnecting when that is a better user experience.
Error Cases
All API errors should use a stable machine-readable code, a human-readable message, and a request correlation id when available.
| HTTP | Code | When |
|---|---|---|
401 | auth_required | Launcher user token is missing or invalid. |
403 | player_mismatch | Requested playerId does not belong to the authenticated launcher user. |
403 | access_denied | Player is not allowed to join the requested server or match. |
404 | server_not_found | serverId is unknown or not joinable through Connect. |
404 | match_not_found | matchId does not exist or no longer accepts players. |
409 | route_unavailable | No active minecraft-tcp route is available for the server. |
409 | server_full | Server capacity is exhausted. |
409 | match_full | Match capacity is exhausted. |
410 | ticket_expired | Ticket was presented after its TTL. |
409 | ticket_used | Ticket was already consumed by another join attempt. |
422 | unsupported_client | Launcher, Minecraft version, loader, or required mod state is incompatible. |
429 | rate_limited | Player or account exceeded join-ticket issuance limits. |
503 | gateway_unavailable | Gateway routing cannot currently accept the connection. |
TTL And Security
- Default join-ticket TTL:
120seconds. - Maximum join-ticket TTL:
300seconds. - Verification may allow at most
15seconds of clock skew. - Tickets are single use and bound to
playerId,serverId,sessionId, optionalmatchId, routehost, routeport, andserviceId. - Public API and gameserver logs may include
ticketIdandsessionId, but must never log the opaqueticket. - Persist only ticket hashes or signed-token metadata needed for revocation and single-use checks.
- Rate-limit ticket issuance per player and per launcher account.
- Do not rely on client-provided
matchIdalone for authorization; Public API must resolve the requested game target against server-side state.
Hide And Seek Readiness
Hide and Seek should use matchId for the queue or running match identifier:
- Launcher selects Hide and Seek and requests a ticket with the chosen
matchId. - Public API resolves the match target, selects the active Connect
serverId, and returns a route plus ticket. - Launcher connects Minecraft through Gateway and hands the ticket to the Connect client.
- Connect sends
connect:join_ticketafter channel setup. - Gameserver consumes the ticket, attaches
sessionIdandmatchId, then routes the player into the Hide and Seek queue or match.
Next Implementation Steps
- Add the Public API
POST /v1/connect/join-ticketsissuer and a server-authenticated consume or verify endpoint. - Add the
connect:join_ticketwire payload beside the existingconnect:minigame_joinpayload. - Extend Launcher launch state so managed play actions can carry the join-ticket handoff.
- Add gameserver ticket validation and Hide and Seek match admission using
sessionIdandmatchId.