Skip to content

Commit a0204a8

Browse files
jacekradkobrkalowpanteliselef
authored
feat(nextjs): Next.js@15 compatibility (clerk#4366)
Co-authored-by: Bryce Kalow <bryce@clerk.dev> Co-authored-by: panteliselef <pantelis@clerk.dev>
1 parent 8ddb3ea commit a0204a8

File tree

80 files changed

+3122
-1987
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+3122
-1987
lines changed

.changeset/gorgeous-suits-rush.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clerk/nextjs": major
3+
---
4+
5+
Stop `<ClerkProvider>` from opting applications into dynamic rendering. A new prop, `<ClerkProvider dynamic>` can be used to opt-in to dynamic rendering and make auth data available during server-side rendering. The RSC `auth()` helper should be preferred for accessing auth data during dynamic rendering.

.changeset/grumpy-hairs-remember.md

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
---
2+
"@clerk/nextjs": major
3+
"@clerk/upgrade": minor
4+
---
5+
6+
@clerk/nextjs: Converting auth() and clerkClient() interfaces to be async
7+
@clerk/upgrade: Adding required codemod for @clerk/nextjs breaking changes
8+
9+
# Migration guide
10+
11+
## `auth()` is now async
12+
13+
Previously the `auth()` method from `@clerk/nextjs/server` was synchronous.
14+
15+
```typescript
16+
import { auth } from '@clerk/nextjs/server';
17+
18+
export function GET() {
19+
const { userId } = auth();
20+
return new Response(JSON.stringify({ userId }));
21+
}
22+
```
23+
24+
The `auth` method now becomes asynchronous. You will need to make the following changes to the snippet above to make it compatible.
25+
26+
```diff
27+
- export function GET() {
28+
+ export async function GET() {
29+
- const { userId } = auth();
30+
+ const { userId } = await auth();
31+
return new Response(JSON.stringify({ userId }));
32+
}
33+
```
34+
35+
## Clerk middleware auth is now async
36+
37+
```typescript
38+
import { clerkClient, clerkMiddleware } from '@clerk/nextjs/server';
39+
import { NextResponse } from 'next/server';
40+
41+
export default clerkMiddleware(async (auth, request) => {
42+
const resolvedAuth = await auth();
43+
44+
const count = await resolvedAuth.users.getCount();
45+
46+
if (count) {
47+
return NextResponse.redirect(new URL('/new-url', request.url));
48+
}
49+
});
50+
51+
export const config = {
52+
matcher: [...],
53+
};
54+
```
55+
56+
## clerkClient() is now async
57+
58+
Previously the `clerkClient()` method from `@clerk/nextjs/server` was synchronous.
59+
60+
```typescript
61+
import { clerkClient, clerkMiddleware } from '@clerk/nextjs/server';
62+
import { NextResponse } from 'next/server';
63+
64+
export default clerkMiddleware((auth, request) => {
65+
const client = clerkClient();
66+
67+
const count = await client.users?.getCount();
68+
69+
if (count) {
70+
return NextResponse.redirect(new URL('/new-url', request.url));
71+
}
72+
});
73+
74+
export const config = {
75+
matcher: [...],
76+
};
77+
```
78+
79+
The method now becomes async. You will need to make the following changes to the snippet above to make it compatible.
80+
81+
```diff
82+
- export default clerkMiddleware((auth, request) => {
83+
- const client = clerkClient();
84+
+ export default clerkMiddleware(async (auth, request) => {
85+
+ const client = await clerkClient();
86+
const count = await client.users?.getCount();
87+
88+
if (count) {
89+
}
90+
```

.changeset/shiny-numbers-walk.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@clerk/nextjs": major
3+
---
4+
5+
Support `unstable_rethrow` inside `clerkMiddleware`.
6+
We changed the errors thrown by `protect()` inside `clerkMiddleware` in order for `unstable_rethrow` to recognise them and rethrow them.

.changeset/ten-worms-report.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clerk/nextjs": major
3+
---
4+
5+
Removes deprecated APIs: `authMiddleware()`, `redirectToSignIn()`, and `redirectToSignUp()`. See the migration guide to learn how to update your usage.

.changeset/two-bottles-report.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clerk/clerk-react": minor
3+
---
4+
5+
Internal changes to support `<ClerkProvider dynamic>`

integration/presets/longRunningApps.ts

+2
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ export const createLongRunningApps = () => {
2323
{ id: 'react.vite.withEmailLinks', config: react.vite, env: envs.withEmailLinks },
2424
{ id: 'remix.node.withEmailCodes', config: remix.remixNode, env: envs.withEmailCodes },
2525
{ id: 'next.appRouter.withEmailCodes', config: next.appRouter, env: envs.withEmailCodes },
26+
{ id: 'next.appRouter.15RCwithEmailCodes', config: next.appRouter15Rc, env: envs.withEmailCodes },
2627
{
2728
id: 'next.appRouter.withEmailCodes_persist_client',
2829
config: next.appRouter,
2930
env: envs.withEmailCodes_destroy_client,
3031
},
3132
{ id: 'next.appRouter.withCustomRoles', config: next.appRouter, env: envs.withCustomRoles },
33+
{ id: 'next.appRouter.15RCwithCustomRoles', config: next.appRouter15Rc, env: envs.withCustomRoles },
3234
{ id: 'quickstart.next.appRouter', config: next.appRouterQuickstart, env: envs.withEmailCodesQuickstart },
3335
{ id: 'elements.next.appRouter', config: elements.nextAppRouter, env: envs.withEmailCodes },
3436
{ id: 'astro.node.withCustomRoles', config: astro.node, env: envs.withCustomRoles },

integration/presets/next.ts

+55
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,60 @@ const appRouter = applicationConfig()
1616
.addDependency('react-dom', constants.E2E_REACT_DOM_VERSION)
1717
.addDependency('@clerk/nextjs', constants.E2E_CLERK_VERSION || clerkNextjsLocal);
1818

19+
const appRouter15Rc = applicationConfig()
20+
.setName('next-app-router')
21+
.useTemplate(templates['next-app-router'])
22+
.setEnvFormatter('public', key => `NEXT_PUBLIC_${key}`)
23+
.addScript('setup', constants.E2E_NPM_FORCE ? 'npm i --force' : 'npm i')
24+
.addScript('dev', 'npm run dev')
25+
.addScript('build', 'npm run build')
26+
.addScript('serve', 'npm run start')
27+
.addDependency('next', 'rc')
28+
.addDependency('react', 'rc')
29+
.addDependency('react-dom', 'rc')
30+
.addDependency('@clerk/nextjs', constants.E2E_CLERK_VERSION || clerkNextjsLocal)
31+
.addFile(
32+
'src/middleware.ts',
33+
() => `import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
34+
import { unstable_rethrow } from 'next/navigation';
35+
36+
const csp = \`default-src 'self';
37+
script-src 'self' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' 'nonce-deadbeef';
38+
img-src 'self' https://img.clerk.com;
39+
worker-src 'self' blob:;
40+
style-src 'self' 'unsafe-inline';
41+
frame-src 'self' https://challenges.cloudflare.com;
42+
\`;
43+
44+
const isProtectedRoute = createRouteMatcher(['/protected(.*)', '/user(.*)', '/switcher(.*)']);
45+
const isAdminRoute = createRouteMatcher(['/only-admin(.*)']);
46+
const isCSPRoute = createRouteMatcher(['/csp']);
47+
48+
export default clerkMiddleware(async (auth, req) => {
49+
if (isProtectedRoute(req)) {
50+
try { await auth.protect() }
51+
catch (e) { unstable_rethrow(e) }
52+
}
53+
54+
if (isAdminRoute(req)) {
55+
try { await auth.protect({role: 'admin'}) }
56+
catch (e) { unstable_rethrow(e) }
57+
}
58+
59+
if (isCSPRoute(req)) {
60+
req.headers.set('Content-Security-Policy', csp.replace(/\\n/g, ''));
61+
}
62+
});
63+
64+
export const config = {
65+
matcher: [
66+
'/((?!.*\\\\..*|_next).*)', // Don't run middleware on static files
67+
'/', // Run middleware on index page
68+
'/(api|trpc)(.*)',
69+
], // Run middleware on API routes
70+
};`,
71+
);
72+
1973
const appRouterTurbo = appRouter
2074
.clone()
2175
.setName('next-app-router-turbopack')
@@ -48,6 +102,7 @@ const appRouterAPWithClerkNextV4 = appRouterQuickstart
48102

49103
export const next = {
50104
appRouter,
105+
appRouter15Rc,
51106
appRouterTurbo,
52107
appRouterQuickstart,
53108
appRouterAPWithClerkNextLatest,
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { auth } from '@clerk/nextjs/server';
22

3-
export function GET() {
4-
const { userId } = auth();
3+
export async function GET() {
4+
const { userId } = await auth();
55
return new Response(JSON.stringify({ userId }));
66
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { auth } from '@clerk/nextjs/server';
22

3-
export function GET() {
4-
const { userId } = auth().protect(has => has({ role: 'admin' }) || has({ role: 'org:editor' }));
3+
export async function GET() {
4+
const { userId } = await auth.protect((has: any) => has({ role: 'admin' }) || has({ role: 'org:editor' }));
55
return new Response(JSON.stringify({ userId }));
66
}

integration/templates/next-app-router/src/app/csp/page.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { headers } from 'next/headers';
22
import { ClerkLoaded } from '@clerk/nextjs';
33

4-
export default function CSPPage() {
4+
export default async function CSPPage() {
5+
const cspHeader = await headers().get('Content-Security-Policy');
6+
57
return (
68
<div>
7-
CSP: <pre>{headers().get('Content-Security-Policy')}</pre>
9+
CSP: <pre>{cspHeader}</pre>
810
<ClerkLoaded>
911
<p>clerk loaded</p>
1012
</ClerkLoaded>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <div>User is admin</div>;
3+
}

integration/templates/next-app-router/src/app/organizations-by-id/[id]/page.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { auth } from '@clerk/nextjs/server';
22

3-
export default function Home({ params }: { params: { id: string } }) {
4-
const { orgId } = auth();
3+
export default async function Home({ params }: { params: { id: string } }) {
4+
const { orgId } = await auth();
55

66
if (params.id != orgId) {
77
console.log('Mismatch - returning nothing for now...', params.id, orgId);

integration/templates/next-app-router/src/app/organizations-by-id/[id]/settings/page.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { auth } from '@clerk/nextjs/server';
22

3-
export default function Home({ params }: { params: { id: string } }) {
4-
const { orgId } = auth();
3+
export default async function Home({ params }: { params: { id: string } }) {
4+
const { orgId } = await auth();
55

66
if (params.id != orgId) {
77
console.log('Mismatch - returning nothing for now...', params.id, orgId);

integration/templates/next-app-router/src/app/organizations-by-slug/[slug]/page.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { auth } from '@clerk/nextjs/server';
22

3-
export default function Home({ params }: { params: { slug: string } }) {
4-
const { orgSlug } = auth();
3+
export default async function Home({ params }: { params: { slug: string } }) {
4+
const { orgSlug } = await auth();
55

66
if (params.slug != orgSlug) {
77
console.log('Mismatch - returning nothing for now...', params.slug, orgSlug);

integration/templates/next-app-router/src/app/organizations-by-slug/[slug]/settings/page.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { auth } from '@clerk/nextjs/server';
22

3-
export default function Home({ params }: { params: { slug: string } }) {
4-
const { orgSlug } = auth();
3+
export default async function Home({ params }: { params: { slug: string } }) {
4+
const { orgSlug } = await auth();
55

66
if (params.slug != orgSlug) {
77
console.log('Mismatch - returning nothing for now...', params.slug, orgSlug);
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { auth } from '@clerk/nextjs/server';
22

3-
export default function Page() {
4-
auth().protect();
3+
export default async function Page() {
4+
await auth.protect();
5+
56
return <div>Protected Page</div>;
67
}

integration/templates/next-app-router/src/app/personal-account/page.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { auth } from '@clerk/nextjs/server';
22

3-
export default function Home(): {} {
4-
const { orgId } = auth();
3+
export default async function Home() {
4+
const { orgId } = await auth();
55

66
if (orgId != null) {
77
console.log('Oh no, this page should only activate on the personal account!');

integration/templates/next-app-router/src/app/settings/auth-has/page.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { auth } from '@clerk/nextjs/server';
22

3-
export default function Page() {
4-
const { userId, has } = auth();
3+
export default async function Page() {
4+
const { userId, has } = await auth();
55
if (!userId || !has({ permission: 'org:posts:manage' })) {
66
return <p>User is missing permissions</p>;
77
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { auth } from '@clerk/nextjs/server';
22

3-
export default function Page() {
4-
auth().protect({ role: 'admin' });
3+
export default async function Page() {
4+
await auth.protect({ role: 'admin' });
55
return <p>User has access</p>;
66
}

integration/templates/next-app-router/src/middleware.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@ const csp = `default-src 'self';
99
`;
1010

1111
const isProtectedRoute = createRouteMatcher(['/protected(.*)', '/user(.*)', '/switcher(.*)']);
12+
const isAdminRoute = createRouteMatcher(['/only-admin(.*)']);
1213
const isCSPRoute = createRouteMatcher(['/csp']);
1314

14-
export default clerkMiddleware((auth, req) => {
15+
export default clerkMiddleware(async (auth, req) => {
1516
if (isProtectedRoute(req)) {
16-
auth().protect();
17+
await auth.protect();
18+
}
19+
20+
if (isAdminRoute(req)) {
21+
await auth.protect({ role: 'admin' });
1722
}
1823

1924
if (isCSPRoute(req)) {

integration/tests/dynamic-keys.test.ts

+10-8
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,24 @@ test.describe('dynamic keys @nextjs', () => {
1313
.clone()
1414
.addFile(
1515
'src/middleware.ts',
16-
() => `import { clerkClient, clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
17-
import { NextResponse } from 'next/server'
16+
() => `import { clerkClient, clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
17+
import { NextResponse } from 'next/server';
1818
1919
const isProtectedRoute = createRouteMatcher(['/protected']);
2020
const shouldFetchBapi = createRouteMatcher(['/fetch-bapi-from-middleware']);
2121
2222
export default clerkMiddleware(async (auth, request) => {
2323
if (isProtectedRoute(request)) {
24-
auth().protect();
24+
await auth.protect();
2525
}
2626
2727
if (shouldFetchBapi(request)){
28-
const count = await clerkClient().users.getCount();
28+
const client = await clerkClient();
29+
30+
const count = await client.users?.getCount();
2931
3032
if (count){
31-
return NextResponse.redirect(new URL('/users-count', request.url))
33+
return NextResponse.redirect(new URL('/users-count', request.url));
3234
}
3335
}
3436
}, {
@@ -45,7 +47,7 @@ test.describe('dynamic keys @nextjs', () => {
4547
() => `import { clerkClient } from '@clerk/nextjs/server'
4648
4749
export default async function Page(){
48-
const count = await clerkClient().users.getCount()
50+
const count = await clerkClient().users?.getCount() ?? 0;
4951
5052
return <p>Users count: {count}</p>
5153
}
@@ -62,7 +64,7 @@ test.describe('dynamic keys @nextjs', () => {
6264
await app.teardown();
6365
});
6466

65-
test('redirects to `signInUrl` on `auth().protect()`', async ({ page, context }) => {
67+
test('redirects to `signInUrl` on `await auth.protect()`', async ({ page, context }) => {
6668
const u = createTestUtils({ app, page, context });
6769

6870
await u.page.goToAppHome();
@@ -74,7 +76,7 @@ test.describe('dynamic keys @nextjs', () => {
7476
await u.page.waitForURL(/foobar/);
7577
});
7678

77-
test('resolves auth signature with `secretKey` on `auth().protect()`', async ({ page, context }) => {
79+
test('resolves auth signature with `secretKey` on `await auth.protect()`', async ({ page, context }) => {
7880
const u = createTestUtils({ app, page, context });
7981
await u.page.goToRelative('/page-protected');
8082
await u.page.waitForURL(/foobar/);

0 commit comments

Comments
 (0)