Skip to content
Merged
4 changes: 4 additions & 0 deletions docs/_partials/metadata-callout.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
> [!WARNING]
> 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).
>
> 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.
4 changes: 1 addition & 3 deletions docs/_partials/token-size-callout.mdx
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
> [!CAUTION]
> 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.
> It's recommended to move particularly large claims out of the JWT and fetch these using a separate API call from your backend.
> [Learn more](/docs/backend-requests/resources/session-tokens#size-limitations).
> 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).
4 changes: 2 additions & 2 deletions docs/backend-requests/custom-session-token.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ This guide will show you how to customize a session token to include additional
export async function GET() {
const { sessionClaims } = await auth()

const fullName = sessionClaims?.fullName
const fullName = sessionClaims.fullName

const primaryEmail = sessionClaims?.primaryEmail
const primaryEmail = sessionClaims.primaryEmail

return NextResponse.json({ fullName, primaryEmail })
}
Expand Down
236 changes: 233 additions & 3 deletions docs/backend-requests/resources/session-tokens.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,17 @@ Read more about Clerk session tokens and how they work in [the guide on how Cler
</Tab>
</Tabs>

## Custom claims

If you would like to add custom claims to your session token, you can [customize it](/docs/backend-requests/custom-session-token).

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

## Size limitations

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.
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.

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.
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.

Claims to monitor for size limits:

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

If you include any of these claims in your token, use caution to ensure the stored data doesn't exceed the size limit.
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.

> [!NOTE]
> 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).

### Example

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.

For example, if you were storing several fields in `user.public_metadata`, like this:

```js {{ prettier: false }}
// user.public_metadata
{
onboardingComplete: true,
birthday: '2000-01-01',
country: 'Canada',
bio: 'This is a bio -- imagine it is 6kb of written info',
}
```

Instead of storing all of that data in the session token, and possibly exceeding the 4KB limit, like this:

```json
// Custom claims in the session token
{
"metadata": "{{user.public_metadata}}"
}
```

You could store only the necessary data in the session token - for example, just the `onboardingComplete` field - like this:

```json
// Custom claims in the session token
{
"onboardingComplete": "{{user.public_metadata.onboardingComplete}}"
}
```

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.

<Tabs items={["Next.js", "Astro", "Express", "React Router", "Remix", "Tanstack React Start"]}>
<Tab>
<CodeBlockTabs options={["App Router", "Pages Router"]}>
```tsx {{ filename: 'app/api/example/route.ts' }}
import { auth, clerkClient } from '@clerk/nextjs/server'

export async function GET() {
// Use `auth()` to get the user's ID
const { userId } = await auth()

// Protect the route by checking if the user is signed in
if (!userId) {
return new NextResponse('Unauthorized', { status: 401 })
}

const client = await clerkClient()

// Use the Backend SDK's `getUser()` method to get the Backend User object
const user = await client.users.getUser(userId)

// Return the Backend User object
return NextResponse.json({ user: user }, { status: 200 })
}
```

<Include src="_partials/nextjs/get-auth" />
</CodeBlockTabs>
</Tab>

<Tab>
```tsx {{ filename: 'src/api/example.ts' }}
import { clerkClient } from '@clerk/astro/server'

export async function GET(context) {
// Use `locals.auth()` to get the user's ID
const { userId } = context.locals.auth()

// Protect the route by checking if the user is signed in
if (!userId) {
return new Response('Unauthorized', { status: 401 })
}

// Use the Backend SDK's `getUser()` method to get the Backend User object
const user = await clerkClient(context).users.getUser(userId)

// Return the Backend User object
return new Response(JSON.stringify({ user }))
}
```
</Tab>

<Tab>
```js {{ filename: 'index.js' }}
import { createClerkClient, getAuth } from '@clerk/express'
import express from 'express'

const app = express()
const clerkClient = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY })

app.get('/user', async (req, res) => {
// Use `getAuth()` to get the user's ID
const { userId } = getAuth(req)

// Protect the route by checking if the user is signed in
if (!userId) {
res.status(401).json({ error: 'User not authenticated' })
}

// Use the Backend SDK's `getUser()` method to get the Backend User object
const user = await clerkClient.users.getUser(userId)

// Return the Backend User object
res.json(user)
})
```
</Tab>

