Migrate from Clerk
Move Clerk sign-in, route protection and sign-in tokens onto the MainSDK auth interface and switch drivers when you choose.
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 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 migrateworks on a branch and never touches yours. - Node 22 and
npx mainsdk. Nomain.yamlyet?migratewrites 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
npx mainsdk migrate clerk --report-onlyThe 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
| Where | Before | After |
|---|---|---|
app/api/orders/route.ts:4 | const { userId } = auth() | auth.require(req) |
app/sign-in/route.ts:6 | const token = await clerkClient.signInTokens.createSignInToken({ userI | auth.signIn() |
middleware.ts:3 | const isPublic = createRouteMatcher(['/sign-in(.*)', '/']) | auth.routes() |
middleware.ts:5 | export 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
- Export users from the Clerk dashboard (Users, Export) or the Backend API.
- Import them into the new driver on staging and compare counts.
- 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
npx mainsdk migrate clerkThis 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.
npx mainsdk init --template mainsdk/saas-webWas this page helpful?