MainSDK
Search⌘K
Architecture

Drivers and conformance

How drivers declare what they touch, how one conformance suite proves each driver, and how you build or swap one.

Building for 0.4

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

A driver connects an interface to one provider. We wrap the best providers instead of rebuilding them, and every driver, ours or a third party's, passes the same conformance suite for each interface version it claims.

flowchart LR
  iface["Interface<br/>email@1.4"] --> drv["Driver<br/>implements: email@^1.2"]
  drv --> man["Manifest<br/>regions · data labels · hosts · subprocessors"]
  man --> pe["Policy engine refuses<br/>anything undeclared"]
  drv --> conf["Conformance suite<br/>generated per interface version"]
  conf --> prof["Capability profile<br/>what the driver supports"]
Diagram

A driver declares what it may touch

relaymail/index.ts
import { defineDriver, type EmailMessage } from "mainsdk";

export default defineDriver({
  implements: "email@1",
  id: "relaymail",
  manifest: {
    regions: ["us", "eu", "in"],
    data: ["email-address", "message-body"],   // the only labels it may touch
    subprocessors: ["Relaymail Inc."],
    hosts: ["api.relaymail.dev"],          // added to the app's network allowlist
  },
  async send(msg: EmailMessage, ctx: { http: { post(path: string, body: unknown): Promise<{ id: string }> } }) {
    const res = await ctx.http.post("/v1/send", msg);
    return { id: res.id };
  },
});

Permissions work like a mobile app's: the policy engine refuses data labels, regions and hosts a driver didn't declare. Drivers run in a WebAssembly sandbox where possible. defineDriver comes from mainsdk today. Whether the driver kit becomes its own package or part of @mainsdk/testing is still open.

One conformance suite per interface version

text
$ npx mainsdk driver test ./relaymail --interface email@1.4
email@1.4 conformance: 86 passed, 0 failed · capability profile: attachments=no · idempotency=sdk
  • The suite is generated from the same contract as the bindings, so it can't drift.
  • mainsdk driver test runs exactly the suite our own drivers pass.
  • The harness fails a driver that touches undeclared hosts or labels.
  • Results publish as a public compatibility matrix.

Capability profiles

Not every provider supports every feature. A driver's capability profile records what it supports, for example attachments=no or idempotency=sdk (the SDK supplies idempotency because the provider lacks it). mainsdk check names any capability your app uses that its driver lacks, before production.

Local drivers

Every interface ships a local driver: SQLite for db, a local inbox for email, a test card flow for pay, files on disk, an in-process queue for jobs. mainsdk dev runs your app offline with no account or provider key. check refuses a local driver in production:

text
MAIN-CHECK-0026 local driver in production: secrets

Swapping a provider

You change the driver in main.yaml. What happens next depends on whether the provider holds state:

KindHow a swap works
Stateless: email, SMS, push, webhooks out, realtime, AISwaps at once: the contract's tests run against the new driver, it sends in shadow mode, then it switches
Stateful: users in auth, customers in payments, rows in a databaseA guided migration job with a rollback, never a silent switch
Provider-only featuresThrough raw(), which check lists so you always know where you depend on one provider

Testing with fakes and chaos

@mainsdk/testing gives in-memory fakes for every phase 1 interface. Fakes enforce the same guarantees as production, and chaos drills inject outages, latency and malformed responses:

test/signup.test.ts
import { createTestApp, mailbox, chaos } from "@mainsdk/testing";

const app = createTestApp({ main: "./main.yaml" });   // main.yaml has fallback: { email: [resend, ses] }
chaos.on("email:resend", { outage: true });           // or { latency: "3s" } or { respond: "malformed" }
await signUp(app, "a@pinecart.dev");                  // your own helper; the send fails over to ses
mailbox(app).toHaveSent({ to: "a@pinecart.dev", template: "welcome" });

Publishing and the marketplace

WhatPhase
Driver kit, conformance suite, local harness1 (0.4)
mainsdk driver publish: Sigstore-signed, versioned against the contract; verified publishers with their own TUF delegations2
Driver marketplace: conformance, p50 and p99 from our own runs, capability profile, publisher level, ratings. Free only: no paid listings, no revenue share, drivers stay Apache-2.03

Delisting or revoking a driver goes through TUF metadata, and check warns in every app that uses it.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet