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 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.
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.yamlin 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.
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.
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);
}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).
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);
}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);
}auth:
driver: clerk # or auth0
methods: [passkeys, sso]See Sign-in and sessions and Data and records.
3. OpenAI SDK to ai
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;
}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
});
}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:
import { pay } from "mainsdk";
const stripe = pay.raw(); // logged on every use, listed by mainsdk checkEach use is marked as bypassing that interface's guarantees, so you always know where you depend on one provider.
5. Verify each pull request
$ npx mainsdk check
mainsdk check · notes · env dev · Main 27
config secrets policy network auth data
Passed · 6 gates · 0 findings · 1.2sYour 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.
npx mainsdk migrate stripe --report-only --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?