Skip to content

Schemas as documents

Take any document type — say task. Its schema is itself a document: sys:schema:task, of type sys:schema. It is created and updated through the same API as user data, synced to clients like user data, and validated like user data — against a meta-schema.

A schema document declares a document type’s shape, plus the rules that govern it:

  • Field types — objects (fixed, named keys), records (open string-keyed maps), arrays, strings, numbers, booleans, and enums. And json — a field that holds any JSON value, the escape hatch from a declared shape.
  • References — fields that point at other documents (or at records within the same document), with integrity rules for what happens when the target disappears. See References & integrity.
  • Defs — reusable type definitions, referenced from fields so a shape can be declared once and shared across the schema.
  • Access rules — the declarative per-type role rules the engine evaluates on every write and read. See Authorization.
  • Migrations — an append-only log of schema evolutions (rename, remove, remap). A document written under an old schema version is brought forward on its next read and the migrated data is persisted back — the full story is on Schema evolution.

Every document type has a schema — there is no schemaless document. When you need loose or open-ended data, reach for a json field: it rides inside a validated type and syncs and patches like any other field — accepting any JSON-encodable value, just without a declared shape.

One rule holds for every document, typed fields and json alike: strings — values and object keys — are well-formed Unicode, as I-JSON requires. A lone surrogate (half an emoji, left by a careless .slice) is rejected at the write like any other invalid data.

One API. Evolving the data model is a document write, not a deploy. The same subscription machinery that gives you live user data gives you live schema changes.

The model is editable at runtime. Introducing a document type is a document write — no deploy, no code generation, no out-of-band tooling. A user building types from a UI and an AI agent that decides it needs a new type take the very same path.

One timeline, not two. As documents, schemas have their history in the same event log as the data they govern — introspecting a document as it was at any point in time means looking in exactly one place, not correlating deploy times against data versions. It’s also what makes the data portable: a folder’s history carries its own interpretation with it.

Schemas version like data. Sequence numbers, event history, and guarded writes apply to schema changes exactly as they do to data changes.

Schemas being documents doesn’t mean giving up TypeScript. There are three ways to source them, and they compose:

Authored in TypeScript. A schema can be defined in code, and the document data types are inferred from the schema literal — compile-time validation and typed reads/writes with no code-generation step. The defined value is a plain schema document’s data; the types are phantom.

Upserted at startup. The host application writes its TypeScript-defined schemas into the sys:schema:* documents when a folder boots — a no-op when nothing changed. Rolling out a schema change in code becomes a document update in each folder, still subject to the server’s append-only migration rules. This is what makes TypeScript authoring compatible with the one-timeline argument above: the upsert turns every code change back into document history, so even when git is where schemas are written, the schema documents remain where their history lives — the repository never becomes a second timeline you’d have to consult.

Document-driven only. Types created at runtime — by a user, or by an agent — exist purely as schema documents, with no TypeScript counterpart.

Whatever the source, the server’s write path always validates against the schema documents — the TypeScript definitions are never a second, shadow truth. Clients then choose per app: bundle the static TypeScript schemas (typed, instant optimistic validation, no waiting for schemas to sync) or, with dynamic schemas enabled, subscribe to sys:schema:* and validate against whatever the folder currently declares — required when document types are born at runtime.

A dynamic-schema client doesn’t write a type until it knows that type’s schema. createDocumentAndWait and updateDocumentAndWait wait for it before they stage the write. The fire-and-forget createDocument and updateDocument throw if the schema isn’t resolved yet, so wait first:

const schema = await session.waitForSchema("chatConversation", { signal });
session.createDocument({ docId, type: "chatConversation", data });

waitForSchema asks about the type alone. No document of the type has to exist, and nothing is subscribed on your behalf. It resolves with where the schema comes from:

source Meaning
dynamic The client holds the type’s sys:schema:<type> document. Carries sequence and confirmed
static The folder has no schema document for the type; the bundled schema validates it
none No schema document and no bundled schema. Nothing validates the type, so writes are refused
staged A staging session only: the schema is staged in that session and not committed yet

A schema held in the offline cache counts. confirmed is false until the server has answered, and the write is still accepted, so an app that boots offline with its schemas cached can author. A client that boots offline with nothing cached has no way to know, and the wait stays pending until it connects. Pass a signal to bound it.

The answer holds for a write you issue right away. It can change later: when someone creates a schema document for a type that had none, the type is unresolved until that document has synced. Wait again before a later plain write, or use the awaited writes, which hold through it.

If the schema could not be read (the server answered an error for sys:index or for the schema document), the wait rejects with a SchemaUnavailableError and writes of the type are refused with the same error. An absent schema is not an error: that is the static or none answer.

Before an update of a document you know exists, await the prepared document’s available. It includes the schema wait and also asserts the document is readable, so it rejects for a missing or deleted document. For a create-or-update, use waitForSchema.