Skip to main content
Docs

User metadata

To store information about a user that Clerk doesn't collect, you can use metadata, which will get stored on the user's .

Types of metadata

There are three types of metadata: "public", "private", and "unsafe".

MetadataFrontend APIBackend API
PublicRead accessRead & write access
PrivateNo read or write accessRead & write access
UnsafeRead & write accessRead & write access

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.

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 after metadata changes or handle the delay in your application logic.

Public metadata

Public metadata is accessible by both the frontend and the backend, but can only be set on the backend. This is useful for storing data that you want to expose to the frontend, but don't want the user to be able to modify. For example, you could store a user's birthday.

Set public metadata

Note

Importing clerkClient varies based on your framework. Refer to the for usage details, including guidance on .

route.ts
export async function POST(req) {
  const { birthday, userId } = await req.json()

  await clerkClient.users.updateUserMetadata(userId, {
    publicMetadata: {
      birthday,
    },
  })

  return Response.json({ success: true })
}

If you're using Next.js, you must await the instantiation of the clerkClient instance, like so:

const client = await clerkClient()

const response = await client.users.updateUserMetadata()
public.ts
import { clerkClient } from '@clerk/express'

app.post('/updateBirthday', async (req, res) => {
  const { birthday, userId } = req.body

  await clerkClient.users.updateUserMetadata(userId, {
    publicMetadata: {
      birthday,
    },
  })
  res.status(200).json({ success: true })
})
public.go
var client clerk.Client

func addStripeCustomerID(user *clerk.User, birthday string) error {
    Role := map[string]interface{}{
        "birthday": birthday,
    }
  user, err := s.clerkClient.Users().UpdateMetadata(sess.UserID, &clerk.updateMetadataRequest{
    PublicMetadata: birthday,
  })

  if err != nil {
    panic(err)
  }
}
public.rb
# ruby json example with a private metadata and stripe id
require 'clerk'
require 'json'

birthday = {
  "birthday": "1990-01-01",
}

clerk = Clerk::SDK.new(api_key: "your_clerk_secret_key")
clerk.users.updateMetadata("user_xyz", public_metadata: birthday)
curl.sh
curl -XPATCH -H 'Authorization: Bearer CLERK_SECRET_KEY' -H "Content-type: application/json" -d '{
  "public_metadata": {
    "birthday": "1990-01-01"
  }
}' 'https://api.clerk.com/v1/users/{user_id}/metadata'

Retrieve public metadata

There are multiple ways to retrieve public metadata.

On the frontend, it's available on the object, which can be accessed using the useUser() hook.

On the backend, it's available on the object which can be accessed using the JavaScript Backend SDK's method. This method will return the User object which contains the public metadata. However, this method is subject to rate limits, so if you are retrieving the metadata frequently, it's recommended to attach it to the user's session token.

Private metadata

Private metadata is only accessible by the backend, which makes this useful for storing sensitive data that you don't want to expose to the frontend. For example, you could store a user's Stripe customer ID.

Set private metadata

Note

Importing clerkClient varies based on your framework. Refer to the for usage details, including guidance on .

route.ts
export async function POST(req) {
  const { stripeId, userId } = await req.json()

  await clerkClient.users.updateUserMetadata(userId, {
    privateMetadata: {
      stripeId: stripeId,
    },
  })

  return Response.json({ success: true })
}

If you're using Next.js, you must await the instantiation of the clerkClient instance, like so:

const client = await clerkClient()

const response = await client.users.updateUserMetadata()
private.ts
import { clerkClient } from '@clerk/express'

app.post('/updateStripe', async (req, res) => {
  const { stripeId, userId } = req.body

  await clerkClient.users.updateUserMetadata(userId, {
    privateMetadata: {
      stripeId: stripeId,
    },
  })

  res.status(200).json({ success: true })
})
private.go
 import 	(
	 "context"
	 "encoding/json"

   "github.com/clerk/clerk-sdk-go/v2/user"
	 "github.com/clerk/clerk-sdk-go/v2"
 )

func addStripeCustomerID(userId string, stripeCustomerId string) (*clerk.User, error) {
  ctx := context.Background()

  metadata := map[string]any{"stripe_id": stripeCustomerId}

  metadataJSON, err := json.Marshal(metadata)

  if err != nil {
	  return nil, err
    }

  rawMessage := json.RawMessage(metadataJSON)

  updatedUser, err := user.UpdateMetadata(ctx, userId, &user.UpdateMetadataParams{
	  PrivateMetadata: &rawMessage,
  })

  if err != nil {
	  return nil, err
  }

  return updatedUser, nil
}
private.rb
# ruby json example with a private metadata and stripe id
require 'clerk'
require 'json'

