Skip to content

Contract-first workflow ​

CLAUDE.md's invariant 1: packages/contract/openapi.yaml is the source of truth. Controllers conform to it (and are tested for that), the SDK is generated from it, and neither is ever hand-written to differ from it. This page is the concrete workflow that invariant implies.

The contract itself ​

packages/contract/openapi.yaml is a single ≈9,200-line file describing 135 paths under /v1 (figures as of 2026-08-31 — the file only grows; wc -l and a count of its paths: keys are the live truth). It's lint-checked (redocly lint openapi.yaml, packages/contract's own lint script) and is what the API Reference is generated from — the same file, no separate description to keep in sync.

Adding or changing an endpoint ​

┌─────────────────────┐
│ 1. Edit              │
│ openapi.yaml          │
└──────────┬───────────┘
           │
           ▼
┌─────────────────────┐     regenerates packages/sdk/src/generated/
│ 2. pnpm --filter      │     {types,paths}.ts from the contract via
│ @loombre/contract       │     openapi-typescript + a small hand-rolled
│ run codegen            │     operations table
└──────────┬───────────┘
           │
           ▼
┌─────────────────────┐     gate step: git diff --exit-code -- packages/sdk
│ 3. Commit the          │     — the generated SDK must be committed and
│ generated SDK diff     │     must exactly match what codegen produces.
└──────────┬───────────┘     Never hand-edit files under packages/sdk/src/
           │                  generated/ — the "GENERATED — do not edit"
           │                  banner at the top of each is not a suggestion.
           ▼
┌─────────────────────┐     gate step: oasdiff breaking PREVIOUS.yaml
│ 4. oasdiff checks      │     CURRENT.yaml, against `main`. Field removal,
│ for breaking changes   │     rename, or type change fails here (docs/PLAN.md
└──────────┬───────────┘     §4.1: "additive-only within a major version").
           │
           ▼
┌─────────────────────┐
│ 5. Implement/adjust    │
│ the controller         │
└──────────┬───────────┘
           │
           ▼
┌─────────────────────┐     apps/server/test/conformance.spec.ts +
│ 6. Conformance tests   │     seeded-conformance.spec.ts must pass — every
│ must pass               │     non-public op returns a proper 401 when
└──────────┬───────────┘     unauthenticated, public ops validate against
           │                  their documented schema, and every mounted
           │                  route maps to something actually documented
           │                  (no undocumented surface area).
           ▼
┌─────────────────────┐
│ 7. pnpm gate            │
└─────────────────────┘

What conformance testing actually checks ​

apps/server/test/conformance.spec.ts boots a real Nest application against a seeded database and walks the contract itself as its test data — there's no separately maintained list of "endpoints to check." For every documented, non-public operation it asserts an unauthenticated request gets a proper RFC 9457 401. The public operations — 11 today, spanning the auth pair, system capabilities, the setup pair, the invite-claim pair, the three password-recovery operations (forgot / reset / reset-state), and the remote-access probe page; the spec's own PUBLIC_OPERATION_IDS set is the live list — are exempt from that 401 walk, and several of them (GET /system/capabilities, POST /auth/login's TokenPair, GET /setup/state, POST /auth/forgot-password) get their responses validated against the contract's own schemas via Ajv, alongside Ajv checks of RFC 9457 problem bodies throughout. For authenticated requests it asserts they're not walled off; and it asserts /healthz stays public. Crucially, it also asserts that every route Express actually has mounted maps to a documented contract path — an endpoint that exists in code but not in openapi.yaml fails this check, not just the reverse.

Why this order ​

sdk-drift and oasdiff run early in pnpm gate (right after codegen) because they're structural checks over the contract and its generated artifact — cheap, and a failure there means the rest of the gate is checking against a contract state that isn't what will actually ship. See scripts/gate.mjs's own header comment for the full step order and the reasoning behind each step's position.

Released under the AGPL-3.0-only license. No telemetry, ever.