<Tab>
```tsx {{ filename: 'src/routes/profile.tsx' }}
import { redirect } from 'react-router'
import { getAuth } from '@clerk/react-router/ssr.server'
import { createClerkClient } from '@clerk/react-router/api.server'
import type { Route } from './+types/profile'

export async function loader(args: Route.LoaderArgs) {
// Use `getAuth()` to get the user's ID
const { userId } = await getAuth(args)

// Protect the route by checking if the user is signed in
if (!userId) {
return redirect('/sign-in?redirect_url=' + args.request.url)
}

// Use the Backend SDK's `getUser()` method to get the Backend User object
const user = await createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY }).users.getUser(
userId,
)

// Return the Backend User object
return {
user: JSON.stringify(user),
}
}
```
</Tab>

<Tab>
<CodeBlockTabs options={["Loader Function", "Action Function"]}>
```tsx {{ filename: 'routes/profile.tsx' }}
import { LoaderFunction, redirect } from '@remix-run/node'
import { getAuth } from '@clerk/remix/ssr.server'
import { createClerkClient } from '@clerk/remix/api.server'

export const loader: LoaderFunction = async (args) => {
// Use `getAuth()` to get the user's ID
const { userId } = await getAuth(args)

// If there is no userId, then redirect to sign-in route
if (!userId) {
return redirect('/sign-in?redirect_url=' + args.request.url)
}

// Use the Backend SDK's `getUser()` method to get the Backend User object
const user = await createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY }).users.getUser(
userId,
)

// Return the Backend User object
return { serialisedUser: JSON.stringify(user) }
}
```

```tsx {{ filename: 'routes/profile.tsx' }}
import { ActionFunction, redirect } from '@remix-run/node'
import { getAuth } from '@clerk/remix/ssr.server'
import { createClerkClient } from '@clerk/remix/api.server'

export const action: ActionFunction = async (args) => {
// Use `getAuth()` to get the user's ID
const { userId } = await getAuth(args)

// If there is no userId, then redirect to sign-in route
if (!userId) {
return redirect('/sign-in?redirect_url=' + args.request.url)
}

// Prepare the data for the mutation
const params = { firstName: 'John', lastName: 'Wicker' }

// // Use the Backend SDK's `updateUser()` method to update the Backend User object
const updatedUser = await createClerkClient({
secretKey: process.env.CLERK_SECRET_KEY,
}).users.updateUser(userId, params)

// Return the updated user
return { serialisedUser: JSON.stringify(updatedUser) }
}
```
</CodeBlockTabs>
</Tab>

<Tab>
```tsx {{ filename: 'app/routes/api/example.tsx' }}
import { createClerkClient } from '@clerk/backend'
import { json } from '@tanstack/react-start'
import { createAPIFileRoute } from '@tanstack/react-start/api'
import { getAuth } from '@clerk/tanstack-react-start/server'

export const Route = createAPIFileRoute('/api/example')({
GET: async ({ request, params }) => {
// Use `getAuth()` to get the user's ID
const { userId } = await getAuth(req)

// Protect the route by checking if the user is signed in
if (!userId) {
return json({ error: 'Unauthorized' }, { status: 401 })
}

// Instantiate the Backend SDK
const clerkClient = createClerkClient({ secretKey: import.meta.env.CLERK_SECRET_KEY })

// Use the Backend SDK's `getUser()` method to get the Backend User object
const user = userId ? await clerkClient.users.getUser(userId) : null

// Return the Backend User object
return json({ user })
},
})
```
</Tab>
</Tabs>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth adding a note that if you are doing this call frequently, it's probably better to sync to your database, or better yet, not even store it in Clerk metadata in the first place, and just store it in the database under your own user table, but put in the clerk_id as a column so you can quickly look it up using the id returned from the JWT. Doing tons of backend API calls puts you at risk of hitting rate limits, and is also a lot slower than making a database query.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

## Validate session tokens

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.
Expand Down
Loading