Contracts and versioning
Every MainSDK boundary is a versioned contract written once in TypeSpec and checked by machine on every change.
Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
Every boundary in MainSDK is a written, versioned, machine-checked contract (ADR-0002). We write each one once in TypeSpec and generate everything else from it, so bindings, docs, tests and check rules cannot drift.
Ten contracts
| Contract | Between | Versioned as |
|---|---|---|
| Interface | Your app and the SDK (email, pay, policy …) | email@1.4 |
| Driver | The SDK and each provider driver | implements: email@^1.2 |
| Config | main.yaml, overlays, main.org.yaml | schema: 1 plus a published JSON Schema |
| Policy | Policies, packs and the engine | policy@1 |
| Events | Events, webhooks, CloudEvents types | Per event type, in a schema registry |
| Telemetry | The data contract: every field we collect | telemetry@1 |
| Wire | SDK, CLI and MainSDK Cloud | mainsdk-protocol header on every call |
| Parity vectors | The golden test vectors each language SDK may adopt (from 0.4, ADR-0075) | Vector set version |
| Main mark | The signed release record and its open spec | mainsdk-mark@1 |
| MCP tools | Actions exposed to agents | Tool schema version |
One source, many outputs
flowchart LR tsp["TypeSpec source<br/>packages/core/contracts/"] --> oa["OpenAPI"] tsp --> aa["AsyncAPI"] tsp --> js["JSON Schema<br/>main.yaml schema"] tsp --> ts["TypeScript types"] tsp -.-> rs["Rust, Python and Go types<br/>each language SDK, phase 5"] tsp --> docs["Reference docs"] tsp --> conf["Conformance stubs"] tsp --> rules["check rules and error codes"]
Generated files are never edited by hand: a hand edit fails tools/check fast with generated file changed by hand. Running the build twice produces no diff.
Still open: how AsyncAPI is emitted, since TypeSpec has no official AsyncAPI emitter.
Breaking changes fail CI
mainsdk contract diff compares the source with the last release, the way buf breaking guards Protobuf.
mainsdk contract diff [--base <git-ref|release>] [--json]
exit 0 no change, or only additive changes in a minor
exit 1 a breaking change not marked as a new majorBreaking means: a removed or renamed field, a narrowed type, a new required input, a changed default, or a removed error code. Removing the optional field replyTo from email@1 fails:
{"breaking":[{"contract":"email@1","kind":"field-removed","path":"replyTo"}]}The same removal released as email@2.0, or a new optional field in email@1.5, passes.
Negotiated at sync: main.lock
The resolver picks driver versions that satisfy every interface range and writes them, with hashes, to main.lock. The output is deterministic: the same inputs on two machines give byte-identical files. Each entry is a valid TUF target, so delivery can verify it (ADR-0005).
lock: 1
interfaces:
email: { range: "^1.2", resolved: "1.4.0" }
drivers:
resend:
version: 2.3.1
implements: ["email@^1.2"]
sha256: "9f2c…"When no version fits, the resolver fails with MAIN-CONTRACT-0003 and names the conflicting ranges.
Upgrading one call site at a time
timeline title Moving from email@1 to email@2 email@2 released : both majors importable side by side Deprecation : check warns at each call site with file, line and removal date Codemod : every breaking change names its codemod 12 months later : the old major can be removed
import { email } from "mainsdk/email"; // current major
import { email as emailV2 } from "mainsdk/email/v2"; // the other supported major- Two majors of an interface run side by side during a migration.
- A deprecation carries the version it was deprecated in and a removal date at least 12 months later.
- Local usage counts of deprecated calls show when removal is safe.
- MainSDK Cloud supports the current and two previous SDK majors (phase 2).
Stability levels
Every interface declares a level in its contract: stable, beta or experimental.
- Phase 1 stable core: config, policy, access, auth, db, secrets, log, email, pay, files, webhooks, jobs.
- Beta until proven: any other interface stays beta until it has two drivers, a passing conformance suite and three production users.
mainsdk checklists every beta or experimental interface your app uses, and experimental interfaces need an explicit opt-in inmain.yaml.
A baseline is a bill of contracts
Main 27 lists the exact version of every contract it includes, so "Main 27" means the same thing to every app, driver and auditor. The signed Main mark record repeats those versions:
"contracts": { "email": "1.4", "policy": "1", "telemetry": "1" }Next
- Drivers and conformance: proving a driver meets each version it claims.
- Supply chain: how lock entries are delivered and verified.
Was this page helpful?