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
| Route | Skin | What you get |
|---|---|---|
/ | lab | Two live clients side by side. Default users are Ada and Beau. + client adds more panes (up to six). |
/app/:ns/:user | app | One client. Use this to open Ada and Beau in two real tabs or windows. |
/app/:ns/:user/:room | app | Same, with that room already selected. |
/app?ns=demo&user=ada&room=notes | app | Query-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:
general— channelnotes— documentboard— canvas- users
adaandbeau
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.
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-.
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
- One
Hubper pane, oneSessionper open domain. - One WebSocket per session, authenticated as the pane's current user.
ClientStore+Outboxpersist each pending mutation before send, keyedlive:{ns}:{user}:{domain}.- Private view
availableActionsdrives disabled buttons. It is not authority. - Storage keys are
v2:. Old prototype dumps are ignored. Development data is disposable.
Things to try
- On
/, send a message as Ada and watch Beau. - Open /app/demo/ada and /app/demo/beau in two tabs. Draw on
board. - As Ada, set Beau to
reader. Beau's compose box disables; a forced write would nack. - Click stream on
noteswhile the other client is watching. - Create a new channel from the catalog, then add a third user from the picker.
- Reload a tab with a pending edit after stopping the Worker — the outbox should still be there when the socket returns.
Current playground limits
- The text editor is whole-run, not a collaborative binding with stable positions or IME.
- Editing an elided body still needs a projected-update protocol.
- Undo of a text deletion does not restore the run.
- Automatic reconnect/backoff is incomplete; a closed socket may need a reload or navigation.
- There is no production login, logout, or identity-provider adapter in the UI.
See Limits for engine-level edges that the playground inherits.