@repo/datadata/schema
Defining document schemas: field types, access rules, and the registry that clients and servers validate documents against.
Classes
PatchBringForwardError
export declare class PatchBringForwardError extends ErrorA patch that cannot be carried across a migration faithfully (see above).
constructor(message: string);Constructs a new instance of the PatchBringForwardError class
Functions
acceptedContentTypes
export declare function acceptedContentTypes(candidates: readonly BlobRefConstraints[]): readonly string[] | null;The media-type patterns a candidate set admits, for a file picker's accept (join with ","): the union of every candidate's accept, or null when some candidate accepts any content type (no accept declared), so the picker should not be narrowed at all. Empty candidates ⇒ empty list.
appendMigrations
export declare function appendMigrations(committed: DocumentSchema["migrations"], added: readonly MigrationToAppend[]): Record<string, SchemaMigration>;The committed log committed extended with added, in the order given, each under a key minted to sort after every committed key and the one minted before it — for a host that writes migrations on behalf of an author who never sees the committed log (an LLM tool, a form-based schema editor), so cannot pick a key itself. Works for any committed log, however its keys were chosen.
A key is a zero-padded counter, then -<label> when the migration has one. The counter continues one the last committed key ends in (0003-… → 0004-…, zebra~0003-… → zebra~0004-…; a number followed by - and a digit, like a date's year, is not a counter) and otherwise extends that key (zebra → zebra~0001-…); an empty log starts at 0001. parseMigrationKey reads a minted key back into those parts, which is why a label cannot start with a digit or contain ~ (it throws): either would make the key read differently.
Every key is checked against compareMigrationKeys, so the result passes migrationLogRefusal against committed — the collation is why a key cannot just be extended blindly (a Thai consonant appended after a prevowel sorts the longer key first) — and a log the rule would refuse throws instead of returning.
bringForward
export declare function bringForward(data: Record<string, unknown>, schema: DocumentSchema | undefined, fromSequence: number): Record<string, unknown>;Bring data forward from fromSequence to the schema's current sequence by replaying the schema's migrations stamped after fromSequence, in key order. Returns a NEW (deep-cloned) object; the input is not mutated. A no-op when the schema is absent or nothing applies.
bringForwardPatch
export declare function bringForwardPatch(patch: readonly Operation[], schema: DocumentSchema | undefined, fromSequence: number, data: Record<string, unknown>, untrusted?: ApplyBudget): Operation[];Rewrite patch, authored against the shape at fromSequence, so it targets the schema's current shape — replaying the schema's migrations stamped after fromSequence over the patch's paths and values, in key order, the way bringForward replays them over a document. data is the document the patch will apply to, already at the current shape; it decides whether a remove actually deleted an operation's target (an object field) or left it (an array slot). An operation whose target a remove migration deletes is dropped: the edit has nothing left to apply to. Throws PatchBringForwardError for a move/copy a migration rewrites inside of differently at its two ends. Returns a NEW array of new operations; the input is not mutated. A no-op when the schema is absent or nothing applies.
An untrusted budget holds the working document's applies to it — together, as one apply of the patch would be — for a patch from a client: a limit it meets is thrown (a JsonPatchError isPatchLimitError matches). Without one the patch is trusted: already accepted once, or this client's own.
checkBlobRefCandidates
export declare function checkBlobRefCandidates(candidates: readonly BlobRefConstraints[], facts: BlobFacts): BlobRefConstraintViolation | null;The verdict over a CANDIDATE set — every blobRef leaf an upload might land in (JsonValidator.blobRefConstraintsAt, or the type-wide blobRefConstraints): null when some candidate accepts the facts, else the violation to report. Where one candidate refused only on size (the content type was fine) the size story is the actionable one, so it wins over a content-type refusal. This is the rule upload staging applies; the write boundary checks the exact leaf.
checkBlobRefConstraints
export declare function checkBlobRefConstraints(constraints: BlobRefConstraints, facts: BlobFacts): BlobRefConstraintViolation | null;The first constraint facts violate, or null when the blob satisfies the leaf. accept is checked before maxBytes; an unknown size (null) cannot violate maxBytes — the write boundary only ever sees finalized (sized) blobs, and staging with an undeclared length defers the size check to it.
compareMigrationKeys
export declare function compareMigrationKeys(a: string, b: string): number;Compares two migration keys in replay order, as a sort comparator: English collation, case- and accent-sensitive, with code-unit order breaking ties so distinct keys never compare equal. The locale is fixed, so every runtime orders a log the same way.
contentTypeMatchesPattern
export declare function contentTypeMatchesPattern(pattern: string, contentType: string): boolean;Whether contentType matches one media-type pattern: an exact essence (image/png) or a type wildcard (image/*). Anything else — a bare type, an extension, * alone — matches nothing, so a mistyped pattern rejects loudly rather than admitting everything.
createJsonParser
export declare function createJsonParser<T extends object = Record<string, unknown>>(schema: DocumentSchema): (data: unknown) => T;Build a throwing parser from a DocumentSchema — the throwing twin of createSafeJsonParser: it throws on a bad value instead of returning a ValidatorResult. This is the shape the wire-event parse sites want — they call it once per inbound envelope and let the error propagate up to the connection-level error handler. The validator is built once at module load; the returned closure reuses it across every call.
Built on the throwing createJsonValidator rather than createSafeJsonParser, so a *malformed schema* fails fast at construction (these parsers are built at module load from static schemas, where a bad schema is a programmer error that should crash loudly). Both factories route through createSafeJsonValidator, so they apply the same checks and never disagree on a value — only the failure channel differs.
Callers pin T (typically via InferJsonSchema<typeof someSchema>) the same way they would with a Zod schema's z.infer.
createJsonValidator
export declare function createJsonValidator<T extends object = Record<string, unknown>, Id extends string = string>(schema: DocumentSchema): JsonValidator<T, Id>;Throwing twin of createSafeJsonValidator: build a JsonValidator from a DocumentSchema, or throw on a malformed schema. This is the fail-fast entry for callers that pass a *trusted* schema — the system registry, tests, and resolveValidator over an already-gated stored schema — where a bad schema is a programmer error that should surface loudly at construction rather than be threaded through a result. Wraps the safe twin, so the two never drift: same checks, only the failure channel differs.
createJsParser
export declare function createJsParser<T = unknown>(schema: JsSchema): (data: unknown) => T;Build a throwing parser from a JsSchema — the js-dialect twin of createJsonParser: one call per value, throws on a bad one. The validator is built once; the returned closure reuses it across every call.
createJsValidator
export declare function createJsValidator<T = unknown>(schema: JsSchema): JsValidator<T>;Build a JsValidator from a JsSchema, or throw on a malformed schema — the js-dialect twin of createJsonValidator. Fail-fast for statically-authored schemas, where a bad schema is a programmer error.
createSafeJsonParser
export declare function createSafeJsonParser<T extends object = Record<string, unknown>>(schema: DocumentSchema): (data: unknown) => ValidatorResult<T>;Build a fully non-throwing parser from a DocumentSchema. A wire-format twin of createSafeJsonValidator: same construction, same validation rules, but exposed as a single-call function returning a ValidatorResult so a caller can branch on result.ok rather than wrap the call in try/catch. This is the shape a per-frame handler wants — one inbound value, one result, no exceptions to unwind. The validator is built once at module load; the returned closure reuses it across every call.
"Fully non-throwing" extends to construction: a *malformed schema* is reported through the same channel as a malformed value — every parse returns the construction error — so the factory itself never throws. A caller that wants a bad *static* schema to fail loudly at module load should reach for the throwing twin, createJsonParser, instead.
Callers pin T (typically via InferJsonSchema<typeof someSchema>) the same way they would with a Zod schema's z.infer.
createSafeJsonValidator
export declare function createSafeJsonValidator<T extends object = Record<string, unknown>, Id extends string = string>(schema: DocumentSchema): ValidatorResult<JsonValidator<T, Id>>;Build a JsonValidator from a DocumentSchema, reporting a malformed schema as { ok: false, error, issues } instead of throwing — the non-throwing twin of createJsonValidator, mirroring the createSafeJsonParser/createJsonParser split one layer down. The gate that validates a *runtime-supplied* schema (a sys:schema write, via createMetaSchemaValidator) consumes this result directly, so schema validation needs no try/catch.
A malformed schema has no value position, so the failure carries a single root (path: []) issue whose message keeps the createJsonValidator: prefix — it labels the construction-time concern and stays greppable across the throwing twin and this one.
createSafeJsParser
export declare function createSafeJsParser<T = unknown>(schema: JsSchema): (data: unknown) => ValidatorResult<T>;Build a fully non-throwing parser from a JsSchema — the js-dialect twin of createSafeJsonParser: a malformed schema is reported through the same channel as a malformed value (every parse returns the construction failure), so the factory itself never throws.
createSafeJsValidator
export declare function createSafeJsValidator<T = unknown>(schema: JsSchema): ValidatorResult<JsValidator<T>>;Build a JsValidator, reporting a malformed schema as { ok: false, error, issues } instead of throwing — the js-dialect twin of createSafeJsonValidator. The failure carries a single root (path: []) issue whose message keeps the createJsValidator: prefix.
defineSchema
export declare function defineSchema<const S extends DocumentSchema>(schema: S): S;Define a document schema. Replaces the … as const satisfies DocumentSchema boilerplate: the const type parameter narrows the literal (so InferJsonSchema still recovers the exact data shape) while validating it against DocumentSchema.
const TestSchema = defineSchema({
root: { type: "object", fields: { title: { type: "string", default: "" } } },
});
type Test = InferJsonSchema<typeof TestSchema>;
To brand the docType's document id — so createDocument / getDocument type its docId as the brand (e.g. ProjectId) instead of a bare string — name the brand tag in the schema, exactly as a record names its keyBrand:
type ProjectId = Brand<string, "ProjectId">;
const ProjectSchema = defineSchema({
docIdBrand: "ProjectId",
root: { type: "object", fields: { … } },
});
What ties the tag to the app's ProjectId alias is the tag string, not a type argument: mistype it and the two brands no longer match, so the first call site handing a ProjectId to this docType fails to compile. The schema-first sibling of createJsonValidator's second type argument.
isArrayIndexSegment
export declare function isArrayIndexSegment(segment: unknown): segment is number;Whether segment can name a JSON array element: a non-negative safe integer.
isBlobIntentPath
export declare function isBlobIntentPath(value: unknown, maxSegments: number): value is readonly (string | number)[];Whether value is a well-formed blob intent path: an array of at most maxSegments segments, each an object field / record key (string) or an array index (a non-negative safe integer — anything else can name no JSON element, so it is refused rather than resolved to nothing).
lintSchema
export declare function lintSchema(schema: DocumentSchema): SchemaLintWarning[];Lint a DocumentSchema for design smells. The schema must already have passed the meta-schema (as one a type is registered with has); this lints design, it does not validate. Returns an empty array for an idiomatic schema and never throws on a valid one. Warnings are ordered by a depth-first walk from the root, defs after the root.
migrationLogRefusal
export declare function migrationLogRefusal(prev: DocumentSchema["migrations"], next: DocumentSchema["migrations"], docId: string): MigrationLogRefusal | null;The first reason the log next cannot follow the committed log prev for the schema document docId, or null when it only extends it. Committed keys are checked first, in replay order, then the new ones.
migrationShapeRefusal
export declare function migrationShapeRefusal(previous: DocumentSchema | undefined, prev: DocumentSchema["migrations"], next: DocumentSchema["migrations"], docId: string): MigrationLogRefusal | null;The first of the NEW migrations of next (the keys prev does not commit) that previous — the schema the write replaces, absent for a schema document's first write — refuses: one whose path steps into an array other than through *, a rename onto itself, a rename without an onConflict policy where to may already be present, or one with a policy where it cannot be. Null when none is. See the header.
orderedMigrations
export declare function orderedMigrations(log: DocumentSchema["migrations"]): [key: string, migration: SchemaMigration][];A migration log's entries in replay order.
parseMigrationKey
export declare function parseMigrationKey(key: string): MigrationKeyParts | null;The parts of key read in the format appendMigrations mints, or null for a key outside it — for a host that re-mints staged keys after the committed log moved on and keeps each one's label. A hand-picked key can be in the format too (0003, zebra~7), and reads as though it had been minted.
standardPropsFromValidate
export declare function standardPropsFromValidate<T>(validate: (data: unknown) => ValidatorResult<T>): StandardSchemaV1.Props<T, T>;Build the Standard Schema ~standard props from a validator's validate, so every datadata validator is consumable by any Standard-Schema-aware tool (tRPC, TanStack Form, Hono validators, …). Synchronous, reports every issue found (the validators collect across the whole value, not fail-fast), and input aliased to output — datadata validators transform (defaults filled, extras stripped), so a faithful distinct input type is future work. types is phantom (type-level only); the runtime value is omitted, which InferInput/InferOutput recover via NonNullable.
validateWithReferences
export declare function validateWithReferences<T>(validator: JsonValidator<T>, data: unknown, refs: ReferenceContext): ValidatorResult<T>;The write gate: validate shape, then (if the validator carries references) check integrity against refs. Only document-write paths call this — the server at create/update, the client for its optimistic writes — so the cost and the ReferenceContext plumbing stay confined to where a write could actually break an invariant. Reads and shape-only checks call validate directly.
Interfaces
BlobFacts
export interface BlobFactsThe catalog facts a constraint is checked against. size is null while unknown (an undeclared upload length).
contentType: string;The blob's media type as stored, parameters included (e.g. image/png; charset=binary).
size: number | null;The blob's length in bytes, or null when not yet known.
BlobRefConstraints
export interface BlobRefConstraintsThe per-field constraints a blobRef leaf may declare over the blobs committed into it. Both are checked against the blob CATALOG's facts (the content type declared at upload, the byte size recorded at finalize), never against the bytes: accept is a UX/schema constraint, not a security boundary. Enforced twice — at upload staging as a type-wide early reject (an upload no blobRef field of the intended docType could accept is refused before any bytes move) and, authoritatively, at the write boundary, where the membership walk pairs every committed handle with the exact leaf it sits in.
accept?: readonly string[];Media-type patterns the blob's content type must match: an exact essence (image/png) or a type wildcard (image/*). Parameters on the stored content type (;charset=…) are ignored, matching is case-insensitive. Absent = any content type.
maxBytes?: number;The blob's maximum byte size (inclusive). Absent = only the server-wide maxBlobSizeBytes applies.
BlobRefConstraintViolation
export interface BlobRefConstraintViolationOne violated constraint, with the category the server rejection should carry.
constraint: "accept" | "maxBytes";"accept" when the content type matches none of the field's patterns, "maxBytes" when the blob is too large.
message: string;A human-readable description naming the offending value and the limit.
JsonValidator
export interface JsonValidator<T = unknown, Id extends string = string, In = T, Mut = T> extends StandardSchemaV1<T, T>A validator for one document schema, built by createJsonValidator: it checks shape and referential integrity, fills defaults, and reports the Yjs and blob references a document holds. Also a Standard Schema (StandardSchemaV1).
T is the stored (read) data type, Id the docType's document-id type, In the create-input type and Mut the type an updateDocument callback edits. Id, In and Mut exist only at the type level, carried by the phantom __docId, __input and __mutable properties.
readonly __docId?: Id;Phantom carrier for the docType's branded document-id type (e.g. ProjectId). Compile-time only — never assigned at runtime; it exists so a registry can recover the id brand by type (TypedDocumentId), letting the client brand docId per docType the same way TypedDocumentData brands data. Defaults to string, so a validator built without an explicit id brand (the system types, the dynamic/AI path) leaves docId an unbranded string.
readonly __input?: In;Phantom carrier for the docType's *create-input* type — the write shape createDocument accepts, where fields with a declared default (and optional/autoIndex fields) may be omitted because the validator backfills them. Compile-time only, like JsonValidator.__docId; it lets a registry recover the input type by type (TypedDocumentInput) the same way TypedDocumentData recovers the output type. Defaults to T (the output), so a validator built without a pinned input type treats create input and stored output as one — the prior behaviour.
readonly __mutable?: Mut;Phantom carrier for the docType's *mutable* type — the shape an updateDocument callback's doc.data takes: keys present like the output T, but json leaves are the write shape JsonInput so a value can be written into a json field. Compile-time only, like JsonValidator.__input; it lets a registry recover the mutable type by type (TypedDocumentMutable). Defaults to T (the output), so a validator built without a pinned mutable type hands the plain output shape to the callback — the prior behaviour.
readonly "~standard": StandardSchemaV1.Props<T, T>;The Standard Schema v1 surface (https://standardschema.dev). Every datadata validator is a Standard Schema: this property exposes what validate(data) does with its defaults — the shape check plus *intra-document* referential integrity (repair followed by the restrict/inbound/scoped-existence checks) — mapped to { value } / { issues }, so any Standard-Schema-aware tool can consume a datadata schema directly. The only part it omits is *cross-document* (toType) and Yjs reference integrity: that needs a ReferenceContext a generic consumer can't supply (see validateWithReferences). input is aliased to output for now.
blobRefConstraints: readonly BlobRefConstraints[];The constraints declared by EVERY blobRef leaf in the schema — one entry per leaf, every union variant and oneOf option included, a leaf with no constraints contributing {}. Data-independent: upload staging uses it as the type-wide early reject (an upload no leaf could accept is refused before any bytes move), leaving the exact per-field verdict to the write boundary's data-aware JsonValidator.collectBlobRefs.
blobRefConstraintsAt(path: readonly (string | number)[], data?: unknown): readonly BlobRefConstraints[];The constraints of the blobRef leaf at path — the superset an upload bound for that position must satisfy, for UI that builds a picker from a (possibly runtime) schema and for narrowing upload staging. A path segment is an object field / record key (string) or an array index (number). With data, a union along the path resolves to the variant the data's discriminator selects, so the result is normally ONE entry; without it (or where the data has not set the discriminator yet) every variant's leaf at that path is a candidate. A oneOf narrows to the option the data at that node validates against (the same pick as validation), else contributes each of its blobRef options — note that a slot not yet filled under a REQUIRED blobRef validates against no option, so it keeps every option; a discriminated union narrows on the tag alone. Empty when no blobRef leaf sits at the path (an unknown field, a non-blob leaf). Pair with checkBlobRefCandidates for the "some candidate accepts" pre-check — the same rule staging applies; the write boundary's data-aware check stays authoritative.
checkCrossReferences?(data: unknown, refs: ReferenceContext): ValidationIssue[];Check *cross-document* referential integrity — every toType rule — against refs, returning one "reference" issue per broken reference, located at the referring value and naming its rule and target id, or [] when all hold. This is the half that needs out-of-document facts, so it lives outside validate; the write gate (validateWithReferences) calls it via optional chaining. Present only when the schema declares toType rules.
CONTRACT: the set of ids consulted through refs.documentExists must be a function of data alone — an existence answer may fail the check (and short-circuit on failure) but must never steer WHICH further ids get consulted. The server's two-pass validation (#validateWithReferences in create-server.ts) relies on this: its recording pass answers every lookup true and trusts that this walks a superset of the ids any real pass consults. An existence-dependent rule kind would silently break it.
checkIntraReferences?(data: unknown): string | undefined;Check *intra-document* referential integrity of already-shape-valid (and repaired) data — restrict/inbound/scoped existence — returning an error message or undefined when every rule holds. Pure (no external context). validate runs this by default; it's exposed for callers wanting the check half on its own. Present only when the schema declares intra-document rules.
collectBlobRefs(data: unknown): ReadonlyMap<string, readonly BlobRefConstraints[]>;The blob ids reachable from data per the schema's blobRef fields — the document's live blob membership (the map's keys) — each paired with the constraints of every blobRef leaf it sits in (one entry per occurrence: a handle held in two fields must satisfy both). The write boundary records the keys as the document's blob reference edges (transactionally with the document row) and checks the catalog's facts against the constraints; the blob GC sweep and read authorization both key off the edge index. Pure and deterministic (no external context), like JsonValidator.collectYjsRefs. Empty for a schema with no blobRef fields.
collectYjsRefs(data: unknown): Set<string>;The set of Yjs sub-document ids reachable from data per the schema's yjsRef fields — the document's live Yjs membership. The write-boundary orphan-GC pass diffs this against the document's stored Y.Docs and deletes any whose id is absent (it lost its last referrer). Pure and deterministic (no external context) so the server and client compute the same set — a sync-convergence requirement. Empty for a schema with no yjsRef fields.
declaresBlobRefs: boolean;Whether the schema declares ANY blobRef field anywhere — a data-independent capability (distinct from JsonValidator.collectBlobRefs, which reports the ids a given data actually references). A server without an object storage adapter rejects writes against a blob-declaring schema up front, so a handle can never be committed with no byte store behind it. Equivalent to blobRefConstraints.length > 0.
declaresYjsRefs: boolean;Whether the schema declares ANY yjsRef field anywhere — a data-independent capability (distinct from JsonValidator.collectYjsRefs, which reports the ids a given data actually references). A schema with none can hold no collaborative sub-document at all, so the write gate rejects a Y.Doc creation against such a "fully permissive" schema up front rather than creating it and letting the orphan GC immediately reclaim it.
repairReferences?(data: unknown): unknown;Repair data to intra-document referential consistency — drop cascade referrers whose target is gone and orphan targets that lost their last referrer — before the restrict/inbound checks run, returning the repaired document. Copy-on-write: data is never modified, and the result shares every container no delete passed through (data itself when nothing needed repair). validate applies this for you; it's exposed for callers that want the repair half on its own. Present only on validators built from a schema with intra-document references. Pure and deterministic so client and server repair to the same state (sync convergence).
sourceSchema?: DocumentSchema;The DocumentSchema this validator was constructed from. Carried so schema- level metadata that is NOT part of the value shape — the access block in particular — is reachable wherever a validator is (the client resolves validators for both static-registry and dynamic types through one path, resolveValidator, and reads capability facts off this).
toJsonSchema?(): unknown;Returns a JSON Schema describing the data shape, if the validator can produce one. Used by callers that need to advertise the shape to external tools (AI tool descriptions, API docs, codegen).
validate(data: unknown, options?: ValidateOptions): ValidatorResult<T>;Validate data against the schema: shape, and — by default — *intra-document* referential integrity (cascade/orphan repair followed by the restrict/inbound/scoped-existence checks), everything decidable from the schema and the document alone. The input is never modified. The returned value is the parsed, repaired document, copy-on-write: it shares with the input every container validation left as it was — a plain object or array whose members all passed unchanged — and is the input itself when nothing changed, so treat both as immutable. Pass { references: false } for a pure shape check. *Cross-document* (toType) and Yjs references are never checked here — they need a ReferenceContext; see validateWithReferences.
JsValidator
export interface JsValidator<T = unknown> extends StandardSchemaV1<T, T>A validator over in-memory JavaScript values — the js-dialect twin of JsonValidator, built from a JsSchema (the superset admitting the JS-only leaves bytes/date). Deliberately the *shape half* only:
- No toJsonSchema: a Uint8Array or Date has no faithful JSON Schema representation, so rather than emit a lossy one the surface is absent — a schema that needs an LLM/JSON-Schema face belongs in the json dialect. - No reference/Yjs machinery and no sourceSchema: those are document concerns (references, access, sub-document membership), and a JsSchema cannot declare them.
What remains is exactly what an event/callback/in-memory gate needs: the recursive shape check (defaults filled, extras policy, issue paths) and the Standard Schema surface, so a js-dialect validator plugs into any Standard-Schema-aware tool the same way a document validator does. Leaf values pass through by reference — a validated Uint8Array/Date is the caller's instance, not a copy.
Unlike JsonValidator, T is unconstrained: a js schema's root may be any value shape (a bare { type: "date" } root infers to Date).
readonly "~standard": StandardSchemaV1.Props<T, T>;The Standard Schema v1 surface (https://standardschema.dev) over JsValidator.validate.
validate(data: unknown): ValidatorResult<T>;Validate data against the schema's shape. The input is never modified; the returned value is copy-on-write, sharing with it every container (and every leaf) validation left as it was, so treat both as immutable.
MigrationKeyParts
export interface MigrationKeyPartsThe parts of a key in the format appendMigrations mints.
counter: number;The key's counter as a number (3 in zebra~0003-label).
label?: string;The text after the counter's - (label in zebra~0003-label); absent when the key has none.
stem: string;The key the counter extends (zebra in zebra~0003-label), or "" when the key leads with it.
MigrationLogRefusal
export interface MigrationLogRefusalWhy a submitted migration log is refused, and the key the refusal is about.
key: string;The migration key the refusal concerns.
message: string;A human-readable reason, naming the schema document and the migration.
MigrationToAppend
export interface MigrationToAppendA migration to append, with an optional label to carry in its minted key.
label?: string;Text appended to the minted key after a -; it cannot start with a digit or contain ~.
migration: Unstamped<SchemaMigration>;The migration, without sequence: the server stamps every new migration with the write's own.
ReferenceContext
export interface ReferenceContextThe out-of-document facts a validator needs to enforce *cross-document* references — those a single document's data can't answer on its own. Intra- document references (the task board's taskOrder.*.* -> $.tasks) are pure and never consult this, but a toType rule (a documentRef to another doc) does.
Both client and server build one of these from what they can see — the server resolves documentExists through its storage adapter (one getDocument per referenced id, so a toType rule costs one lookup per referrer) and answers yjsDocExists from the document's own yjsDocs keys — and pass it to the write gate (validateWithReferences) so the same integrity rules hold on both sides.
documentExists(docId: string, type?: string): boolean;Whether a document with docId exists (optionally constrained to type).
yjsDocExists(yjsId: string): boolean;Whether a Y.Doc with yjsId exists (for the reserved yjsRef form).
SchemaLintWarning
export interface SchemaLintWarningOne design smell lintSchema found.
code: SchemaLintCode;Which rule fired.
message: string;What is wrong and the idiomatic shape to use instead, for a schema author to read.
path: string;Dotted path of the offending node from the root, in the migration-path dialect: a named object field by name, a record value or array item as *, a union variant by its key. The root itself is ""; a def's nodes start with defs.<name>.
StandardSchemaV1
export interface StandardSchemaV1<Input = unknown, Output = Input>The Standard Schema v1 interface (https://standardschema.dev), mirrored from the spec. A datadata JsonValidator conforms to it, so any Standard Schema aware tool (tRPC, TanStack Form, Hono validators, ...) can validate with a datadata schema directly.
readonly "~standard": StandardSchemaV1.Props<Input, Output>;The Standard Schema properties: version, vendor, validate and the phantom types.
export declare namespace StandardSchemaV1The types that make up the Standard Schema v1 interface, and helpers to infer its input and output.
interface FailureResultA failed validation: a non-empty list of issues.
readonly issues: ReadonlyArray<Issue>;The issues found, at least one.
type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["input"];Infer the input type a Standard Schema accepts.
type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["output"];Infer the output type a Standard Schema produces.
interface IssueA single validation issue.
readonly message: string;A human-readable description of the issue.
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;The path from the validated root to the offending value, if known.
interface PathSegmentOne step of an issue path, when a bare PropertyKey isn't enough.
readonly key: PropertyKey;The key of this path step.
interface Props<Input = unknown, Output = Input>The ~standard properties a conforming schema exposes.
readonly types?: Types<Input, Output> | undefined;Inferred types — present only at the type level (phantom at runtime).
readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;Validates unknown input and returns a (possibly async) result.
readonly vendor: string;The vendor name of the schema library.
readonly version: 1;The version number of the standard.
type Result<Output> = SuccessResult<Output> | FailureResult;The result of a validation: success carries value, failure carries issues.
interface SuccessResult<Output>A successful validation. The *absence* of issues is the discriminant.
readonly issues?: undefined;Absent on success — its presence is what marks a failure.
readonly value: Output;The validated (and possibly transformed) value.
interface Types<Input = unknown, Output = Input>The phantom input/output type carrier read by StandardSchemaV1.InferInput and StandardSchemaV1.InferOutput.
readonly input: Input;The type the schema accepts as input (type-level only).
readonly output: Output;The type the schema produces on success (type-level only).
ValidateOptions
export interface ValidateOptionsOptions for JsonValidator.validate.
references?: boolean;Whether to enforce intra-document referential integrity (cascade/orphan repair, then restrict/inbound/scoped existence) in addition to the shape check. Defaults to true — these are decidable from the schema and the document alone, so they're on by default. Set false for a pure shape check (e.g. a hot read path that trusts the stored document is already consistent). Cross-document and Yjs references are never checked here regardless; they need a ReferenceContext (see validateWithReferences).
ValidationIssue
export interface ValidationIssueA single validation failure: a message describing *what* is wrong and the property-key path to *where* ([] is the validated root). The message is the leaf description only — it carries no location prefix, because path already encodes the location. This is what lets a Standard-Schema consumer (a form resolver, say) render the message at exactly the field its path points to without a redundant Field 'x': echo. The recursive validators build the path as they descend — each composite node prepends its own key segment to the child's path; the message is left untouched.
readonly kind?: ValidationIssueKind;What broke: "schema" — the value does not conform to the schema's shape (the default; producers may omit it) — or "reference" — a cross-document reference rule failed (the target is missing, wrong-typed, or was deleted out from under the ref). Consumers that surface issues (the invalid marker) normalize the absent case to "schema".
readonly message: string;What is wrong, without a location prefix (the location is path).
readonly path: readonly PropertyKey[];Property keys and array indices from the validated root to the offending value; [] is the root.
readonly reference?: ValidationIssueReference;For a "reference" issue raised by a cross-document reference rule: the rule's name (its key in the schema's references) and the id the value names. Absent on every other issue.
ValidationIssueReference
export interface ValidationIssueReferenceWhich reference rule a "reference" ValidationIssue broke, and the id it names.
readonly rule: string;The rule's name, its key in the schema's references.
readonly targetDocId: string;The id the referring value names, which does not resolve to a document of the rule's toType.
Types
AccessBlock
export type AccessBlock = {
create?: AccessRule;
update?: AccessRule;
delete?: AccessRule;
rename?: AccessRule;
restore?: AccessRule;
purge?: AccessRule;
write?: AccessRule;
read?: AccessRule;
commands?: Readonly<Record<string, AccessRule>>;
};A docType's access rules (a DocumentSchema's access), one AccessRule per operation.
create, update, delete, rename and restore fall back to write when absent, and to any authenticated subject when both are absent. read does not fall back to write: absent, anyone admitted to the folder may read, and a denied read is answered as not found. purge does not fall back either: absent, nobody (not even an admin) may purge over the wire.
commands grants the docType's domain commands, one rule per command name. Each command is its own kind beside update: a principal may hold a command (a postMessage, say) without update, and the rule does not fall back to write — a command with no rule is denied to everyone. A command's rule gates who may invoke it; what the command does to the document is decided by its mutator, and a principal holding update can patch past it (see defineCommands).
AccessCondition
export type AccessCondition = "anyone" | "authenticated" | "nobody" | {
role: AccessRole;
} | {
grant: string;
} | {
subject: string;
};One authorization condition: "anyone" (anonymous included), "authenticated" (any principal with a subject), "nobody" (only trusted server code), { role } (that effective role or higher), { grant } (the principal holds that literal grant) or { subject } (the principal is that subject).
{ subject } is what expresses ownership in a per-document entry of sys:access; it is also accepted in a docType's AccessBlock.
AccessRole
export type AccessRole = (typeof ACCESS_ROLES)[number];An access role. A principal's effective role is the higher of its role in the folder's sys:access document and any role:<r> grant it connected with.
AccessRule
export type AccessRule = AccessCondition | {
anyOf: readonly AccessCondition[];
};One AccessCondition, or { anyOf }: satisfied when any listed condition is.
Brand
export type Brand<K, T> = K & {
__brand: T;
};A nominal type: base type K tagged with brand T, e.g. Brand<string, "TaskId">. The tag is compile-time only; at runtime the value is a plain K.
A schema's brand (on a string), keyBrand (on a record) or docIdBrand annotation infers to exactly this shape, so an app's own type TaskId = Brand<string, "TaskId"> and the inferred id are interchangeable.
BrandedDocumentId
export type BrandedDocumentId<Registry extends ValidatorRegistry> = {
[Type in keyof Registry]: string extends TypedDocumentId<Registry, Type> ? never : TypedDocumentId<Registry, Type>;
}[keyof Registry];The union of every branded document-id type in a registry. DocTypes without an id brand are left out, so a plain string does not satisfy it. A value of this type identifies its own docType, which is what lets getDocument infer the data type from a branded id alone.
DocIdBrandOf
export type DocIdBrandOf<S> = S extends {
docIdBrand: infer B extends string;
} ? string extends B ? string : Brand<string, B> : string;The branded document-id type a schema declares through its docIdBrand tag, or string when it declares none — the schema-registry sibling of the validator's __docId phantom, and the document-id counterpart of the value-level brand / keyBrand tags InferJsonSchema reads. The tag is a string literal (kept literal by defineSchema's const type parameter), so docIdBrand: "TodoListId" reconstructs exactly the Brand<string, "TodoListId"> the app declared as TodoListId.
A tag that WIDENED to plain string (a schema object authored without defineSchema or as const) names no particular brand, so it reads as unbranded rather than as Brand<string, string> — a brand no app type can satisfy, which would leave every call site for that docType uncallable without a cast. Degrading to string keeps such a schema merely untyped.
DocumentSchema
export type DocumentSchema = {
root: JsonSchemaValue;
docIdBrand?: string;
references?: Record<string, ReferenceRule>;
migrations?: Readonly<Record<string, SchemaMigration>>;
access?: AccessBlock;
defs?: Record<string, JsonSchemaValue>;
};A document schema: the shape of one docType's documents, plus its reference rules, migration log, access rules and reusable sub-shapes. Usually written with defineSchema.
root always describes the current shape, whatever the root's kind (an object in the common case). The schema's version is not stored in it: it is the sequence of the governing sys:schema:<docType> document. A document stored under an older sequence is brought forward on read by replaying migrations (see SchemaMigration), then backfilling defaults.
references holds the referential-integrity rules (ReferenceRule) keyed by a rule name, which stays stable across schema edits and appears in violation messages. Absent or {} both mean no rules.
DocumentTypeForId
export type DocumentTypeForId<Registry extends ValidatorRegistry, Id> = {
[Type in keyof Registry]: [TypedDocumentId<Registry, Type>] extends [Id] ? [Id] extends [TypedDocumentId<Registry, Type>] ? Type : never : never;
}[keyof Registry];The docType whose document-id type is exactly Id: the inverse of TypedDocumentId. never when no docType matches; the union of both when two docTypes share one brand.
InferField
export type InferField<F extends JsonSchemaField, D = {}> = InferValue<F, D>;The TS type a JsonSchemaField contributes to its parent object — identical to InferValue; the field-level optional? flag only affects whether the *key* is optional, which InferFieldsG handles.
InferJsonSchema
export type InferJsonSchema<S extends DocumentSchema> = InferJsonSchemaG<"read", S>;The document type produced by createJsonValidator(schema), derived from a DocumentSchema literal. Pass typeof someSchema:
const TodoSchema = { root: { type: "object", fields: { title: { type: "string", default: "" } } } }
as const satisfies DocumentSchema;
type Todo = InferJsonSchema<typeof TodoSchema>; // { title: string }
type TodoInput = InferJsonSchemaInput<typeof TodoSchema>; // { title?: string }
If the schema declares defs, refs under root resolve through them. Defs are picked off the literal type with S["defs"], with {} as the fallback when no defs are declared — so schemas without defs infer identically to before.
Seeing an error like InferValueG<"mutable", …> (or "input") is not assignable to InferValueG<"read", …>? See the triage note on InferJsonSchemaMutable.
InferJsonSchemaInput
export type InferJsonSchemaInput<S extends DocumentSchema> = InferJsonSchemaG<"input", S>;The *input* (write) shape accepted by createDocument: identical to InferJsonSchema except a field carrying a default may be omitted — the validator backfills it at the write gate, exactly like an optional or autoIndex field. This mirrors Zod's input vs output split: callers pass InferJsonSchemaInput to create a document; reads come back as InferJsonSchema. The relaxation applies at every nesting depth, matching the validator's recursive default backfill.
Seeing an error like InferValueG<"input", …> is not assignable to InferValueG<"read", …>? The triage note on InferJsonSchemaMutable covers both modes — substitute "input" for "mutable" throughout: input mode has the same JsonInput leaves, so a create payload passed to a read-typed helper hits the same mismatch.
InferJsonSchemaMutable
export type InferJsonSchemaMutable<S extends DocumentSchema> = InferJsonSchemaG<"mutable", S>;The *mutable* shape handed to an updateDocument callback's doc.data. Keys are present exactly like InferJsonSchema — the callback mutates a document whose defaults are already filled, so nothing is omittable — but a json leaf is the write shape JsonInput (as in InferJsonSchemaInput), so a value written into a json field (e.g. doc.data.events.push(rawEvent)) accepts an undefined-tolerant type. The optimistic overlay and server both sanitize json on write (undefined object props dropped), so JsonInput at the write site matches runtime behavior; reads elsewhere stay InferJsonSchema/JsonValue.
### Triage: InferValueG<"mutable", …> is not assignable to InferValueG<"read", …>
A diagnostic of that form — the mode is deliberately the first type argument so the mismatch is visible up front; the rest is the schema literal expanded at every level, often ending in Type 'JsonInput[]' is not assignable to type 'JsonValue[]' — means a helper reached from an updateDocument callback is typed with the *read* alias while the callback hands out this mutable shape. If the diagnostic adds "Two different types with this name exist, but they are unrelated", that line is a red herring: it is the same generic, instantiated with different modes. The two shapes differ only at json leaves (JsonInput in mutable, the stricter JsonValue in read), which is why a shape with no json field never hits this. The same mismatch appears with "input" in place of "mutable" when a create payload (InferJsonSchemaInput) reaches a read-typed helper — input mode has the same JsonInput leaves, and the same fix applies with the input alias.
The fix is mechanical: derive a mutable twin of the helper's parameter type and use it in every helper the update callback calls:
type ChatMessage = InferJsonSchema<typeof Schema>["messages"][MessageId];
type ChatMessageMutable = InferJsonSchemaMutable<typeof Schema>["messages"][MessageId];
function appendPart(message: ChatMessageMutable, part: Part) { … } // write path
function renderPart(message: ChatMessage) { … } // read path
InferJsSchema
export type InferJsSchema<S extends JsSchema> = InferJsSchemaG<"read", S>;The value type produced by createJsValidator(schema), derived from a JsSchema literal — the js-dialect twin of InferJsonSchema. Same structural walk (the js-only leaves infer to their live types: bytes → Uint8Array, date → Date), same authoring rule: keep the literal narrow with as const satisfies JsSchema.
Two aliases, not three: the input twin is InferJsSchemaInput, but there is deliberately no mutable twin — mutable types an updateDocument callback's doc.data, a document path the js dialect doesn't have.
InferJsSchemaInput
export type InferJsSchemaInput<S extends JsSchema> = InferJsSchemaG<"input", S>;The *input* shape a js validator/parser accepts — the js-dialect twin of InferJsonSchemaInput: identical to InferJsSchema except a field carrying a default may be omitted (the validator backfills it, exactly like an optional/autoIndex field), and a json leaf is the undefined-tolerant JsonInput. The js-only leaves are unchanged — a live Uint8Array/Date is its own read and write shape.
InferValue
export type InferValue<V, D = {}> = InferValueG<"read", V, D>;Map a JsonSchemaValue literal type to the TS value type it describes — the *output* shape, with every declared default filled in. The input twin is InferValueInput, which relaxes defaulted keys.
D (defaulting to {}) is the schema's defs map. A ref node looks its target up there; a non-ref value ignores it.
InferValueInput
export type InferValueInput<V, D = {}> = InferValueG<"input", V, D>;The *input* twin of InferValue: what a value of the shape V may be written as before validation fills its defaults, so an object field with a default (or optional) may be left out, at any depth. A command's args are sent in this shape (see the commands subpath's CommandArgsInput).
JsonInput
export type JsonInput = JsonPrimitive | JsonInput[] | {
[key: string]: JsonInput | undefined;
};The JSON shape a json field accepts on write (what InferJsonSchemaInput infers for it). Unlike JsonValue, an object property may be undefined; the validator drops such keys, so nothing undefined reaches storage or the read shape.
Array elements still may not be undefined: JSON.stringify would turn [undefined] into [null], a silent data change, so the validator rejects it.
JsonObject
export type JsonObject = {
[key: string]: JsonValue;
};A JSON object, as read and stored: an absent optional field is a missing key, never a present undefined.
A type that declares both a JsonValue index signature and a named optional property ({ foo?: string; [k: string]: JsonValue }) is rejected by TypeScript (TS2411); use JsonInput, the undefined-tolerant write shape, for such types.
JsonPrimitive
export type JsonPrimitive = string | number | boolean | null;A JSON scalar: string, number, boolean or null.
JsonSchemaField
export type JsonSchemaField = JsonSchemaValue & {
optional?: boolean;
};A JsonSchemaValue at an object-field position, where it may also be optional (the key may be absent). A field cannot be both optional and have a default.
JsonSchemaFieldType
export type JsonSchemaFieldType = "string" | "number" | "boolean" | "literal" | "enum" | "json" | "array" | "object" | "record" | "union" | "oneOf" | "ref" | "yjsRef" | "blobRef";The type keywords of the json (document) dialect: every kind a JsonSchemaValue can be.
JsonSchemaUnionVariant
export type JsonSchemaUnionVariant = Record<string, JsonSchemaField>;One variant of a discriminated union: its fields other than the discriminator, keyed by field name. The discriminator value that selects it is its key in JsonSchemaUnionVariants.
JsonSchemaUnionVariants
export type JsonSchemaUnionVariants = Record<string, JsonSchemaUnionVariant>;The variants of a discriminated union, keyed by the discriminator value that selects each.
A key matches the discriminator value exactly unless it ends in *, in which case it matches any value starting with the part before the * (e.g. tool-*, for AI SDK parts that encode the tool name in the discriminator). An exact key wins over a prefix key; between two matching prefix keys, the first declared wins.
variants: {
text: { text: { type: "string" } }, // kind === "text"
"tool-*": { toolCallId: { type: "string" } }, // kind starting with "tool-"
}
JsonSchemaValue
export type JsonSchemaValue = (SchemaLeafShape | JsonSchemaCompositeShape) & SchemaModifiers;A value shape in the json (document) dialect: what a field, record value, array item or union option may hold, discriminated by type (see JsonSchemaFieldType). Every shape also takes nullable (admit null) and description (prose emitted into the JSON Schema, with no runtime or type effect).
JsonValue
export type JsonValue = JsonPrimitive | JsonValue[] | JsonObject;Any JSON value: the domain of everything a document holds, since documents are persisted as JSON and synced as JSON patches. A json schema field infers to it, and the validator rejects anything outside it (undefined, functions, Date, cyclic objects).
JsSchema
export type JsSchema = {
root: JsSchemaValue;
defs?: Record<string, JsSchemaValue>;
};A js-dialect schema, for validating in-memory values with createJsValidator: a root shape and optional named defs. It has none of DocumentSchema's document-only blocks (references, migrations, access, docIdBrand).
JsSchemaField
export type JsSchemaField = JsSchemaValue & {
optional?: boolean;
};A JsSchemaValue at an object-field position, where it may also be optional.
JsSchemaFieldType
export type JsSchemaFieldType = JsonSchemaFieldType | "bytes" | "date";The type keywords of the js dialect: everything JsonSchemaFieldType has, plus the in-memory-only bytes and date (see SchemaDialect).
JsSchemaUnionVariant
export type JsSchemaUnionVariant = Record<string, JsSchemaField>;The js-dialect counterpart of JsonSchemaUnionVariant.
JsSchemaUnionVariants
export type JsSchemaUnionVariants = Record<string, JsSchemaUnionVariant>;The js-dialect counterpart of JsonSchemaUnionVariants.
JsSchemaValue
export type JsSchemaValue = (SchemaLeafShape | JsOnlyLeafShape | JsSchemaCompositeShape) & SchemaModifiers;A value shape in the js dialect: every JsonSchemaValue shape, plus { type: "bytes" } (a Uint8Array, Node Buffer included) and { type: "date" } (a Date with a valid time).
PresenceTypeOf
export type PresenceTypeOf<R> = [PresenceTail<keyof R & string>] extends [never] ? string : PresenceTail<keyof R & string>;The presence-channel types a registry declares, recovered from its presence:<presenceType> schema keys (the docType prefix presence channels are keyed under, see presenceSchemaType). Narrowing is OPT-IN: a registry that declares any presence:<type> schema narrows to exactly those channels — so getYjsAwareness / createTypedPresenceViewAtom give the same compile-time nudge docTypes already have, and a typo ("gatherng") fails to compile. A registry that declares none (an open ValidatorRegistry, or a concrete one whose presence channels are seeded only at runtime) stays permissive at string, so it isn't locked out of presence.
ReferenceRule
export type ReferenceRule = {
from: RefPath;
fromPosition: "key" | "value";
to: RefPath;
onDelete: "restrict" | "cascade";
onUnreferenced?: "delete";
inbound?: {
min?: number;
max?: number;
};
toType?: string;
};A referential-integrity rule, declared under a name in a DocumentSchema's references: ids found at from must exist among the keys at to. Enforced by the validator on client and server.
- from: the RefPath whose locations carry the referencing ids. - fromPosition: whether the id is the key of each from location ("key", as in taskOrder.*.<taskId>) or the value stored there ("value"). - to: the RefPath whose keys are the legal target ids. Not consulted when toType is set. - onDelete: "restrict" rejects a write that leaves a referrer dangling; "cascade" removes referrers whose target is gone, so deleting an entity also drops its placements and owned children. - onUnreferenced: "delete" removes a target once it has no referrers left (e.g. reclaiming a message when its last placement goes). Absent leaves it in place. Within one document only. - inbound: how many referrers each target must or may have ({ min: 1 } forbids unreferenced targets). Within one document only. - toType: for a cross-document reference, the docType the target id names a document of.
RefPath
export type RefPath = string;A dotted path selecting a set of locations in a document, as used by reference rules and migrations. Each * segment matches any key at that depth (taskOrder.*.*); a leading $. roots the path at the document ($.tasks, a record whose keys are a reference target set).
RenameConflictPolicy
export type RenameConflictPolicy = "keep" | "replace" | "discard";What a rename migration does where its to key is already present: "keep" does nothing (both keys stay, and validation reports the undeclared one), "replace" moves from's value onto the name and drops the occupant, and "discard" keeps the occupant and drops from.
Required on a rename exactly when the schema it replaces cannot rule out an occupied to. Absent, a rename that meets an occupied to behaves as "keep"; that only happens in a document that was already invalid.
SchemaDialect
export type SchemaDialect = "json" | "js";The two schema dialects. "json" (JsonSchemaValue, DocumentSchema) describes documents, where every value is a JsonValue; it is the only dialect a stored schema document may use. "js" (JsSchemaValue, JsSchema) adds the in-memory-only leaves bytes (a Uint8Array) and date (a valid Date), for validating values that never reach storage or the wire.
The dialect is chosen by the entry point: createJsonValidator rejects js-only kinds at construction and createJsValidator admits them. A DocumentSchema is assignable to JsSchema, never the reverse.
SchemaExtras
export type SchemaExtras = "reject" | "strip" | "keep";How an object or union shape treats keys it does not declare: "reject" (the default) makes them a validation error, "strip" silently drops them from the output, and "keep" retains them (they must still be JSON-encodable).
Use "strip" for a shape that is open by design, such as AI SDK message parts that gain optional fields across SDK versions. On a union it is declared once and applies to every variant.
SchemaLintCode
export type SchemaLintCode =
/**
* An `array` whose items are objects (entities). A collection of like items
* belongs in a `record` keyed by the entity's id: records give each entry a
* stable address (`items.<id>`) so concurrent edits to different entries
* merge instead of conflicting on array positions, references can point at an
* entry, and ordering — when it matters — is a `fractionalIndex` field on the
* value (with `autoIndex` to fill it on insert) rather than array position.
*/
"array-of-objects"
/**
* A `record` whose value object declares an `id` string field. The record's
* key *is* the entity's id; a second copy inside the value can drift from it
* and buys nothing. Only a plain object value is checked: a union value's
* variants may legitimately differ in whether they carry an `id`.
*/
| "redundant-id-in-record-value";The rules lintSchema applies, by code.
SchemaMigration
export type SchemaMigration = {
op: "rename";
sequence: number;
from: string;
to: string;
at?: RefPath;
onConflict?: RenameConflictPolicy;
} | {
op: "remove";
sequence: number;
path: RefPath;
} | {
op: "remap";
sequence: number;
path: RefPath;
values: readonly SchemaMigrationRemapPair[];
};An explicit schema migration: a transformation a schema diff cannot infer, so it is recorded in the schema's migrations log rather than derived.
- rename changes a key from from to to (both single keys, never dotted). By default they are top-level fields; at, a RefPath, names the container(s) instead, such as "address" or "items.*". onConflict is the RenameConflictPolicy. - remove deletes the data at path, a RefPath that reaches any depth. Deletion is never inferred: a stored field the schema neither declares nor removes fails validation. A path whose last segment lands on an array element is a no-op; remove the whole array field instead. - remap rewrites a scalar leaf's value at every location path reaches, by the from/to pairs in values (many-to-one allowed, duplicate from rejected). A last segment of * rewrites every element of an array of scalars.
Everything else is implicit: an added field with a default is backfilled, and a changed type or constraint re-validates.
Each migration is an entry of the migrations log of a DocumentSchema under a key its author picks. The key is the entry's identity and its replay order (compareMigrationKeys); a new key must sort after every committed one. sequence is stamped by the server at schema-write time (the schema document's sequence) and only filters: reading a document replays, in key order, the migrations stamped after the schema sequence the document last conformed to.
SchemaMigrationRemapPair
export type SchemaMigrationRemapPair = {
from: SchemaMigrationRemapValue;
to: SchemaMigrationRemapValue;
};One substitution in a remap migration: a stored value equal to from becomes to. Values matching no pair are left as they are.
SchemaMigrationRemapValue
export type SchemaMigrationRemapValue = string | number | boolean;A value in a remap pair, matched against the stored value with ===: the scalar kinds a closed-set field holds. null is not remappable; it belongs to the nullable modifier.
SchemaRegistry
export type SchemaRegistry = Record<string, DocumentSchema>;A caller's app docTypes, expressed as schemas keyed by docType.
SchemaStringFormat
export type SchemaStringFormat = "fractionalIndex" | "uuid" | "datetime";A named runtime check for a string field, applied identically on client and server: fractionalIndex (a valid generateKeyBetween order key), uuid (a well-formed UUID) or datetime (an ISO-8601 date-time).
TypedDocumentData
export type TypedDocumentData<Registry extends ValidatorRegistry, Type extends keyof Registry> = Registry[Type] extends JsonValidator<infer T, infer _Id, infer _In, infer _Mut> ? T : never;The stored (read) data type of docType Type in a validator registry: every default filled. See TypedDocumentInput for the create shape and TypedDocumentMutable for the update shape.
TypedDocumentId
export type TypedDocumentId<Registry extends ValidatorRegistry, Type extends keyof Registry> = Registry[Type] extends JsonValidator<infer _T, infer Id, infer _In, infer _Mut> ? Id : string;The document-id type of docType Type in a validator registry, recovered from its validator's phantom __docId: the brand the schema declares in docIdBrand, or string when it declares none.
TypedDocumentInput
export type TypedDocumentInput<Registry extends ValidatorRegistry, Type extends keyof Registry> = Registry[Type] extends JsonValidator<infer _T, infer _Id, infer In, infer _Mut> ? In : never;The create-INPUT data type for a docType, recovered from its validator's phantom In (see JsonValidator.__input). The write-side sibling of TypedDocumentData: where that is the stored/read shape (every default filled), this is what createDocument accepts — defaulted/optional/autoIndex fields may be omitted, since the validator backfills them. Falls back to the output type for a validator built without a pinned input type (the In = T default) — so createDocument on a hand-built, non-registry validator still requires every field; the relaxation only kicks in for registry validators, whose In is pinned to InferJsonSchemaInput by ValidatorsFromSchemas.
TypedDocumentMutable
export type TypedDocumentMutable<Registry extends ValidatorRegistry, Type extends keyof Registry> = Registry[Type] extends JsonValidator<infer _T, infer _Id, infer _In, infer Mut> ? Mut : never;The mutable data type for a docType, recovered from its validator's phantom Mut (see JsonValidator.__mutable). What an updateDocument callback's doc.data takes: keys present like TypedDocumentData, but json leaves accept the write shape JsonInput. Falls back to the output type for a validator built without a pinned mutable type (the Mut = T default) — so an update callback on a hand-built, non-registry validator sees the plain output shape; the relaxation only kicks in for registry validators, whose Mut is pinned to InferJsonSchemaMutable by ValidatorsFromSchemas.
ValidationIssueKind
export type ValidationIssueKind = (typeof VALIDATION_ISSUE_KINDS)[number];What a ValidationIssue is about: "schema" when the value does not conform to the schema's shape, "reference" when a cross-document reference rule failed.
ValidatorRegistry
export type ValidatorRegistry = Record<string, JsonValidator>;Validators keyed by docType, as a client or server is configured with.
ValidatorResult
export type ValidatorResult<T> = {
ok: true;
value: T;
} | {
ok: false;
error: string;
issues: readonly ValidationIssue[];
};The outcome of a validation. Every failure carries a non-empty issues list: a *shape* failure collects one issue per bad field / array item / record entry (the validators gather across the whole value rather than stopping at the first), a cross-document reference failure one issue per broken reference at the referring value, and a schema *construction* failure a single root (path: []) issue. error is a convenience summary — each issue rendered as path: message (or just message at the root) and joined with "; " — so the many call sites that interpolate a result's error into a log line get a located, human-readable string for free.
ValidatorsFromSchemas
export type ValidatorsFromSchemas<S extends SchemaRegistry> = {
[K in keyof S]: JsonValidator<InferJsonSchema<S[K]>, DocIdBrandOf<S[K]>, InferJsonSchemaInput<S[K]>, InferJsonSchemaMutable<S[K]>>;
};The ValidatorRegistry a SchemaRegistry projects to: every docType's runtime validator, with its document-data type recovered structurally from the schema via InferJsonSchema (no hand-asserted <TDocument> needed) and its branded document-id recovered from any docIdBrand tag via DocIdBrandOf, its create-input type via InferJsonSchemaInput (the write shape that lets defaulted fields be omitted), and its update-callback mutable type via InferJsonSchemaMutable (keys present, json leaves as JsonInput). This is what createClient makes its client generic over, so TypedDocumentData / TypedDocumentId / TypedDocumentInput / TypedDocumentMutable keep inferring exactly as they would off a hand-built validator map.
Variables
ACCESS_ROLES
ACCESS_ROLES: readonly ["viewer", "editor", "admin"]The built-in access roles, lowest first. A role satisfies any condition naming it or a lower role, so an admin passes an { role: "editor" } condition.
EMPTY_REFERENCE_CONTEXT
EMPTY_REFERENCE_CONTEXT: ReferenceContextA ReferenceContext that knows of no other documents. Use it where only intra-document references can appear (they ignore the context entirely) — e.g. validating the task board, or any schema with no toType rules. A cross-document reference checked against it always fails, which is the correct answer when there is nothing to point at.