MainSDK
Search⌘K
How-to

Migrate from Clerk

Move Clerk sign-in, route protection and sign-in tokens onto the MainSDK auth interface and switch drivers when you choose.

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 Clerk sign-in, route protection and sign-in tokens onto the MainSDK auth interface and switch drivers when you choose. Your Clerk account stays; nothing here deletes it.

Before you start:

  • A git repository with a clean working tree. mainsdk migrate works on a branch and never touches yours.
  • Node 22 and npx mainsdk. No main.yaml yet? migrate writes one; or start with Adopt an existing app.
  • Your Clerk keys stay where they are. Move them to secret: references when the driver is chosen (see Secrets).

1. Read the report

text
npx mainsdk migrate clerk --report-only

The report writes no file. It sorts every Clerk call in your code into three lists: mapped (an interface call replaces it), raw (it stays behind raw(), with the reason) and needs a person (a policy, a rule file or a call with no interface). Add --json for a machine-readable report.

For the sample app of this guide (packages/cli/test/fixtures/migrate/clerk), the report finds 4 mapped, 0 raw and 1 for a person.

2. What the codemod changes

WhereBeforeAfter
app/api/orders/route.ts:4const { userId } = auth()auth.require(req)
app/sign-in/route.ts:6const token = await clerkClient.signInTokens.createSignInToken({ userIauth.signIn()
middleware.ts:3const isPublic = createRouteMatcher(['/sign-in(.*)', '/'])auth.routes()
middleware.ts:5export default clerkMiddleware(async (guard, req) => {auth.middleware()

Each rewrite is mechanical and one line at a time. Imports of Clerk go when no call needs them, and mainsdk is added to package.json.

3. What stays behind raw()

  • Nothing in the sample stays behind raw().

A call behind raw() keeps working through the Clerk host on your network allow list, and mainsdk check still sees it. Each one has a marker comment, // mainsdk migrate: ..., so you can find it later.

4. What needs a person

  • app/layout.tsx:4: no interface call for <ClerkProvider; a person decides

The codemod leaves these lines alone and marks them. No tool should guess a policy.

5. Stateful moves are guided jobs

Data does not move with the code. Each stateful move is a job with steps and a rollback, listed in the pull request and never run by the codemod.

users: Move Clerk users to the auth driver you choose

  1. Export users from the Clerk dashboard (Users, Export) or the Backend API.
  2. Import them into the new driver on staging and compare counts.
  3. Switch sign-in for 1% of traffic, then all.

Rollback: Keep auth.driver: clerk until the new driver has served a week; switching back needs no data move.

6. Open the pull request

text
npx mainsdk migrate clerk

This records your sign-in and checkout flows as locked flows first, then creates the branch migrate/clerk, applies the codemods, adds .github/workflows/mainsdk-migrate.yml, runs mainsdk check and the locked flows, commits, pushes and opens one pull request with gh. If a gate fails, the branch is kept and no pull request opens (MAIN-MIGRATE-0001). --no-pr stops after the commit.

Locked flows run again in CI: npx mainsdk migrate flows.

Where you end

You end at the reference architecture mainsdk/saas-web: the same shape, with its tests, benchmark and chaos drills.

text
npx mainsdk init --template mainsdk/saas-web

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet