---
title: "Interfaces and drivers"
description: "Your app calls one interface per capability; a driver talks to the provider you pick in main.yaml. Swap the provider, keep the code."
section: "concepts"
canonical: "https://mainsdk.dev/docs/concepts/interfaces-and-drivers"
updated: "2026-10-28"
---

# Interfaces and drivers

> Your app calls one interface per capability; a driver talks to the provider you pick in main.yaml. Swap the provider, keep the code.

Canonical: https://mainsdk.dev/docs/concepts/interfaces-and-drivers · Updated: 2026-10-28

MainSDK gives your app one **interface** for each thing almost every app does: sign people in, store records, send email, take payments, call a model. Your code calls the interface. A **driver** talks to the provider you chose in `main.yaml`. Changing the provider is a change to `main.yaml`, not to your code.

Engineers know this pattern as ports and adapters. Dapr uses it for backend services, OpenFeature for flags and OpenTelemetry for telemetry. MainSDK adds guarantees, a policy engine and an MCP surface on top.

## Four layers under every call

```mermaid
flowchart LR
  A[Your code] --> I[Interface<br/>email.send]
  I --> G[Guarantees<br/>unsubscribe, consent, idempotency]
  G --> P[Policy engine<br/>consent, region, audit]
  P --> D[Driver<br/>resend]
  D --> X[(Provider API)]
```

1. **Interface:** the call you write, such as `email.send()` or `db.orders.for(user).list()`. Its name and shape are the same whatever the provider.
2. **Guarantees:** what the interface always does. A marketing email without an unsubscribe link fails; a query without an owner scope does not compile.
3. **Policy engine:** decides the call in process (who, which data, which region) and writes the decision to the audit log. See [Policy as code](https://mainsdk.dev/docs/concepts/policy).
4. **Driver:** talks to the provider. It declares the hosts, data labels and regions it needs, and nothing else is allowed.

## What you write

```ts title="app/api/orders/route.ts"
import { auth, email, models, s } from "mainsdk";

const { orders } = models({
  orders: s.object({ item: s.string(), total: s.int() }),
});

export async function POST(req: Request) {
  const session = await auth.require(req);                      // 401 problem details if signed out
  const input = await req.json() as { item: string; total: number };
  const order = await orders.for(session.user.id).create(input); // owner scope is required
  await email.send({ to: session.user.email, template: "receipt", data: { orderId: order.id } });
  return Response.json(order, { status: 201 });
}
```

```yaml title="main.yaml"
providers:
  email: resend              # was ses: change this line, no code change
auth:
  driver: github
db:
  driver: postgres
  regions: { us: secret:DATABASE_URL_US, eu: secret:DATABASE_URL_EU }
email:
  key: secret:RESEND_API_KEY
fallback:
  email: [resend, ses]       # fail over if the first is down
```

Each interface has its own page with its calls, drivers, guarantees and check codes, for example [auth](https://mainsdk.dev/docs/interfaces/auth), [db](https://mainsdk.dev/docs/interfaces/db), [email](https://mainsdk.dev/docs/interfaces/email) and [ai](https://mainsdk.dev/docs/interfaces/ai).

## How seamless a swap is

| Kind of interface | What a swap takes |
| --- | --- |
| Stateless: email, SMS, push, webhooks out, realtime, AI | Fast. The contract's tests run on the new driver, it sends in shadow mode, then traffic switches. |
| Stateful: users in an auth provider, customers in a payment provider, rows in a database | A guided migration job with a rollback, never a silent switch. |

See [Swap a driver](https://mainsdk.dev/docs/guides/swap-a-driver).

## Capability profiles

Every driver declares what it supports. An `access` driver lists its models (RBAC, ABAC, ReBAC), listing, conditions and consistency tokens; an `auth` driver lists the sign-in methods it covers. `mainsdk check` compares the profile with what your app uses and names the gap before production. There is no silent fallback that weakens a decision.

```text
$ npx mainsdk check
mainsdk check · pinecart · env dev · Main 27
✗ access    1 finding
  MAIN-ACCESS-0004  high  policies/folders.cedar:1
  fix: relations need a ReBAC driver (openfga, spicedb, permit) or Cedar entities
✓ config  ✓ secrets  ✓ policy  ✓ network  ✓ data
1 gate failed · exit 1
```

## The escape hatch

When you need a feature only one provider has, every interface gives you the provider's own client through `raw()`:

```ts
import { email } from "mainsdk";

const resend = email.raw();   // logged on each use and listed by mainsdk check
```

`check` marks each use as bypassing that interface's guarantees, so you always know where you depend on one provider.

## Versioned contracts

Every interface is a versioned contract, such as `email@1.4`. Drivers declare the range they implement (`implements: email@^1.2`), and `mainsdk sync` resolves versions into [`main.lock`](https://mainsdk.dev/docs/concepts/main-yaml) with hashes. Two majors can run side by side during an upgrade: import `mainsdk/email` for the current one and `mainsdk/email/v2` for the other. A deprecation gives at least 12 months before removal, and every breaking change names its codemod. How contracts are defined and diffed is in [Contracts and versioning](https://mainsdk.dev/docs/architecture/contracts-and-versioning).

## Write your own driver

The driver kit lets anyone build a driver for any interface. The manifest declares the hosts, data labels and regions the driver touches; the harness fails a driver that reaches anything undeclared. `mainsdk driver test` runs the same conformance suite first-party drivers pass.

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

export default defineDriver({
  implements: "email@1",
  id: "relaymail",
  manifest: {
    regions: ["us", "eu", "in"],
    hosts: ["api.relaymail.dev"],
    data: ["email-address", "message-body"],
    subprocessors: ["Relaymail Inc."],
  },
  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 };
  },
});
```

`defineDriver` comes from `mainsdk` today. Whether the driver kit becomes its own package or part of `@mainsdk/testing` is still open.

## Next

- [The main.yaml file](https://mainsdk.dev/docs/concepts/main-yaml)
- [Swap a driver](https://mainsdk.dev/docs/guides/swap-a-driver)
- [Drivers and conformance](https://mainsdk.dev/docs/architecture/drivers-and-conformance)
