Every item on this page links into the reference. Read it once top to bottom, then use it as an index. The long versions are why, how it works, the schema language, typed documents, user stories.
The model in ten lines
Every value is one of nine kinds: null, boolean, integer, float, string, bytes, list, map, link. struct is a map with named fields. The bytes are DAG-CBOR, and the readable form is dag-json.
A schema is a map that constrains a value: type, properties (one property per field: {value, required?, description?}), items, values, anyOf, target, generics, and leaf constraints. A bare literal ("draft", 1) is also a schema, and accepts exactly that value. See the schema language.
The meta-schema describes what a schema is: a union of struct-schema, map-schema, list-schema, scalar-schema, link-schema, include-schema, anyof, var-schema, literal-schema. It validates itself.
A schema is itself a DAG-CBOR blob with a CID. Schemas reference each other by hm:// name (references), so types can recurse and form cycles (the fixpoint problem). Library schemas are named by the domain hyper.media: example/person is hm://hyper.media/example/person. The SDK resolves these names from its bundle. The network does not resolve domains in hm:// URLs yet, so the published pages carry the docs space's key.
{type: X} alone includes X. {type: X, properties: …} extends it: the parent's fields plus new ones, closedness kept.
Validation is advisory in the editors (warn, never block) and strict in the reference validator, the CLI's checks, and the agent's refusals for blobs.
The library, by link
Refined primitives: date, date-time, timestamp, url, hm-url, ipfs-url, cid, principal, signature, any, value, key-value.
Network blobs (the blobs chapter): every signed object extends the blob envelope: change (with change/body and the ops), ref, comment, capability, contact, profile. The union is blob/any.
The document model: document is {metadata, content}. metadata carries the three binding keys below. content is a tree of block/node, each a block such as paragraph, heading, image, file, embed, query (a query), with annotations.
Examples (the index). Structs: person, employee (extends person), address, stats, geo, constrained. Maps and lists: counts, tags, matrix, tree (recursive), json (generic). A custom block and a custom Change: poll-block, app-block, myapp-change. Attributes schemas for typed documents: person-doc, world-doc, character-doc, place-doc, faction-doc, event-doc. Instances, which are data: alice, bob, carol, dave, root.
Typed documents: three keys
key | on which document | says | value |
|---|---|---|---|
| the type's home page | "I define a schema." |
|
| an instance page | "My attributes follow that schema." | the type page's |
| a folder | "My children's attributes follow that schema." | the type page's |
An attributes schema is a plain struct of the fields a document's metadata carries. person-doc is {surname required, givenName}. It never extends document and never mentions metadata or content. Build one type on another by extending the struct: employee = person + employeeId.
A document's effective schema is its own attributesSchema, else its parent's childAttributesSchema, else none. This goes one level deep: a folder types its direct children.
When a document is checked, the base metadata fields (name, summary, icon, the three keys, …) are folded in beneath the type's fields. The result stays open to extra keys, so a typed page is still a full document with a body.
A type has a URL because its home page does: hm://acme/person resolves through that page's schemaDefinition to the blob. Reference it by URL to follow the type as it changes, or by CID to pin it (pinning versus following).
The worked demo is the World Builder: a world page typed by world-doc, four type pages, four folders bound with childAttributesSchema, and starter pages with dates, title pills and linked objects.
Working with them
In the app
A type's home page (any document with schemaDefinition) shows New Document and New Collection. New Document starts a draft whose attributesSchema is the page. New Collection starts a draft whose childAttributesSchema is the page. The options menu adds Extend Schema, New Raw Value (a bare IPFS blob of the type, for developers), and Inspect Schema. New Schema (Developer Mode) opens the editor on the meta-schema.
Any document's Attributes tab opens with its two schema bindings, Attributes schema and Children attributes schema. Each shows the full schema editor in place when the document owns the schema. The options menu's Attributes Schema / Children Attributes Schema entries jump there, and draft an empty struct when nothing is bound. Publishing freezes edits into IPFS objects that the binding keys point at.
A typed document's Attributes tab shows the type's required fields as fixed rows, the optional ones as chips, the right control per field (date picker, title pill, file picker, dropdown), and violations in red. It never blocks a save.
With the CLI
The Seed CLI reads the same library and resolves references the same way the app does. Every command is in the CLI reference.
# type a document, a folder, and publish a type
seed-cli document create -f bob.md --attributes-schema hm://acme/person
seed-cli document update hm://acme/people --child-attributes-schema hm://acme/person
seed-cli document create -f person.md --schema-definition person.schema.json # publishes the blob, binds schemaDefinition
# frontmatter works too: attributesSchema: hm://acme/person / childAttributesSchema: … / schemaDefinition: ipfs://…
# check
seed-cli document validate hm://acme/people/bob # effective schema, each violation on a line, exit 1 on any
seed-cli document validate hm://acme/people/bob --content --json
seed-cli space import self -d ./site --check # every file against its schema; publishes nothing on a violation
seed-cli schema get hm://acme/person --resolve # the schema, references followed, extensions merged
# find
seed-cli query acme --where 'attributesSchema=hm://acme/person' # every page typed by person (any attribute condition works)
seed-cli query '*' --where 'has:childAttributesSchema' # every typed folder, anywhere
seed-cli attributes acme # which attribute keys documents carry, with kinds
seed-cli attributes acme --values status # the distinct values of one key
seed-cli schema validate ipfs://<cid> # is this blob a valid Hypermedia schema?
# raw objects
seed-cli blob create -f value.json --schema hm://acme/person # validate, publish, link the blob to its type
seed-cli blob validate -f value.json --schema hm://acme/person
seed-cli blob verify ipfs://<cid> # signature + schema of a published blobFrom code (@seed-hypermedia/client)
The SDK holds the one implementation every surface shares. The CLI and the agents service call these same functions.
import {HM_SCHEMAS, validate, resolveSchema, structFields} from '@seed-hypermedia/client/schema-engine'
import {
classifyRef, resolveSchemaRef, loadSchemaRef, hydrateSchemaRegistry,
effectiveSchemaRef, checkDocumentSchema, checkSchemaDefinition,
metadataSchemaOf, documentMetadataSchema, blobSchemaRef, withoutSchemaLink,
} from '@seed-hypermedia/client/schema-resolve'
validate(HM_SCHEMAS['example/person'], {name: 'Bob', age: 41}) // [] — errors as "$.path: message" strings
const person = await loadSchemaRef(client, 'hm://acme/person') // {schema, cid, registry} — refs fetched
validate(person.schema, value, '$', {}, person.registry)
await checkDocumentSchema(client, docId, doc.metadata) // {schema, via: 'own'|'inherited'|'none', required, missing, violations}
await checkSchemaDefinition(client, 'ipfs://<cid>') // violations of the meta-schema, or why it failed to load
await client.request('QueryDocuments', {filter: {comparison: {key: 'attributesSchema', operator: 'EQUAL', value: {stringValue: 'hm://acme/person'}}}})
await client.request('ListDocumentAttributeNames', {account: 'acme'}) // keys documents carry, with kinds
await client.request('ListDocumentAttributeValues', {path: ['status'], kind: 'DOCUMENT_ATTRIBUTE_KIND_STRING'})
documentMetadataSchema(metadataSchemaOf(person.schema)) // the open, base-folded metadata schema the editor usesclassifyRef sorts a reference into a bundled library name, a CID, or a document URL without fetching. resolveSchemaRef follows it: from a document URL to its schemaDefinition, then to the blob.
effectiveSchemaRef applies the own-else-parent rule. hydrateSchemaRegistry fetches every type a schema references so nested type references validate.
QueryDocuments takes a recursive DocumentFilter (and / or / not, comparison, exists / missing, stringMatch, spaceMatch, pathMatch, urlMatch) in protobuf JSON. It sorts by attribute or built-in field, and pages. The two attribute listings answer "which keys exist" and "which values does this key take". The Explore grammar compiles to the same filter: compileExploreQuery(parseExploreQuery(q), {type: 'node'}).filter?.toJson() from the shared package.
Generated TypeScript for the whole library ships as schema-types.generated.ts (HMDocument, HMMetadata, HMChange<B>, …), from scripts/hypermedia/typegen.mjs.
Through an agent
The Seed Agents verbs (read, write, call) expose the same checks. A document that does not conform gets a warning. A raw object that does not conform is refused.
read hm://acme/people/bob: a typed document returns a schema block: {schema, via, required, missing, violations}. /:attributes reads only the metadata.
read ipfs://<cid>: a DAG-CBOR object decodes to value, signature (who signed, whether it verifies), and schema (violations against the schema it links to, or options: {schema}).
write hm://acme/people/bob with options.metadata: {surname: "Smith", attributesSchema: "hm://acme/person"}. Any key is allowed. The result reports schema and warnings beside the published id. options.metadata.childAttributesSchema types a folder, and options.metadata.schemaDefinition makes a page a type. dryRun: true returns the same report without publishing.
call tool query takes q in the Explore grammar, or a raw filter. Examples of q: attributesSchema=hm://acme/person (every page typed by person), in:hm://acme/places kind=fortress, has:childAttributesSchema, status="In Progress" AND priority>=3. It returns each document with its full attributes, sortable and paged. call tool attributes lists the attribute keys documents carry, with kinds. With key, it lists the distinct values of one key.
write ipfs:// with JSON content and options: {schema: "hm://acme/person"} validates the value and refuses it on a violation. force: true publishes anyway, with warnings. The published object carries a schema link. options.schema: "hypermedia-schema" publishes a schema blob after checking it against the meta-schema. options.sign: true signs a blob whose type extends blob.
Who can check what
check | app | CLI | SDK | agent |
|---|---|---|---|---|
a document against its effective schema | Attributes tab, live |
|
|
|
a | schema editor, live |
|
|
|
a raw object against a type | value editor, live |
|
|
|
a signed blob's signature | inspector |
|
|
|
resolve a type by URL, CID or name | schema browser |
|
|
|
find documents by attribute, or typed by a schema | Explore (advanced search) |
|
|
|
which attribute keys exist, and their values | Explore's attribute pickers |
|
|
|
See also
Hypermedia Schemas: the meta-schema and the index of schema pages.
Why Hypermedia Schemas: the problem they solve.
The schema language: every key, with examples.
Typed documents: the three binding keys in depth.
References and naming: include, typed link, and hm:// names.
Query grammar: the Explore grammar behind query --where.
User stories: what each surface can do today.
Do you like what you are reading? Subscribe to receive updates.
Unsubscribe anytime