privateMetadata = {
  "stripeID": stripeCustomerID
}


clerk = Clerk::SDK.new(api_key: "your_clerk_secret_key")
clerk.users.updateMetadata("user_xyz", private_metadata: privateMetadata)
curl.sh
curl -XPATCH -H 'Authorization: Bearer CLERK_SECRET_KEY' -H "Content-type: application/json" -d '{
  "private_metadata": {
    "stripeId": "12356"
  }
}' 'https://api.clerk.com/v1/users/{user_id}/metadata'

Retrieve private metadata

You can retrieve the private metadata for a user by using the JavaScript Backend SDK's method. This method will return the User object which contains the private metadata. However, this method is subject to rate limits, so if you are retrieving the metadata frequently, it's recommended to attach it to the user's session token.

Note

Importing clerkClient varies based on your framework. Refer to the for usage details, including guidance on .

route.ts
export async function GET(req) {
  const { userId } = await req.json()

  const user = await clerkClient.users.getUser(userId)

  return Response.json(user.privateMetadata)
}

If you're using Next.js, you must await the instantiation of the clerkClient instance, like so:

const client = await clerkClient()

const response = await client.users.getUser()
private.ts
import { clerkClient } from '@clerk/express'

app.post('/updateStripe', async (req, res) => {
  const { userId } = req.body

  const user = await clerkClient.users.getUser(userId)

  res.status(200).json(user.privateMetadata)
})
private.go
import 	(
	 "context"
	 "encoding/json"

   "github.com/clerk/clerk-sdk-go/v2/user"
	 "github.com/clerk/clerk-sdk-go/v2"
 )

type PrivateMetadata struct {
  StripeID string `json:"stripe_id"`
}

func getPrivateMetadata(userId string) (*PrivateMetadata, error) {
  ctx := context.Background()

  clerkUser, err := user.Get(ctx, userId)

  if err != nil {
	  return nil, err
  }

  privateMetadata := &PrivateMetadata{}

  err = json.Unmarshal(clerkUser.PrivateMetadata, privateMetadata)

  if err != nil {
	  return nil, err
  }

  return privateMetadata, nil
}
private.rb
# ruby json example with a private metadata and stripe id
require 'clerk'
clerk = Clerk::SDK.new(api_key: "your_clerk_secret_key")
clerk.users.getUser("user_xyz")
curl.sh
curl -XGET -H 'Authorization: CLERK_SECRET_KEY' -H "Content-type: application/json" 'https://api.clerk.com/v1/users/{user_id}'

Unsafe metadata

Unsafe metadata can be both read and set from the frontend and the backend. It's called "unsafe" metadata because it can be modified directly from the frontend, which means malicious users could potentially tamper with these values.

Unsafe metadata is the only metadata property that can be set during sign-up, so a common use case is to use it in . Custom data collected during the onboarding (sign-up) flow can be stored in the object. After a successful sign-up, SignUp.unsafeMetadata is copied to the User object as User.unsafeMetadata. From that point on, the unsafe metadata is accessible as a direct attribute of the User object.

Set unsafe metadata

The following examples demonstrate how to update unsafe metadata for an existing user. Updating unsafeMetadata replaces the previous value; it doesn't perform a merge. To merge data, you can pass a combined object such as { …user.unsafeMetadata, …newData } to the unsafeMetadata parameter.

The following examples demonstrate how to update unsafeMetadata on the server-side versus the client-side.

Note

Importing clerkClient varies based on your framework. Refer to the for usage details, including guidance on .

route.ts
export async function POST(req) {
  const { userId } = await req.json()

  await clerkClient.users.updateUserMetadata(userId, {
    unsafeMetadata: {
      birthday: '11-30-1969',
    },
  })

  return Response.json({ success: true })
}

If you're using Next.js, you must await the instantiation of the clerkClient instance, like so:

const client = await clerkClient()

const response = await client.users.updateUserMetadata()
private.ts
import { clerkClient } from '@clerk/express'

