From 436d99e04b02d5ed784e09dba8601383e7fc8e76 Mon Sep 17 00:00:00 2001
From: Mario Zechner
Date: Sat, 31 Oct 2026 14:01:40 +0000
Subject: [PATCH] docs(chord): tutorial: one schema for contract and document
---
.../docs/next/guide-plugins-hosts/book.md | 18 ++++++++++++------
.../docs/next/guide-plugins-hosts/index.html | 18 ++++++++++++------
2 files changed, 24 insertions(+), 12 deletions(-)
diff --git a/packages/chord/docs/next/guide-plugins-hosts/book.md b/packages/chord/docs/next/guide-plugins-hosts/book.md
index 83fe5051a..ab04e2119 100644
--- a/packages/chord/docs/next/guide-plugins-hosts/book.md
+++ b/packages/chord/docs/next/guide-plugins-hosts/book.md
@@ -84,18 +84,20 @@ Start without plugins. One host, one object that keeps notes:
```ts
import { createHost, defineFacet, defineState, local, method, obj, arr, str } from "@earendil-works/chord/next";
-// the contract: what a Notes object looks like from outside
+const NoteList = obj({ items: arr(str()) }); // one schema, used by the contract and the document
+
+// the contract: what a Notes object offers
const Notes = local("notes.store", {
version: 1,
params: {}, // one object per host: no params
members: {
- list: obj({ items: arr(str()) }), // state anyone can read and watch
+ list: NoteList, // state anyone can read and watch
add: method(obj({ text: str() })), // a method: arguments checked by Chord
},
});
-// the document the object keeps
-const NotesDoc = defineState("notes", { schema: obj({ items: arr(str()) }), version: 1, initial: () => ({ items: [] }) });
+// the document the object stores (its own version: storage can change without changing the contract)
+const NotesDoc = defineState("notes", { schema: NoteList, version: 1, initial: () => ({ items: [] }) });
// the facet: which types this host serves, and how to make each object
const facet = defineFacet((host) => {
@@ -115,7 +117,11 @@ await notes.add({ text: "buy milk" }, ctx);
console.log(notes.list.value); // { items: ["buy milk"] }
```
-Four things happened. The **contract** `Notes` says what the object offers. The **facet** says this host serves
+Four things happened. The **contract** `Notes` says what the object offers; the **document** says what it stores.
+Here they have the same shape, so one schema serves both; they are separate because they change separately (a
+derive or a guest's redacted view offers something other than storage, and storage can migrate without users
+noticing).
+The **facet** says this host serves
`Notes`, with a **factory** that declares the object's parts: a document, and a method writing it in a transaction.
`load` publishes the facet. `get` returns a **reference** once the object is live; its members are the contract's.
@@ -135,7 +141,7 @@ export const Notes = remote("notes", {
version: 1,
params: { project: str() },
hostAddress: ({ project }) => Project({ id: project }), // where an object of these params lives
- variants: { editor: { state: { list: NotesSchema }, methods: { add: method(AddArgs) } } },
+ variants: { editor: { state: { list: NoteList }, methods: { add: method(obj({ text: str() })) } } },
});
```
diff --git a/packages/chord/docs/next/guide-plugins-hosts/index.html b/packages/chord/docs/next/guide-plugins-hosts/index.html
index ec7ab3f9c..677e54ab6 100644
--- a/packages/chord/docs/next/guide-plugins-hosts/index.html
+++ b/packages/chord/docs/next/guide-plugins-hosts/index.html
@@ -174,18 +174,20 @@ extension point: one plugin defines it, others contribute. A rolloutStart without plugins. One host, one object that keeps notes:
import { createHost, defineFacet, defineState, local, method, obj, arr, str } from "@earendil-works/chord/next";
-// the contract: what a Notes object looks like from outside
+const NoteList = obj({ items: arr(str()) }); // one schema, used by the contract and the document
+
+// the contract: what a Notes object offers
const Notes = local("notes.store", {
version: 1,
params: {}, // one object per host: no params
members: {
- list: obj({ items: arr(str()) }), // state anyone can read and watch
+ list: NoteList, // state anyone can read and watch
add: method(obj({ text: str() })), // a method: arguments checked by Chord
},
});
-// the document the object keeps
-const NotesDoc = defineState("notes", { schema: obj({ items: arr(str()) }), version: 1, initial: () => ({ items: [] }) });
+// the document the object stores (its own version: storage can change without changing the contract)
+const NotesDoc = defineState("notes", { schema: NoteList, version: 1, initial: () => ({ items: [] }) });
// the facet: which types this host serves, and how to make each object
const facet = defineFacet((host) => {
@@ -203,7 +205,11 @@ extension point: one plugin defines it, others contribute. A rolloutconst notes = await server.get(Notes, {}, ctx); // a reference: live, with its state
await notes.add({ text: "buy milk" }, ctx);
console.log(notes.list.value); // { items: ["buy milk"] }
-Four things happened. The contract Notes says what the object offers. The facet says this host serves
+
Four things happened. The contract Notes says what the object offers; the document says what it stores.
+Here they have the same shape, so one schema serves both; they are separate because they change separately (a
+derive or a guest's redacted view offers something other than storage, and storage can migrate without users
+noticing).
+The facet says this host serves
Notes, with a factory that declares the object's parts: a document, and a method writing it in a transaction.
load publishes the facet. get returns a reference once the object is live; its members are the contract's.
The factory only declares: it is synchronous and opens nothing. Chord activates the object (loads its documents, opens
@@ -220,7 +226,7 @@ lives on its project's worker:
version: 1,
params: { project: str() },
hostAddress: ({ project }) => Project({ id: project }), // where an object of these params lives
- variants: { editor: { state: { list: NotesSchema }, methods: { add: method(AddArgs) } } },
+ variants: { editor: { state: { list: NoteList }, methods: { add: method(obj({ text: str() })) } } },
});
hostAddress is the whole routing story. A tab asks for Notes { project: "p1" }. Its host computes the address,
Project(p1), and sends the request to its parent, the server. The server has a route for Project: it starts (or