Skip to main content

The contract

Written before the code · shaped by what callers need · versioned

A contract is the specification of what an owner offers: the operations, their inputs and outputs, and the guarantees attached to them. It is a file in git. It is not the code, and it is not generated from the code.

Three properties, and only the third is optional to argue about​

It is written before the code​

The spec comes first and the code is generated from it — server stubs and client packages both. The reverse direction, generating a spec from an implementation, produces a document that describes whatever the code happens to do, including its accidents. Callers then depend on the accidents.

It is shaped by what callers need, not by what the tables look like​

This is the one that decides whether item 30 is worth doing.

A CRUD API mirroring the tables one-to-one — GET /orders, GET /order_items, one endpoint per table, columns as fields — keeps every bit of the coupling and adds a network hop. It is worse than direct access, because a schema change now has to move the API and its callers in step instead of just the callers.

A contract that decouples is written from the caller's question. The caller does not want order_items; it wants "what did this customer buy, with prices". Answer that, and the tables behind it are free to change.

Table-shapedCaller-shaped
EndpointGET /order_items?order_id=GET /orders/{id} returning lines inline
Caller must knowthat lines live in a second tablenothing about storage
Split the table in twobreaks every callerinvisible

It is versioned​

Additive change happens in place. Breaking change gets a new version, and the old one keeps working until its callers are gone — which is knowable, because the owner sees who calls it.

What counts as a breaking change​

The distinction that CI enforces. If it is on the left, it needs a new version; if it is on the right, it ships.

Breaking — needs a new versionNot breaking — ships in place
Removing a field from a responseAdding an optional field to a response
Renaming a fieldAdding a new endpoint
Narrowing a type (string → enum, wider → narrower range)Widening a type
Making an optional request field requiredAdding an optional request field
Adding a new value to an enum a caller switches onAdding a value to an enum documented as open
Changing pagination semantics — page size, cursor meaning, orderingAdding a new filter parameter
Changing an error code for an existing conditionAdding a new error code for a new condition
Tightening a rate limit or timeout callers rely onRelaxing a validation rule

Two of these are easy to get wrong:

  • Enums. Adding a value is breaking unless the contract said from the start that the list is open and callers were required to handle the unknown case. Decide this per field, in the first version.
  • Ordering. An unspecified sort order is not a licence to change it. If callers can observe it, it is part of the contract whether it was written down or not — so write it down.

Caller identity​

A contract names its caller. "The owner knows the caller, the operation and the row" holds only if every request carries an identity the owner can verify — so identity is part of the contract, not plumbing underneath it.

The caller is a Zitadel service user​

Each calling service is a Zitadel service user. It exchanges its credentials for a short-lived JWT whose audience is the owning service, and the owner validates that token locally against cached JWKS — signature, iss, aud, exp — then checks a role the owner defines. No round-trip per request. Authorization moves from "this credential may read the table" to "this service may perform this operation".

The flow — the token request, the verifying middleware — is worked through on the audit-log authentication page. Two things differ here: the audience is the owning service's own, and the role is one the owner names. audit.writer is that page's role, not a convention.

That identity is the service, not the person​

A service-user token says which application is calling. It says nothing about which end user the call is made on behalf of. Where an owner must authorize or audit per user, the contract has to carry that user context explicitly; how it does so is decided per contract, at contract-design time on milestone 30.2. It is not prescribed here.

Identity on the API does not close the direct path​

A service holding a valid JWT and a database password has two routes, and all the coupling of the second. The grant is still revoked on milestone 30.4, and new credentials still come only from the secrets engine — the completion criterion on migration is unchanged.

What CI checks​

A contract nothing enforces is documentation, and documentation does not stop a caller from breaking. The pipeline compares the spec on the branch against the spec on the main line and fails on anything in the left column that is not accompanied by a version bump.

The specs are published in the Backstage catalog (roadmap item 22), so a caller finds the contract without asking anyone.