Skip to content

MVP Join Contract

This page defines the canonical MVP player join flow for Uebliche-managed play:

text
Launcher -> Public API -> Gateway/Connect -> Gameserver

It 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 joinServer calls 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_join plugin message.
  • Public API already issues short-lived Minecraft auth join tokens and Connect network join tokens.
  • Connect servers register with Public API, publish minecraft-tcp gateway 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

FieldOwnerMeaning
playerIdMinecraft/Public APICanonical Minecraft profile UUID for the player. It must match the authenticated launcher account and the in-game login profile.
sessionIdPublic APIOne join attempt created by Public API. It is stable across the short launcher/connect retry window.
serverIdConnect/Public APITarget Connect server id. Gateway routing and gameserver validation are scoped to this id.
matchIdGame serviceOptional gameplay target such as a Hide and Seek queue, lobby, or running match. Direct server joins may omit it.
ticketIdPublic APIPublic 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.

http
POST /v1/connect/join-tickets
Authorization: Bearer <launcher user access token>
Content-Type: application/json
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.host and route.port returned by Public API.
  • Keep the existing Mojang joinServer bridge path for vanilla session authorization.
  • Pass the join handoff to the Connect client. The canonical MVP handoff is connect:join_ticket; the older connect:minigame_join payload 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.

json
{
  "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 playerId belongs to that user.
  • Authorize access to the requested serverId and matchId.
  • Ensure the minecraft-tcp route 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, and matchId.

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-tcp Connect route for serverId.
  • Require the existing server tunnel authorization using serverId and serverSecret.
  • 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, optional matchId, and the opaque ticket.
  • 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 optional matchId.
  • Consume the ticket once; duplicate consumes must fail or return the already-consumed result for the same player/session.
  • Attach sessionId and matchId to 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.

HTTPCodeWhen
401auth_requiredLauncher user token is missing or invalid.
403player_mismatchRequested playerId does not belong to the authenticated launcher user.
403access_deniedPlayer is not allowed to join the requested server or match.
404server_not_foundserverId is unknown or not joinable through Connect.
404match_not_foundmatchId does not exist or no longer accepts players.
409route_unavailableNo active minecraft-tcp route is available for the server.
409server_fullServer capacity is exhausted.
409match_fullMatch capacity is exhausted.
410ticket_expiredTicket was presented after its TTL.
409ticket_usedTicket was already consumed by another join attempt.
422unsupported_clientLauncher, Minecraft version, loader, or required mod state is incompatible.
429rate_limitedPlayer or account exceeded join-ticket issuance limits.
503gateway_unavailableGateway routing cannot currently accept the connection.

TTL And Security

  • Default join-ticket TTL: 120 seconds.
  • Maximum join-ticket TTL: 300 seconds.
  • Verification may allow at most 15 seconds of clock skew.
  • Tickets are single use and bound to playerId, serverId, sessionId, optional matchId, route host, route port, and serviceId.
  • Public API and gameserver logs may include ticketId and sessionId, but must never log the opaque ticket.
  • 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 matchId alone 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:

  1. Launcher selects Hide and Seek and requests a ticket with the chosen matchId.
  2. Public API resolves the match target, selects the active Connect serverId, and returns a route plus ticket.
  3. Launcher connects Minecraft through Gateway and hands the ticket to the Connect client.
  4. Connect sends connect:join_ticket after channel setup.
  5. Gameserver consumes the ticket, attaches sessionId and matchId, then routes the player into the Hide and Seek queue or match.

Next Implementation Steps

  • Add the Public API POST /v1/connect/join-tickets issuer and a server-authenticated consume or verify endpoint.
  • Add the connect:join_ticket wire payload beside the existing connect:minigame_join payload.
  • 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 sessionId and matchId.