Docs / data / Two-layer data model — shared vs per-user

Two-layer data model — shared vs per-user

guide

How useCollection and the agent's data tools resolve a collection to one of two namespaces — a physically isolated per-user database or a single world-readable shared pool — via the layer option.

useCollection (and the agent's data tools) resolve a collection to one of two namespaces via the layer option:

- `layer: 'user'` (per-user) — each end-user gets their own physically isolated MongoDB database (u_{sha256(user_id)[:16]}), not just a filtered view. The backend always keys this DB off the requesting user, so one user literally cannot read or write another user's partition through any path. Reads are already scoped — do not add a user_id/key filter. Use for anything truly private: results, drafts, private history (e.g. matches). - `layer: 'shared'` — one builder database (b_{...}, one per app) that every signed-in user reads (e.g. a directory, catalog, leaderboard). Writes are restricted: the app cannot insert() directly; a collection marked `agent_writable` in the data plan lets the app's own agent append (e.g. a registrar writing member profiles), while blocking arbitrary end-user writes. - `layer: 'auto'` (default) — resolves from the collection's configured data model.

Security — `shared` is not access-controlled per user. Rows in a shared collection carry a _flowstack.user_id field, but that is a filterable field, not an enforced read ACL — any signed-in user can read every row. Treat a shared collection as a public bulletin board at the storage layer; never store data one user must not see from another there. Only the 'user' layer is private.

Platform gap — no cross-user delivery (today). Because the per-user DB is always keyed to the requester, you cannot write into another user's private partition. Patterns that deliver data privately to a specific recipient — direct messages, per-user notifications — are not achievable: the only private store ('user') is unreachable cross-user, and the only cross-user store ('shared') is world-readable. The correct design (deliver into the recipient's per-user partition) needs a cross-user write primitive the platform does not yet expose.

Reads of a shared collection via useCollection({ layer: 'shared' }) always hit the shared pool. Agent-side reads of shared collections are also routed correctly as of P0-132, but the recommended pattern remains "read in the app, pass into the prompt" — it's the most reliable and keeps PII out of the agent.

File Upload → MongoDB: parse a file in the app and insert() the rows directly via useCollection; all useCollection instances for that collection auto-refresh.

Examples

File upload to MongoDB via useCollection
const { insert } = useCollection('transactions');

const handleCSVUpload = async (file: File) => {
  const text = await file.text();
  const rows = Papa.parse(text, { header: true }).data;
  await insert(rows.map(row => ({
    ...row,
    amount: Number(row.amount),
    imported_at: new Date().toISOString(),
  })));
  // All useCollection('transactions') instances auto-refresh
};
Notelayer: 'user' reads are already scoped to the current user — do not add a user_id/key filter. The partition is a separate physical MongoDB database (u_{hash}), always keyed to the requesting user.
Critical`shared` is world-readable. The _flowstack.user_id field on shared rows is a filter, not an access-control boundary — any signed-in user can read every row. Never store private data (messages, PII, secrets) in a shared collection, even filtered by user. Only layer: 'user' is private.
WarningNo cross-user delivery primitive yet. You cannot write into another user's per-user partition, so private per-recipient delivery (DMs, targeted notifications) isn't achievable with current layers — the private store is unreachable cross-user and the shared store is world-readable.
WarningThe app cannot insert() directly into a shared collection. Only an agent acting on a collection marked agent_writable in the data plan may append to the shared pool; arbitrary end-user writes stay blocked.
NoteRecommended pattern remains "read in the app, pass into the prompt" — most reliable and keeps PII out of the agent.

See also

useCollection · reason-write-read · useAgent

Source: README.md#two-layer-data-model--shared-vs-per-user · Also available in llms-full.txt and registry.json.