> ## Documentation Index
> Fetch the complete documentation index at: https://docs.playflowcloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Lobby Lifecycle

> The complete lifecycle of a PlayFlow lobby — from creation to cleanup. Creating, joining, leaving, host actions, and every edge case.

A lobby progresses through several states during its lifetime. Understanding the lifecycle helps you design your game's UI and handle every edge case cleanly.

## Status Flow

Lobbies never dead-end. They either stay alive (rematch-ready) or get deleted entirely. There's no "completed" terminal state.

```
waiting ─────→ in_queue ─────→ matched ─────→ in_game ─────→ waiting
    │             │                  │              ↑         (rematch-ready)
    │             │                  │              │
    │             │                  └──→ match_found (if matchConfirmation enabled)
    │             │                         │
    │             │                         └──→ in_game (all confirm)
    │             │                         └──→ waiting  (decline / timeout)
    │             │
    │             └──→ waiting (timeout / cancel / no opponents)
    │
    └──→ starting ─────→ in_game (host started game directly)
              │
              └──→ waiting (server create failed)
```

| Status        | Meaning                                                                                           |
| :------------ | :------------------------------------------------------------------------------------------------ |
| `waiting`     | Open lobby, accepting players. The default resting state.                                         |
| `in_queue`    | Matchmaking active, searching for opponents                                                       |
| `starting`    | Host triggered direct start, server provisioning                                                  |
| `matched`     | Claimed for a match, server being created                                                         |
| `match_found` | Match proposed, waiting for player `POST /me/confirm-match` (only if `matchConfirmation.enabled`) |
| `in_game`     | Game server running, players connected                                                            |

<Info>
  **Self-healing lobbies.** When the game server stops — whether the host clicks "End Match", the server crashes, or its TTL expires — the lobby automatically returns to `waiting` with all players retained. There is no `completed` status. Players can queue or start another match immediately.
</Info>

***

## Creating a Lobby

```bash theme={null}
POST /v3/lobbies/{config}
x-player-id: host_player
Content-Type: application/json

{
  "name": "Friday Night CTF",
  "maxPlayers": 8,
  "region": "us-east",
  "isPrivate": false,
  "allowLateJoin": true,
  "settings": { "map": "forest", "mode": "ctf" },
  "state": { "ready": true }
}
```

The creator becomes the **host**. An invite code is generated automatically (even for public lobbies, so friends can share it).

| Field           | Required | Default   | Notes                                       |
| :-------------- | :------- | :-------- | :------------------------------------------ |
| `name`          | Yes      | -         | Display name                                |
| `maxPlayers`    | No       | `2`       | 1–100                                       |
| `region`        | No       | `us-east` | Game server region                          |
| `isPrivate`     | No       | `false`   | Hide from `GET /v3/lobbies/{config}` browse |
| `allowLateJoin` | No       | `true`    | Can players join after game starts?         |
| `settings`      | No       | `{}`      | Game-specific data passed to the server     |
| `state`         | No       | `null`    | Host's initial player state                 |

<Warning>
  The host can only be in **one lobby at a time** per config. Trying to create a second returns `409 Already in another lobby`.
</Warning>

### Fresh lobby on reconnect

Set `forceFresh: true` on create to atomically replace a lobby the caller already **hosts** in this config. The backend deletes the old lobby (stopping its game server best-effort) and then creates the new one — all in a single call. It's a no-op when the player has no prior lobby, so it's safe to always pass on reconnect. This replaces the old 3-call workaround (`TryReconnect` → detect stale host lobby → `DeleteLobby` → `CreateLobby`) with one request.

```bash theme={null}
POST /v3/lobbies/{config}
x-player-id: host_player
Content-Type: application/json

{
  "name": "Friday Night CTF",
  "maxPlayers": 8,
  "forceFresh": true
}
```

Only lobbies the caller **hosts** are force-deleted. If the caller is merely a guest in someone else's lobby, the create still returns `409 Already in another lobby` — they need to leave that one first.

***

## Joining a Lobby

Two ways: by **invite code** or by **lobby ID**.

<Tabs>
  <Tab title="By Invite Code">
    ```bash theme={null}
    POST /v3/lobbies/{config}/join
    x-player-id: new_player

    {
      "code": "MEOW-42",
      "state": { "team": "blue" }
    }
    ```

    Best for "friend shares a code" flow. Works for both public and private lobbies.
  </Tab>

  <Tab title="By Lobby ID">
    ```bash theme={null}
    POST /v3/lobbies/{config}/join
    x-player-id: new_player

    {
      "lobbyId": "a1b2c3d4-...",
      "state": { "team": "blue" }
    }
    ```

    Best when the player browsed public lobbies and clicked one.
  </Tab>
</Tabs>

### Join Errors

