Skip to content

Architecture

Passage is built on a modular, adapter-based architecture that separates concerns into three distinct phases. This design allows for maximum flexibility while maintaining simplicity.

flowchart LR
    Player[Player] --> Passage[Passage]
    Passage --> GameServer[Game Server]

    Passage --> Status[Status Adapter]
    Passage --> Discovery[Target Discovery Adapter]
    Passage --> Strategy[Target Strategy Adapter]

Every player connection goes through three distinct phases:

When a player first connects, Passage:

  1. Receives the handshake and status request
  2. Queries the Status Adapter for server information (MOTD, player count, favicon)
  3. Returns the status to the client
  4. Authenticates with Mojang using the player’s username and shared secret
  5. Encrypts the connection using AES-128-CFB8
  6. Optionally handles resource packs if configured

This phase ensures only legitimate players proceed to routing.

Once authenticated, Passage needs to know which backend servers are available:

  1. Queries the Target Discovery Adapter
  2. Receives a list of available targets with metadata
  3. Each target includes:
    • Unique identifier
    • Network address (IP:port)
    • Metadata (key-value pairs like player count, server type, etc.)

Examples of discovery adapters:

  • Fixed: Static list from configuration
  • gRPC: Dynamic list from a custom service
  • Agones: Auto-discovery of Kubernetes game servers

With the list of available servers, Passage selects the best one:

  1. Queries the Target Strategy Adapter with:
    • List of available targets
    • Player information (UUID, username)
    • Client address and protocol version
  2. Strategy returns the selected target
  3. Passage sends the Transfer packet with the target address
  4. Connection closes - player is now directly connected to the backend

Examples of strategy adapters:

  • Fixed: Always select the first available target
  • Player Fill: Fill servers sequentially to maximize occupancy
  • gRPC: Custom logic (e.g., region-based, queue priority, etc.)

The adapter pattern allows Passage to remain:

  • Simple: Core logic is unchanged regardless of your deployment
  • Flexible: Swap adapters without modifying code
  • Extensible: Implement custom gRPC adapters for your specific needs
  • Fixed: Static configuration (name, MOTD, favicon)
  • HTTP: Query status from HTTP endpoint with caching
  • gRPC: Dynamic status from custom service
  • Fixed: Static server list from config
  • gRPC: Dynamic server list from custom service
  • Agones: Kubernetes game server auto-discovery
  • Fixed: Simple first-available selection
  • Player Fill: Fill servers to capacity before starting new ones
  • gRPC: Custom selection logic
flowchart TB
    subgraph Passage
        subgraph Connection["Connection Handler"]
            H[Handshake]
            A[Authentication]
            E[Encryption]
            T[Transfer]
        end

        subgraph Adapters["Adapter Layer"]
            Status[Status Supplier<br/>Fixed/HTTP/gRPC]
            Discovery[Target Selector<br/>Fixed/gRPC/Agones]
            Strategy[Target Strategy<br/>Fixed/Fill/gRPC]
        end

        subgraph Support["Supporting Components"]
            RL[Rate Limiter<br/>Per-IP limits]
            Obs[Observability<br/>OpenTelemetry/Sentry]
        end

        Connection --> Status
        Connection --> Discovery
        Connection --> Strategy
    end

Let’s follow a player as they connect to your Minecraft network through Passage:

The player’s Minecraft client opens a TCP connection to Passage and sends a Handshake packet with:

  • Server address (e.g., play.example.com)
  • Server port: 25565
  • Protocol version (e.g., 769 for Minecraft 1.21)
  • Next state: Status (2)

Passage receives the handshake and transitions to Status state.

Passage queries the Status Adapter and returns the server information to the client. The player sees your server in their server list with the configured MOTD, player count, and favicon.

When the player clicks “Join Server”, the client sends a Login Start packet and Passage responds with an Encryption Request containing the public key.

The player’s client now:

  1. Generates a shared secret (16 random bytes)
  2. Computes the server hash using Mojang’s algorithm
  3. Authenticates with Mojang’s session servers:
    POST https://sessionserver.mojang.com/session/minecraft/join
    {
    "accessToken": "player's access token",
    "selectedProfile": "player's UUID",
    "serverId": "computed server hash"
    }
  4. Encrypts the shared secret and verify token with Passage’s public key
  5. Sends Encryption Response to Passage

Passage decrypts the response, verifies with Mojang’s hasJoined endpoint, and receives the player’s profile (UUID, name, properties). The connection is now encrypted with AES-128-CFB8, and Passage sends Login Success.

Passage can optionally:

  • Request the client to download resource packs
  • Store cookies for authentication persistence
  • Configure other client settings