app.post('/updateStripe', async (req, res) => {
  const { stripeId, userId } = await req.body

  await clerkClient.users.updateUserMetadata(userId, {
    unsafeMetadata: {
      birthday: '11-30-1969',
    },
  })

  res.status(200).json({ success: true })
})
private.go
import 	(
	 "context"
	 "encoding/json"

   "github.com/clerk/clerk-sdk-go/v2/user"
	 "github.com/clerk/clerk-sdk-go/v2"
 )

func addBirthdayToUser(userId string, birthday string) (*clerk.User, error) {
  ctx := context.Background()

  metadata := map[string]any{"birthday": birthday}

  metadataJSON, err := json.Marshal(metadata)

  if err != nil {
	  return nil, err
  }

  rawMessage := json.RawMessage(metadataJSON)

  updatedUser, err := user.UpdateMetadata(ctx, userId, &user.UpdateMetadataParams{
	  UnsafeMetadata: &rawMessage,
  })

  if err != nil {
	  return nil, err
  }

  return updatedUser, nil
}
private.rb
require 'clerk'
require 'json'

unsafeMetadata = {
  "birthday": "04-20-1969"
}

clerk = Clerk::SDK.new(api_key: "your_clerk_secret_key")
clerk.users.updateMetadata("user_123", unsafe_metadata: unsafeMetadata)
curl.sh
curl -XPATCH -H 'Authorization: Bearer CLERK_SECRET_KEY' -H "Content-type: application/json" -d '{
  "unsafe_metadata": {
    "birthday": "11-30-1969"
  }
}' 'https://api.clerk.com/v1/users/{user_id}/metadata'

For React-based SDKs, such as Next.js, Tanstack React Start, and React Router, use the useUser() hook to update unsafe metadata.

page.tsx
export default function Page() {
  const { user } = useUser()
  const [birthday, setBirthday] = useState('')

  return (
    <div>
      <input type="text" value={birthday} onChange={(e) => setBirthday(e.target.value)} />

      <button
        onClick={() => {
          user?.update({
            unsafeMetadata: { birthday },
          })
        }}
      >
        Update birthday
      </button>
    </div>
  )
}

When using the JavaScript SDK, use the method to update unsafe metadata.

main.js
import { Clerk } from '@clerk/clerk-js'

// Initialize Clerk with your Clerk Publishable Key
const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY

const clerk = new Clerk(pubKey)
await clerk.load()

if (clerk.isSignedIn) {
  await clerk.user
    .update({
      unsafeMetadata: {
        birthday: '01-01-2000',
      },
    })
    .then((res) => console.log(res))
    .catch((error) => console.log('An error occurred:', error.errors))
} else {
  document.getElementById('app').innerHTML = `
    <div id="sign-in"></div>
  `

  const signInDiv = document.getElementById('sign-in')

  clerk.mountSignIn(signInDiv)
}

Retrieve unsafe metadata

There are multiple ways to retrieve unsafe metadata.

On the frontend, it's available on the object, which can be accessed using the useUser() hook.

On the backend, it's available on the object which can be accessed using the JavaScript Backend SDK's method. This method will return the User object which contains the unsafe metadata. However, this method is subject to rate limits, so if you are retrieving the metadata frequently, it's recommended to attach it to the user's session token.

Metadata in the session token

Retrieving metadata from the User object on the server-side requires making an API request to Clerk's Backend API, which is slower and is subject to rate limits. You can store it in the user's session token, which doesn't require making an API request as it's available on the user's authentication context. However, there is a size limitation to keep in mind. Clerk stores the session token in a cookie, and most browsers cap cookie size at 4KB. 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.

If you need to store more than 1.2KB of metadata, you should store the extra data in your own database instead. If this isn't an option, you can move particularly large claims out of the token and fetch them using a separate API call from your backend, but this approach brings back the issue of making an API request to Clerk's Backend API, which is slower and is subject to rate limits.

Another limitation of storing metadata in the session token is that when you modify metadata server-side, the changes won't appear in the session token until the next refresh. To avoid race conditions, either force a JWT refresh after metadata changes or handle the delay in your application logic.

If you've considered the limitations, and you still want to store metadata in the session token:

  1. In the Clerk Dashboard, navigate to the Sessions page.
  2. Under Customize session token, in the Claims editor, you can add any claim to your session token that you need and select Save. To avoid exceeding the session token's 1.2KB limit, it's not recommended to add the entire user.public_metadata object. Instead, add individual fields as claims, like user.public_metadata.birthday. When doing this, it's recommended to leave particularly large claims out of the token to avoid exceeding the session token's size limit. See the example for more information.

Feedback

What did you think of this content?

Last updated on