YouBothAgent▾
You — Business rules and flows you own. Read these yourself.
Both — Know the idea; your agent follows the details.
Agent — Conventions and references your agent follows. Look up as needed.
General▾
Interface▾
Observability▾
Realtime
Realtime keeps one WebSocket open, so the server and the browser trade small events without a new request each time. Chat, games, live editors, dashboards and presence all run on it.
| Tool | Direction |
|---|---|
| ↳ Use it for | |
| message | Browser → server |
| e.g. read receipts, cursor moves, typing status, game input | |
| pubsub | Server → everyone subscribed to the room |
| e.g. a new chat message, a notification | |
| .live() | Database → every screen showing the list |
| e.g. a list of saved rows, such as a chat history | |
- A room decides who receives. A
pubsubevent reaches only the browsers subscribed to that room. - A saved list needs no pubsub. With
.live()on its slice, saving the row is enough.
Send With message
Use
message for small actions the browser sends to the server: a read receipt, a cursor move, typing status, or game input. A read receipt declares its arguments with .msg():apps/myapp/lib/chat/chat.signal.ts
- Each
.msg()is one argument. The browser callsfetch.readMessage(chatId, messageId)in the same order. - The reader comes from
.with(Self). Never trust a user id the browser sends. - The answer goes to the sender only. The return value arrives at
fetch.listenReadMessage(fn)in the browser that sent it. - Name the guards yourself. A
messageis checked only by theguardsin its own option; hereUserrequires a signed-in user.
Broadcast With pubsub
Use
pubsub when the server sends one event to everyone in a room. A new chat message is the simplest example. The signal names the room and what each subscription sets up:apps/myapp/lib/chat/chat.signal.ts
.room()names the room. The room is this endpoint plus thechatId, so every chat is a room of its own.execruns when a browser subscribes. It sends nothing; publishing is the service's job, shown in Chat Flow below.- Register cleanup for both events.
unsubscribefires when the browser leaves the room anddisconnectwhen the socket closes; a handler on both runs only once. ws.socketIdis a connection, not a user. A reconnect gets a new id, so keep per-user state on the account.


A pubsub has no guard unless it declares one. The slice's guard map reaches only the generated query and mutation endpoints, so a room without its own
guards array is open to anyone who can open a socket.Calling From the Browser
Each
message and pubsub becomes a fetch function in the browser:CallWhat it does
fetch.readMessage(chatId, messageId)
Sends the message and returns nothing.
fetch.listenReadMessage(fn)
Runs
fn with each answer this browser gets and returns a function that stops listening.fetch.subscribeMessageAdded( chatId, fn)
Joins the chat's room, runs
fn on every publish, and returns an unsubscribe function.Subscribe inside a
useEffect and return the unsubscribe function as its cleanup.Chat Flow
A chat message reaches the screen in four steps. The list itself never needs a hand-written subscription.
1. Save, Then Publish
For data that must be saved, write to the database first and publish after the service succeeds:
apps/myapp/lib/chat/chat.service.ts
- Room arguments first, payload last.
messageAdded(chatId, chatMessage)publishes to that chat's room. - A failed save publishes nothing. If
createChatMessagethrows, no browser sees a message that was never stored. - The signal is injected by field name. A field named
chatSignalresolves to thechatmodule's signal.
2. Declare a Live Slice
Declare
.live() on the slice that feeds the list:apps/myapp/lib/chatMessage/chatMessage.signal.ts
- Saving is enough for the list. Every document create, update and remove reaches each open list, so
messageAddedis only for other listeners. - Query-level writes are not seen.
update<Filter>,remove<Filter>,updateByIdandremoveByIdfire no hooks, so they never reach a live list. - The room uses the slice's guards. A live room delivers the same rows the list would, so name the guards on
init().
3. Load the Snapshot in the Route
The browser does not fetch the first list itself. The route loads the slice before the first byte and hands the snapshot down as an
init prop, so the first paint is server HTML:apps/myapp/page/chat/[chatId]/_index.tsx
4. Draw It With Load.Units
Load.Units seeds the store from that snapshot, and the live slice keeps the list in sync from there:apps/myapp/lib/chatMessage/ChatMessage.Zone.tsx
- The room opens by itself.
Load.Unitssubscribes to the live slice, so the list needs no effect. - Nothing in the Zone fetches. No
useStateholds server data.
Design Rooms
The room key decides who receives an event, so keep it as narrow as the audience.
- Use a narrow key such as
chatId,gameId, ordocumentId. - Avoid one huge room for all users unless everyone truly needs the event.
- Guard who may send and subscribe.
Useronly checks sign-in; to keep a room to its members, write a guard that readschatIdwithcontext.getArg("chatId"). - Guards run again when the login changes. Each subscribed room is re-checked and dropped if it now fails, so keep guards free of side effects.
Tips
- Keep payloads small. Send ids and small patches instead of whole pages of data.
- Commands up, notifications down. Use
messagefor commands andpubsubfor notifications. - Save first when losing the event is dangerous. Publish only after the save succeeds.
- Throttle very frequent events on the client. Game input and cursor moves are the usual cases.
- Stream bytes as
pubsub(Binary). It skips JSON, and a subscriber that falls behind gets only the newest frame unless you declare{ backpressure: "queue" }.