Upgrading to Clerk Core 3
Use this pre-built prompt to upgrade your project with AI assistance.
Overview
Core 3 focuses on consistency and cleanup across Clerk's SDKs. Most projects can be upgraded in under 30 minutes using the upgrade CLI.
Here's a summary of what changed:
- Component consolidation:
Protect,SignedIn, andSignedOutare replaced by a singleShowcomponent. - Package renaming:
@clerk/clerk-reactbecomes@clerk/react.@clerk/clerk-expobecomes@clerk/expo.createThememoves to@clerk/ui/themes/experimental.- Types consolidate under
@clerk/shared/types.
- Appearance updates:
appearance.layoutis nowappearance.options,showOptionalFieldsdefaults tofalse, and color variables apply at full opacity. - Behavior alignment: Legacy redirect props and billing flags are removed in favor of the newer patterns.
- Token handling:
getToken()now throwsClerkOfflineErrorwhen offline and uses proactive background refresh for better performance. - Platform requirements:
- For all packages: Node.js 20.9.0+ is required.
- For
@clerk/nextjs: Next.js 15.2.3+ is required.
- Satellite apps: Satellite apps no longer perform automatic redirects on first visit.
In addition to the cross-SDK changes listed above, each SDK has its own specific changes. See the SDK-specific changes section for details on your framework.
Preparing to upgrade
Before upgrading, make sure your project meets the following requirements:
- Node.js 20.9.0 or later is installed. You can check your version by running
node -vin your terminal. If you need to update, see the Node.js download page. - Your Clerk SDKs are on the latest Core 2 version. Updating to the latest Core 2 release first allows you to resolve deprecation warnings incrementally, which will make the Core 3 upgrade smoother. For example, for Next.js, run
npm install @clerk/nextjs@6.
Upgrade using the CLI
The recommended way to upgrade is to use the @clerk/upgrade CLI. This tool will scan your project, detect the breaking changes that affect your codebase, and apply fixes automatically where possible.
To run the CLI, open your terminal, navigate to your project directory, and run the command that matches your package manager:
npx @clerk/upgradepnpm dlx @clerk/upgradeyarn dlx @clerk/upgradebun x @clerk/upgradeThe CLI will walk you through the changes it detects. For most projects, this will handle the majority of the upgrade automatically.
The rest of this guide covers each breaking change in detail, which is useful as a reference for changes the CLI can't fully automate or if you'd like to understand what changed and why.
Breaking changes
The following breaking changes apply to all Clerk SDKs.
Deprecation removals
The following deprecated APIs have been removed from all Clerk SDKs.
SDK-specific changes
The following changes only apply to specific SDKs. Select your SDK below for additional upgrade steps.
Encryption key required when passing secretKey at runtime
When passing secretKey as a runtime option to clerkMiddleware(), you must now also provide a CLERK_ENCRYPTION_KEY environment variable.
Add the encryption key to your environment:
CLERK_ENCRYPTION_KEY=your-encryption-keySee the clerkMiddleware documentation.
ClerkProvider should be inside <body> for Next.js 16 cache components
For Next.js 16 cache components support (cacheComponents: true), ClerkProvider must be positioned inside <body> rather than wrapping <html>. This prevents "Uncached data was accessed outside of <Suspense>" errors.
<ClerkProvider>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
<html lang="en">
<body>
<ClerkProvider>
{children}
</ClerkProvider>
</body>
</html>If you're using Next.js 16 with cacheComponents: true, you may also need to wrap ClerkProvider in a <Suspense> boundary.
Minimum Next.js version increased to 15.2.3
Support for Next.js 13 and 14 has been dropped. @clerk/nextjs now requires next@>=15.2.3.
{
"dependencies": {
"next": "^14.0.0",
"next": "^15.2.3"
}
}See the Next.js upgrade guide for help migrating your application.
auth.protect() returns 401 instead of 404 for unauthenticated server actions
auth.protect() in clerkMiddleware() now returns a 401 Unauthorized response instead of a 404 Not Found when an unauthenticated request is made from a server action. If you have client-side error handling that checks for 404 responses from server actions when the user is signed out, update it to handle 401 instead:
try {
await myServerAction();
} catch (error) {
if (error.status === 404) {
if (error.status === 401) {
// Handle unauthenticated user
}
}No changes are required if you are not explicitly checking the HTTP status code in your error handling.
@clerk/clerk-react renamed to @clerk/react
The @clerk/clerk-react package has been renamed to @clerk/react.
Update your imports:
import { ClerkProvider, useUser } from '@clerk/clerk-react'
import { ClerkProvider, useUser } from '@clerk/react'And update your package.json:
"@clerk/clerk-react": "^5.0.0",
"@clerk/react": "^7.0.0",@clerk/react-router/api.server export removed
The @clerk/react-router/api.server export has been removed. Use @clerk/react-router/server instead:
import { getAuth } from '@clerk/react-router/api.server'
import { getAuth } from '@clerk/react-router/server'rootAuthLoader requires clerkMiddleware()
rootAuthLoader now requires clerkMiddleware() to be installed. Using rootAuthLoader without middleware will throw a runtime error.
- Enable the
v8_middlewarefuture flag:
export default {
future: {
v8_middleware: true,
},
} satisfies Config- Use
clerkMiddleware()alongsiderootAuthLoader:
import { clerkMiddleware, rootAuthLoader } from '@clerk/react-router/server'
export const middleware: Route.MiddlewareFunction[] = [clerkMiddleware()]
export const loader = (args: Route.LoaderArgs) => rootAuthLoader(args)Upgrade CLI does not auto-fix .astro template files
The upgrade CLI will fix .ts/.tsx files (React islands), but .astro template files must be updated manually. Components are imported from @clerk/astro/components:
---
import { SignedIn, SignedOut, Protect } from '@clerk/astro/components'
import { Show } from '@clerk/astro/components'
---
<SignedIn>
<Show when="signed-in">
<Dashboard />
</SignedIn>
</Show>
<SignedOut>
<Show when="signed-out">
<SignInPage />
</SignedOut>
</Show>
<Protect role="admin">
<Show when={{ role: "admin" }}>
<AdminPanel />
</Protect>
</Show>as prop removed from button components
The deprecated as prop has been removed from unstyled button components (SignInButton, SignUpButton, SignOutButton, CheckoutButton, PlanDetailsButton, SubscriptionDetailsButton). Use the asChild prop with a custom element in the default slot instead:
<SignInButton as="a" href="/sign-in"> Sign in </SignInButton>
<SignInButton asChild>
<a href="/sign-in">Sign in</a>
</SignInButton>Runtime environment variables now take precedence over build-time values
Environment variable resolution in @clerk/astro now prefers process.env over import.meta.env. This means runtime environment variables (e.g., set in the Node.js adapter or container) take precedence over values statically replaced by Vite at build time.
The new resolution order is:
locals.runtime.env(Cloudflare Workers)process.env(Node.js runtime)import.meta.env(Vite build-time static replacement)
If you rely on build-time PUBLIC_* values that differ from your runtime process.env, update your configuration to ensure the correct values are set in process.env at runtime.
Minimum TanStack version increased
@clerk/tanstack-react-start v1 requires @tanstack/react-start v1.157.0 or later (along with matching versions of @tanstack/react-router and @tanstack/react-router-devtools).
If you pin TanStack versions, update them:
{
"dependencies": {
"@tanstack/react-router": "1.154.14",
"@tanstack/react-router-devtools": "1.154.14",
"@tanstack/react-start": "1.154.14",
"@tanstack/react-router": "1.160.2",
"@tanstack/react-router-devtools": "1.160.2",
"@tanstack/react-start": "1.160.2"
}
}@clerk/clerk-expo renamed to @clerk/expo
The @clerk/clerk-expo package has been renamed to @clerk/expo.
Update your imports:
import { ClerkProvider, useUser } from '@clerk/clerk-expo'
import { ClerkProvider, useUser } from '@clerk/expo'And update your package.json:
"@clerk/clerk-expo": "^2.0.0",
"@clerk/expo": "^3.0.0",Clerk export removed
The Clerk export has been removed from @clerk/expo. Use useClerk() hook instead:
import { Clerk } from '@clerk/expo'
import { useClerk } from '@clerk/expo'
Clerk.signOut()
const { signOut } = useClerk()
signOut()Minimum Expo version increased to 53
@clerk/expo v3 requires Expo SDK 53 or later.
"expo": "~52.0.0",
"expo": "~53.0.0",See the Expo upgrade guide for help migrating your application.
useSignInWithApple and useSignInWithGoogle moved to separate entry points
The useSignInWithApple and useSignInWithGoogle hooks have been moved to dedicated entry points to avoid bundling optional dependencies:
import { useSignInWithApple } from '@clerk/expo'
import { useSignInWithApple } from '@clerk/expo/apple'
import { useSignInWithGoogle } from '@clerk/expo'
import { useSignInWithGoogle } from '@clerk/expo/google'publishableKey prop required in ClerkProvider
The publishableKey prop is now required in ClerkProvider for Expo apps. Previously, it would fall back to EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY, but environment variables inside node_modules are not inlined during production builds in React Native/Expo, which could cause apps to crash in production.
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!;
<ClerkProvider>
<ClerkProvider publishableKey={publishableKey}>
{/* Your app */}
</ClerkProvider>getAuth() helper removed
The getAuth() helper has been removed. Use auth() instead:
import { getAuth } from '@clerk/nuxt/server'
import { auth } from '@clerk/nuxt/server'
const { userId } = getAuth(event)
const { userId } = auth()Routing strategy now defaults to path
The default routing strategy for Nuxt has changed from hash to path. If you were relying on hash-based routing, explicitly set the strategy:
// nuxt.config.ts
export default defineNuxtConfig({
clerk: {
routingStrategy: 'hash', // Restore Core 2 behavior
},
})enableHandshake option removed
The enableHandshake option had no effect and has been removed from clerkMiddleware:
app.use(clerkMiddleware({ enableHandshake: false }))
app.use(clerkMiddleware())req.auth object-access removed
Accessing req.auth as a plain object (legacy clerk-sdk-node style) is no longer supported. Use getAuth() instead:
const { userId } = req.auth
const { userId } = getAuth(req)Feedback
Last updated on