> For the complete documentation index, see [llms.txt](https://jordan-7.gitbook.io/jordan-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://jordan-7.gitbook.io/jordan-docs/concepts/concepts/rooms-and-seats.md).

# Rooms and seats

**A room** is a streamer's configured space: a name, a set of prices, a seat count, an overlay layout, and the switches that decide what viewers may do there. **A seat** is one viewer's place on camera inside a room, alive for as long as they stay and are charged for.

## The room record

A room is one JSON record in `data/rooms.json`, created by `createRoom()` in `rooms-store.js` with an 8-character id, a name, `active: true`, an optional password hash, and a `config` object seeded from environment defaults (`getEnvDefaults()`, `rooms-store.js`). Every read goes through `resolveRoomConfig()` (`rooms-store.js`), which normalises the stored config against those defaults so an older record renders exactly as it did before a field existed.

The fields that shape what a viewer sees, all from `resolveRoomConfig()`:

| Field                                    | Meaning                                                                                                              | Default                         |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| `active`                                 | Whether the room accepts joins. A paused room still serves direct links but refuses new seats.                       | `true`                          |
| `unlisted`                               | Hidden from the browse board; still works by link.                                                                   | `false`                         |
| `maxSeats`                               | Paid seats on camera at once. Clamped to 1–3.                                                                        | `3`                             |
| `passkeyTickSeconds`, `passkeyTickPrice` | The metering unit for a live seat.                                                                                   | 1 s, `0.001`                    |
| `maxSession`                             | The most one seat can spend before it ends.                                                                          | `2`                             |
| `letters`                                | The MegaChat settings — see [MegaChats](/jordan-docs/concepts/concepts/megachats.md).                                | enabled                         |
| `joinStream`                             | The live-seat settings, with their own admission and gates.                                                          | enabled                         |
| `layout`                                 | Where tiles sit on the canvas — see [The overlay](/jordan-docs/concepts/concepts/the-overlay.md).                    | top-right, 320×180              |
| `transport`                              | `livekit` when the server has LiveKit credentials, else `vdo`.                                                       | `resolveTransport()`            |
| `twitchChannel`, `twitchAuto`            | The broadcast the room follows — see [Follow my stream](/jordan-docs/features/features/follow-my-stream.md).         | adopted from the linked account |
| `handle`                                 | The permanent `/<handle>` link — see [Identity and handles](/jordan-docs/concepts/concepts/identity-and-handles.md). | none                            |

The cap of three paid seats is enforced twice: at creation (`createRoom`, `Math.min(3, …)`) and on every read (`resolveRoomConfig`, the same clamp). A whitelisted guest can sit *above* that cap, up to a hard ceiling of ten tiles — the derivation is in `server.js` above `MAX_EFFECTIVE_SEATS` and on the [Guest whitelist](/jordan-docs/features/features/guest-whitelist.md) page.

## Who controls a room

Two credentials, deliberately different in scope (`auth.js`, the comment above `canManageRoom`):

* **The owner** — the signed-in account that created the room. Ownership is stamped on the record as `ownerKey` (the account id, `roomOwnerKey()` in `auth.js`) and checked against the sealed identity cookie, never against anything the client asserts.
* **The room password** — optional, hashed with scrypt (`room-auth.js`), shared with moderators to run *one* room. A room created without a password is owner-only (`createRoomWithPassword`, `rooms-store.js`).

## A seat's life

A seat exists in one in-memory map, `activeSeats` in `server.js`. It is created by a join route, becomes **live** when the viewer hits GO LIVE and their page sends `camera_ready` (`activateSeatLive`, `server.js`) — in a LiveKit room the camera is published only after the server confirms the seat live, so a preview is never on air (`goLiveNow`, `web/lib/join-page.ts`) — is charged every tick while live, and is torn down by `removeParticipant()` (`server.js`) whether the viewer left, was kicked, ran out of balance, or vanished.

A seat id is public (`/api/seats` lists them), so it proves nothing on its own. The join response also carries a `seatToken`, and every act on the seat — registering its socket, going live, leaving, the LiveKit publisher token, the quality and echo reports — is refused without it (`seatTokenMatches`, `server.js`).

The reasons a seat ends are the reasons the code passes to `removeParticipant`: `left`, `disconnected`, `camera_timeout`, `out_of_funds`, `payment_stalled`, `ledger_corrupt` (a money ledger is damaged and the doors are halted), the dashboard's `kicked` and `room_ended`, and refusals of a misconfigured seat (`no_payout_address`, `bad_money_mode`, `escrow_deposit_failed`). When a seat ends the overlay is told (`seat_removed`), the media server drops the participant (`livekit.kickParticipant`), and whatever was not spent is dealt with by `refundSeat()` — what that means depends on how the seat was paid for, which is the subject of [Money and metering](/jordan-docs/concepts/concepts/money-and-metering.md).

Seats do not survive a server restart: the map is memory, not disk (`server.js`, `activeSeats`). A room does — it is on disk — unless nobody can manage it: at boot the server removes rooms with neither an owner nor a password, keeping the default room and the demo (`isOrphanRoom`, `rooms-store.js`; `KEEP_ORPHAN_ROOMS=true` keeps them all).

## What this does NOT do

* A room does not record who sat in it. The seat map is transient; the durable traces are the airing record ([Airings and evidence](/jordan-docs/concepts/concepts/airings-and-evidence.md)), which stores usernames of guests as labels, not viewer identities, and — for a paid seat — the seat ledger, which keeps the paying wallet address because that is where a refund goes (`seat-escrow.js`, `SEAT_OPEN`).
* A room's `maxSeats` is not a promise of three *simultaneous* streams of income; it is a cap on tiles.
* `followersOnly` and `subsOnly` on a room's gates are stored but not enforced — `checkFeatureGates()` in `server.js` enforces `minWatchSeconds` only, and says so in its comment.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://jordan-7.gitbook.io/jordan-docs/concepts/concepts/rooms-and-seats.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
