Docs

User metadata

Metadata allows for custom data to be saved on the User object. There are three types of metadata: "unsafe", "public", and "private".

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

Warning

Metadata is limited to 8kb maximum.

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.

Setting private metadata

app/route/private.ts
import { NextResponse } from 'next/server'
import { clerkClient } from '@clerk/nextjs/server'

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

  const client = await clerkClient()

  await client.users.updateUserMetadata(userId, {
    privateMetadata: {
      stripeId: stripeId,
    },
  })
  return NextResponse.json({ success: true })
}
pages/api/private.ts
import { clerkClient } from '@clerk/nextjs/server'
import { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { stripeId, userId } = req.body

  const client = await clerkClient()

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

  res.status(200).json({ success: true })
}

Caution

On January 8, 2025, the Node SDK will no longer be available. Upgrade to the Express SDK.

private.ts
import { clerkClient } from '@clerk/clerk-sdk-node'

app.post('/updateStripe', async (req, res) => {
  const { stripeId, userId } = await body.json()
  await clerkClient.users.updateUserMetadata(userId, {
    privateMetadata: {
      stripeId: stripeId,
    },
  })
  res.status(200).json({ success: true })
})
private.go
var client clerk.Client

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

	if err != nil {
		panic(err)
	}
}
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'

Retrieving private metadata

You can retrieve the private metadata for a user by using the getUser method. This method will return the User object which contains the private metadata.

app/private/route.ts
import { NextResponse } from 'next/server'
import { clerkClient } from '@clerk/nextjs/server'

export async function GET(request: Request) {
  const { stripeId, userId } = await request.body.json()

  const client = await clerkClient()

  const user = await client.users.getUser(userId)
  return NextResponse.json(user.privateMetadata)
}
pages/api/private.ts
import { clerkClient } from '@clerk/nextjs/server'
import { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { userId } = await req.body.json()

  const client = await clerkClient()

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

  res.status(200).json(user.privateMetadata)
}

Caution

On January 8, 2025, the Node SDK will no longer be available. Upgrade to the Express SDK.

private.ts
import { clerkClient } from '@clerk/clerk-sdk-node'

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

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

  res.status(200).json(user.privateMetadata)
})
curl.sh
curl -XGET -H 'Authorization: CLERK_SECRET_KEY' -H "Content-type: application/json" 'https://api.clerk.com/v1/users/{user_id}'
private.go
var client clerk.Client

func GetUserMetadata(user *clerk.User, stripeCustomerID string) error {
  user, err := s.clerkClient.Users().Read(sess.UserID)

  if err != nil {
    panic(err)
  }
}
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")

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 custom role for a user.

Setting public metadata

app/route/public.ts
import { NextResponse } from 'next/server'
import { clerkClient } from '@clerk/nextjs/server'

export async function POST() {
  const { role, userId } = await body.json()

  const client = await clerkClient()

  await client.users.updateUserMetadata(userId, {
    publicMetadata: {
      role,
    },
  })
  return NextResponse.json({ success: true })
}
pages/api/public.ts
import { clerkClient } from '@clerk/nextjs/server'
import { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { role, userId } = req.body

  const client = await clerkClient()

  await client.users.updateUserMetadata(userId, {
    publicMetadata: {
      role,
    },
  })

  res.status(200).json({ success: true })
}

Caution

On January 8, 2025, the Node SDK will no longer be available. Upgrade to the Express SDK.

public.ts
import { clerkClient } from '@clerk/clerk-sdk-node'

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

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

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

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

publicMetadata = {
  "role": "awesome-user",
}

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

Retrieving public metadata

There are multiple ways to retrieve public metadata. It is available on the User object returned by the useUser hook, and it can also be attached to the session token to be retrieved with auth() and useAuth(). If you need to retrieve public metadata frequently in the backend, the best option is to attach it to the session token and retrieve it from the session token.

To read about customizing your session token and retrieving that data, check out our session token customization guide.

Unsafe metadata

If you need to implement custom fields that will be attached to the User object from the frontend (using our ClerkJS library or the Frontend API), you should choose the unsafeMetadata property.

One common use case for this attribute is to implement custom fields that will be attached to the User object.

Clerk has named this type of metadata "unsafe" since it can be set and accessed by both the Frontend API and the Backend API. This provides a quick method to add custom attributes to a user. These attributes will be stored on the User object and will be available for access at all times.

The "unsafe" custom attributes can be set upon sign-up when creating or updating a SignUp object. After a successful sign-up, these attributes will be copied to the User object. From that point on they can be accessed as a direct attribute of the User object.

Setting unsafe metadata

Updating this value overrides the previous value; it does not merge. To perform a merge, you can pass something like { …user.unsafeMetadata, …newData } to this parameter.

Using the Backend API

app/route/private.ts
import { NextResponse } from 'next/server'
import { clerkClient } from '@clerk/nextjs/server'

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

  const client = await clerkClient()

  await client.users.updateUserMetadata(userId, {
    unsafeMetadata: {
      birthday: '11-30-1969',
    },
  })
  return NextResponse.json({ success: true })
}
pages/api/private.ts
import { clerkClient } from '@clerk/nextjs/server'
import { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { stripeId, userId } = req.body

  const client = await clerkClient()

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

  res.status(200).json({ success: true })
}

Caution

On January 8, 2025, the Node SDK will no longer be available. Upgrade to the Express SDK.

private.ts
import { clerkClient } from '@clerk/clerk-sdk-node'

app.post('/updateStripe', async (req, res) => {
  const { stripeId, userId } = await body.json()
  await clerkClient.users.updateUserMetadata(userId, {
    unsafeMetadata: {
      birthday: '11-30-1969',
    },
  })
  res.status(200).json({ success: true })
})
private.go
var client clerk.Client

func addStripeCustomerID(user *clerk.User, stripeCustomerID string) error {
    birthday := map[string]interface{}{
        "birthday": "04-20-1969",
    }
  user, err := s.clerkClient.Users().UpdateMetadata(sess.UserID, &clerk.updateMetadataRequest{
    UnsafeMetadata: birthday,
  })

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

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

clerk = Clerk::SDK.new(api_key: "your_clerk_secret_key")
clerk.users.updateMetadata("user_xyz", 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'
app/route/unsafe.tsx
'use client'
import { useUser } from '@clerk/nextjs'
import { useState } from 'react'

export default function UnSafePage() {
  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>
  )
}
pages/unsafe.tsx
import { useUser } from '@clerk/nextjs'
import { useState } from 'react'

export default function UnSafePage() {
  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>
  )
}
unsafe.tsx
import { useUser } from '@clerk/clerk-react'
import { useState } from 'react'

export default function UnSafePage() {
  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>
  )
}
unsafe.tsx
import { useUser } from '@clerk/remix'
import { useState } from 'react'

export default function UnSafePage() {
  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>
  )
}
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.user) {
  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)
}

Retrieving unsafe metadata

There are multiple ways to retrieve unsafe metadata. It is available in the User object returned by the useUser() hook. It can also be attached to a session token, which can be retrieved with the auth() helper in Next.js applications, or with the useAuth() hook. If you need to retrieve unsafe metadata frequently in the backend, the best option is to attach it to the session token and retrieve it from the session token.

To read about customization your session token and retrieving that data, check out our session token customization guide.

Feedback

What did you think of this content?

Last updated on