--- title: "Multiplayer" description: "Use Rivet Actors to coordinate shared sessions." icon: "users" --- For multiplayer orchestration, use [Rivet Actors](https://rivet.dev/docs/actors). Recommended model: - One actor per collaborative workspace/thread. - The actor owns Sandbox Agent session lifecycle and persistence. - Clients connect to the actor and receive realtime broadcasts. Use [actor keys](https://rivet.dev/docs/actors/keys) to map each workspace to one actor, [events](https://rivet.dev/docs/actors/events) for realtime updates, and [lifecycle hooks](https://rivet.dev/docs/actors/lifecycle) for cleanup. ## Example ```ts Actor (server) import { actor, setup } from "rivetkit"; import { SandboxAgent, type SessionPersistDriver, type SessionRecord, type SessionEvent, type ListPageRequest, type ListPage, type ListEventsRequest } from "sandbox-agent"; interface RivetPersistData { sessions: Record; events: Record; } type RivetPersistState = { _sandboxAgentPersist: RivetPersistData }; class RivetSessionPersistDriver implements SessionPersistDriver { private readonly stateKey: string; private readonly ctx: { state: Record }; constructor(ctx: { state: Record }, options: { stateKey?: string } = {}) { this.ctx = ctx; this.stateKey = options.stateKey ?? "_sandboxAgentPersist"; if (!this.ctx.state[this.stateKey]) { this.ctx.state[this.stateKey] = { sessions: {}, events: {} }; } } private get data(): RivetPersistData { return this.ctx.state[this.stateKey] as RivetPersistData; } async getSession(id: string) { const s = this.data.sessions[id]; return s ? { ...s } : undefined; } async listSessions(request: ListPageRequest = {}): Promise> { const sorted = Object.values(this.data.sessions).sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id)); const offset = Number(request.cursor ?? 0); const limit = request.limit ?? 100; const slice = sorted.slice(offset, offset + limit); return { items: slice, nextCursor: offset + slice.length < sorted.length ? String(offset + slice.length) : undefined }; } async updateSession(session: SessionRecord) { this.data.sessions[session.id] = { ...session }; if (!this.data.events[session.id]) this.data.events[session.id] = []; } async listEvents(request: ListEventsRequest): Promise> { const all = [...(this.data.events[request.sessionId] ?? [])].sort((a, b) => a.eventIndex - b.eventIndex || a.id.localeCompare(b.id)); const offset = Number(request.cursor ?? 0); const limit = request.limit ?? 100; const slice = all.slice(offset, offset + limit); return { items: slice, nextCursor: offset + slice.length < all.length ? String(offset + slice.length) : undefined }; } async insertEvent(sessionId: string, event: SessionEvent) { const events = this.data.events[sessionId] ?? []; events.push({ ...event, payload: JSON.parse(JSON.stringify(event.payload)) }); this.data.events[sessionId] = events; } } type WorkspaceState = RivetPersistState & { sandboxId: string; baseUrl: string; }; export const workspace = actor({ createState: async () => { return { sandboxId: "sbx_123", baseUrl: "http://127.0.0.1:2468", } satisfies Partial; }, createVars: async (c) => { const persist = new RivetSessionPersistDriver(c); const sdk = await SandboxAgent.connect({ baseUrl: c.state.baseUrl, persist, }); const session = await sdk.resumeOrCreateSession({ id: "default", agent: "codex" }); const unsubscribe = session.onEvent((event) => { c.broadcast("session.event", event); }); return { sdk, session, unsubscribe }; }, actions: { getSessionInfo: (c) => ({ workspaceId: c.key[0], sandboxId: c.state.sandboxId, }), prompt: async (c, input: { userId: string; text: string }) => { c.broadcast("chat.user", { userId: input.userId, text: input.text, createdAt: Date.now(), }); await c.vars.session.prompt([{ type: "text", text: input.text }]); }, }, onSleep: async (c) => { c.vars.unsubscribe?.(); await c.vars.sdk.dispose(); }, }); export const registry = setup({ use: { workspace }, }); ``` ```ts Client (browser) import { createClient } from "rivetkit/client"; import type { registry } from "./actors"; const client = createClient({ endpoint: process.env.NEXT_PUBLIC_RIVET_ENDPOINT!, }); const workspaceId = "workspace-42"; const room = client.workspace.getOrCreate([workspaceId]); const conn = room.connect(); conn.on("chat.user", (event) => { console.log("user message", event); }); conn.on("session.event", (event) => { console.log("sandbox event", event); }); await conn.prompt({ userId: "user-123", text: "Propose a refactor plan for auth middleware.", }); ``` ## Notes - Keep sandbox calls actor-only. Browser clients should not call Sandbox Agent directly. - Copy the Rivet persist driver from the example above into your project so session history persists in actor state. - For client connection patterns, see [Rivet JavaScript client](https://rivet.dev/docs/clients/javascript).