Add custom onboarding to your authentication flow

Onboarding is a crucial part of many authentication flows. Sometimes you need to make sure certain criteria is met and collected before allowing access to parts of your application. With Clerk, you can leverage customizable session tokens, public metadata, and Middleware to create a custom onboarding experience.

This guide demonstrates how to create a custom onboarding flow that requires users to complete a form before they can access the application. After a user authenticates using Clerk's Account portal, the user is prompted to fill out a form with an application name and type. Once the user has completed the form, they are redirected to the application's homepage.

In this guide, you will learn how to:

  1. Add custom claims to your session token
  2. Configure your Middleware to read session data
  3. Update the user’s onboarding state

For the sake of this guide, examples are written for Next.js App Router, but can be used with Next.js Pager Router as well. The examples have been pared down to the bare minimum to enable you to easily customize them to your needs.


To see this guide in action, check out the repository.

Add custom claims to your session token

Session tokens are JWTs that are generated by Clerk on behalf of your instance, and contain claims that allow you to store data about a user's session. With Clerk, when a session token exists for a user, it indicates that the user is authenticated, and the associated claims can be retrieved at any time.

For this guide, you will use an onboardingComplete property in the user's public metadata to track their onboarding status. But first, you need to add a custom claim to the session token that will allow you to access the user's public metadata in your Middleware.

To edit the session token:

  1. Navigate to the Clerk Dashboard.
  2. In the navigation sidebar, select Sessions.
  3. In the Customize session token section, select the Edit button.
  4. In the modal that opens, you can add any claim to your session token that you need. For this guide, add the following:
  "metadata": "{{user.public_metadata}}"
  1. Select Save.

To get auto-complete and prevent TypeScript errors when working with custom session claims, you can define a global type.

  1. In your application's root folder, add a types directory.
  2. Inside of the types directory, add a globals.d.ts file.
  3. Create the CustomJwtSessionClaims interface and declare it globally.
  4. Add the custom claims to the CustomJwtSessionClaims interface.

For this guide, your globals.d.ts file should look like this:

export {};

declare global {
  interface CustomJwtSessionClaims {
    metadata: {
      onboardingComplete?: boolean;

Configure your Middleware to read session data

Clerk's clerkMiddleware() allows you to configure access to your routes with fine grained control. It also allows you to retrieve claims directly from the session and redirect your user accordingly.

The following example demonstrates how to use Clerk's clerkMiddleware() to redirect users based on their onboarding status. If the user is signed in and has not completed onboarding, they will be redirected to the onboarding page.

Note that the following example protects all routes except one. This is so that any user visiting your application is forced to authenticate, and then forced to onboard. You can customize the array in the createRouteMatcher() function assigned to isPublicRoute to include any routes that should be accessible to all users, even unauthenticated ones.

import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
import { NextRequest, NextResponse } from "next/server";

const isOnboardingRoute = createRouteMatcher(["/onboarding"])
const isPublicRoute = createRouteMatcher(["/public-route-example"])

export default clerkMiddleware((auth, req: NextRequest) => {
    const { userId, sessionClaims, redirectToSignIn } = auth();

    // For users visiting /onboarding, don't try to redirect
    if (userId && isOnboardingRoute(req)) {

    // If the user isn't signed in and the route is private, redirect to sign-in
    if (!userId && !isPublicRoute(req))
      return redirectToSignIn({ returnBackUrl: req.url });

    // Catch users who do not have `onboardingComplete: true` in their publicMetadata
    // Redirect them to the /onboading route to complete onboarding
    if (userId && !sessionClaims?.metadata?.onboardingComplete) {
      const onboardingUrl = new URL("/onboarding", req.url);
      return NextResponse.redirect(onboardingUrl);

    // If the user is logged in and the route is protected, let them view.
    if (userId && !isPublicRoute(req)) return;

export const config = {
  matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],

Create a layout for the /onboarding route

You will need a layout for the /onboarding route that will redirect users to the homepage if they have already completed onboarding.

  1. In your /app directory, create an /onboarding folder.
  2. In your /onboarding directory, create a layout.tsx file and add the following code to the file. This file could also be expanded to handle multiple steps, if multiple steps are required for an onboarding flow.
import { auth } from "@clerk/nextjs/server";
import { redirect } from "next/navigation";

export default function RootLayout({
}: {
  children: React.ReactNode;
}) {
  if (auth().sessionClaims?.metadata.onboardingComplete === true) {

  return <>{children}</>;

Add fallback and force redirect URLs

To ensure a smooth onboarding flow, add redirect URL's to your environment variables. The fallback redirect URL is used when there is no redirect_url in the path. The force redirect URL will always be used after a successful sign up.


Use publicMetadata to track user onboarding state

Each Clerk user has a User object that contains a publicMetadata property, which can be used to store custom data about the user. This information can be accessed on the client side and can be used to drive application state. Learn more about public metadata.

You can use the user's publicMetadata to track the user's onboarding state. To do this, you will create:

  • A process in your frontend with logic to collect and submit all the information for onboarding. In this guide, you will create a simple form.
  • A method in your backend to securely update the user's publicMetadata

Collect user onboarding information

To collect the user's onboarding information, create a form that will be displayed on the /onboarding page. This form will collect the user's application name and application type. This is a very loose example — you can use this step to capture information from the user, sync user data to your database, have the user sign up to a course or subscription, or more.

  1. In your /onboarding directory, create a page.tsx file.
  2. Add the following code to the file.
"use client";

import * as React from "react";
import { useUser } from "@clerk/nextjs";
import { useRouter } from "next/navigation";
import { completeOnboarding } from "./_actions";

export default function OnboardingComponent() {
  const [error, setError] = React.useState("");
  const { user } = useUser();
  const router = useRouter();

  const handleSubmit = async (formData: FormData) => {
    const res = await completeOnboarding(formData);
    if (res?.message) {
      await user?.reload();
    if (res?.error) {
  return (
      <form action={handleSubmit}>
          <label>Application Name</label>
          <p>Enter the name of your application.</p>
          <input type="text" name="applicationName" required />

          <label>Application Type</label>
          <p>Describe the type of your application.</p>
          <input type="text" name="applicationType" required />
        {error && <p className="text-red-600">Error: {error}</p>}
        <button type="submit">Submit</button>

Update the user's publicMetadata in your backend

Now that there is a form to collect the user's onboarding information, you need to create a method in your backend to update the user's publicMetadata with this information. This method will be called when the user submits the form.

  1. In your /onboarding directory, create an _actions.ts file.
  2. Add the following code to the file. This file includes a method that will be called on form submission and will update the user's publicMetadata accordingly. The following example uses the clerkClient wrapper to interact with the Backend API and update the user's publicMetadata.
"use server";

import { auth, clerkClient } from "@clerk/nextjs/server";

export const completeOnboarding = async (formData: FormData) => {
  const { userId } = auth();

  if (!userId) {
    return { message: "No Logged In User" };

  try {
    const res = await clerkClient.users.updateUser(userId, {
      publicMetadata: {
        onboardingComplete: true,
        applicationName: formData.get("applicationName"),
        applicationType: formData.get("applicationType"),
    return { message: res.publicMetadata };
  } catch (err) {
    return { error: "There was an error updating the user metadata." };

Wrap up

Your onboarding flow is now complete! 🎉 Users who have not onboarded yet will now land on your /onboarding page. New users signing up or signing in to your application will have to complete the onboarding process before they can access your application. By using Clerk, you have streamlined the user authentication and onboarding process, ensuring a smooth and efficient experience for your new users.


What did you think of this content?