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 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.