Migrate from other authentication providers to Clerk. Use when migrating from Auth0, Firebase, Supabase Auth, NextAuth, or custom authentication solutions. Trigger with phrases like "migrate to clerk", "clerk migration", "switch to clerk", "auth0 to clerk", "firebase auth to clerk".
80
77%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./plugins/saas-packs/clerk-pack/skills/clerk-migration-deep-dive/SKILL.md!npm list @auth0/nextjs-auth0 next-auth @supabase/auth-helpers-nextjs firebase 2>/dev/null | grep -E "auth0|next-auth|supabase|firebase" || echo 'No auth providers detected'
Comprehensive guide to migrating from Auth0, Firebase Auth, Supabase Auth, or NextAuth to Clerk. Covers user data export, bulk import, parallel running, and phased migration.
Auth0 Export:
# Export users via Auth0 Management API
curl -s -H "Authorization: Bearer $AUTH0_TOKEN" \
"https://$AUTH0_DOMAIN/api/v2/users?per_page=100&page=0" \
| jq '[.[] | {email: .email, name: .name, picture: .picture, created_at: .created_at}]' \
> auth0-users.jsonNextAuth (Prisma) Export:
// scripts/export-nextauth-users.ts
const users = await prisma.user.findMany({
include: { accounts: true },
})
const exported = users.map((u) => ({
email: u.email,
name: u.name,
image: u.image,
provider: u.accounts[0]?.provider,
createdAt: u.createdAt,
}))
await fs.writeFile('nextauth-users.json', JSON.stringify(exported, null, 2))// scripts/import-to-clerk.ts
import { createClerkClient } from '@clerk/backend'
import users from './auth0-users.json'
const clerk = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY! })
interface MigrationResult {
email: string
status: 'created' | 'exists' | 'error'
clerkId?: string
error?: string
}
async function importUsers(): Promise<MigrationResult[]> {
const results: MigrationResult[] = []
for (const user of users) {
try {
const created = await clerk.users.createUser({
emailAddress: [user.email],
firstName: user.name?.split(' ')[0],
lastName: user.name?.split(' ').slice(1).join(' '),
skipPasswordRequirement: true, // User will set password on first sign-in
})
results.push({ email: user.email, status: 'created', clerkId: created.id })
console.log(`Created: ${user.email} -> ${created.id}`)
} catch (err: any) {
if (err.status === 422) {
results.push({ email: user.email, status: 'exists' })
} else {
results.push({ email: user.email, status: 'error', error: err.message })
}
}
// Respect rate limits
await new Promise((resolve) => setTimeout(resolve, 100))
}
return results
}
importUsers().then((results) => {
const summary = {
total: results.length,
created: results.filter((r) => r.status === 'created').length,
exists: results.filter((r) => r.status === 'exists').length,
errors: results.filter((r) => r.status === 'error').length,
}
console.log('Migration summary:', summary)
fs.writeFileSync('migration-results.json', JSON.stringify(results, null, 2))
})// scripts/update-db-references.ts
import { createClerkClient } from '@clerk/backend'
const clerk = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY! })
async function updateDatabaseReferences() {
// Get all users from your database
const dbUsers = await db.user.findMany()
for (const dbUser of dbUsers) {
// Find corresponding Clerk user by email
const clerkUsers = await clerk.users.getUserList({
emailAddress: [dbUser.email],
})
if (clerkUsers.totalCount > 0) {
const clerkUser = clerkUsers.data[0]
await db.user.update({
where: { id: dbUser.id },
data: {
clerkId: clerkUser.id,
// Keep old auth ID for rollback
legacyAuthId: dbUser.authProviderId,
},
})
console.log(`Mapped: ${dbUser.email} -> ${clerkUser.id}`)
}
}
}// BEFORE: NextAuth
// import { getServerSession } from 'next-auth'
// import { authOptions } from '@/lib/auth'
// const session = await getServerSession(authOptions)
// const userId = session?.user?.id
// AFTER: Clerk
import { auth } from '@clerk/nextjs/server'
const { userId } = await auth()// BEFORE: NextAuth client hook
// import { useSession } from 'next-auth/react'
// const { data: session } = useSession()
// AFTER: Clerk client hook
import { useUser } from '@clerk/nextjs'
const { user, isLoaded } = useUser()// BEFORE: NextAuth middleware
// export { default } from 'next-auth/middleware'
// export const config = { matcher: ['/dashboard(.*)'] }
// AFTER: Clerk middleware
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isProtected = createRouteMatcher(['/dashboard(.*)'])
export default clerkMiddleware(async (auth, req) => {
if (isProtected(req)) await auth.protect()
})// lib/auth-bridge.ts — run both auth systems during transition
import { auth as clerkAuth } from '@clerk/nextjs/server'
export async function getAuthUser() {
// Try Clerk first (new system)
const { userId: clerkUserId } = await clerkAuth()
if (clerkUserId) {
return { provider: 'clerk', userId: clerkUserId }
}
// Fall back to legacy system during migration window
// const legacySession = await getLegacySession()
// if (legacySession) return { provider: 'legacy', userId: legacySession.userId }
return null
}# After migration is verified (2+ weeks in production):
npm uninstall next-auth @auth0/nextjs-auth0 # Remove old auth packages
# Delete old auth files: pages/api/auth/[...nextauth].ts, lib/auth.ts
# Remove legacy database columns after confirming all users migrated| Error | Cause | Solution |
|---|---|---|
| Duplicate email on import | User already exists in Clerk | Skip (status: 'exists') or merge |
| Invalid email format | Dirty data from export | Clean/validate before import |
| Rate limited during import | Too many API calls | Add 100ms delay between creates |
| Password can't be migrated | Passwords are hashed | Use skipPasswordRequirement, user sets new password |
| OAuth accounts | Social login tokens non-transferable | Users re-link OAuth accounts on first Clerk sign-in |
// scripts/verify-migration.ts
async function verifyMigration() {
const dbUsers = await db.user.findMany({ where: { clerkId: { not: null } } })
const unmapped = await db.user.findMany({ where: { clerkId: null } })
console.log(`Mapped: ${dbUsers.length}, Unmapped: ${unmapped.length}`)
if (unmapped.length > 0) {
console.log('Unmapped users:', unmapped.map((u) => u.email))
}
}After migration, review clerk-prod-checklist for production readiness.
3e83543
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.