MainSDK
Search⌘K
Architecture

Contracts and versioning

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

Planned for 0.1

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.
Stability levels
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

ContractBetweenVersioned as
InterfaceYour app and the SDK (email, pay, policy …)email@1.4
DriverThe SDK and each provider driverimplements: email@^1.2
Configmain.yaml, overlays, main.org.yamlschema: 1 plus a published JSON Schema
PolicyPolicies, packs and the enginepolicy@1
EventsEvents, webhooks, CloudEvents typesPer event type, in a schema registry
TelemetryThe data contract: every field we collecttelemetry@1
WireSDK, CLI and MainSDK Cloudmainsdk-protocol header on every call
Parity vectorsThe golden test vectors each language SDK may adopt (from 0.4, ADR-0075)Vector set version
Main markThe signed release record and its open specmainsdk-mark@1
MCP toolsActions exposed to agentsTool 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"]
Diagram

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).

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

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
Diagram
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

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet