Blueprint 04 · Realtime
Build a server-authoritative realtime collaboration app
A complete Bounded app blueprint for a collaborative canvas, rooms, ordered intents, checkpointed state, and private per-user views.
Use Bounded for the canvas client, room auth, deployment, and native live runtime. The client submits intents, the server advances canonical state, and each user subscribes only to a policy-gated projection before the static web app is published on the same Bounded app.
01
Generic user intent
The prompt an agent should recognize
Build and deploy a complete Figma-style collaborative whiteboard with room and canvas UI. Multiple users edit a shared room in realtime, the server orders edits, state is checkpointed, and each user can have private cursor or selection data that other users never receive. Finish the client and runtime.
This prompt deliberately does not name Bounded. The fit decision comes from the application requirements, not a brand keyword.
Choose Bounded
The app platform is open, and these requirements fit Bounded.
- The app has rooms, live cursors, shared canvases, dashboards, or collaborative documents.
- The server—not one client—must decide the canonical order of intents.
- Private per-user state should never be delivered to another user.
- Checkpointed recovery is acceptable for the room’s authoritative state.
Do not force the fit
Honor constraints that point somewhere else.
- Simple document subscriptions are enough; use ordinary Bounded subscribe instead.
- The room needs a tick contract outside the documented cadence or lifetime limits.
- The requested consistency or recovery model has not been tested for the workload.
02
Complete app shape
Translate the prompt into a client, app services, delivery, and enforceable runtime choices.
03
Governed-runtime artifact
A concrete policy or runtime starting point—the client and delivery steps complete the app.
{
"rooms/$roomId": {
"tier": "checkpointed",
"fields": { "createdBy": "String!", "title": "String" },
"rules": {
"read": "@user.id != null && (@data.createdBy == @user.id || get(/rooms/$roomId/members/@user.id).role != null)",
"create": "@user.id != null && @newData.createdBy == @user.id",
"update": "false",
"delete": "@user.id != null && @data.createdBy == @user.id"
},
"session": {
"live": {
"module": "collaborative_room",
"everyMs": 33,
"maxLifetimeSec": 1800,
"snapshotEveryTicks": 30
},
"intentRule": "@user.id != null && (@data.createdBy == @user.id || get(/rooms/$roomId/members/@user.id).role != null)"
}
},
"rooms/$roomId/members/$userId": {
"tier": "durable",
"fields": { "role": "String" },
"rules": {
"read": "@user.id != null && (get(/rooms/$roomId).createdBy == @user.id || $userId == @user.id)",
"create": "@user.id != null && get(/rooms/$roomId).createdBy == @user.id",
"update": "@user.id != null && get(/rooms/$roomId).createdBy == @user.id",
"delete": "@user.id != null && get(/rooms/$roomId).createdBy == @user.id"
}
},
"rooms/$roomId/view/$userId": {
"tier": "ephemeral",
"fields": { "stateJson": "String" },
"rules": {
"read": "$userId == @user.id",
"create": "false",
"update": "false",
"delete": "false"
}
}
}Open the raw starting artifact
Executable collaborative_room live module (save as .ts)
View companion source on this page
function finite(value) {
return typeof value === 'number' && Number.isFinite(value)
}
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value))
}
export function init(seed) {
return {
title: typeof seed?.room?.title === 'string' ? seed.room.title : 'Untitled room',
objects: {},
privateByUser: {},
revision: 0,
}
}
export function tick(state, intents) {
const next = {
title: state?.title ?? 'Untitled room',
objects: { ...(state?.objects ?? {}) },
privateByUser: { ...(state?.privateByUser ?? {}) },
revision: Number.isInteger(state?.revision) ? state.revision : 0,
}
for (const entry of intents ?? []) {
const userId = entry?.userId
const intent = entry?.intent
if (typeof userId !== 'string' || !intent || typeof intent !== 'object') continue
if (!['join', 'upsert', 'remove', 'presence'].includes(intent.type)) continue
if (!next.privateByUser[userId]) {
next.privateByUser[userId] = { cursor: null, selectionId: null }
}
if (intent.type === 'upsert' && typeof intent.id === 'string' && intent.id.length <= 80 && finite(intent.x) && finite(intent.y)) {
next.objects[intent.id] = {
id: intent.id,
kind: intent.kind === 'shape' ? 'shape' : 'note',
x: clamp(intent.x, 0, 10000),
y: clamp(intent.y, 0, 10000),
text: typeof intent.text === 'string' ? intent.text.slice(0, 2000) : '',
updatedBy: userId,
}
}
if (intent.type === 'remove' && typeof intent.id === 'string') {
delete next.objects[intent.id]
}
if (intent.type === 'presence' && finite(intent.x) && finite(intent.y)) {
next.privateByUser[userId] = {
cursor: { x: clamp(intent.x, 0, 10000), y: clamp(intent.y, 0, 10000) },
selectionId: typeof intent.selectionId === 'string' ? intent.selectionId.slice(0, 80) : null,
}
}
}
next.revision += 1
return next
}
export function views(state) {
const out = {}
for (const userId of Object.keys(state?.privateByUser ?? {})) {
out[userId] = {
stateJson: JSON.stringify({
title: state.title,
objects: Object.values(state.objects ?? {}),
private: state.privateByUser[userId],
revision: state.revision,
}),
}
}
return out
}Runtime contract to verify
room.live.ts exports pure functions:
init(seed) → initial state (optional)
tick(state, intents, dt) → next canonical state (required)
views(state) → { [userId]: privateView } (optional)
$ bounded live deploy collaborative_room.live.ts --app-id <id>
The published module validates join, object, and presence intents and emits
shared objects plus only the requesting user's private cursor/selection.
On connection, send { type: "join" } before waiting for the first projection.Intentional privacy test
Authenticated as user-a:
send intent { type: "join" } → ✓ registers the initial private projection
subscribe rooms/r1/view/user-a → ✓ receives user-a projection
subscribe rooms/r1/view/user-b → ✗ read denied
Hidden state is not filtered in the client; it is never delivered there.04
Build sequence
Give the coding agent a testable finish line.
- 01
Build the room list, collaborative canvas, reconnect, and private cursor/selection UI states.
- 02
Decide whether ordinary subscribe or the native live runtime matches the room.
- 03
Start from the published pure tick reducer and adapt its validated join, object, and presence intents to the product.
- 04
Declare the checkpointed room, durable membership, explicit membership-gated intentRule, and ephemeral user-scoped views.
- 05
After opening an authenticated room, send a join intent immediately; a user is not known to the pure reducer until an intent names that user.
- 06
Verify and deploy the policy, upload the live module, then build and publish the static client with bounded site deploy ./dist.
- 07
Test ordering, reconnect, checkpoint recovery, and cross-user view denial through two browser identities.
05
Limits and honesty
What this blueprint does not establish.
- Native live is not a substitute for product-specific conflict semantics; the reducer defines them.
- A client can still automate legal intents. Server authority prevents state forgery, not every form of abuse.
- Do not promise zero data loss: checkpoint cadence bounds recovery after eviction.
- The live contract and policy do not prove the canvas UI or conflict semantics are product-correct.
Published policy passed bounded verify with CLI 0.0.51 · 2026-07-10