MainSDK
Search⌘K
How-to

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.

Planned for 0.7

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

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.
  • Your provider keys stored as secret: references (see 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.

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.

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);
}
main.yaml
payments: { driver: stripe }
pay:
  stripe: { key: secret:STRIPE_SECRET_KEY, webhook: secret:STRIPE_WEBHOOK_SECRET }

See Payments.

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).

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);
}
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);
}
main.yaml
auth:
  driver: clerk          # or auth0
  methods: [passkeys, sso]

See Sign-in and sessions and Data and records.

3. OpenAI SDK to ai

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;
}
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
  });
}
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.

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

snapshot="check-pass"
$ 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.

Later: mainsdk migrate

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.

Next

Where you end

You end at the reference architecture mainsdk/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.

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet