Skip to main content
This is the design, and it now has a schema. The model below is CONTRACT-SCHEMA v0, which ships as JSON Schema 2020-12 with a hand-mirrored TypeScript type set, three worked examples and a dependency-free validator. It has no backend behind it. It is published because arguing with a model is cheaper before it is built than after.
Everything contracts.io does rests on twelve nouns. Read this page once and the rest of the site reads itself.

The twelve

The contract is the atom

There is no “create a workspace” step and no envelope to put things in. A Contract exists, it gets sent, and the Room between the two sides comes into being because a contract crossed between them. A draft has no Room at all. The Room is born at the first Send, and its createdAt is that Send’s timestamp — not a moment somebody chose.

Blocks hold words nobody can overwrite

A Block’s text is not a string you can write to. Words move only when a Proposal is accepted. That is the rule the whole system turns on: words never move by proposal alone. There is no call, button, key or agent that writes clause text directly. You propose; the other side decides; the words move or they do not. Block number is display-only. Never key on one — renumbering a contract must not be able to void a signature.

Accepting and signing are two records

Acceptance

A party agreeing to a Version’s words, recorded against that Version’s wordsHash.

Signature

A party’s name on that same wordsHash. It requires an Acceptance by the same party on the same hash, recorded no later.
A paper can sit fully accepted and wholly unsigned. That is a normal state, and the schema’s second worked example is exactly that: two Versions, a Proposal, a Decision, an Acceptance, and zero signatures.

Signatures have three states

held

Signed, and a question is now open on the paper. The words have not moved, so what that person signed still reads exactly as it did. The signature survives.

bound

Signed and standing. The words it names are the words in force.

void

The words moved to a new hash, so the signature names text nobody agreed to. It is dead, a reason says why, and its party has a re-sign waiting.
There is no call that migrates consent onto text nobody saw. The replacement for a void signature is always a fresh one.

Versions are minted at two moments

A Version is an immutable snapshot plus a wordsHash. One is minted on every Send, and one on the first signature against a set of words. A mintedBy field says which moment made it. Where the words have not moved since the last Send, the signing mint carries the identical hash. That is not duplication: the hash is the identity of the words, and the Version is the identity of the moment. Both mints are needed. Mint only at signing and an unsent negotiation has nothing to cite — a redline’s “before” would be a quote from a snapshot nobody kept. Mint only at Send and a signature taken in the room, which is the normal case, binds to a snapshot that was never struck.

Governing law is a list

Real papers name more than one place — law and forum differ, a schedule carries its own law, a group names one law per entity. governingLaw is therefore always an array of place slugs, and the door already lets a person pick more than one. The four router countries — united-kingdom, united-states, canada, australia — hold several legal systems each, so nothing can be governed by them. The door refuses them and the schema refuses them too.

The eight laws

Each of these is carried in the schema itself with a named check in the validator. A law with no check fails the run, so none can be left as a comment.
Separate records. A Signature requires an Acceptance by the same party on the same wordsHash, no later than the signature.
LexNote has three kinds, none of them a verb that leaves the room. An Event with a Lex actor may only be created, edited, proposed or decided.
Lex finds words and says where. No kind, state or event type in the schema is named read, reviewed, analysed or summarised — the validator greps to keep it that way.
A Contract may carry a LexNote only once its sender has an account.
An array, always.
An account id is optional everywhere, and the schema has no price, plan, seat, billing, invoice, payment or subscription property anywhere — checked by name.
A draft’s room is null. Otherwise the room’s createdAt equals the first sent event.
Sequence runs 1, 2, 3…, times never go backwards, and the same words always give the same hash.

The gap: CANON-1 does not exist

Every signature in this model binds to sha256 of CANON-1 canonical text, the certificate names contracts.io/canon/1, and no file anywhere defines what CANON-1 does. Nothing about signing is checkable by anybody outside this system until it is written. The examples’ hashes use a stated stand-in so the files are internally consistent, and every one of them changes the day CANON-1 lands. It has to answer six things and nothing else: whitespace, whether numbering is excluded (it should be), Unicode normalisation, block order and separators, whether governing law is inside the hash (it should be), and the exact encoding.
This is named here rather than buried because it is the one hole in an otherwise closed model, and a reader deciding whether to trust the signing story deserves to know it is open.

The nouns, settled

Contract not paper, Proposal not ask, Version not receipt — the glossary picks one of each and says what lost.