From 29e6888ca081f39c0e45f397a624acf398cd651c Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 30 Jun 2025 01:51:00 +0800 Subject: [PATCH 01/16] writing guide :) --- docs/guides/create-a-custom-get-user.mdx | 180 +++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 docs/guides/create-a-custom-get-user.mdx diff --git a/docs/guides/create-a-custom-get-user.mdx b/docs/guides/create-a-custom-get-user.mdx new file mode 100644 index 0000000000..788a14109d --- /dev/null +++ b/docs/guides/create-a-custom-get-user.mdx @@ -0,0 +1,180 @@ +--- +title: Create a custom getUser function +description: Build +--- + +It can be a bit daunting when getting stated with Clerk on how to make your user data work for you. Bringing Clerk into your application can very easily bring in a split brain situation where managing user data can get complicated (and slow). + +Clerk supports a handful of different ways to manage this disconnect. A lot of people turn to using webhooks to sync the Clerk user data in to their own databases, but this can introduce some out of sync issues. + +Here we are going to take a different approach, building on top of Clerk's `auth()` function we are going to create our own function. I'm going to call it `getUser` for simplicity, but feel free to use `authenticateUser`, `authRequest`, `fetchUser`, `verifyAuth` or whatever floats your boat. + +To get started, we will simply call `auth()` and pull out what we need. + +```ts +// Different Clerk SDKs have different ways import and call `auth()` +import { auth } from '@clerk/nextjs' + +export const getUser = async () => { + // I highly recommend taking some time to understand all the methods + // and properties this function returns, `has()` is useful for permission + // checks, and `getToken()` is great for authenticating external api calls + // Alongside `userId`, you'll likely want `orgId` in a b2b setup + const { isAuthenticated, userId } = await auth() + + if (!isAuthenticated) { + // This is our first major option, and we have many choices + + // A common option is going to be to `throw new Error('Unauthorized')` + // ensuring that we can only continue if the user is logged in. + + // But if we are using a framework we may want to `throw new TRPCError()` + // or call `unauthorized()` (https://nextjs.org/docs/app/api-reference/functions/unauthorized) + + // A more advanced pattern is to throw a response + // `throw new Response("Unauthorized", { status: 401 })` + // Using a higher level function to catch and return the response object. + + // If we are using EffectTS we may want to return an Error object + + // Or simply we can just return null, signifying that user can't be + // authenticated, letting the request handler do what it wants with + // that information + return null + } + + return { + // For now we simply want to return the user identifier. + userId, + } +} +``` + +Feel free to use this inside a server action, rsc component, a api route, and anywhere else we run code on the server side. But we aren't looking at a particularly helpful return object so lets truck on. + +It is not uncommon that Clerk support get's a request to raise our rate limits as someones application is hitting them and causing issues. Commonly it's being the application is abusing `currentUser()`, calling it in every request made to their backend. While this function is useful, giving you all of the users data, that also means we need to limit it's use to stop our infra being overrun. + +So you may ask, well how the hell do I get my users data than? Well it's simply, we just need to modify the session token. + +1. Head over to [Sessions](https://dashboard.clerk.com/~/sessions) on the dashboard and scroll down to "Customize session token" +1. Update the claims (if they are not already set to something) to simply include the users email: + +```json +{ + "email": "{{user.primary_email_address}}" +} +``` + +3. Back at our custom `getUser` function, lets pull something new out of the `auth()` return + +```ts +import { auth } from '@clerk/nextjs' + +export const getUser = async () => { + // Extract out `sessionClaims` from the `auth()` call for us to use + const { isAuthenticated, userId, sessionClaims } = await auth() + + if (!isAuthenticated) return null + + return { + userId, + + // for now let's just return it + sessionClaims, + } +} +``` + +Looking at the return from our custom `getUser()` we will see + +```json {{ mark: [8] }} +{ + "userId": "user_2u823dCrAIzoQfgjnsimCYJvJaI", + "sessionClaims": { + "azp": "http://localhost:3001", + // Here we have it, because we configured in the dashboard to include the + // users email, it shows up here, no network request to clerks infra + // needed, no rate limits. + "email": "your-email@example.me", + "exp": 1751214847, + "fva": [137, -1], + "iat": 1751214787, + "iss": "https://awake-cobra-28.clerk.accounts.dev", + "jti": "002332cec9e6fb9c9efd", + "nbf": 1751214777, + "sid": "sess_2zBZjlFCRMOq1LSDOcJwMBAzQSR", + "sub": "user_2u823dCrAIzoQfgjnsimCYJvJaI", + "v": 2 + } +} +``` + +4. But you may have noticed in your ide that typescript doesn't know that `email` is a key (and what its value is) on the `sessionClaims` object, well theres a trick to fix that. + +```ts {{ filename: 'types.d.ts' }} +declare global { + interface CustomJwtSessionClaims { + // If you have clerk configured that users can sign up without their email, + // you'll want to make the type `string | undefined` to ensure the types + // aren't lying. + email: string + // don't worry about adding the types of the other data we see in + // `sessionClaims` above, the Clerk types have already covered that for us + } +} + +export {} +``` + +By simply creating this file in the root of your project, typescript will pick up and keep us type-safe. + +5. Ok so let's go back to our growing `getUser()` function and refine down the return type + +```ts +import { auth } from '@clerk/nextjs/server' + +export const getUser = async () => { + const { isAuthenticated, userId, sessionClaims } = await auth() + + if (!isAuthenticated) return null + + // Now that sessionClaims is type safe, we are free to extract out our `email` + // property, if you marked it as optional you may want to add in a if check. + const { email } = sessionClaims + + return { + userId, + // Return the users email ready to be used. + email, + } +} +``` + +## Reading and writing custom user data. + +This is cool and all, reducing network requests will keep our application quick and reduces dependency on Clerk infra being available. But as soon as we need to attach a `stripeCustomerId` or a `australianBusinessNumber` your out of luck, right? + +Wrong, Clerk has `metadata`, in-fact we have three types, `unsafe`, `private`, and `public`. These live on the user object, are separate and private between users, and let us store any kind of information we want. For each type of metadata, per user we have 4kb of available json, to put that in human terms that's about 2 to 3 pages of text, or about 1,000 words. + +Before we get in to the code, we need to choice which variant of `metadata` we want to use: + +- `unsafe` - Sounds scary! all this means is the user can edit this themselves, they can put anything they want in here, can't really trust this data on the server otherwise you open yourself to security risks. +- `private` - This is the most secure, it can only be written and read from your server using the `clerkClient`, but this means it can't be included in the session token, as then the user would be able to read it. +- `public` - Unlike the unsafe metadata, public metadata can't be written by the user directly, but unlike private, this can be read by the user, allowing it to be included in the session token! + +So we can't use `private` for what we need, so it's up to you to choice between `unsafe` and `public` but for my use I am going to go with `public`, as I don't trust the user. + +1. We need to update the Session Token in the dashboard + +- Back on the [Sessions](https://dashboard.clerk.com/~/sessions) page, update it to include `{{user.public_metadata}}`, you can remove email if you don't want it anymore. + +```json + { + "email": "{{user.primary_email_address}}", + // You can call this whatever you'd like, + // I went with 'details' but it's completely up to you ++ "details": "{{user.public_metadata}}" + } +``` + +2. \ No newline at end of file From e3d623acce7409b47b90fb32f5b3fa6781f941ef Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 30 Jun 2025 18:05:53 +0800 Subject: [PATCH 02/16] Continue on the guide --- docs/guides/create-a-custom-get-user.mdx | 151 ++++++++++++++++++++++- 1 file changed, 148 insertions(+), 3 deletions(-) diff --git a/docs/guides/create-a-custom-get-user.mdx b/docs/guides/create-a-custom-get-user.mdx index 788a14109d..b7f63c964c 100644 --- a/docs/guides/create-a-custom-get-user.mdx +++ b/docs/guides/create-a-custom-get-user.mdx @@ -150,7 +150,7 @@ export const getUser = async () => { } ``` -## Reading and writing custom user data. +## Custom user data. This is cool and all, reducing network requests will keep our application quick and reduces dependency on Clerk infra being available. But as soon as we need to attach a `stripeCustomerId` or a `australianBusinessNumber` your out of luck, right? @@ -166,7 +166,7 @@ So we can't use `private` for what we need, so it's up to you to choice between 1. We need to update the Session Token in the dashboard -- Back on the [Sessions](https://dashboard.clerk.com/~/sessions) page, update it to include `{{user.public_metadata}}`, you can remove email if you don't want it anymore. +Back on the [Sessions](https://dashboard.clerk.com/~/sessions) page, update it to include `{{user.public_metadata}}`, you can remove email if you don't want it anymore. ```json { @@ -177,4 +177,149 @@ So we can't use `private` for what we need, so it's up to you to choice between } ``` -2. \ No newline at end of file +2. To keep everything aligned we will start by updating our magical types.d.ts file + +```ts {{ filename: 'types.d.ts' }} + declare global { + interface CustomJwtSessionClaims { + email: string ++ details: { ++ favoriteSnack?: string ++ favoriteMovie?: string ++ } + } + } + + export {} +``` + +Not only do we define that 'details' exists on the users session, but we can now actually in our codebase define our own user data. We no longer need to go over to the dashboard to manage what data we want, it's entirely within our app. + +I would start by defining these properties as optional, as any existing users won't have them set. If you're so inclined you could run a migration script to pull the data from say your database and put it in each users metadata, then update the types to make it not optional. + +This is a standard json object, so stay away from types that can't be json parsed, eg dates, maps, and sets. + +3. Let's update our `getUser()` to now extract and return our custom user data + +```ts +import { auth } from '@clerk/nextjs/server' + +export const getUser = async () => { + const { isAuthenticated, userId, sessionClaims } = await auth() + + if (!isAuthenticated) return null + + // Just like the email property we added to the custom session, we now can + // pull out the `details` property for our use. The magic here is we get + // access to this data without any network request to Clerk's infra. + // So your website stays fast. + const { email, details } = sessionClaims + + // With the details property, we have a couple different options for + // how we want to return it. + + // This is the safest option, as we have no risk that a property + // inside `details` could conflict with a `userId` or `email` + // but it means you'll need to access it one layer deeper + // eg user.details.favoritePizza + return { + userId, + email, + details, + } + + // I wouldn't recommend this option, in the future a developer + // may add `email` to the custom metadata by accident + // and this would overwrite the users email we are expecting + // Clerk to manage for us + return { + userId, + email, + ...details, + } + + // For me this is the happy middle ground, it keeps the data access + // short (eg user.favoritePizza) but doesn't let us accidentally + // overright the other properties we are expecting + return { + ...details, + userId, + email, + } +} +``` + +4. Success! + +Depending on which return you picked above, we will get something along these lines. + +```json +{ + "favoritePizza": "Pepperoni", + "favoriteSnack": "Oreos", + "userId": "user_2u823dCrAIzoQfgjnsimCYJvJaI", + "email": "clerk-test@wylynko.me" +} +``` + +Our beautiful `getUser()` function gives us both our standard Clerk info (emails, phone numbers, usernames, etc) and has our custom user data. + +## Writing the custom user data + +Ok, but we can't spend all day writing the users metadata in the dashboard manually, so how do we update it as we need? + +This can be achieved in a couple ways, but for a clean api of our `getUser()` function, my thought is we will return an `update()` function. + +```ts +// We will start by bringing in the `clerkClient`, this can change between Clerk SDKs +import { auth, clerkClient } from '@clerk/nextjs/server' + +export const getUser = async () => { + const { isAuthenticated, userId, sessionClaims } = await auth() + + if (!isAuthenticated) return null + + const { email, details } = sessionClaims + + // Bit of function inception here, we are defining our own update function + // inside the getUser function, but trust me, this is perfectly natural + + // One cool typescript trick, We can simply use the typescript tool `typeof` + // to extract out our custom type from the types.d.ts, instead of having to + // re-define the type here. + const update = async (newDetails: Partial) => { + // We are going to need to call the clerk backend api, so we will start by + // instantiating it ready for our use. Despite the await here, this is + // simply for a dynamic import, no network fetch yet. + // `clerkClient()` will pick up our clerk environment variables automatically + // no need to pass them in here. + const clerk = await clerkClient() + + // Finally we update the metadata, This function actually returns the full + // user object (same as calling currentUser()) so we can use that if we want + await clerk.users.updateUserMetadata(userId, { + // Since we picked public metadata earlier at the start we want to update that + publicMetadata: { + // Here I am spreading in the existing details first, so any values not + // getting updated, won't get deleted + ...details, + // And second I am spreading in the new details, overwriting or adding + // as need be. + ...newDetails, + }, + }) + } + + return { + ...details, + userId, + email, + + // And lets just return our `update()` function ready to be used. + update, + } +} +``` + +## Using the function +## Using with a database \ No newline at end of file From 7488795bcebf71ae572cd5b0b0c24d3e269f7648 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 30 Jun 2025 18:34:36 +0800 Subject: [PATCH 03/16] Add guide to manifest --- docs/manifest.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/manifest.json b/docs/manifest.json index f86ed9b6e4..460cd3f76b 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1728,6 +1728,10 @@ "title": "Authorize users", "href": "/docs/guides/authorization-checks" }, + { + "title": "Your Custom `getUser()`", + "href": "/docs/guides/create-a-custom-get-user" + }, { "title": "Deployments & Migrations", "collapse": true, From d38083e07997093a577981e6b05b7595e49a9200 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 30 Jun 2025 23:19:46 +0800 Subject: [PATCH 04/16] add example --- docs/guides/create-a-custom-get-user.mdx | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/guides/create-a-custom-get-user.mdx b/docs/guides/create-a-custom-get-user.mdx index b7f63c964c..3c4797b716 100644 --- a/docs/guides/create-a-custom-get-user.mdx +++ b/docs/guides/create-a-custom-get-user.mdx @@ -322,4 +322,30 @@ export const getUser = async () => { ``` ## Using the function + +With our custom `getUser()` we can use it anywhere on the server to power our application. + +### On a page load + +```ts +import { getUser } from "~/lib/get-user" + +export default async function UserPage() { + const user = await getUser(); + + if (!user) return Unauthenticated :(; + + return ( +
+

Welcome {user.email}

+ {user.favoriteMovie ? ( + I hear your favorite movie is {user.favoriteMovie} + ) : null} +
+ ); +} +``` + +### In a server action + ## Using with a database \ No newline at end of file From c0020ce6004a0ff2b9372bef76940bbaa9014c1b Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 1 Jul 2025 12:29:09 +0800 Subject: [PATCH 05/16] wip --- docs/guides/create-a-custom-get-user.mdx | 165 ++++++++++++++++++++++- 1 file changed, 161 insertions(+), 4 deletions(-) diff --git a/docs/guides/create-a-custom-get-user.mdx b/docs/guides/create-a-custom-get-user.mdx index 3c4797b716..133fe67dbc 100644 --- a/docs/guides/create-a-custom-get-user.mdx +++ b/docs/guides/create-a-custom-get-user.mdx @@ -185,7 +185,7 @@ Back on the [Sessions](https://dashboard.clerk.com/~/sessions) page, update it t email: string + details: { + favoriteSnack?: string -+ favoriteMovie?: string ++ favoritePizza?: string + } } } @@ -338,8 +338,8 @@ export default async function UserPage() { return (

Welcome {user.email}

- {user.favoriteMovie ? ( - I hear your favorite movie is {user.favoriteMovie} + {user.favoritePizza ? ( + I hear your favorite pizza is {user.favoritePizza} ) : null}
); @@ -348,4 +348,161 @@ export default async function UserPage() { ### In a server action -## Using with a database \ No newline at end of file +```ts +"use server" + +import { getUser } from "~/lib/get-user" + +export const processPizzaOrder = async ( + pizzaChoice: "pepperoni" | "barbecue" | "chicken" +) => { + const user = await getUser(); + + if (user === null) { + return { + error: "Unauthenticated", + }; + } + + user.update({ + favoritePizza: pizzaChoice, + }); +}; +``` + +### In a api route + +```ts +// I am using zod for validation here, but you can use whatever you want +import { z } from "zod"; +import { getUser } from "~/lib/get-user"; + +// In Get requests, you typically don't want to modify any data, +// so we aren't going to use the `update()` function here. +export const GET = async (request: Request) => { + const user = await getUser(); + + if (user === null) { + // You may want to fail more gracefully here, + // for example redirecting to a login page. + // or returning `We don't know your favorite snack` + return new Response("Unauthenticated", { status: 401 }); + } + + return new Response( + `${user.email}'s favorite snack is ${user.favoriteSnack}` + ); +}; + +const bodySchema = z.object({ + snack: z + .string() + .min(1, { + message: "Snack is required", + }) + .max(20, { + message: "Snack must be less than 20 characters", + }), +}); + +export const POST = async (request: Request) => { + // We don't even bother validating the request body yet + // If the users not authenticated, don't waste the compute + // validating the request body. + const user = await getUser(); + + if (user === null) { + return new Response("Unauthenticated", { status: 401 }); + } + + const body = bodySchema.safeParse(await request.json()); + + if (!body.success) { + return new Response(body.error.message, { status: 400 }); + } + + // With the new data, we can update the users metadata. + await user.update({ + favoriteSnack: body.data.snack, + }); + + // We may want to also send an email notification here + // Or duel save the data to our own database. + // Possibly we want to log the change to monitoring server + + return new Response( + `${user.email}'s favorite snack is ${body.data.snack}` + ); +}; +``` + +## Using with a database + +The beauty of everything we have done so far is none of it requires you as a app developer to deploy, manage and scale a database. Just with using Clerk you are enabled to build powerful applications that let your users achieve meaningful results. + +But this kind of document based storage does have it's limitations and bringing in a relational database (eg postgres, mysql) will allow your applications to share data between users, store more complex data types, give you strong query performance, and more. + +In addition it's very possible you started with a database and webapp before you started using Clerk, and already have custom user data in the database that would need to be migrated out take take advantage of user metadata. + +So let's take a look at a couple options you have to land on the perfect setup for your application, giving you the performance you desire with the tradeoffs that make sense for you. + +### Starting fresh + +Let's imagine we are creating an online food ordering platform, users can place orders for their favorite local Japanese restaurant. Users will need to create a new order, filled with food options, and we will need to know the status of their current order. + +So we need two database tables, an `orders` table and a `items` table, then we can store the `orderId` and the `orderStatus` in the Clerk user metadata. + +```ts +// I am using postgres, but none of what I am doing is particularly specific to postgres, so feel free to use whatever dialect you want. +import { index, pgEnum, pgTable } from "drizzle-orm/pg-core"; + +export const orderStatus = pgEnum("order_status", [ + "pending", + "shipped", + "delivered", +]); + +export const orders = pgTable( + "orders", + (t) => ({ + orderId: t.integer().primaryKey().generatedAlwaysAsIdentity(), + + // It's possible you will want to call this something more specific, eg `clerkUserId` + userId: t.varchar().notNull(), + status: orderStatus().default("pending"), + address: t.varchar().notNull(), + createdAt: t.timestamp().defaultNow(), + updatedAt: t.timestamp().defaultNow(), + }), + + // Because this is an external id, we need to index it to tell the database we will be querying by it + (table) => [index().on(table.userId)] +); + +export const items = pgTable("items", (t) => ({ + itemId: t.integer().primaryKey().generatedAlwaysAsIdentity(), + orderId: t.integer().references(() => orders.orderId), + name: t.varchar().notNull(), + price: t.integer().notNull(), + quantity: t.integer().notNull(), +})); +``` + +```ts +declare global { + interface CustomJwtSessionClaims { + email: string; + phone: string; // I've added the users phone number to their session token, so we have it to easily send them text message updates + details: { + currentOrder: { + orderId: number; + orderStatus: "pending" | "shipped" | "delivered"; + } | null; + }; + } +} + +export {}; +``` + +## Considerations for client side \ No newline at end of file From 184ff701d87676d18670645f4c2a8e68f0042b83 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 1 Jul 2025 13:09:17 +0800 Subject: [PATCH 06/16] add example --- docs/guides/create-a-custom-get-user.mdx | 80 ++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 5 deletions(-) diff --git a/docs/guides/create-a-custom-get-user.mdx b/docs/guides/create-a-custom-get-user.mdx index 133fe67dbc..6cb3b7f393 100644 --- a/docs/guides/create-a-custom-get-user.mdx +++ b/docs/guides/create-a-custom-get-user.mdx @@ -469,8 +469,7 @@ export const orders = pgTable( // It's possible you will want to call this something more specific, eg `clerkUserId` userId: t.varchar().notNull(), - status: orderStatus().default("pending"), - address: t.varchar().notNull(), + status: orderStatus().notNull().default("pending"), createdAt: t.timestamp().defaultNow(), updatedAt: t.timestamp().defaultNow(), }), @@ -481,19 +480,29 @@ export const orders = pgTable( export const items = pgTable("items", (t) => ({ itemId: t.integer().primaryKey().generatedAlwaysAsIdentity(), - orderId: t.integer().references(() => orders.orderId), name: t.varchar().notNull(), price: t.integer().notNull(), + discount: t.integer(), +})); + +export const orderItems = pgTable("order_items", (t) => ({ + orderId: t.integer().references(() => orders.orderId), + itemId: t.integer().references(() => items.itemId), quantity: t.integer().notNull(), })); ``` -```ts +```ts {{ filename: 'types.d.ts' }} declare global { interface CustomJwtSessionClaims { email: string; - phone: string; // I've added the users phone number to their session token, so we have it to easily send them text message updates + + // I've added the users phone number to their session token, so we have it to easily send them text message updates + phone: string; + details: { + + // I've marked this as possibly null as a user won't always have an active order currentOrder: { orderId: number; orderStatus: "pending" | "shipped" | "delivered"; @@ -505,4 +514,65 @@ declare global { export {}; ``` +Here we are making a decision, what data is important enough that we want to be able to access fast, what data can we afford to reach out to a service (eg database) for, and what data do we want shared between the two for access from both. + +So with this setup we can create a server action to handle adding an item to an order. + +```ts +"use server"; + +import { db } from "~/db"; +import { orderItems, orders } from "~/db/schema"; +import { getUser } from "~/lib/get-user"; + +export const addItemToOrder = async (itemId: number, quantity: number) => { + const user = await getUser(); + + if (user === null) { + return { error: "Unauthenticated" }; + } + + if (user.currentOrder === null) { + const [newOrder] = await db + .insert(orders) + .values({ + userId: user.userId, + }) + .returning({ + orderId: orders.orderId, + status: orders.status, + }); + + await user.update({ + currentOrder: { + orderId: newOrder.orderId, + orderStatus: newOrder.status, + }, + }); + + await db.insert(orderItems).values({ + orderId: newOrder.orderId, + itemId, + quantity, + }); + + return { + message: "Order created and item added", + }; + } + + await db.insert(orderItems).values({ + orderId: user.currentOrder.orderId, + itemId, + quantity, + }); + + return { + message: "Item added to existing order", + }; +}; +``` + +### We can place orders + ## Considerations for client side \ No newline at end of file From b6e766b2f338b27910c5ffc8268ce98434a9695f Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 2 Jul 2025 00:06:35 +0900 Subject: [PATCH 07/16] Add more code examples --- docs/guides/create-a-custom-get-user.mdx | 262 ++++++++++++++++------- 1 file changed, 184 insertions(+), 78 deletions(-) diff --git a/docs/guides/create-a-custom-get-user.mdx b/docs/guides/create-a-custom-get-user.mdx index 6cb3b7f393..2d6fc91221 100644 --- a/docs/guides/create-a-custom-get-user.mdx +++ b/docs/guides/create-a-custom-get-user.mdx @@ -285,7 +285,7 @@ export const getUser = async () => { // inside the getUser function, but trust me, this is perfectly natural // One cool typescript trick, We can simply use the typescript tool `typeof` - // to extract out our custom type from the types.d.ts, instead of having to + // to extract out our custom type from the types.d.ts, instead of having to // re-define the type here. const update = async (newDetails: Partial) => { // We are going to need to call the clerk backend api, so we will start by @@ -328,112 +328,104 @@ With our custom `getUser()` we can use it anywhere on the server to power our ap ### On a page load ```ts -import { getUser } from "~/lib/get-user" +import { getUser } from '~/lib/get-user' export default async function UserPage() { - const user = await getUser(); + const user = await getUser() - if (!user) return Unauthenticated :(; + if (!user) return Unauthenticated :( return (

