---
title: "Contracts and versioning"
description: "Every MainSDK boundary is a versioned contract written once in TypeSpec and checked by machine on every change."
section: "architecture"
status: "Planned for 0.1"
spec: "versioned-contracts"
canonical: "https://mainsdk.dev/docs/architecture/contracts-and-versioning"
updated: "2026-10-28"
---

# Contracts and versioning

> Every MainSDK boundary is a versioned contract written once in TypeSpec and checked by machine on every change.

Status: Planned for 0.1 · Canonical: https://mainsdk.dev/docs/architecture/contracts-and-versioning · Updated: 2026-10-28

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

```mermaid
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.

```text
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 major
```

Breaking 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:

```json
{"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).

```yaml title="main.lock"
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

```mermaid
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
```

```ts
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 check` lists every beta or experimental interface your app uses, and experimental interfaces need an explicit opt-in in `main.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:

```jsonc
"contracts": { "email": "1.4", "policy": "1", "telemetry": "1" }
```

## Next

- [Drivers and conformance](https://mainsdk.dev/docs/architecture/drivers-and-conformance): proving a driver meets each version it claims.
- [Supply chain](https://mainsdk.dev/docs/architecture/supply-chain): how lock entries are delivered and verified.
