Schemas for network blobs
The Hypermedia Network stores its data as DAG-CBOR blobs in IPFS. There are six signed blob types: Change, Ref, Profile, Comment, Capability and Contact. They are related, because every one embeds the same signed envelope. These schemas type production data, use only the features of the schema language, and are named under the library's authority, hm://hyper.media: the blob schemas at the root (change, ref, …) and the block model under block/.
The shared envelope
Every blob embeds a base envelope, Signed blob:
field | type | meaning |
|---|---|---|
| string | the blob discriminator ("Change", "Ref", …) |
|
| the signer's public key |
|
| signature over the blob |
|
| Unix-millisecond time |
Each concrete type extends the envelope. It inherits those four fields and pins type to a literal:
change: an append-only document change, linked into a causal DAG by deps. It carries a change/body of ops.
ref: a signed pointer from a space and path to the current head Changes.
profile: an account's name, avatar and description, or an alias.
comment: a threaded comment. Its body is a tree of block/comment blocks.
capability: a delegation of a role (WRITER or AGENT) to a key.
contact: one account's named reference to another.
Open change in the schema explorer. It shows signer, sig and ts as inherited and the rest as added. The Dependents list of blob is exactly these six types.
Define your own signed blob type
Any schema that extends Signed blob and pins a type tag is a signed blob type the app can create. The six built-in types do not reserve the envelope. In the schema editor, tick Signed blob type, set the tag (for example Vote), add your fields, and publish the type as a schema-definition page. Its Create button opens the signing form. You fill in only your fields. The app adds signer, ts and sig at signing time with the selected account's key. It publishes the blob with the convention the daemon verifies: sign the canonical CBOR with the signature zeroed. The result is a verifiable blob on the network. The built-in indexer ignores its type, but any app that resolves your schema can trust and render it. Browse any schema, built-in or yours, at /hm/schema/<cid>.
The blob union
blob/any is the discriminated union of all six types, tagged on type. You can validate a value against "any Hypermedia blob" as one type.
Nested structure
A Change's body is a list of ops. The op schema is itself a union of SetAttributes, MoveBlocks, ReplaceBlock, DeleteBlocks and SetKey. Content is modeled by block and annotation. Both are open maps: known fields plus arbitrary inline attributes. Document metadata is an open struct of known keys (name, summary, icon, cover, layout, …) plus extras.
Block types: a strict core anyone can extend
Document content is made of blocks. The block model needs two things that conflict. Implementations need strict, concrete types so they can dispatch on block.type to type-safe handlers. Documents need openness so a newer client's block type does not make an older client reject the whole document. One validation pass cannot do both, because an open fallback always accepts a malformed known block. So the model provides layers, and each workflow picks one:
workflow | needs | use |
|---|---|---|
rendering / dispatch | strict per-type shapes + graceful fallback | concrete types + |
authoring / editing | strict validation |
|
sync / storage (forward-compat) | never reject unknown |
|
codegen | the enumerable set |
|
The fifteen concrete blocks are block/paragraph, block/heading, block/code, block/math, block/image, block/video, block/file, block/button, block/embed, block/web-embed, block/nostr, block/table, block/table-row, block/table-column and block/query. Each one extends block/base, is closed, and has a type literal and typed attributes.
block/core is the core union of those fifteen. It is strict and rejects any other block.
block is the open block: id, type and arbitrary fields (through any). It is the forward-compatible wire type. A block type this client has no schema for, future or third-party, is still a valid Block, so a document is never rejected because of it. Use block only as the fallback for unknown types. A custom block type of your own uses extension and a union, as shown below.
Adding a block type
To add a block type, do what the core blocks do. Extend block/base, then put your block in a union with the core. No new machinery is needed:
// example/app-block: the core, PLUS this app's custom Poll block
{ "anyOf": [ { "type": "hm://hyper.media/block/core" },
{ "type": "hm://hyper.media/example/poll-block" } ] }See example/poll-block, a custom block that extends the same base, and example/app-block. That union is strict for its app. It accepts core blocks and Polls and rejects any block type it does not know. The wire type block stays open.
Change is generic over its block type
An app can also make Change itself strict over its block set. change is a generic Change<Block>. The Block parameter passes through change, change/body, change/op and change/op/replace-block. Each level passes it down with args, and it defaults to the open block. An app instantiates it: example/myapp-change is Change<example/app-block>. A ReplaceBlock op that carries a block type the app does not know is then rejected four levels deep, at $.body.ops[0].block. The default Change still accepts any block. This uses the language's generics (params, var and args), described in the schema language.
CBOR value shapes
The wire types map onto primitive schemas, wrapped in aliases with plain names:
Hypermedia | CBOR | schema |
|---|---|---|
| byte string |
|
| CBOR tag-42 link |
|
| int64 (Unix ms) |
|
validate.mjs checks every one of these schemas. It checks that each is a well-formed schema. It also validates real blob-shaped data against them: a Ref, a Capability, a Change with ops, the union, and metadata. Negative cases cover wrong type tags, missing required fields and unknown keys.
See also
Blobs: signed blobs, envelopes and CIDs as protocol concepts.
Documents: how Changes and Refs become a document.
Blocks: the block tree, annotations and embeds.
The schema language: extension, unions and generics.
Encoding: canonical DAG-CBOR and the dag-json form.
Do you like what you are reading? Subscribe to receive updates.
Unsubscribe anytime