Passage queries the Target Discovery Adapter to get a list of available backend servers with their metadata (player counts, server types, etc.).

The Target Strategy Adapter selects the best server for this player based on the configured strategy (e.g., Player Fill selects the fullest server below capacity).

Passage sends a Transfer Packet with the backend server’s address and closes the connection. The client seamlessly reconnects to the backend server—from the player’s perspective, this is instantaneous and transparent.

The player is now fully connected to the backend server. Passage has no further involvement—the player’s packets go directly to the backend server.

sequenceDiagram
    participant Client as Player's Client
    participant Passage
    participant StatusAdapter as Status Adapter
    participant Mojang as Mojang API
    participant Discovery as Discovery Adapter
    participant Strategy as Strategy Adapter
    participant Backend as Backend Server

    Note over Client,Passage: Status Phase
    Client->>Passage: Handshake (Status)
    Client->>Passage: Status Request
    Passage->>StatusAdapter: Get Status
    StatusAdapter-->>Passage: MOTD, Player Count, Favicon
    Passage-->>Client: Status Response

    Note over Client,Passage: Authentication
    Client->>Passage: Handshake (Login)
    Client->>Passage: Login Start
    Passage-->>Client: Encryption Request (Public Key)

    Note over Client,Mojang: Client authenticates with Mojang
    Client->>Mojang: POST /session/minecraft/join
    Mojang-->>Client: OK

    Client->>Passage: Encryption Response (Shared Secret)
    Passage->>Mojang: GET /session/minecraft/hasJoined
    Mojang-->>Passage: UUID, Profile

    Note over Client,Passage: Connection now encrypted (AES-128-CFB8)
    Passage-->>Client: Login Success

    Note over Client,Passage: Configuration Phase
    Passage-->>Client: Transfer to Configuration
    Client->>Passage: Configuration Acknowledged
    Client->>Passage: Finish Configuration

    Note over Passage,Strategy: Routing Decision
    Passage->>Discovery: Get Targets
    Discovery-->>Passage: List of available servers
    Passage->>Strategy: Select Target
    Strategy-->>Passage: Selected server address

    Note over Client,Backend: Transfer
    Passage-->>Client: Transfer Packet
    Passage->>Passage: Close connection

    Note over Client,Backend: Direct connection - Passage no longer involved
    Client->>Backend: New TCP Connection
    Backend-->>Client: Login, Spawn Chunks

✅ TCP connection handling ✅ Minecraft protocol handshake ✅ Status responses (MOTD, player count) ✅ Encryption key generation ✅ Mojang authentication verification ✅ AES-128-CFB8 encryption setup ✅ Configuration phase management ✅ Server discovery and selection ✅ Transfer packet with target address

❌ Maintain persistent connections ❌ Transcode game packets ❌ Proxy gameplay traffic ❌ Store player state after transfer ❌ Handle disconnects from backend

Passage’s stateless architecture means:

  1. No memory per player: After transfer, Passage has zero memory of the player
  2. Instant restart: Restarting Passage doesn’t affect connected players
  3. Horizontal scaling: Run multiple Passage instances with simple load balancing
  4. No synchronization: No need to sync state between instances
  • Memory: Less than 5MB
  • CPU: Minimal - mostly I/O bound
  • Network: ~5KB per player connection (authentication + transfer)

Typical connection times:

PhaseDurationNotes
TCP Handshake1-5msNetwork latency
Status Request1-10msAdapter query time
Login & Encryption100-500msMojang API latency
Configuration10-50msOptional resource packs
Target Discovery1-50msAdapter dependent
Target Selection1-10msStrategy complexity
Transfer1-5msPacket transmission
Backend Connection50-200msNew TCP + login
Total~200-800msMostly Mojang API

The majority of time is spent waiting for Mojang’s authentication servers. The actual Passage routing logic adds only ~10-60ms.

Passage ← Mojang: "Invalid session"
Player's Client ← Disconnect
"Failed to verify username"

The player is disconnected with an error message.

Target Discovery → Empty list []
Target Strategy → None selected
Player's Client ← Disconnect
"No available server"

The player receives a localized error message (configurable).

This happens AFTER the transfer. The client will connect to the backend server and either:

  • Get a connection refused error (server down)
  • Timeout during connection
  • Successfully connect

Passage has already closed the connection and has no involvement at this point.

Passage supports authentication cookies to skip Mojang verification for returning players:

Player → Passage → Mojang (verify)
Cookie stored in client
Player → Passage (includes cookie)
Passage verifies cookie signature
Skip Mojang verification!
→ Transfer (much faster)

This reduces connection time from ~500ms to ~50ms for returning players.