# LobbyLab client SDK (`@lobbylab/client`)

LobbyLab is a hosted multiplayer backend. Your game keeps its own UI, engine and domain; players join LobbyLab rooms over WebSockets through `@lobbylab/client`, and LobbyLab keeps the rooms, invite codes, matchmaking and shared game state. Game rules that run on LobbyLab's servers come later. Request an alpha key at https://lobbylab.gg/developers.

Status: preview. `@lobbylab/client` and `@lobbylab/node` are not on npm yet; both arrive with the alpha, and the API below may change before then. Do not add them to a project yet. To connect today from any language, use the wire protocol (the protocol doc).

## What the SDK does for you

- Signs players in: anonymous players with your publishable key, or players from your own login through a token your backend mints with `@lobbylab/node`.
- Creates, joins and quick-joins rooms, and lists open public rooms.
- Keeps each player's view of the room current: the shared document, every player's public slot, the player's own private data, the host, room metadata and whether the room is locked.
- Sends state changes and messages, and resolves or rejects each with the server's answer.
- Reconnects on its own, and each change carries its own id, so a retry never applies it twice; your state is current after every reconnect.
- Runs in browsers, Node, Bun, Deno and React Native, with no dependencies.

## Connect

```js
import { LobbyLab } from "@lobbylab/client";

const lobby = new LobbyLab({
  key: "llpk_...",             // publishable key: safe in a browser, checked against your allowed origins
  name: "Maya",                // display name for an anonymous player
  // or: authEndpoint: "/api/lobbylab-token", for players from your own login
});
```

Keys: a publishable key (`llpk_...`) for clients and a secret key (`llsk_...`) for your backend only. A secret key sent from a browser is refused with `secret_key_in_browser`.

## Rooms

```js
const room = await lobby.joinOrCreate("default");   // quick join: the fullest open public room of this type, or a new one
const byCode = await lobby.join("KXPTQM");          // a 6-letter room code a friend shared
const mine = await lobby.create({ maxPlayers: 4 }); // a new room; share mine.code
const open = await lobby.listRooms({ type: "default" });
```

A room type (set once for your project, for example `default` or `race`) decides whether rooms are turn-based or live (20 Hz), how many players they hold, whether they are public, and who may write where.

## Shared state

Every player sees a `RoomView`:

```ts
interface RoomView {
  doc: Json;                      // the shared document everyone reads
  players: Record<string, Json>;  // one public slot per player, by memberId
  private: Json | null;           // this player's private data only
  host: string | null;            // the host's memberId
  meta: Record<string, Json>;     // public room metadata, at most 2 KB
  locked: boolean;
  version: number;                // goes up by one on every accepted change
}
```

```js
room.state.on("change", (view, info) => render(view));
await room.state.set("doc/board/4", "x");            // resolves when the server accepts it
await room.state.inc("doc/score/red", 1);
await room.state.push("doc/log", { move: "e4" }, { max: 50 });
await room.state.transact(
  [{ op: "set", path: "doc/turn", value: "blue" }],
  { guards: [{ path: "doc/turn", equals: "red" }] },  // refused with guard_failed if someone moved first
);
room.state.me.set({ x: 12, y: 40 });                  // your own slot; in live rooms it is your input each tick
```

Paths start with `doc/`, `players/<memberId>/`, `private/<memberId>/` or `meta/`. Up to 16 operations go in one change, applied in order, all or none. A refused change rejects with a code such as `forbidden_path`, `guard_failed`, `version_conflict` or `too_large`.

## Messages

```js
room.send("emote", { kind: "wave" });                       // to everyone
room.send("hint", { cell: 3 }, { to: ["m_2"] });            // to chosen players ("all", "others", "host", or member ids)
room.on("message:emote", ({ from, data }) => showEmote(from, data));
```

A message's data is at most 4 KB.

## Members, the host and the room's end

```js
room.members.on("change", () => drawRoster(room.members.list()));
if (room.isHost) await room.lock(true);                      // the host can also kick, set metadata, hand over the host role and close
room.on("reconnecting", () => showBanner("Reconnecting"));
room.on("closed", ({ reason }) => showEnd(reason));          // host_closed, empty, time_limit, host_left, ...
await room.leave();
```

The room lifecycle, limits and close reasons are in the rooms doc; every error code has a page at https://lobbylab.gg/docs/errors.

## Your backend: `@lobbylab/node`

```js
import { LobbyLabServer } from "@lobbylab/node";
const lobbylab = new LobbyLabServer({ secretKey: process.env.LOBBYLAB_SECRET_KEY });
const { token } = await lobbylab.tokens.create({ userId: user.id, name: user.displayName });
```

Use it to mint player tokens for your own signed-in players, create reserved rooms, list rooms and read usage. It uses only `fetch` and Web Crypto, so it also runs in Workers, Vercel Edge, Deno and Bun.

## Until the SDK ships

Keep your game's networking behind one small module (connect, send, and a callback when the state changes), so moving to LobbyLab later changes one file. The planned REST routes are in the API reference: https://lobbylab.gg/docs/api.