Welcome {user.email}

- {user.favoritePizza ? ( - I hear your favorite pizza is {user.favoritePizza} - ) : null} + {user.favoritePizza ? I hear your favorite pizza is {user.favoritePizza} : null}
- ); + ) } ``` ### In a server action ```ts -"use server" +'use server' -import { getUser } from "~/lib/get-user" +import { getUser } from '~/lib/get-user' -export const processPizzaOrder = async ( - pizzaChoice: "pepperoni" | "barbecue" | "chicken" -) => { - const user = await getUser(); +export const processPizzaOrder = async (pizzaChoice: 'pepperoni' | 'barbecue' | 'chicken') => { + const user = await getUser() if (user === null) { return { - error: "Unauthenticated", - }; + error: 'Unauthenticated', + } } user.update({ favoritePizza: pizzaChoice, - }); -}; + }) +} ``` ### In a api route ```ts // I am using zod for validation here, but you can use whatever you want -import { z } from "zod"; -import { getUser } from "~/lib/get-user"; +import { z } from 'zod' +import { getUser } from '~/lib/get-user' // In Get requests, you typically don't want to modify any data, // so we aren't going to use the `update()` function here. export const GET = async (request: Request) => { - const user = await getUser(); + const user = await getUser() if (user === null) { // You may want to fail more gracefully here, // for example redirecting to a login page. // or returning `We don't know your favorite snack` - return new Response("Unauthenticated", { status: 401 }); + return new Response('Unauthenticated', { status: 401 }) } - return new Response( - `${user.email}'s favorite snack is ${user.favoriteSnack}` - ); -}; + return new Response(`${user.email}'s favorite snack is ${user.favoriteSnack}`) +} const bodySchema = z.object({ snack: z .string() .min(1, { - message: "Snack is required", + message: 'Snack is required', }) .max(20, { - message: "Snack must be less than 20 characters", + message: 'Snack must be less than 20 characters', }), -}); +}) export const POST = async (request: Request) => { // We don't even bother validating the request body yet // If the users not authenticated, don't waste the compute // validating the request body. - const user = await getUser(); + const user = await getUser() if (user === null) { - return new Response("Unauthenticated", { status: 401 }); + return new Response('Unauthenticated', { status: 401 }) } - const body = bodySchema.safeParse(await request.json()); + const body = bodySchema.safeParse(await request.json()) if (!body.success) { - return new Response(body.error.message, { status: 400 }); + return new Response(body.error.message, { status: 400 }) } - + // With the new data, we can update the users metadata. await user.update({ favoriteSnack: body.data.snack, - }); + }) // We may want to also send an email notification here // Or duel save the data to our own database. // Possibly we want to log the change to monitoring server - return new Response( - `${user.email}'s favorite snack is ${body.data.snack}` - ); -}; + return new Response(`${user.email}'s favorite snack is ${body.data.snack}`) +} ``` ## Using with a database @@ -454,64 +446,59 @@ So we need two database tables, an `orders` table and a `items` table, then we c ```ts // I am using postgres, but none of what I am doing is particularly specific to postgres, so feel free to use whatever dialect you want. -import { index, pgEnum, pgTable } from "drizzle-orm/pg-core"; +import { index, pgEnum, pgTable } from 'drizzle-orm/pg-core' -export const orderStatus = pgEnum("order_status", [ - "pending", - "shipped", - "delivered", -]); +export const orderStatus = pgEnum('order_status', ['pending', 'shipped', 'delivered']) export const orders = pgTable( - "orders", + 'orders', (t) => ({ orderId: t.integer().primaryKey().generatedAlwaysAsIdentity(), // It's possible you will want to call this something more specific, eg `clerkUserId` userId: t.varchar().notNull(), - status: orderStatus().notNull().default("pending"), + status: orderStatus().notNull().default('pending'), createdAt: t.timestamp().defaultNow(), updatedAt: t.timestamp().defaultNow(), }), // Because this is an external id, we need to index it to tell the database we will be querying by it - (table) => [index().on(table.userId)] -); + (table) => [index().on(table.userId)], +) -export const items = pgTable("items", (t) => ({ +export const items = pgTable('items', (t) => ({ itemId: t.integer().primaryKey().generatedAlwaysAsIdentity(), name: t.varchar().notNull(), price: t.integer().notNull(), discount: t.integer(), -})); +})) -export const orderItems = pgTable("order_items", (t) => ({ +export const orderItems = pgTable('order_items', (t) => ({ orderId: t.integer().references(() => orders.orderId), itemId: t.integer().references(() => items.itemId), quantity: t.integer().notNull(), -})); +})) ``` ```ts {{ filename: 'types.d.ts' }} declare global { interface CustomJwtSessionClaims { - email: string; + email: string // I've added the users phone number to their session token, so we have it to easily send them text message updates - phone: string; + phone: string details: { - // I've marked this as possibly null as a user won't always have an active order currentOrder: { - orderId: number; - orderStatus: "pending" | "shipped" | "delivered"; - } | null; - }; + orderId: number + orderStatus: 'pending' | 'shipped' | 'delivered' + } | null + } } } -export {}; +export {} ``` Here we are making a decision, what data is important enough that we want to be able to access fast, what data can we afford to reach out to a service (eg database) for, and what data do we want shared between the two for access from both. @@ -519,19 +506,23 @@ Here we are making a decision, what data is important enough that we want to be So with this setup we can create a server action to handle adding an item to an order. ```ts -"use server"; +'use server' -import { db } from "~/db"; -import { orderItems, orders } from "~/db/schema"; -import { getUser } from "~/lib/get-user"; +import { db } from '~/db' +import { orderItems, orders } from '~/db/schema' +import { getUser } from '~/lib/get-user' export const addItemToOrder = async (itemId: number, quantity: number) => { - const user = await getUser(); + const user = await getUser() if (user === null) { - return { error: "Unauthenticated" }; + return { error: 'Unauthenticated' } } + // Here we are handling the situation that no order exists yet + // We start by updating the database to create a new order for the user + // We use our user.update function to get the important details on the user + // And we finish off with inserting the item into the order if (user.currentOrder === null) { const [newOrder] = await db .insert(orders) @@ -541,38 +532,153 @@ export const addItemToOrder = async (itemId: number, quantity: number) => { .returning({ orderId: orders.orderId, status: orders.status, - }); + }) await user.update({ currentOrder: { orderId: newOrder.orderId, orderStatus: newOrder.status, }, - }); + }) await db.insert(orderItems).values({ orderId: newOrder.orderId, itemId, quantity, - }); + }) return { - message: "Order created and item added", - }; + message: 'Order created and item added', + } } + // But once the order is created, we can simply add the item + // We don't even need to verify that the order belongs + // to the user, as we are using public metadata so it can only be + // set server side, as we do above await db.insert(orderItems).values({ orderId: user.currentOrder.orderId, itemId, quantity, - }); + }) return { - message: "Item added to existing order", - }; -}; + message: 'Item added to existing order', + } +} +``` + +### Migrating data over to metadata + +It wouldn't be unlikely that your using Clerk from the very start, we may very well have a database already, with a `users` table. So we want to pull those important user details out and store them on the Clerk metadata to quick access. + +We can run this migration using the expand and contract pattern, starting by finding everywhere the `users` table is currently being updated, adding in a call to update the users metadata in the same fashion. Next we update our `getUser()` function to first try the metadata, and if it doesn't exist, we reach out to the database. + +Let's look at this stripe example. + +```ts {{ filename: 'app/onboarding/payments/actions.ts' }} + 'use server' + + import Stripe from 'stripe' + import { eq } from 'drizzle-orm' + import { db } from '~/db' + import { users } from '~/db/schema' + import { getUser } from '~/lib/get-user' + + export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { + apiVersion: '2025-05-28.basil', + }) + + export const setupStripeCustomer = async () => { + const user = await getUser() + + if (user === null) { + throw new Error('User not authenticated') + } + + const customer = await stripe.customers.create({ + email: user.email, + }) + + // We see here an existing insert in to the `users` table, setting a `stripeCustomerId` + await db + .update(users) + .set({ + stripeCustomerId: customer.id, + }) + .where(eq(users.clerkUserId, user.userId)) + + // So let's update it to also write this new id to the clerk user metadata ++ await user.update({ ++ stripeCustomerId: customer.id, ++ }) + + return customer.id + } +``` + +The first step is find all the places in your application that you edit the users table, and mirror the update in the clerk metadata. You may be tempted to write complex data in to the metadata, removing the need for entire tables. But until you have a full grasp of migrating the data over I would hold off. + +Only once we have the two data stores being kept in sync can we upgrade `getUser()` to handle some of the migration workload. + +```ts + import { auth, clerkClient } from '@clerk/nextjs/server' + import { eq } from 'drizzle-orm' + import { db } from '~/db' + import { users } from '~/db/schema' + import { setupStripeCustomer } from '~/app/onboarding/payments/actions' + + export const getUser = async () => { + const { isAuthenticated, userId, sessionClaims } = await auth() + + if (!isAuthenticated) return null + + const { email, details } = sessionClaims + + const update = async (newDetails: Partial) => { + const clerk = await clerkClient() + + await clerk.users.updateUserMetadata(userId, { + publicMetadata: { + ...details, + ...newDetails, + }, + }) + } + ++ let stripeCustomerId = details.stripeCustomerId + + // So now anytime this function is called we are going to check if the user doesn't have a stripeCustomerId set + // This check alone has no cost, as it's information stored in the users session ++ if (details.stripeCustomerId === undefined) { ++ const [user_db] = await db ++ .select({ ++ stripeCustomerId: users.stripeCustomerId, ++ }) ++ .from(users) ++ .where(eq(users.clerkUserId, userId)) + ++ if (user_db === undefined) { ++ stripeCustomerId = await setupStripeCustomer() ++ } else { ++ await update({ ++ stripeCustomerId: user_db.stripeCustomerId, ++ }) + ++ stripeCustomerId = user_db.stripeCustomerId ++ } ++ } + + return { + ...details, ++ stripeCustomerId, + userId, + email, + update, + } + } ``` -### We can place orders +### You probably don't need clerk webhooks -## Considerations for client side \ No newline at end of file +## Considerations for client side From 1a5e12ab3a1f9babd713136f683ed98e2a5ef913 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 2 Jul 2025 10:59:47 -0700 Subject: [PATCH 08/16] wip --- docs/guides/create-a-custom-get-user.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/create-a-custom-get-user.mdx b/docs/guides/create-a-custom-get-user.mdx index 2d6fc91221..d663a701ae 100644 --- a/docs/guides/create-a-custom-get-user.mdx +++ b/docs/guides/create-a-custom-get-user.mdx @@ -658,7 +658,7 @@ Only once we have the two data stores being kept in sync can we upgrade `getUser + .from(users) + .where(eq(users.clerkUserId, userId)) -+ if (user_db === undefined) { ++ if (user_db.stripeCustomerId === null) { + stripeCustomerId = await setupStripeCustomer() + } else { + await update({ From 7a06ef8cd73a29771c3a39b4b17fc0b54742e730 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 2 Jul 2025 16:53:35 -0700 Subject: [PATCH 09/16] Add header --- docs/guides/create-a-custom-get-user.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/guides/create-a-custom-get-user.mdx b/docs/guides/create-a-custom-get-user.mdx index d663a701ae..a3805afab9 100644 --- a/docs/guides/create-a-custom-get-user.mdx +++ b/docs/guides/create-a-custom-get-user.mdx @@ -682,3 +682,5 @@ Only once we have the two data stores being kept in sync can we upgrade `getUser ### You probably don't need clerk webhooks ## Considerations for client side + +### Refreshing the client \ No newline at end of file From 037a8ae54a8f996d1d9cbfc824fb6f8118f8fdf3 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 7 Jul 2025 23:50:10 -0700 Subject: [PATCH 10/16] Add selection on working with webhooks --- docs/guides/create-a-custom-get-user.mdx | 56 +++++++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/docs/guides/create-a-custom-get-user.mdx b/docs/guides/create-a-custom-get-user.mdx index a3805afab9..15bd46785b 100644 --- a/docs/guides/create-a-custom-get-user.mdx +++ b/docs/guides/create-a-custom-get-user.mdx @@ -1,6 +1,7 @@ --- title: Create a custom getUser function description: Build +sdk: nextjs --- It can be a bit daunting when getting stated with Clerk on how to make your user data work for you. Bringing Clerk into your application can very easily bring in a split brain situation where managing user data can get complicated (and slow). @@ -12,7 +13,6 @@ Here we are going to take a different approach, building on top of Clerk's `auth To get started, we will simply call `auth()` and pull out what we need. ```ts -// Different Clerk SDKs have different ways import and call `auth()` import { auth } from '@clerk/nextjs' export const getUser = async () => { @@ -679,7 +679,59 @@ Only once we have the two data stores being kept in sync can we upgrade `getUser } ``` -### You probably don't need clerk webhooks +### Use Clerk webhooks as your plan B + +When building an application, it's not unusual that your users will interact with each other, maybe you want to share files between users (Dropdox style), or your users will comment on videos (Youtube style), or possibly users will play a game with each other (Geo-guesser style). It makes sense as such that you'll want to look up a resource in your database, and you will want to know more than just the `userId` of the user that created that resource. This can create an issue when the user details (full name, username, email, etc) are stored in Clerk, you can setup your application to call `await clerk.users.getUser(...)`, but this will be rate-limited and will slow down your page load times. + +Typically developers will turn to using Webhooks to solve this issue, creating a `users` table with the user details they want to store and have at the ready, they will get clerk to call there webhook endpoints on user details changes to store it for later querying. Unfortunately webhooks have some inherit problems that need to be accounted for, two common ones are: + +- Race conditions, our infra aims to send out the webhook request in a timely manner, but for new sign ups their can be a sizeable delay, if a new user signs up and leaves a comment on your website, you may have out of sync issues where the comment will fail to be saved because they don't have a row in your `users` table yet. +- Extra hassle with testing & development, to properly test your webhook endpoints are working, you need to proxy the requests to your local machine, or deploy and test in a preview environment. And can have added complexity with multi developers collaborating on the same application. + +So as a counter measure to these issues, I advice you to use webhooks as your plan B, as a backup. + +For a more durable, performant application, I suggest you start by creating a `syncUserData()` function. + +```ts +import { getUser } from "~/lib/get-user" +import { db } from "~/db" +import { userDetails } from "~/db/schema" + +// Call this function whenever we are adding or updating a resource that is tied to a user, for example a user video upload, or a comment in a comment section, or a document getting shared. +export const syncUserData = async () => { + // Use our existing getUser() function to get the user data, whether that is clerk provided information, or custom metadata we are storing. + // For this example you would need to update the Session Token to include the users full name, primary email, and profile image. + const user = await getUser() + + if (user === null) { + throw new Error('User not authenticated') + } + + // Do a database upsert, essentially saying add the user details to the database if they don't exist, or update their details if they do. + db + .insert(userDetails) + .values({ + clerkUserId: user.userId, + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + imageUrl: user.imageUrl, + }) + .onConflictDoUpdate({ + target: userDetails.clerkUserId, + set: { + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + imageUrl: user.imageUrl, + }, + }) +} +``` + +Now with this function in hand, whenever a user does any kind of action, eg submitting a rental application on your next gen ai rental platform, just call this during it (or even in a [WaitUntil](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package#waituntil)) to ensure your database is in sync with Clerk. Removing any race condition issues. + +Once that is in place, feel free to setup webhooks as a backup. For example if a user goes in, changes a user data in a clerk component, then leaves without doing any action on your site, you'd still want that to sync through. ## Considerations for client side From cb7e1bffd10b855b876d8ea3216aecabe6ff573c Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 8 Jul 2025 09:25:06 -0700 Subject: [PATCH 11/16] wip --- docs/guides/create-a-custom-get-user.mdx | 65 +++++++++++++++--------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/docs/guides/create-a-custom-get-user.mdx b/docs/guides/create-a-custom-get-user.mdx index 15bd46785b..cf3913fa94 100644 --- a/docs/guides/create-a-custom-get-user.mdx +++ b/docs/guides/create-a-custom-get-user.mdx @@ -681,7 +681,7 @@ Only once we have the two data stores being kept in sync can we upgrade `getUser ### Use Clerk webhooks as your plan B -When building an application, it's not unusual that your users will interact with each other, maybe you want to share files between users (Dropdox style), or your users will comment on videos (Youtube style), or possibly users will play a game with each other (Geo-guesser style). It makes sense as such that you'll want to look up a resource in your database, and you will want to know more than just the `userId` of the user that created that resource. This can create an issue when the user details (full name, username, email, etc) are stored in Clerk, you can setup your application to call `await clerk.users.getUser(...)`, but this will be rate-limited and will slow down your page load times. +When building an application, it's not unusual that your users will interact with each other, maybe you want to share files between users (Dropdox style), or your users will comment on videos (Youtube style), or possibly users will play a game with each other (Geo-guesser style). It makes sense as such that you'll want to look up a resource in your database, and you will want to know more than just the `userId` of the user that created that resource. This can create an issue when the user details (full name, username, email, etc) are stored in Clerk, you can setup your application to call `await clerk.users.getUser(...)`, but this will be rate-limited and will slow down your page load times. Typically developers will turn to using Webhooks to solve this issue, creating a `users` table with the user details they want to store and have at the ready, they will get clerk to call there webhook endpoints on user details changes to store it for later querying. Unfortunately webhooks have some inherit problems that need to be accounted for, two common ones are: @@ -693,38 +693,33 @@ So as a counter measure to these issues, I advice you to use webhooks as your pl For a more durable, performant application, I suggest you start by creating a `syncUserData()` function. ```ts -import { getUser } from "~/lib/get-user" -import { db } from "~/db" -import { userDetails } from "~/db/schema" +import { getUser } from '~/lib/get-user' +import { db } from '~/db' +import { userDetails } from '~/db/schema' // Call this function whenever we are adding or updating a resource that is tied to a user, for example a user video upload, or a comment in a comment section, or a document getting shared. -export const syncUserData = async () => { - // Use our existing getUser() function to get the user data, whether that is clerk provided information, or custom metadata we are storing. +export const syncUserData = async (user: Awaited>) => { + // Use our existing getUser() user object to get the user data, whether that is clerk provided information, or custom metadata we are storing. // For this example you would need to update the Session Token to include the users full name, primary email, and profile image. - const user = await getUser() - - if (user === null) { - throw new Error('User not authenticated') - } // Do a database upsert, essentially saying add the user details to the database if they don't exist, or update their details if they do. - db + await db .insert(userDetails) .values({ - clerkUserId: user.userId, + clerkUserId: user.userId, + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + imageUrl: user.imageUrl, + }) + .onConflictDoUpdate({ + target: userDetails.clerkUserId, + set: { firstName: user.firstName, lastName: user.lastName, email: user.email, imageUrl: user.imageUrl, - }) - .onConflictDoUpdate({ - target: userDetails.clerkUserId, - set: { - firstName: user.firstName, - lastName: user.lastName, - email: user.email, - imageUrl: user.imageUrl, - }, + }, }) } ``` @@ -735,4 +730,28 @@ Once that is in place, feel free to setup webhooks as a backup. For example if a ## Considerations for client side -### Refreshing the client \ No newline at end of file +Everything in this guide we have touched on so far has been server side, using the new react server components and next.js server actions. This leaves out an important aspect of the web, the users browser. Fortunately we can fully replicate our `getUser()` server side call as a client side `useUser()` react hook. + +```ts +"use client"; + +import { useAuth } from "@clerk/nextjs"; + +export const useUser = () => { + const { isSignedIn, userId, sessionClaims } = useAuth(); + + if (!isSignedIn) return null; + + const { email, details } = sessionClaims; + + return { + ...details, + userId, + email, + }; +}; +``` + +The one main emission is not including an `update()` function, as this is client side we don't particularly trust the user. So to perform an update to the user details, create a server action, validate the request data (using something like zod), run any business logic checks to ensure the data is as expected. The use your `getUser().update()` function to update the users details. + +### Refreshing the users Session Token From d28b3d0ae062e96c0dda2f3798ce2b63f2302e9c Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 9 Jul 2025 07:13:09 -0700 Subject: [PATCH 12/16] wip --- docs/guides/create-a-custom-get-user.mdx | 54 +++++++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/docs/guides/create-a-custom-get-user.mdx b/docs/guides/create-a-custom-get-user.mdx index cf3913fa94..88b69919d6 100644 --- a/docs/guides/create-a-custom-get-user.mdx +++ b/docs/guides/create-a-custom-get-user.mdx @@ -711,6 +711,9 @@ export const syncUserData = async (user: Awaited>) => lastName: user.lastName, email: user.email, imageUrl: user.imageUrl, + + // We want to know when this data was last synced, so we don't accidentally overwrite newer details with older details if update requests come in out of order. + lastClerkSync: new Date() }) .onConflictDoUpdate({ target: userDetails.clerkUserId, @@ -719,6 +722,7 @@ export const syncUserData = async (user: Awaited>) => lastName: user.lastName, email: user.email, imageUrl: user.imageUrl, + lastClerkSync: new Date() }, }) } @@ -726,7 +730,7 @@ export const syncUserData = async (user: Awaited>) => Now with this function in hand, whenever a user does any kind of action, eg submitting a rental application on your next gen ai rental platform, just call this during it (or even in a [WaitUntil](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package#waituntil)) to ensure your database is in sync with Clerk. Removing any race condition issues. -Once that is in place, feel free to setup webhooks as a backup. For example if a user goes in, changes a user data in a clerk component, then leaves without doing any action on your site, you'd still want that to sync through. +Once that is in place, feel free to setup webhooks as a backup. For example if a user goes in, changes a user data in a clerk component, then leaves without doing any action on your site, you'd still want that to sync through. You should check the `lastClerkSync` against the time the webhook is received so you don't accidentally sync outdated webhook information over the latest user data. ## Considerations for client side @@ -740,8 +744,10 @@ import { useAuth } from "@clerk/nextjs"; export const useUser = () => { const { isSignedIn, userId, sessionClaims } = useAuth(); + // Like the `getUser()` function, feel free to customize how you want to handle the user not being signed in. If you are expecting this hook to only be used in authenticated components, you could throw an Error() here. if (!isSignedIn) return null; + // Extract out the session claims, when we set the custom token details in the types.d.ts above, those types will work here too, so we keep the type safety. const { email, details } = sessionClaims; return { @@ -752,6 +758,50 @@ export const useUser = () => { }; ``` -The one main emission is not including an `update()` function, as this is client side we don't particularly trust the user. So to perform an update to the user details, create a server action, validate the request data (using something like zod), run any business logic checks to ensure the data is as expected. The use your `getUser().update()` function to update the users details. +The one main emission is not including an `update()` function, as this is client side we don't particularly trust the user. So to perform an update to the custom user details, create a server action, validate the request data (using something like zod), run any business logic checks to ensure the data is as expected. The use your `getUser().update()` function to update the users details. ### Refreshing the users Session Token + +The beauty of using metadata combined with a customized session token is that no database query needs to happen to get the data, enabling your website to be very fast, but that information still needs to be stored somewhere. So we bundle it up in to what's know as a `Json Web Token`, cryptographically signing it using public/private keys, and give it to the user to hold on to. When the user sends a request to your website, it can validate the JWT, containing the information we need. But the user will only go and refresh this token once per minute, meaning any recent updates can take a considerable amount of time to pull through. + +To fix this issue, on the client we can force a refresh, getting the users browser to go to Clerk and ask for up to date information. With an up to date JWT requests to your application backend with be accurate. + +```ts +import { useUser } from "@clerk/nextjs" + +const { user } = useUser() + +user.reload() +``` + +```ts +import { useSession } from "@clerk/nextjs" + +const { session } = useSession() + +session.reload() +``` + +```ts +"use client"; + +import { useAuth, useSession } from "@clerk/nextjs"; + +export const useUser = () => { + const { isSignedIn, userId, sessionClaims } = useAuth(); + const { reload } = useSession() + + if (!isSignedIn) return null; + + const { email, details } = sessionClaims; + + return { + ...details, + userId, + email, + reload + }; +}; +``` + +If you are using a more tradition api, you could add to the `update()` function in our `getUser()` a line that attaches a header to the response say `x-refresh-token` and then client side you could check the response of all requests and run a `reload()` when the header is present. But in while using server actions we don't get such abilities. So we will need to be diligent and ensure that server actions that change clerk user data, custom or not, also `reload()` on the client side once resolved back to the user. \ No newline at end of file From 0e72b0115ac10f10fd8cebbc6e0540c5229de487 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 9 Jul 2025 07:23:57 -0700 Subject: [PATCH 13/16] wip --- docs/guides/create-a-custom-get-user.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guides/create-a-custom-get-user.mdx b/docs/guides/create-a-custom-get-user.mdx index 88b69919d6..3bddf0314f 100644 --- a/docs/guides/create-a-custom-get-user.mdx +++ b/docs/guides/create-a-custom-get-user.mdx @@ -1,5 +1,5 @@ --- -title: Create a custom getUser function +title: Create a custom `getUser()` function description: Build sdk: nextjs --- @@ -95,7 +95,7 @@ Looking at the return from our custom `getUser()` we will see // Here we have it, because we configured in the dashboard to include the // users email, it shows up here, no network request to clerks infra // needed, no rate limits. - "email": "your-email@example.me", + "email": "your-email@example.com", "exp": 1751214847, "fva": [137, -1], "iat": 1751214787, @@ -258,11 +258,11 @@ Depending on which return you picked above, we will get something along these li "favoritePizza": "Pepperoni", "favoriteSnack": "Oreos", "userId": "user_2u823dCrAIzoQfgjnsimCYJvJaI", - "email": "clerk-test@wylynko.me" + "email": "your-email@example.com" } ``` -Our beautiful `getUser()` function gives us both our standard Clerk info (emails, phone numbers, usernames, etc) and has our custom user data. +Our beautiful `getUser()` function can give us both our standard Clerk info (emails, phone numbers, usernames, etc) and has our custom user data. ## Writing the custom user data From a5869255c13b91a12b230bcf1d858cbe0692cacb Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 10 Jul 2025 15:38:37 -0400 Subject: [PATCH 14/16] Switch over to using `getToken()` to force user session refresh --- docs/guides/create-a-custom-get-user.mdx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/guides/create-a-custom-get-user.mdx b/docs/guides/create-a-custom-get-user.mdx index 3bddf0314f..dc2a92217c 100644 --- a/docs/guides/create-a-custom-get-user.mdx +++ b/docs/guides/create-a-custom-get-user.mdx @@ -782,14 +782,21 @@ const { session } = useSession() session.reload() ``` +```ts +import { useAuth } from "@clerk/nextjs" + +const { getToken } = useAuth() + +await getToken({ skipCache: true }) +``` + ```ts "use client"; -import { useAuth, useSession } from "@clerk/nextjs"; +import { useAuth } from "@clerk/nextjs"; export const useUser = () => { - const { isSignedIn, userId, sessionClaims } = useAuth(); - const { reload } = useSession() + const { isSignedIn, userId, sessionClaims, getToken } = useAuth(); if (!isSignedIn) return null; @@ -799,9 +806,9 @@ export const useUser = () => { ...details, userId, email, - reload + refresh: async () => { await getToken({ skipCache: true }) } }; }; ``` -If you are using a more tradition api, you could add to the `update()` function in our `getUser()` a line that attaches a header to the response say `x-refresh-token` and then client side you could check the response of all requests and run a `reload()` when the header is present. But in while using server actions we don't get such abilities. So we will need to be diligent and ensure that server actions that change clerk user data, custom or not, also `reload()` on the client side once resolved back to the user. \ No newline at end of file +If you are using a more tradition api, you could add to the `update()` function in our `getUser()` a line that attaches a header to the response say `x-refresh-token` and then client side you could check the response of all requests and run a `refresh()` when the header is present. But in while using server actions we don't get such abilities. So we will need to be diligent and ensure that server actions that change clerk user data, custom or not, also `refresh()` on the client side once resolved back to the user. \ No newline at end of file From f11585d3c58a542d96bbb49226d7ddf3643455fb Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 10 Jul 2025 15:40:44 -0400 Subject: [PATCH 15/16] Run linter --- docs/guides/create-a-custom-get-user.mdx | 44 +++++++++++++----------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/docs/guides/create-a-custom-get-user.mdx b/docs/guides/create-a-custom-get-user.mdx index dc2a92217c..d1cf5daf16 100644 --- a/docs/guides/create-a-custom-get-user.mdx +++ b/docs/guides/create-a-custom-get-user.mdx @@ -713,7 +713,7 @@ export const syncUserData = async (user: Awaited>) => imageUrl: user.imageUrl, // We want to know when this data was last synced, so we don't accidentally overwrite newer details with older details if update requests come in out of order. - lastClerkSync: new Date() + lastClerkSync: new Date(), }) .onConflictDoUpdate({ target: userDetails.clerkUserId, @@ -722,7 +722,7 @@ export const syncUserData = async (user: Awaited>) => lastName: user.lastName, email: user.email, imageUrl: user.imageUrl, - lastClerkSync: new Date() + lastClerkSync: new Date(), }, }) } @@ -737,25 +737,25 @@ Once that is in place, feel free to setup webhooks as a backup. For example if a Everything in this guide we have touched on so far has been server side, using the new react server components and next.js server actions. This leaves out an important aspect of the web, the users browser. Fortunately we can fully replicate our `getUser()` server side call as a client side `useUser()` react hook. ```ts -"use client"; +'use client' -import { useAuth } from "@clerk/nextjs"; +import { useAuth } from '@clerk/nextjs' export const useUser = () => { - const { isSignedIn, userId, sessionClaims } = useAuth(); + const { isSignedIn, userId, sessionClaims } = useAuth() // Like the `getUser()` function, feel free to customize how you want to handle the user not being signed in. If you are expecting this hook to only be used in authenticated components, you could throw an Error() here. - if (!isSignedIn) return null; + if (!isSignedIn) return null // Extract out the session claims, when we set the custom token details in the types.d.ts above, those types will work here too, so we keep the type safety. - const { email, details } = sessionClaims; + const { email, details } = sessionClaims return { ...details, userId, email, - }; -}; + } +} ``` The one main emission is not including an `update()` function, as this is client side we don't particularly trust the user. So to perform an update to the custom user details, create a server action, validate the request data (using something like zod), run any business logic checks to ensure the data is as expected. The use your `getUser().update()` function to update the users details. @@ -767,7 +767,7 @@ The beauty of using metadata combined with a customized session token is that no To fix this issue, on the client we can force a refresh, getting the users browser to go to Clerk and ask for up to date information. With an up to date JWT requests to your application backend with be accurate. ```ts -import { useUser } from "@clerk/nextjs" +import { useUser } from '@clerk/nextjs' const { user } = useUser() @@ -775,7 +775,7 @@ user.reload() ``` ```ts -import { useSession } from "@clerk/nextjs" +import { useSession } from '@clerk/nextjs' const { session } = useSession() @@ -783,7 +783,7 @@ session.reload() ``` ```ts -import { useAuth } from "@clerk/nextjs" +import { useAuth } from '@clerk/nextjs' const { getToken } = useAuth() @@ -791,24 +791,26 @@ await getToken({ skipCache: true }) ``` ```ts -"use client"; +'use client' -import { useAuth } from "@clerk/nextjs"; +import { useAuth } from '@clerk/nextjs' export const useUser = () => { - const { isSignedIn, userId, sessionClaims, getToken } = useAuth(); + const { isSignedIn, userId, sessionClaims, getToken } = useAuth() - if (!isSignedIn) return null; + if (!isSignedIn) return null - const { email, details } = sessionClaims; + const { email, details } = sessionClaims return { ...details, userId, email, - refresh: async () => { await getToken({ skipCache: true }) } - }; -}; + refresh: async () => { + await getToken({ skipCache: true }) + }, + } +} ``` -If you are using a more tradition api, you could add to the `update()` function in our `getUser()` a line that attaches a header to the response say `x-refresh-token` and then client side you could check the response of all requests and run a `refresh()` when the header is present. But in while using server actions we don't get such abilities. So we will need to be diligent and ensure that server actions that change clerk user data, custom or not, also `refresh()` on the client side once resolved back to the user. \ No newline at end of file +If you are using a more tradition api, you could add to the `update()` function in our `getUser()` a line that attaches a header to the response say `x-refresh-token` and then client side you could check the response of all requests and run a `refresh()` when the header is present. But in while using server actions we don't get such abilities. So we will need to be diligent and ensure that server actions that change clerk user data, custom or not, also `refresh()` on the client side once resolved back to the user. From d928315745c04e9b82587a789e8759a527087459 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 11 Jul 2025 09:34:24 -0400 Subject: [PATCH 16/16] wip --- docs/guides/create-a-custom-get-user.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/create-a-custom-get-user.mdx b/docs/guides/create-a-custom-get-user.mdx index d1cf5daf16..e99f1b07f1 100644 --- a/docs/guides/create-a-custom-get-user.mdx +++ b/docs/guides/create-a-custom-get-user.mdx @@ -13,7 +13,7 @@ Here we are going to take a different approach, building on top of Clerk's `auth To get started, we will simply call `auth()` and pull out what we need. ```ts -import { auth } from '@clerk/nextjs' +import { auth } from '@clerk/nextjs/server' export const getUser = async () => { // I highly recommend taking some time to understand all the methods @@ -68,7 +68,7 @@ So you may ask, well how the hell do I get my users data than? Well it's simply, 3. Back at our custom `getUser` function, lets pull something new out of the `auth()` return ```ts -import { auth } from '@clerk/nextjs' +import { auth } from '@clerk/nextjs/server' export const getUser = async () => { // Extract out `sessionClaims` from the `auth()` call for us to use