---
title: "Migrate from raw provider SDKs"
description: "Move Stripe, Auth0 or Clerk, and OpenAI SDK calls onto MainSDK pay, auth and ai, one module per pull request, with before and after code."
section: "guides"
status: "Planned for 0.7"
spec: "migration-guides"
canonical: "https://mainsdk.dev/docs/guides/migrate-from-raw-providers"
updated: "2026-10-28"
---

# Migrate from raw provider SDKs

> Move Stripe, Auth0 or Clerk, and OpenAI SDK calls onto MainSDK pay, auth and ai, one module per pull request, with before and after code.

Status: Planned for 0.7 · Canonical: https://mainsdk.dev/docs/guides/migrate-from-raw-providers · Updated: 2026-10-28

Move direct Stripe, Clerk or Auth0, and OpenAI SDK calls onto MainSDK interfaces, so you can swap, audit and check them. Your provider accounts stay; the driver talks to them.

**Before you start:**

- A `main.yaml` in your app. If you don't have one, start with [Adopt an existing app](https://mainsdk.dev/docs/getting-started/existing-app).
- Your provider keys stored as `secret:` references (see [Secrets](https://mainsdk.dev/docs/interfaces/secrets)).

Move one module per pull request, and run `mainsdk check` on each.

## 1. Stripe to `pay`

Before: the key is read in your code, and nothing stops a retry from charging twice.

```ts title="app/api/checkout/route.ts (before)"
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
  const { orderId, amount } = await req.json();
  const session = await stripe.checkout.sessions.create({
    mode: "payment",
    line_items: [{ price_data: { currency: "usd", unit_amount: amount,
      product_data: { name: `Order ${orderId}` } }, quantity: 1 }],
    success_url: "https://orders.pinecart.dev/orders/done",
    cancel_url: "https://orders.pinecart.dev/cart",
  });
  return Response.json({ url: session.url });
}
```

After: hosted checkout, an idempotency key on the call, the key in `secrets`.

```ts title="app/api/checkout/route.ts (after)"
import { auth, models, pay, s } from "mainsdk";

const { orders } = models({
  orders: s.object({ total: s.int() }),
}); // declared once in lib/orders.ts

export async function POST(req: Request) {
  const session = await auth.require(req);
  const { orderId } = (await req.json()) as { orderId: string };
  const order = await orders.for(session.user).get(orderId);
  const checkout = await pay.checkout({
    order: order.id,
    amount: order.total,          // integer minor units
    currency: "USD",
    successUrl: "/orders/done",
    cancelUrl: "/cart",
    idempotencyKey: `checkout:${order.id}`,
  });
  return Response.json(checkout);
}
```

```yaml title="main.yaml"
payments: { driver: stripe }
pay:
  stripe: { key: secret:STRIPE_SECRET_KEY, webhook: secret:STRIPE_WEBHOOK_SECRET }
```

See [Payments](https://mainsdk.dev/docs/interfaces/pay).

## 2. Auth0 or Clerk to `auth`

Your users, sessions and MFA stay in your Auth0 or Clerk tenant. Set the driver to the provider you run (`byo-identity`).

```ts title="app/api/orders/route.ts (before, Clerk)"
import { auth } from "@clerk/nextjs/server";

export async function GET() {
  const { userId } = await auth();
  if (!userId) return new Response("Unauthorized", { status: 401 });
  const orders = await sql`select * from orders where user_id = ${userId}`;
  return Response.json(orders);
}
```

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

const { orders } = models({
  orders: s.object({ total: s.int() }),
}); // declared once in lib/orders.ts

export async function GET(req: Request) {
  const session = await auth.require(req);            // 401 problem details with a fix
  const page = await orders.for(session.user).list({ limit: 20 });
  return Response.json(page);
}
```

```yaml title="main.yaml"
auth:
  driver: clerk          # or auth0
  methods: [passkeys, sso]
```

See [Sign-in and sessions](https://mainsdk.dev/docs/interfaces/auth) and [Data and records](https://mainsdk.dev/docs/interfaces/db).

## 3. OpenAI SDK to `ai`

```ts title="app/lib/summary.ts (before)"
import OpenAI from "openai";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

export async function summarise(orderId: string) {
  const res = await openai.chat.completions.create({
    model: "gpt-4o",
    messages: [{ role: "user", content: `Summarise order ${orderId}` }],
  });
  return res.choices[0].message.content;
}
```

```ts title="app/lib/summary.ts (after)"
import { ai } from "mainsdk";

export async function summarise(user: { id: string }, orderId: string) {
  return ai.generate({
    model: "anthropic/claude-sonnet-4-5",
    prompt: `Summarise order ${orderId}`,
    feature: "order-summary",   // spend limits and audit per feature
    user,                       // and per user
  });
}
```

```yaml title="main.yaml"
ai:
  driver: bedrock        # or anthropic, openai, gateway
  models: [anthropic/claude-*]
```

Personal data is redacted before the call, spend limits apply, and the model allowlist is checked. See [AI calls](https://mainsdk.dev/docs/interfaces/ai).

## 4. Keep what can't move behind `raw()`

Every interface has `raw()`, which returns the provider's own client. Use it for a provider-only feature:

```ts
import { pay } from "mainsdk";
const stripe = pay.raw();    // logged on every use, listed by mainsdk check
```

Each use is marked as bypassing that interface's guarantees, so you always know where you depend on one provider.

## 5. Verify each pull request

```text
$ npx mainsdk check
mainsdk check · notes · env dev · Main 27

  ✓ config   ✓ secrets   ✓ policy   ✓ network   ✓ auth   ✓ data

Passed · 6 gates · 0 findings · 1.2s
```

Your existing tests should pass unchanged. To test the migrated code without provider keys, see [Test your app](https://mainsdk.dev/docs/guides/testing).

## Later: `mainsdk migrate`

> **Note:** `mainsdk migrate` and the step-by-step migration guides ship in phase 2 (milestone 0.7). In phase 1, use `mainsdk adopt` and the migration skill.

`mainsdk migrate <from>` reports what maps to which interface, what stays behind `raw()`, and what needs a person, then opens the change as a pull request. `<from>` is one of `supabase`, `firebase`, `clerk`, `auth0`, `authjs`, `better-auth`, `stripe` or `raw`.

```bash
npx mainsdk migrate stripe --report-only --json
```

```json
{ "from": "stripe",
  "mapped": [{ "file": "app/api/checkout/route.ts", "line": 7,
               "call": "stripe.checkout.sessions.create()", "to": "pay.checkout()" }],
  "raw":    [],
  "person": [],
  "endsAt": "mainsdk/payments-checkout" }
```

`--report-only` changes nothing. Moving stored state (users, customers, rows) is a guided job with a rollback, never a silent switch. See [Swap a driver](https://mainsdk.dev/docs/guides/swap-a-driver).

## Next

- [Adopt an existing app](https://mainsdk.dev/docs/getting-started/existing-app)
- [Interfaces and drivers](https://mainsdk.dev/docs/concepts/interfaces-and-drivers)
- [Eject](https://mainsdk.dev/docs/guides/eject)

## Where you end

You end at the reference architecture [`mainsdk/api-service`](https://mainsdk.dev/docs/guides/reference-api-service). `npx mainsdk migrate raw` runs these moves as one pull request, and `npx mainsdk init --template mainsdk/api-service` starts the finished shape.
