Authorization
Every access in datadata runs as a principal — one identity model that covers a user’s frontend actions, server-side system code, and AI-generated edits alike. The host attaches a principal to each connection (and to in-process clients), and the engine gates it on both sides: it authorizes every stored-document write — create, update, delete, rename, restore, purge — and filters every read. A denied write is rejected like any other rejected write, so the writer’s optimistic update simply rolls back; a document a principal may not read simply does not exist for it (see Read filtering).
The principal
Section titled “The principal”A principal carries four things:
- Subject — who the access is on behalf of: a user or service identity, or nobody for anonymous.
- Actor — what kind of code is exercising it: a user’s frontend, a named
AI agent (
agent:chat), or server-side system code (sys:schema-seed). Actors are provenance and policy-targetable labels, carried so edits stay attributable — never gates of their own. - Grants — facts the app resolved at connect time, like a role or a membership.
- Scopes — an optional hard cap the engine enforces no matter what any rule would otherwise allow.
Subject and actor are independent: the subject is whose behalf the access is on, the actor only what kind of code is exercising it. A named AI agent therefore acts as its subject, not as a separate identity — which is what keeps its edits attributable to the person behind them.
The composition contract
Section titled “The composition contract”Reads and writes run the same gate. At each choke point — one for writes, a parallel one for reads — three checks compose, each able only to further restrict the one before it:
- Scope attenuation. If the principal carries scopes, the access must fall inside them — for every actor, before any rule is consulted. This is the hard cap for AI agents: an agent scoped to creating and updating notes can never delete anything or touch another document type, no matter what the rules say. Scopes attenuate visibility too — read is a scope kind alongside the write kinds, so that same agent sees nothing outside notes unless it is also scoped to read more broadly. (On the read path the system-document exemption is applied even before this cap, so a scoped agent keeps the index, schemas, and its own identity — see Read filtering.)
- Declarative rules. The document type’s access rules (below), evaluated against the folder’s roles — the write-kind rules on the write path, the read rule on the read path.
- App policy. An optional policy for anything the declarative rules can’t express. Both the declarative layer and the policy must allow. The policy decides synchronously, from what it already holds — the principal, synced documents, local state; facts from external sources (a database, an API) belong in connect-time grants, not in the decision path.
An app policy returns either an allow or a reasoned denial, and the reason surfaces to the writer. It is the escape hatch for rules the declarative layer can’t state — “only finance may write invoices”, per-document ownership — decided against the principal and the operation in front of it.
Presence is governed like any document: its presence schema’s access block
decides who may read and write it (a presence write is an update on the
presence:<presenceType> docType, resolved from the channel’s id), so the same
restrictor runs against presence too. The channel is governed independently of
the document it is about — that document’s read rules never gate presence — and
the library adds one structural rule on top, because the schema can’t express
it: per-cell ownership.
System authority
Section titled “System authority”Some host operations must run above the folder’s own policy: seeding schemas at boot, creating the governance document itself, applying an operator’s schema migration. That privilege is a property of the call, never of any principal: a direct server call can carry an explicit system authority marker that skips the policy layers — the governance meta-gate, the access rules, and the app policies — while scope attenuation still binds, so a host can self-attenuate a system call.
Identity never implies authority. No actor value grants anything — a system-actor client connection is an ordinary, policy-bound principal. Only host code holding the server handle can make a system-authority call, and connections have no way to express one, so a client cannot mint privilege by construction. A client that thinks it needs system authority almost certainly wants an admin role grant (which passes the governance meta-gate through policy) or scopes instead.
The maintenance surfaces — export, import, verify, rebuild — sit outside the per-operation gates and require system authority. App policies never see system calls: the policy and the system call are installed by the same host, and a host does not need to guard against itself.
Declarative access rules
Section titled “Declarative access rules”A schema (itself a document) declares access rules in both directions: a write rule per operation — a separate rule for create, update, delete, rename, restore, and purge, plus a fallback for any write kind without its own (purge excepted — see Defaults) — and a read rule governing who may see the type at all. This section covers the write rules; the read rule, whose default is deliberately different, lives under Read filtering.
A rule is one of: anyone, any authenticated subject, nobody, a role, or a
named grant — singly or as a list of alternatives, and the same
vocabulary serves read and write alike. Roles form an ascending hierarchy —
viewer, editor, admin — and a principal’s effective role is the highest
of what the folder’s sys:access roles document assigns it (per subject, or
via folder-wide defaults for authenticated and anonymous subjects) and any
role carried in its connect-time grants. A document type with no write rules
defaults to any authenticated subject; reads default the other way (see
Defaults).
The three roles are fixed — apps cannot define their own or reorder the hierarchy. App-specific privileges beyond the trio ride named grants (matched exactly, with no hierarchy) or the app policy.
Two meta-rules protect the governance surface itself: writing sys:access
or any schema document requires the admin role regardless of the access rules
— an editor cannot rewrite a schema to grant themselves more.
Read filtering
Section titled “Read filtering”Visibility is governed the same way, enforced everywhere the server answers a
read — on direct reads and per recipient on every broadcast. A subscribe
or a history request for a document the principal may not read comes back as
not found (a hidden deleted document included — never revealed as deleted),
the sys:index and sys:trash listings omit it, live updates about it are
withheld, and any listing built on the index never shows it. Hidden
existence is deliberate: nothing a client can observe distinguishes “hidden
from you” from “does not exist”.
Two knobs, mirroring writes:
- The schema’s read rule hides a whole document type from anyone below the required role. Its default is the inverse of the write kinds: with no read rule, anyone admitted to the folder reads — not merely any authenticated subject — and the write fallback does not apply. Read filtering is opt-in per document type.
- An app read policy — where true per-document rules live. Like the write policy it is synchronous and consulted once per recipient per document on a broadcast, so it decides from what it holds: the operation and the principal, not document contents. Deny the matching write too — read filtering hides, it does not lock.
The read rule’s bar is a role, resolved from sys:access and connect-time
grants exactly as the write rules resolve theirs — so sys:access is not a
third read knob but the shared roles dial both axes read. Demote a subject
there and read access falls together with write access in a single edit, which
fires the revocation sweep.
The system documents are shaped, never blocked — blocking one would
strand the client. sys:index and sys:trash arrive filtered to the
reader’s visible entries; schema documents stay readable to everyone admitted
(validation and prediction depend on them); sys:access arrives redacted
(below). Presence is not in this set — it is governed like any document (its
presence schema’s access rules), so a channel can be read-blocked; the denial
is an explicit unauthorized error. Writing without
reading is otherwise legal: a “drop-box” type is readable only by
admins but writable by any authenticated subject (every write kind except
update, which needs the synced copy a non-reader can’t hold). It works
because the authoring connection always receives its own writes’ events, so an
awaited write still resolves for its author.
Defaults
Section titled “Defaults”Every rule is optional, and the defaults are deliberately asymmetric: an untouched document type is easy to write and easy to read. Writes lean on “any authenticated subject”, reads lean on “anyone already in the folder” — so both write restriction and read filtering are opt-in per type.
| Axis | Default when the type declares no rule of its own |
|---|---|
| Write | any authenticated subject |
| Read | anyone admitted to the folder |
| Purge | nobody — explicit opt-in per type |
Writes resolve in two steps before that default applies: a kind without its own
rule first falls back to the block’s write rule; only when the block has no
write rule either does any authenticated subject take over. Reads have no
per-kind rules and no such fallback — a type with no read rule is simply
visible to everyone admitted to the folder.
Purge inverts the write default entirely. Hard deletion is irreversible, so
a type with no purge rule cannot be purged over the wire by anyone — admins
included — and the write fallback does not apply. The asymmetry is
deliberate: in a folder with no sys:access document every authenticated
subject acts as admin (see Bootstrapping access), so
any role-based default would hand irreversible destruction to anyone
authenticated in an ungoverned folder. A schema opts in explicitly, e.g.
access: { purge: { role: "admin" } }. (The host’s own retention sweep runs
under system authority and is unaffected.)
Two things sit outside the table and override it: scopes on the principal
(a hard cap checked first, for every actor) and the governance meta-gate
(writing sys:access or a schema always needs admin, whatever the block
says). And when the whole sys:access document is absent — not just a rule — a
folder-wide default takes over instead, which is the next section.
Bootstrapping access
Section titled “Bootstrapping access”A folder without a sys:access document treats authenticated subjects as
admin: governance is opt-in, and somebody has to be able to create the
document that grants the right to create it. Apps that hand out roles should
seed the roles document when the folder is created; an app whose roles ride
connect-time grants alone should seed an empty one, or the bootstrap
default quietly makes every authenticated subject an admin. On hosts that
support it, this seeding runs once at first boot as a
system-authority direct call and is never clobbered by
later admin edits.
Revocation
Section titled “Revocation”When the facts move — a sys:access edit demotes a subject, a schema tightens
a read rule — the server sweeps every live connection: newly-hidden
documents are evicted (the client drops its copy; the document leaves the UI)
and each connection’s filtered index, trash, and redacted sys:access view
refresh. Facts the engine can’t observe itself — grants in connection
metadata, or whatever an app read policy consults — trigger the sweep on
demand. Eviction only drops the subscription, so a re-grant heals on the next
resubscribe: reveal, don’t push.
Mid-connection principal changes are first-class: a host can rewrite a live connection’s principal — a demotion, a grant widening — and the engine re-sweeps reads and refreshes that connection’s served identity, which the client adopts on the spot (see Capability-aware clients). Enforcement, prediction, and capability UI all flip without a reconnect. Forcibly closing a connection remains for actual offboarding — prefer demotion for revocation, disconnection for removal.
Managing access in practice
Section titled “Managing access in practice”A folder is one sync scope; an organization spans many. The split that keeps
membership changes cheap: org-wide facts live in your app’s own store and
ride the connection; sys:access holds only folder-local exceptions.
- Membership and admission stay in the app’s database. Whether a user may connect to a folder at all is the read boundary, and their baseline role arrives as a grant minted at connect time. Onboarding a member is one row in your store — no folder is touched; each folder learns the identity lazily, at connect. Never fan a role change out across folders.
- Folder-local roles go in
sys:access: per-subject exceptions and folder-wide defaults. The server reads it fresh on every write and clients sync it live, so granting or revoking there takes effect on the very next write, no reconnect. - Put ceilings in the document, floors in grants. Effective role is the
max of the two, so a grant only ever adds role — a role you routinely
lower belongs in
sys:access, where an edit takes effect on the next write. Grants can move mid-connection too: a host can rewrite a live connection’s grants and reauthorize in one motion, so even grant-carried role is revocable without closing the socket. Revoking admission itself (offboarding) blocks future connects at the app layer and closes live sockets. sys:accessis redacted per reader. A non-admin member’s synced copy carries only its own role entry plus the folder defaults — never other subjects’ assignments; admins read the full document (as do system-authority direct reads), and its history is admin-only. Redaction preserves every self-verdict by construction, so a member’s own capability prediction keeps working. But it also means a client can only predict for its own principal. Gate any member-management UI on the admin role.
Capability-aware clients
Section titled “Capability-aware clients”The same rule evaluation runs on the client — one shared, pure evaluation, so the two sides cannot drift:
- Identity is server-fed. Every client subscribes a virtual, per-connection identity document carrying the principal the server actually enforces, and adopts it. Prediction and capability discovery therefore need no client-supplied principal (a pre-connect seed remains, but the first served answer corrects it), the “worker mints it, browser mirrors it” drift class is gone by construction, and a host-driven mid-connection change arrives as a fresh adopted identity on the live socket.
- Prediction. A write the server would deny throws locally, before anything is sent, in the same window as optimistic validation. Prediction never falsely blocks: with no principal or unsynced facts it passes through, and the server stays authoritative — a stale “allowed” is corrected by the normal rollback.
- Discovery. A client can ask whether the current principal may perform a given kind on a document type — answered as yes, no, or unknown (“don’t gate the UI yet”). The kinds span the five write kinds plus read; a read “no” predicts the document would be hidden. Delete, rename, and restore, held only by id, resolve their target’s type first, so they are discoverable the same way — not just create and update. There is deliberately no “is this document hidden” query — absence is the signal (hidden existence, again: a hidden document is indistinguishable from a nonexistent one).
Staging and authorization
Section titled “Staging and authorization”Staged sessions compose with authorization rather than bypass it — commit has no privilege of its own. A commit drains through ordinary client writes, authorized exactly as the committer. That makes “the agent proposes, the human disposes” a configuration: scope an agent so it can stage freely but never drain a target, and let the human commit the same persisted changeset.
- A preflight reports the predicted verdict per stage before committing, and the session’s own projection carries the same per-stage flag.
- A denied stage fails as unauthorized and stays staged for retry.
- Every stored event records the writing principal (subject and actor) — so “which edits did the AI make?” is answerable from the committed data, not from logs.
Session-level attenuation
Section titled “Session-level attenuation”A session can carry its own scopes, drawn from the same vocabulary as a
principal’s scopes — for example, a worker session that may only ever update
kanban cards, never create or delete and never touch another type, regardless
of what its principal could otherwise do. Reads through that session are
filtered too: an out-of-scope document reads as absent, and discovery documents
such as sys:index, sys:trash, sys:session, and sys:stage:<docId> are
shaped to the documents still readable through the session.
Session scopes intersect the client’s principal scopes — both gates run — so a session can only ever narrow what its client may read or write through the session facade, never widen it, and never adopt a different identity: a session always writes as its client’s principal. An out-of-scope write is rejected locally, before it leaves the client, in the same unauthorized shape the principal-scope prediction raises.
Session scopes are advisory — a cooperative guardrail for agent tooling and UI, not a security boundary. The underlying connection still holds whatever its principal may read, and real write enforcement stays server-side against that principal, so a caller that bypasses the session API is not stopped by session scopes; use principal scopes, resolved at connect time, for a hard cap.
What this is not
Section titled “What this is not”The declarative layer is folder-role plus document-type policy, not a
per-document ACL system — sys:access encodes no per-document grants. There
are no per-field rules either: read filtering is whole-document — a principal
sees a document entirely or not at all.