Shared state · lobby · party · room · world state

State that belongs to a group of players, not to one of them.

A shared space is an access boundary with its own versioned JSON documents. Put the lobby, the party, the room, the match or the world state in one, decide who can read and who can write, and let clients be told when it changes instead of asking over and over.

Shape
spaces/{space_id}
  members: player -> can_write
  documents/state    {"phase":"lobby", ...}
  documents/state/wait?after_version=7
        → blocks until version 8
01

One boundary, many documents

A space holds as many keys as you need. Keep state, roster and chat separate instead of rewriting one blob every time anything moves.

02

Membership is the ACL

Members are explicit, and each carries can_write. A read-only member writing gets 403. Only the owner manages membership, and the owner is always writable.

03

Told, not asked

Every shared document has a /wait endpoint. Pass the last version you saw; it blocks until there is a newer one, then hands you the document and the next cursor.

The backend owns the room. The clients watch it.

This is the shape that works today across every engine: something trusted creates the space and decides membership, and each client watches the documents it is allowed to see.

# A trusted backend creates the space and decides who is in it.
# A publishable key cannot: shared spaces need a player Bearer token,
# a server token, or a secret API key.
curl -X POST "$CRUX/v1/projects/$PROJECT/environments/$ENV/spaces" \
  -H "Authorization: ServerToken $SERVER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"key":"match-4821","metadata":{"mode":"coop","map":"harbour"}}'

# key is optional and unique per environment, so it doubles as
# "get me the space for match 4821" - a second create answers 409.

# Add each player. can_write:false makes a spectator.
curl -X PUT "$CRUX/v1/projects/$PROJECT/environments/$ENV/spaces/$SPACE/members/$PLAYER" \
  -H "Authorization: ServerToken $SERVER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"can_write":true}'

Watching costs one parked request.

A long poll returns as soon as the version advances, or empty-handed when its timeout expires. Either way you get the current cursor back, so the loop never misses a write and never re-reads history.

// The client watches. It never re-requests the document on a timer.
let cursor = 0;                 // 0 means "wait for the first write"

while (running) {
  const { document, version } =
    await crux.waitSharedDocument(spaceId, "state", cursor);

  cursor = version;             // always feed the cursor back

  if (document) applyLobbyState(document.value);
  // document === null means nothing changed before the timeout,
  // or the document was deleted. The cursor is live either way.
}

What this is not

It is not realtime messaging. There is no WebSocket and no server push; /wait is a long poll. It tells you a document changed. It does not give you ordering across concurrent writers, message history, pagination, fan-out to thousands of subscribers, retention or moderation. You can absolutely store a message list in a shared document and watch it, and for a lobby or a co-op session that is often all you need. For a chat product, those other problems are yours and they are the hard part.

It is not a per-player key you poll. Writing into one player's document and reading it from another is the pattern this replaces. A plain GET of a key nobody has written answers 404 forever, so a polling loop against it never starts working, however long it runs. after_version=0 on /wait is the fix: it blocks for the first write instead.

Limits and access, as the API actually enforces them

Who can use spacesA player Bearer token, a server token, or a secret API key. A publishable key cannot: log the player in first, or call from trusted server-side code.
Player-owned spaces100 per environment, per player. Spaces created with a server token or secret key are unowned and not counted against any player.
Members per space128
Space keyOptional, up to 255 characters, unique per environment. Creating a second space with the same key answers 409, which makes it a natural idempotency key for "the space for match X".
Space metadataA JSON object, up to 4096 bytes.
Revoking accessTakes effect immediately, including on parked long polls: a wait re-checks membership every time it wakes, so removing a player ends their in-flight watch rather than letting it run to timeout.
SDK coverageJavaScript, Godot and Unity all wrap the lifecycle: create a space, manage members, read and write shared documents, and wait on them. One gap to know about: a watcher whose membership is revoked mid-wait currently sees the same empty result as an idle timeout, because the SDKs collapse the backend's 404 into it.
Verify before you integrate

Check the contract before you build on it.

Crux is small and new. The useful trust signals are the parts you can inspect yourself: the running API, its public contract, the client source, and your exit path.

Live API status

A live check of the hosted API right now. No made-up uptime percentage and no hidden demo environment.

Check current status

Public API contract

The hosted HTTP surface is documented as OpenAPI. Inspect the wire format before choosing an SDK or writing an adapter.

Open the spec

MIT client SDKs

The JavaScript, Godot, Unity, and Roblox client SDK source is public. The hosted backend itself remains a managed proprietary service.

Inspect the SDK source

Documented exit path

Read what is exportable today and the written commitment for advance notice plus an export window if the hosted service winds down.

Read the exit guarantee

Start with a space and one document.

The free tier covers every feature. Create a project, make a space, write state, and watch it from a second client.