useCollection and useDoc hooks that work like Firestore’s onSnapshot — data updates automatically when changed, whether from the app UI or from an approved agent running in the background.
How it works
Data SDK
The SDK is included in the workspace template atsrc/lib/second-sdk.ts alongside the agent hooks.
useCollection(collectionName)
List all documents in a collection with live updates.
| Return value | Type | Description |
|---|---|---|
data | Doc[] | All documents in the collection, updated live |
loading | boolean | true during initial fetch |
insert(data) | (data: object) => void | Insert a new document |
update(docId, data) | (docId: string, data: object) => void | Partial update (merges into data field) |
remove(docId) | (docId: string) => void | Delete a document |
useDoc(collectionName, docId)
Single document with live updates.
| Return value | Type | Description |
|---|---|---|
data | Doc | null | The document, updated live |
loading | boolean | true during initial fetch |
update(data) | (data: object) => void | Partial update |
remove() | () => void | Delete the document |
Optimistic updates
The plan originally relied entirely on Change Streams for reactivity (write → MongoDB → Change Stream → SSE → re-render). This round-trip would feel sluggish. The SDK applies optimistic local state updates ininsert, update, and remove — the app that initiated the write sees it instantly. The Change Stream event arrives shortly after and reconciles state for all other connected clients.
Database
app_data collection
All app data lives in one MongoDB collection, partitioned by workspaceId + appId + collection.
Indexes
| Index | Purpose |
|---|---|
{ workspaceId: 1, appId: 1, collection: 1, updatedAt: -1 } | Primary query pattern |
{ workspaceId: 1, appId: 1, collection: 1, _id: 1 } | Single doc lookups |
Data isolation
All queries includeworkspaceId + a scoped appId — an app can never access
another app’s data, and a workspace can never access another workspace’s data.
Published runtime uses the app’s normal ID. Draft runtime uses an internal draft
ID for the same app.
Schemaless
Apps don’t need to define schemas. They just write objects. The builder agent knows the data shape because it wrote the code. No migrations, no schema files.REST API
Collection-level
| Method | Path | Purpose |
|---|---|---|
GET | /api/workspaces/[wId]/apps/[aId]/data?collection=leads | List documents in a collection. version=draft uses the draft data scope for collaborators |
POST | /api/workspaces/[wId]/apps/[aId]/data | Insert document { collection, data }. version=draft uses the draft data scope for collaborators |
Document-level
| Method | Path | Purpose |
|---|---|---|
GET | /api/workspaces/[wId]/apps/[aId]/data/[docId]?collection=leads | Get single document |
PATCH | /api/workspaces/[wId]/apps/[aId]/data/[docId] | Update document { collection, data } |
DELETE | /api/workspaces/[wId]/apps/[aId]/data/[docId]?collection=leads | Delete document |
requireWorkspaceContext for auth. Draft data access additionally
requires creator, collaborator, admin, or owner access to the app.
Live updates (Change Streams + SSE)
When data changes in MongoDB (from any source — app UI, agent, direct API), all connected clients see the update in real time.Architecture
SSE event format
AppDataBridge shares this EventSource across tabs for the
same app/version with BroadcastChannel and Web Locks. This keeps live data
reactive without opening one persistent MongoDB Change Stream connection per
tab and exhausting the browser’s per-origin connection budget during long
builder streams.
AppDataBridge also buffers live changes before forwarding them into the
iframe. Bursty agent writes are delivered in small chunks so app data can keep
up without monopolizing the browser renderer. The platform’s Data Explorer only
subscribes to those parent-state updates while the explorer is open; otherwise
the iframe receives the live changes directly and the workspace shell does not
re-render for every inserted document.
Change event handling in SDK hooks
When asecond:data:change message arrives from the parent:
- insert → add document to local array
- update → merge changes into matching document
- delete → remove document from local array
Replica set requirement
MongoDB Change Streams require a replica set. In local development,docker-compose.yml starts MongoDB with --replSet rs0 and a healthcheck that auto-initiates the replica set. In production (e.g., MongoDB Atlas), replica sets are the default — no extra configuration needed.
PostMessage protocol
requestId for request/response matching.
Agent data access
Agents can read and write to an app’s data collections when they havedataCollections defined in their agents.json config. Two MCP tools are registered:
update_app_data
Write data to the app’s database. Supports insert, update, upsert, and delete operations.
upsert operation was added because agents often don’t know if a record already exists. The filter must include _id for update and delete operations.
read_app_data
Read data from the app’s database. List all docs in a collection, or fetch a single doc by ID.
Collection access control
ThedataCollections field in agents.json limits which collections an agent can
access. The worker validates this before calling internal endpoints, and the web
internal endpoints validate it again against the approved agents.json payload
for the calling agent. An agent without dataCollections gets neither tool.
Draft app-agent data tools use the draft data scope and require the current
draft versioned canonical agents.json hash to match an admin/owner approval.
Published app-agent data tools use the published data scope and the approved
payload promoted with the published snapshot.
Internal endpoints
| Method | Path | Purpose |
|---|---|---|
POST | /api/internal/app-data-write | Agent writes data to app collection |
POST | /api/internal/app-data-read | Agent reads data from app collection |
INTERNAL_API_TOKEN. They still require explicit workspaceId, appId, source version, agent ID, and collection values and execute database queries scoped by those fields. See Guard and Tenancy — Internal API bypass.
Agent run status from the app
TheuseAgent hook exposes live run status (idle → running → completed).
AppAgentBridge starts the run, listens for compact workspace run events, and
posts second:agent:update messages to the iframe. A low-frequency watchdog
poll remains as a missed-event fallback, but the bridge does not keep one
high-frequency polling loop per active app agent.
The original plan proposed watching the app_agent_runs collection directly
from the browser. The implementation uses existing workspace realtime events
instead, so app-agent status shares the same compact event channel as other
workspace chrome.
Key files
| File | Role |
|---|---|
apps/web/src/lib/db/repositories/app-data.ts | App data CRUD operations |
apps/web/src/components/app-data-bridge.tsx | postMessage bridge + SSE subscription |
apps/web/src/app/api/.../data/route.ts | REST API (list/insert) |
apps/web/src/app/api/.../data/[docId]/route.ts | REST API (get/update/delete) |
apps/web/src/app/api/.../data/stream/route.ts | Change Stream SSE endpoint |
apps/web/src/app/api/internal/app-data-write/route.ts | Agent data write |
apps/web/src/app/api/internal/app-data-read/route.ts | Agent data read |
apps/worker/src/runner.ts | buildAppDataMcpServer — update_app_data and read_app_data tools |
apps/worker/src/workspace-template.ts | SDK with useCollection, useDoc hooks |