| Status | Error                            | Cause                                             |
| :----- | :------------------------------- | :------------------------------------------------ |
| `404`  | `Lobby not found`                | Wrong ID/code                                     |
| `400`  | `Lobby is full`                  | `currentPlayers >= maxPlayers`                    |
| `400`  | `Lobby is not accepting players` | `status: in_game` and `allowLateJoin: false`      |
| `409`  | `Already in another lobby`       | Player is in a different lobby in the same config |

<Tip>
  Joining is **idempotent** — if the player is already in the lobby, it returns the lobby unchanged (no error).
</Tip>

***

## Updating Player State

Every player can update their own state. Use `PATCH /me`:

```bash theme={null}
PATCH /v3/lobbies/{config}/me
x-player-id: my_player

{
  "state": {
    "ready": true,
    "character": "wizard",
    "mmr": 1450
  }
}
```

<Info>
  **State merges, not replaces.** Existing keys are preserved unless your update overwrites them. To clear a key, set it to `null`.
</Info>

State flows automatically into `matchmaking_data` when the lobby queues, so fields like `mmr` are used by matchmaking rules.

***

## Host Actions

Only the **host** can perform these actions. Non-hosts get `403 Not host`.

### Update Lobby Settings

```bash theme={null}
PATCH /v3/lobbies/{config}/me/settings
x-player-id: host_player

{
  "name": "New Name",
  "maxPlayers": 16,
  "isPrivate": true,
  "region": "eu-west",
  "settings": { "map": "desert" }
}
```

Every field is optional — only include what you want to change.

### Kick a Player

```bash theme={null}
DELETE /v3/lobbies/{config}/me/players/{playerId}
x-player-id: host_player
```

The kicker is the host (from `x-player-id`). The `{playerId}` in the path is the target.

<Warning>
  Hosts can't kick themselves. Use `DELETE /me` to leave (which auto-transfers host).
</Warning>

### Start the Game Directly

Skip matchmaking and launch a server immediately with the current players:

```bash theme={null}
POST /v3/lobbies/{config}/me/start
x-player-id: host_player
```

The response has `server.status: "launching"`. Within \~10-30s (or \~5s with pool servers), status transitions to `running` and clients can connect.

### End the Match (Rematch)

```bash theme={null}
POST /v3/lobbies/{config}/me/end-match
x-player-id: host_player
```

The server stops, the lobby returns to `waiting`, and all players stay in the lobby with the same invite code and settings. Great for "Play Again" buttons.

<Info>
  If the game server stops on its own (crashes, hits TTL, or your game logic exits cleanly), the lobby **auto-heals** to `waiting` with the same effect — no call needed. `POST /me/end-match` is just the explicit, host-triggered version.
</Info>

***

## Leaving

Any player can leave at any time:

```bash theme={null}
DELETE /v3/lobbies/{config}/me
x-player-id: my_player
```

**What happens depends on who leaves:**

| Scenario                            | Behavior                                                |
| :---------------------------------- | :------------------------------------------------------ |
| Regular player leaves               | Removed from `players`, `currentPlayers` decrements     |
| Host leaves (with others remaining) | Host auto-transfers to next player (default)            |
| Last player leaves                  | Lobby is deleted, returns `{"status": "lobby_deleted"}` |
| Player leaves during matchmaking    | Matchmaking auto-cancels, lobby returns to `waiting`    |

### Host leave behavior

What happens when the host leaves is controlled per lobby config via `hostLeaveBehavior`:

| Value                 | Behavior                                                                                                                                        |
| :-------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------- |
| `"promote"` (default) | The next player in the lobby becomes the new host. Remaining players stay put with the same invite code, settings, and state.                   |
| `"delete"`            | The entire lobby is destroyed. Every remaining player receives a `lobby_deleted` SSE event on `/me/events` and must create or join a new lobby. |

**When to use each:**

* **`"promote"`** — persistent rooms, squad play, party-based games where the lobby should survive individual churn. This is the default for every lobby config.
* **`"delete"`** — one-shot private rooms, pickup games, host-authored sessions where the host's presence IS the room. Pairs well with `isPrivate: true` invite-only lobbies.

Non-host players leaving never triggers delete — only the host walking out can collapse the lobby under `"delete"`.

Example config JSON:

```json theme={null}
{
  "id": "private-squad",
  "name": "Private Squad Room",
  "enabled": true,
  "timeout": 300,
  "hostLeaveBehavior": "delete",
  "inviteCodeConfig": {
    "type": "alphanumeric",
    "length": 6,
    "prefix": "SQD-"
  },
  "serverSettings": { "serverSize": "small" }
}
```

<Info>
  When `hostLeaveBehavior: "delete"` fires, any game server associated with the lobby is stopped best-effort — same cleanup path as the admin delete endpoint.
</Info>

***

## Browsing Lobbies

Anyone (even without a player-id) can browse public lobbies in a config:

```bash theme={null}
GET /v3/lobbies/{config}?region=us-east&status=waiting&limit=20
```

Returns a paginated list. Private lobbies (`isPrivate: true`) are excluded automatically.

Query params:

