Skip to content

UI contracts and the headless-parity test

The UI contracts are what any screen renders: six answers for every principal and record, generated from the file. The headless-parity test has an agent finish every path a UI offers with no UI.

One contract, two readers

Filled: the six answers, the same for every reader. Hollow: what writes them and what reads them.

Capability families

APIs.Dev specifies no App, client portal or delivery screen. It specifies what each one reads, so any UI, from any builder, renders the product without a private API. The landing site, the developer portal and the docs are specified as products; every other UI gets these contracts.

FamilyWhat it gives every product
The affordance contractSix answers for any principal and record: the state; what they may see; the moves allowed, each with its input schema and price; the moves refused, each with its cure; who else can act; and a live subscription
The app mapWhat a member can reach, in order: the Nouns, views and settings their Role opens, generated from the file
ViewsSaved filters, sorts and groupings as data, each answered over every Interface with its count
FormsPer Verb: fields, order, defaults, validation and help, from its input schema. A form never asks for what the Verb does not take
SettingsThe account's own settings as Verbs: members, Roles, plan, Integrations, preferences
Onboarding as dataThe first steps as a checklist of Verbs, each ticked when its Event arrives, the same for a person and an agent
Branding tokensName, logo, colours and type from brand, for any UI, email and receipt
A reason for every empty stateEvery empty list or view says why (none yet, filtered out, hidden by Role, not in the plan) and what fills it
Contracts per patternMember contracts (Headless SaaS, Content), client and delivery contracts (Service API), storefront and supplier contracts (Marketplace): the six answers, plus the pattern's records
The headless-parity testEvery path a UI offers, finished by an agent through the API or MCP and kept in the proof record. A UI that has to invent a state, a move or a reason fails it, and marks a capability the product lacks

The six paths

The suite runs each path at every deploy, in the Test twin, with an agent and no UI.

PathWhoWhat it covers
An admin's setupA Customer's adminCreate the org, invite, set Roles, connect single sign-on and an Integration
A member's dayA memberCreate, relate, search, move a record, restore one, export
A client's matterA buyer, or its agentOrder, approve the quote, the reserved act by link, accept or contest, pay
A worker's shiftA workerClaim a task, complete it, hand it off, mark a booking attended
A supplier's orderA supplier, or its agentApply, pass verification, publish an item, claim, fill, read the payout
A buyer's purchaseAnother product's agentFind, evaluate, pay, invoke, prove, contest

Lifecycles and states

ThingStatesWhat moves it
A contract, per principal and recordcurrent, then changed and current againThe record's state, the principal's Role, entitlement or mandate, or a new Version; each change goes out on the subscription
A live subscriptionopen, behind, closedThe client; one that falls behind resumes from its last seq
An onboarding checkliststarted, then complete or dismissedEach step's Event; the member
A path in the suitedefined, running, then passed or failedEach deploy in the Test twin; a failure names the step and the missing capability
A UI's conformancechecked, then conforming or inventingThe check of each screen against the contracts; inventing names the screen and what it made up

A contract is never cached past a change: the move that changes a record's state changes every principal's allowed moves in the same commit.

What you declare, what the platform does

You declareThe platform does
Nothing for the six answersBuilds them for every principal and record from the Nouns, lifecycles, Roles, Offers and mandates, and serves them on every Interface
viewsAnswers each as data with its count, masked by Role, and gives each empty answer its reason
ui.appOrders the app map; each member sees only what their Role opens
ui.formsSets field order and help; the rest comes from each Verb's schema
ui.onboarding: a list of VerbsKeeps a checklist per Customer, ticked by Events
ui.empty: a reason per viewServes it with an empty answer; reasons from Roles and plans come without a line
brandServes the tokens to any UI, and applies them to every email and receipt
parity: paths beyond the sixRuns them with the six at every deploy, and refuses a deploy that breaks one

Example

pipeline names its app map, a form, onboarding and the reason its open view can be empty.

definition.ts
21 lines
import { define } from 'apis.dev'

