Where Data Lives
Storage is backed by Cloudflare Durable Objects, each running its own SQLite database. Here’s the key mapping:- One canvas = one RecordRoom = one Durable Object = one SQLite database
- All widgets on the same canvas share the same RecordRoom
- Each RecordRoom is identified by a
roomId(passed to widgets via URL parameter)
schemas.ts.
What’s Stored
| Table | Contains |
|---|---|
records | All your application data, across all collections |
users | Users who have accessed this canvas/app |
teams | Team definitions |
team_members | Team membership + pending invites |
yjs_docs | Collaborative document state (for useYjsText / useYjsField) |
Data Isolation
- Widgets on the same canvas share data. If widget A writes to the
taskscollection, widget B can read from it. - Widgets on different canvases are fully isolated — separate RecordRooms, separate databases.
- When a widget is deployed as a standalone site, it gets its own RecordRoom. Data does not carry over from the canvas.
How It Works
Connection
When a widget loads,RecordProvider (from main.tsx) opens a WebSocket to the RecordRoom:
- Canvas mode: Token relayed from parent frame via
postMessage - Standalone mode: Token from Clerk auth
Query Subscriptions
When you calluseQuery('tasks', { where: { status: 'active' } }), this is what happens:
- Client sends a
SUBSCRIBEmessage over WebSocket with your query - Server runs the query against SQLite and returns matching records
- Server keeps the subscription active — when any record in
taskschanges, it re-evaluates your query and pushes updates - Client’s
RecordStorecaches results and triggers React re-renders
Mutations
When you callcreate(), put(), or remove():
- Client sends the mutation over WebSocket
- Server validates against your schema (fields, types, access rules)
- Server applies the change to SQLite
- Server evaluates all active subscriptions and broadcasts changes to affected clients
- All connected clients (across all widgets on the canvas) get real-time updates
- Fire-and-forget (
create,put,remove) — Returns immediately, doesn’t wait for server confirmation - Confirmed (
createConfirmed,putConfirmed,removeConfirmed) — Returns a promise that resolves when the server acknowledges the write, or rejects on error/timeout
Reconnection
If the WebSocket drops (network change, browser backgrounding, etc.), the client automatically reconnects with exponential backoff (up to 30 seconds). On reconnect, all active subscriptions are re-sent and data is refreshed.Schemas and Access Control
Schemas serve two purposes: they define your data shape, and they control access.Role Hierarchy
admin → member → viewer
admin— Elevated privileges. Can manage users, teams, and admin-level collections.member— Standard user. Can read and write to collections withmemberaccess.viewer— Can view but not modify. Good for public-facing reads.
Per-Record Ownership
owner— Not a role in the hierarchy. Whenaccess.readoraccess.writeis'owner', only the user who created the record (matched byuserId) can access it. This works independently of roles — even an admin can’t read another user’sowner-scoped records.
File Storage (R2)
For files (images, PDFs, etc.), widgets use Cloudflare R2 via theuseR2Files hook. Files are scoped:
'self'(default) — Widget’s own files'user'— Current user’s personal files'widget'— Another widget’s files (requireswidgetId)
Collaborative Editing (Yjs)
For real-time collaborative text editing (like Google Docs), the storage system includes Yjs integration. When you useuseYjsText or useYjsField:
- Client creates a local Yjs document
- Server syncs the document state via binary WebSocket messages
- Local changes are broadcast to all connected clients editing the same document
- Server persists the Yjs state to the
yjs_docstable