| Param    | Purpose                         |
| :------- | :------------------------------ |
| `region` | Filter by region                |
| `status` | Filter by lobby status          |
| `limit`  | Page size (default 50, max 100) |
| `offset` | Pagination offset               |

***

## Cleanup & Timeouts

Lobbies don't stick around forever. The PlayFlow `lobby-sweep` cron runs continuously and handles:

* **Expired waiting lobbies** — deleted after `timeout` seconds (default 5 min) with no activity
* **Stale in-game lobbies** — in-game lobbies past 2 hours are cleaned up
* **Heartbeat expiry** — if `heartbeat: true` is enabled in config, players that stop sending heartbeats are removed
* **Matchmaking timeout** — lobbies in `in_queue` past the mode's timeout drop back to `waiting`

<Tip>
  For most games, **you don't need to send heartbeats**. The SSE connection itself is the heartbeat — as long as the player is connected to `/me/events`, they're alive. Only enable heartbeat in config if your players use HTTP polling instead of SSE.
</Tip>

***

## Heartbeat

Set `heartbeat: true` on a lobby config to require connected players to ping the server periodically. Players who go silent get evicted by the `lobby-sweep` cron.

```json theme={null}
{
  "heartbeat": true,
  "heartbeatTimeout": 30
}
```

* `heartbeat` — when `true`, every player must send heartbeats
* `heartbeatTimeout` — seconds before a player is considered stale. Any player whose last heartbeat is older than this is removed on the next sweep.

**When you need it:** HTTP-polling clients where the SSE connection at `/me/events` isn't available. Mobile background states, low-resource IoT clients, or environments that block long-lived connections.

<Tip>
  SSE is the better alternative. If your client can open `/me/events`, the stream itself is the liveness signal — see [Real-time Events](/lobbies/real-time) — and you can leave `heartbeat` off.
</Tip>

***

## Admin Delete

Force-delete any lobby by ID. Removes all players and cancels any active matchmaking — regardless of who created the lobby.

```bash theme={null}
curl -X DELETE https://api.computeflow.cloud/api/v3/lobbies/{config}/{id} \
  -H "api-key: pf_your_server_key"
```

* No `x-player-id` required. This is a server-side admin operation.
* `{config}` — the lobby config name (e.g. `"ranked_2v2"`).
* `{id}` — the lobby UUID to delete.

**Response (200):**

```json theme={null}
{ "status": "lobby_deleted", "id": "a3f1…-e8c2" }
```

**404** if no lobby with that ID exists in your project.

<Warning>
  Connected SDK clients in that lobby receive a `lobby_deleted` SSE event and disconnect cleanly. In contrast, `DELETE /{config}/me` is "leave lobby" — it only removes the caller, and when the caller is the host a new host is promoted automatically. Use admin delete when you actually mean destroy.
</Warning>

Good uses: dashboard admin actions, anti-cheat or moderation tooling, periodic cleanup for lobbies your app determines are abandoned.

***

## Invite Codes

`inviteCodeConfig` on a lobby config controls the codes PlayFlow generates when a lobby is created. Every new lobby (public **and** private) gets a code so friends can join by sharing it.

```json theme={null}
{
  "inviteCodeConfig": {
    "type": "alphanumeric",
    "length": 6,
    "prefix": "PLAY-"
  }
}
```

| Field    | Values                                  | Notes                                               |
| :------- | :-------------------------------------- | :-------------------------------------------------- |
| `type`   | `numeric`, `alphanumeric`, `alphabetic` | Character set for the generated suffix              |
| `length` | 1–16                                    | Length of the generated suffix (excluding prefix)   |
| `prefix` | any string                              | Optional. Prepended verbatim to the generated code. |

With the example above a lobby might get the code `PLAY-K7Q2F1`. Players then join with:

```bash theme={null}
POST /v3/lobbies/{config}/join
{ "inviteCode": "PLAY-K7Q2F1" }
```

***

## Error Codes

| HTTP  | Code                   | Meaning                                   |
| :---- | :--------------------- | :---------------------------------------- |
| `400` | `LOBBY_FULL`           | Capacity exceeded                         |
| `400` | `LOBBY_NOT_ACCEPTING`  | In-game with no late-join                 |
| `400` | `INVALID_ACTION`       | E.g., host trying to self-kick            |
| `403` | `NOT_HOST`             | Non-host tried a host-only action         |
| `404` | `LOBBY_NOT_FOUND`      | Lobby doesn't exist                       |
| `404` | `PLAYER_NOT_FOUND`     | Player isn't in the lobby                 |
| `409` | `ALREADY_IN_LOBBY`     | Player is already in another lobby        |
| `409` | `GAME_ALREADY_RUNNING` | Started a game that's already in progress |
| `409` | `INVALID_STATE`        | Wrong status for this operation           |

All errors follow the same format:

```json theme={null}
{
  "error": "Lobby is full",
  "detail": "Lobby is full (4 of 4)",
  "status": 400
}
```