/** Deals for each Customer, kicked off when won. */
export default define({
  name: 'pipeline', domain: 'pipeline.apis.dev',
  roles: { rep: { see: { Deal: 'own' } }, manager: { has: 'rep', see: { Deal: 'all' } } },
  Company: { name: 'string', domain: 'url!#', deals: '<-Deal.company' },
  Deal: {
    title: 'string', amount: 'decimal(12,2)', company: '->Company.deals',
    stage: { open: [], qualify: 'opened', lose: ['opened', 'qualified'],
             win: { in: 'qualified', role: 'manager', approve: { above: { amount: 50000 } } } },
  },
  views: { open: { Deal: { stage: ['opened', 'qualified'] }, sort: '-amount' } },
  ui: {
    app: ['views.open', 'Company', 'settings'],           // the app map
    forms: { 'Deal.open': ['title', 'amount', 'company'] },
    onboarding: ['Company.create', 'Deal.open', 'crm.connect'],
    empty: { open: 'No open Deals. Open one, or connect your CRM to bring yours in.' },
  },
  offer: { Team: { month: 49, seats: 5, seat: 9 }, 'Deal.open': 0.001 },
})

Dana is a rep. Her contract for a qualified Deal of 64,000: she may lose it but not win it, and a manager's win waits for approval.

GET /deals/deal_1/affordances · as dana@acme.com
19 lines
{
  "record": "deal_1",
  "state": "qualified",
  "see": { "fields": ["title", "amount", "company", "stage"], "hidden": [] },
  "allowed": [
    { "verb": "Deal.lose", "price": 0,
      "input": { "type": "object", "properties": { "reason": { "type": "string" } } } }
  ],
  "refused": [
    { "verb": "Deal.win", "code": "role_required",
      "cure": "A manager wins a Deal: sam@acme.com" }
  ],
  "others": [
    { "principal": "sam@acme.com", "role": "manager", "can": ["Deal.win", "Deal.lose"],
      "pending": { "Deal.win": "approval_required" } }
  ],
  "live": "/deals/deal_1/live",
  "seq": 41
}

The suite runs at deploy, and checks a screen built on the product against the same contracts.

Terminal
$ npx apis.dev test --parity
✓ an admin's setup     9 moves over MCP, no UI
✓ a member's day      14 moves over MCP, no UI
✕ screen 'Deals board', column 'stalled': no state, view or metric defines it

A stalled view in the file fixes it; a column that only the screen knows does not ship.

Contracts with the other subsystems

The contracts decide nothing. Each answer is a question the other subsystems already answer, asked for one principal and one record.

SubsystemWhat the contracts take from itWhat they give it
Definition and compileNouns, lifecycles, views, ui and brand, checked at deploy: an app map entry or onboarding step that names no Verb or view is refusedThe paths, run in the Test twin at every deploy
Identity, access and policyThe one decision, asked for every move; masks per field and record; who else holds a Role or a mandateNothing new: each refused move carries the decision's own cure
Model and recordsThe record, its state and history; views and metrics as projectionsReads that never show what the Role hides, with the reason
RuntimeEvents and live subscriptionsThe seq a client resumes from
WorkTasks, turns and the seen states of a caseThe client, delivery and supplier contracts a screen renders
CommerceThe price of each move, entitlements, the plan's limitsA refused move's cure: the plan or the payment that lifts it
Interfaces and distributionEvery Interface, so the contract reads the same over the API, MCP and the SDKThe in-app channel, rendered from the record's conversation
Trust and operationsThe proof recordEach path's result per Version, and each UI's conformance

What Customers and their agents meet

  • A screen that cannot lie. Every button is an allowed move, every greyed one carries its cure, and every empty list says why.
  • No private API. The App, a client portal and a supplier's screen read the contracts an agent reads. Anything a UI shows, an agent can fetch.
  • An agent that finishes the job. An agent reads the contract, makes the allowed move, and follows the subscription. It never needs a screen.
  • Onboarding that counts what happened. A checklist ticks itself when its Event arrives, whether a person or an agent made the move.
  • Your brand everywhere. The same tokens style a UI, an email and a receipt.
  • The same answer everywhere. A refusal on a screen, over MCP and in a 403 carries the same code and the same cure.

Any UI is a view of the contracts, so whatever a person can do on a screen, an agent can do without one.