Start

Playground

The playground is a live workspace used to exercise the engine: two clients, three room kinds, streams, presence, paging, and role changes. It is application code. Removing it does not change the reducer.

Open it

Hosted: ytt.rium.dev. Locally: npm run worker and npm run playground, then localhost:5173.

The Worker serves the built playground at / and these docs at /docs. Vite uses the same pretty routes during development.

Two skins

RouteSkinWhat you get
/labTwo live clients side by side. Default users are Ada and Beau. + client adds more panes (up to six).
/app/:ns/:userappOne client. Use this to open Ada and Beau in two real tabs or windows.
/app/:ns/:user/:roomappSame, with that room already selected.
/app?ns=demo&user=ada&room=notesappQuery-form equivalent.

A first visit gets its own random workspace, remembered in this browser and carried in the URL as ?ns= so it can be shared. The lab workspace field changes the slug and reconnects every pane. Hosted demo data may be reset at any time. In the app skin, the header links the other seeded user so you can hop from Ada to Beau without editing the URL by hand.

Seeded workspace

A new workspace is created server-side. The catalog starts with:

Ada is admin. Beau is writer. Readers can open rooms and see commits but cannot mutate. Admins can create rooms, add users, and change roles. Writers can create rooms in the demo catalog and edit content they are allowed to write.

Rooms

Channel

#general is a message list. Compose a line, add emoji reactions, and watch the other pane. The shape keeps the last 12 messages inline. Older messages become a hole — “N older messages” — with no artifact. Elided bodies are requested with want / have when the client scrolls them into view.

Authors are bound to the authenticated user. You can edit or remove your own messages; admins can edit others. user_id and author cannot be forged through a later set_field.

Document

notes is a shared textarea backed by the optional text battery. The playground replaces whole runs and preserves the untouched prefix or suffix of a finalized run. Click stream to append a canned token stream (“hello from the live doc.”). Chunks fan out live; completion persists stream_end and folds the prefix into a generated CRDT run.

Concurrent focused editing, edits inside an active stream, and rejected-text resync are still open. Treat the textarea as a demo binding, not a production editor.

Canvas

board streams pointer strokes as point chunks, then completes them into a durable point extension. Color and width live on the stroke slice. The last 12 strokes stay inline. The elided prefix may carry a derived raster artifact so a late joiner sees old ink without hydrating every body. Erase names your strokes, including elided ones that kept user_id.

New strokes are validated (finite points, positive finite width, at most 4,096 points and 256 KiB of JSON). An unsafe or over-budget raster is omitted; the original strokes remain loadable.

Presence

Text carets and canvas pointers are ephemeral presence frames. Core stores opaque JSON keyed by client id, fans it out, and forgets it when the peer leaves. The playground interprets user_id plus a text index or canvas x/y. The host checks that the claimed user matches the bound principal. Presence is not persisted and does not take a commit number.

Identity and access

Each pane has a user picker. + new user is an admin action: it provisions membership, then switches that pane. The WebSocket URL carries ?as=<user>&workspace=<ns> because YTTRIUM_DEMO_AUTH=true is on. That is a synthetic identity for the live Worker and local wrangler dev, not a credential.

Admins see Workspace access at the bottom of the pane. Changing Ada from admin to writer, or Beau to reader, updates the ACL domain. Other tabs keep their existing sockets and receive a replacement availableActions private view. Controls disable from that hint; the next mutation is still checked on the server.

You cannot remove the last admin. A prefix grant can later let a reader write matching future channels; the example uses lowercase slugs such as team-b-.

Hostile cross-origin pages cannot use the query-param adapter. A production app replaces authenticateDemo with a real provider and must not treat a browser-supplied name as proof.

Domain map

ws/{ns}/acl                         members + grants
ws/{ns}/catalog                     room and user listing
ws/{ns}/user/{user}                 home + mounts
ws/{ns}/channel|document|canvas/{id} content

Opening a room mounts it on that user's home domain, then opens a second session on the content domain. The hub therefore holds a vector of positions, one per subscribed domain. Catalog writes and content writes are independent commits.

What the UI is doing

Things to try

  1. On /, send a message as Ada and watch Beau.
  2. Open /app/demo/ada and /app/demo/beau in two tabs. Draw on board.
  3. As Ada, set Beau to reader. Beau's compose box disables; a forced write would nack.
  4. Click stream on notes while the other client is watching.
  5. Create a new channel from the catalog, then add a third user from the picker.
  6. Reload a tab with a pending edit after stopping the Worker — the outbox should still be there when the socket returns.

Current playground limits

See Limits for engine-level edges that the playground inherits.