Skip to content

Commit 2497d5d

Browse files
Add context on when to sync Clerk data with webhooks (#2368)
Co-authored-by: Sarah Soutoul <sarah@clerk.dev>
1 parent 8051345 commit 2497d5d

File tree

10 files changed

+588
-77
lines changed

10 files changed

+588
-77
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
> [!WARNING]
2+
> Metadata is limited to **8KB** maximum. If you're storing metadata as a custom claim in the session token, it's highly recommended to keep it under **1.2KB**. [Learn more about the session token size limitations](/docs/backend-requests/resources/session-tokens#size-limitations).
3+
>
4+
> If you use Clerk metadata and modify it server-side, the changes won't appear in the session token until the next refresh. To avoid race conditions, either [force a JWT refresh](/docs/guides/force-token-refresh) after metadata changes or handle the delay in your application logic.
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
11
> [!CAUTION]
2-
> The entire session token has a max size of 4KB. Exceeding this size can have adverse effects, including a possible infinite redirect loop for users who exceed this size in Next.js applications.
3-
> It's recommended to move particularly large claims out of the JWT and fetch these using a separate API call from your backend.
4-
> [Learn more](/docs/backend-requests/resources/session-tokens#size-limitations).
2+
> Clerk stores the session token in a cookie, and most browsers cap cookie size at [**4KB**](https://datatracker.ietf.org/doc/html/rfc2109#section-6.3). After accounting for the size of Clerk's default claims, the cookie can support **up to 1.2KB** of custom claims. Exceeding this limit will cause the cookie to not be set, which will break your app as Clerk depends on cookies to work properly. [Learn more](/docs/backend-requests/resources/session-tokens#size-limitations).

docs/backend-requests/custom-session-token.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ This guide will show you how to customize a session token to include additional
3939
export async function GET() {
4040
const { sessionClaims } = await auth()
4141

42-
const fullName = sessionClaims?.fullName
42+
const fullName = sessionClaims.fullName
4343

44-
const primaryEmail = sessionClaims?.primaryEmail
44+
const primaryEmail = sessionClaims.primaryEmail
4545

4646
return NextResponse.json({ fullName, primaryEmail })
4747
}

docs/backend-requests/resources/session-tokens.mdx

Lines changed: 233 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,17 @@ Read more about Clerk session tokens and how they work in [the guide on how Cler
9090
</Tab>
9191
</Tabs>
9292

93+
## Custom claims
94+
9395
If you would like to add custom claims to your session token, you can [customize it](/docs/backend-requests/custom-session-token).
9496

9597
You can also create custom tokens using a [JWT template](/docs/backend-requests/jwt-templates).
9698

9799
## Size limitations
98100

99-
The Clerk session token is stored in a cookie. All modern browsers [limit the maximum size of a cookie to 4kb](https://datatracker.ietf.org/doc/html/rfc2109#section-6.3). Exceeding this limit can have adverse effects, including a possible infinite redirect loop for users who exceed this size in Next.js applications.
101+
Clerk stores the session token in a cookie, and [**most browsers limit cookie size to 4KB**](https://datatracker.ietf.org/doc/html/rfc2109#section-6.3). Exceeding this limit will cause the cookie to not be set, which will break your app as Clerk depends on cookies to work properly.
100102

101-
A session token with the [default session claims](#default-claims) won't run into this issue, as this configuration produces a cookie significantly smaller than 4kb. However, this limitation becomes relevant when implementing a [custom session token](/docs/backend-requests/custom-session-token). In this case, it's recommended to move particularly large claims out of the token and fetch these using a separate API call from your backend.
103+
A session token with the [default session claims](#default-claims) won't run into this issue, as this configuration produces a cookie significantly smaller than 4KB. However, you must consider this limit when implementing a [custom session token](/docs/backend-requests/custom-session-token). After accounting for the size of Clerk's default claims, the cookie can support **up to 1.2KB** of custom claims.
102104

103105
Claims to monitor for size limits:
104106

@@ -108,11 +110,239 @@ Claims to monitor for size limits:
108110
- `org.public_metadata`
109111
- `org_membership.public_metadata`
110112

111-
If you include any of these claims in your token, use caution to ensure the stored data doesn't exceed the size limit.
113+
If you add any of these custom claims in your token, use caution to ensure the stored data doesn't exceed the size limit. It's highly recommended to [store the extra data in your own database](/docs/webhooks/sync-data#storing-extra-user-data) instead of storing it in metadata in the session token. If this isn't an option, you can [move particularly large claims like these out of the token](#example) and fetch them using a separate API call from your backend.
112114

113115
> [!NOTE]
114116
> If your application encounters this issue, the Clerk Dashboard will display a warning: **"Some users are exceeding cookie size limits"**. To resolve this, update your [custom session token](/docs/backend-requests/custom-session-token).
115117
118+
### Example
119+
120+
It's recommended to keep the total size of custom claims in the session token under 1.2KB. The following example shows how to move particularly large claims out of the session token and fetch them using a separate API call from your backend. The limitations of this approach are that if you make this call to Clerk's Backend API frequently, you risk hitting [rate limits](/docs/backend-requests/resources/rate-limits) and it's also slower than making a database query. We highly recommend [storing the extra data in your own database](/docs/webhooks/sync-data#storing-extra-user-data) instead of storing it in metadata in the session token.
121+
122+
For example, if you were storing several fields in `user.public_metadata`, like this:
123+
124+
```js {{ prettier: false }}
125+
// user.public_metadata
126+
{
127+
onboardingComplete: true,
128+
birthday: '2000-01-01',
129+
country: 'Canada',
130+
bio: 'This is a bio -- imagine it is 6kb of written info',
131+
}
132+
```
133+
134+
Instead of storing all of that data in the session token, and possibly exceeding the 4KB limit, like this:
135+
136+
```json
137+
// Custom claims in the session token
138+
{
139+
"metadata": "{{user.public_metadata}}"
140+
}
141+
```
142+
143+
You could store only the necessary data in the session token - for example, just the `onboardingComplete` field - like this:
144+
145+
```json
146+
// Custom claims in the session token
147+
{
148+
"onboardingComplete": "{{user.public_metadata.onboardingComplete}}"
149+
}
150+
```
151+
152+
If you need to access the other fields, you can fetch them using a separate API call from your backend. The following example uses the [`getUser()`](/docs/references/backend/user/get-user) method to access the current user's [Backend `User` object](/docs/references/backend/types/backend-user), which includes the `publicMetadata` field.
153+
154+
<Tabs items={["Next.js", "Astro", "Express", "React Router", "Remix", "Tanstack React Start"]}>
155+
<Tab>
156+
<CodeBlockTabs options={["App Router", "Pages Router"]}>
157+
```tsx {{ filename: 'app/api/example/route.ts' }}
158+
import { auth, clerkClient } from '@clerk/nextjs/server'
159+
160+
export async function GET() {
161+
// Use `auth()` to get the user's ID
162+
const { userId } = await auth()
163+
164+
// Protect the route by checking if the user is signed in
165+
if (!userId) {
166+
return new NextResponse('Unauthorized', { status: 401 })
167+
}
168+
169+
const client = await clerkClient()
170+
171+
// Use the Backend SDK's `getUser()` method to get the Backend User object
172+
const user = await client.users.getUser(userId)
173+
174+
// Return the Backend User object
175+
return NextResponse.json({ user: user }, { status: 200 })
176+
}
177+
```
178+
179+
<Include src="_partials/nextjs/get-auth" />
180+
</CodeBlockTabs>
181+
</Tab>
182+
183+
<Tab>
184+
```tsx {{ filename: 'src/api/example.ts' }}
185+
import { clerkClient } from '@clerk/astro/server'
186+
187+
export async function GET(context) {
188+
// Use `locals.auth()` to get the user's ID
189+
const { userId } = context.locals.auth()
190+
191+
// Protect the route by checking if the user is signed in
192+
if (!userId) {
193+
return new Response('Unauthorized', { status: 401 })
194+
}
195+
196+
// Use the Backend SDK's `getUser()` method to get the Backend User object
197+
const user = await clerkClient(context).users.getUser(userId)
198+
199+
// Return the Backend User object
200+
return new Response(JSON.stringify({ user }))
201+
}
202+
```
203+
</Tab>
204+
205+
<Tab>
206+
```js {{ filename: 'index.js' }}
207+
import { createClerkClient, getAuth } from '@clerk/express'
208+
import express from 'express'
209+
210+
const app = express()
211+
const clerkClient = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY })
212+
213+
app.get('/user', async (req, res) => {
214+
// Use `getAuth()` to get the user's ID
215+
const { userId } = getAuth(req)
216+
217+
// Protect the route by checking if the user is signed in
218+
if (!userId) {
219+
res.status(401).json({ error: 'User not authenticated' })
220+
}
221+
222+
// Use the Backend SDK's `getUser()` method to get the Backend User object
223+
const user = await clerkClient.users.getUser(userId)
224+
225+
// Return the Backend User object
226+
res.json(user)
227+
})
228+
```
229+
</Tab>
230+
231+
<Tab>
232+
```tsx {{ filename: 'src/routes/profile.tsx' }}
233+
import { redirect } from 'react-router'
234+
import { getAuth } from '@clerk/react-router/ssr.server'
235+
import { createClerkClient } from '@clerk/react-router/api.server'
236+
import type { Route } from './+types/profile'
237+
238+
export async function loader(args: Route.LoaderArgs) {
239+
// Use `getAuth()` to get the user's ID
240+
const { userId } = await getAuth(args)
241+
242+
// Protect the route by checking if the user is signed in
243+
if (!userId) {
244+
return redirect('/sign-in?redirect_url=' + args.request.url)
245+
}
246+
247+
// Use the Backend SDK's `getUser()` method to get the Backend User object
248+
const user = await createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY }).users.getUser(
249+
userId,
250+
)
251+
252+
// Return the Backend User object
253+
return {
254+
user: JSON.stringify(user),
255+
}
256+
}
257+
```
258+
</Tab>
259+
260+
<Tab>
261+
<CodeBlockTabs options={["Loader Function", "Action Function"]}>
262+
```tsx {{ filename: 'routes/profile.tsx' }}
263+
import { LoaderFunction, redirect } from '@remix-run/node'
264+
import { getAuth } from '@clerk/remix/ssr.server'
265+
import { createClerkClient } from '@clerk/remix/api.server'
266+
267+
export const loader: LoaderFunction = async (args) => {
268+
// Use `getAuth()` to get the user's ID
269+
const { userId } = await getAuth(args)
270+
271+
// If there is no userId, then redirect to sign-in route
272+
if (!userId) {
273+
return redirect('/sign-in?redirect_url=' + args.request.url)
274+
}
275+
276+
// Use the Backend SDK's `getUser()` method to get the Backend User object
277+
const user = await createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY }).users.getUser(
278+
userId,
279+
)
280+
281+
// Return the Backend User object
282+
return { serialisedUser: JSON.stringify(user) }
283+
}
284+
```
285+
286+
```tsx {{ filename: 'routes/profile.tsx' }}
287+
import { ActionFunction, redirect } from '@remix-run/node'
288+
import { getAuth } from '@clerk/remix/ssr.server'
289+
import { createClerkClient } from '@clerk/remix/api.server'
290+
291+
export const action: ActionFunction = async (args) => {
292+
// Use `getAuth()` to get the user's ID
293+
const { userId } = await getAuth(args)
294+
295+
// If there is no userId, then redirect to sign-in route
296+
if (!userId) {
297+
return redirect('/sign-in?redirect_url=' + args.request.url)
298+
}
299+
300+
// Prepare the data for the mutation
301+
const params = { firstName: 'John', lastName: 'Wicker' }
302+
303+
// // Use the Backend SDK's `updateUser()` method to update the Backend User object
304+
const updatedUser = await createClerkClient({
305+
secretKey: process.env.CLERK_SECRET_KEY,
306+
}).users.updateUser(userId, params)
307+
308+
// Return the updated user
309+
return { serialisedUser: JSON.stringify(updatedUser) }
310+
}
311+
```
312+
</CodeBlockTabs>
313+
</Tab>
314+
315+
<Tab>
316+
```tsx {{ filename: 'app/routes/api/example.tsx' }}
317+
import { createClerkClient } from '@clerk/backend'
318+
import { json } from '@tanstack/react-start'
319+
import { createAPIFileRoute } from '@tanstack/react-start/api'
320+
import { getAuth } from '@clerk/tanstack-react-start/server'
321+
322+
export const Route = createAPIFileRoute('/api/example')({
323+
GET: async ({ request, params }) => {
324+
// Use `getAuth()` to get the user's ID
325+
const { userId } = await getAuth(req)
326+
327+
// Protect the route by checking if the user is signed in
328+
if (!userId) {
329+
return json({ error: 'Unauthorized' }, { status: 401 })
330+
}
331+
332+
// Instantiate the Backend SDK
333+
const clerkClient = createClerkClient({ secretKey: import.meta.env.CLERK_SECRET_KEY })
334+
335+
// Use the Backend SDK's `getUser()` method to get the Backend User object
336+
const user = userId ? await clerkClient.users.getUser(userId) : null
337+
338+
// Return the Backend User object
339+
return json({ user })
340+
},
341+
})
342+
```
343+
</Tab>
344+
</Tabs>
345+
116346
## Validate session tokens
117347

118348
If you're using the middleware provided by our Clerk SDKs, validating session tokens is handled automatically in every request. If you're not using the middleware, you can still use the respective helpers provided by the SDKs to validate the tokens.

0 commit comments

Comments
 (0)