---
title: "Drivers and conformance"
description: "How drivers declare what they touch, how one conformance suite proves each driver, and how you build or swap one."
section: "architecture"
status: "Building for 0.4"
spec: "testing-kit"
canonical: "https://mainsdk.dev/docs/architecture/drivers-and-conformance"
updated: "2026-10-28"
---

# Drivers and conformance

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

Status: Building for 0.4 · Canonical: https://mainsdk.dev/docs/architecture/drivers-and-conformance · 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.

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

## A driver declares what it may touch

```ts title="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:

| Kind | How a swap works |
| --- | --- |
| Stateless: email, SMS, push, webhooks out, realtime, AI | Swaps 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 database | A guided migration job with a rollback, never a silent switch |
| Provider-only features | Through `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:

```ts title="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

| What | Phase |
| --- | --- |
| Driver kit, conformance suite, local harness | 1 (0.4) |
| `mainsdk driver publish`: Sigstore-signed, versioned against the contract; verified publishers with their own TUF delegations | 2 |
| 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.0 | 3 |

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

## Next

- [Contracts and versioning](https://mainsdk.dev/docs/architecture/contracts-and-versioning): the versions a driver claims.
- [Swap a driver](https://mainsdk.dev/docs/guides/swap-a-driver): the step-by-step guide.
- [Security architecture](https://mainsdk.dev/docs/architecture/security-model): threat models per driver class.
