Drivers and conformance
How drivers declare what they touch, how one conformance suite proves each driver, and how you build or swap one.
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.
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"]
A driver declares what it may touch
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
$ 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 testruns 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:
MAIN-CHECK-0026 local driver in production: secretsSwapping 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:
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: the versions a driver claims.
- Swap a driver: the step-by-step guide.
- Security architecture: threat models per driver class.
Was this page helpful?