MainSDK
Search⌘K
How-to

Eject

Leave MainSDK for one interface or all of them. mainsdk eject writes plain provider SDK code in place of MainSDK calls and keeps your tests passing.

Planned for 0.3

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

Replace MainSDK calls for one interface, or all of them, with the provider's own SDK. mainsdk eject keeps your tests passing, writes a report of the guarantees you give up, runs offline and sends nothing anywhere. No lock-in, by design.

Before you start:

  • A clean git working tree, so you can review and revert the change.
  • Your test suite passing, so you can compare the count after eject.

1. See the plan

Eject always plans first. --dry-run lists every file it would change and changes none.

bash
npx mainsdk eject email --dry-run
skip-test="sample output; no CLI snapshot covers eject yet"
$ npx mainsdk eject email --dry-run
eject email · driver resend
  app/lib/receipts.ts      email.send() → resend.emails.send()
  app/lib/newsletter.ts    email.marketing() → resend.emails.send()
  tests/receipts.test.ts   @mainsdk/testing fake → plain mock
  main.yaml                email.key secret:RESEND_API_KEY → process.env.RESEND_API_KEY
no files changed (dry run)

2. Eject one interface

bash
npx mainsdk eject email

Before:

app/lib/receipts.ts
import { email } from "mainsdk";

export async function sendReceipt(to: string, orderId: string) {
  await email.send({ to, template: "receipt", data: { orderId } });
}

After, with the Resend driver:

app/lib/receipts.js
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);

export async function sendReceipt(to: string, orderId: string) {
  await resend.emails.send({
    from: "orders@pinecart.dev",
    to,
    subject: "Your receipt",
    html: renderReceipt({ orderId }),
  });
}

How the template is rendered (renderReceipt here) depends on each driver's recipe.

What eject changes:

  • Every call to that interface becomes idiomatic code for the provider SDK named by the driver in main.yaml.
  • secret: references become plain environment variable reads.
  • Tests that used @mainsdk/testing fakes are rewritten to the provider's test mode or a plain mock. They are never deleted.
  • EJECT-REPORT.md lists each guarantee that no longer holds.

A partial eject keeps every other interface working.

3. Verify it worked

Run your tests. They pass, with the same test count as before.

bash
node --test

Then read the report and decide how you replace each guarantee:

EJECT-REPORT.md
# Eject report: email (resend)

No longer enforced by MainSDK:
- Unsubscribe link on marketing email (was enforced by types)
- Suppression list checked before every send
- Idempotency keys on sends (retries may send twice)

Eject everything

bash
npx mainsdk eject --all --dry-run
npx mainsdk eject --all

After the last interface, main.yaml, the gates and every mainsdk and @mainsdk/* dependency are removed from the project. Interfaces with a built-in driver, such as Cedar for policy or built-in webhooks, eject to the open-source library code they use.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet