Skip to main content

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 the 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. Enforce the onboarding requirement at the resource
  4. 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 Pages Router as well. The examples have been pared down to the bare minimum to enable you to easily customize them to your needs.

Note

To see this guide in action, see 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. In the Clerk Dashboard, navigate to the Sessions page.

  2. Under Customize session token, in the Claims editor, add any claim you need to your session token. For this guide, add the following:

    {
      "metadata": "{{user.public_metadata}}"
    }
  3. 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:

types/globals.d.ts
export {}

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

Configure your Middleware to read session data

clerkMiddleware()Next.js Icon can read claims directly from the session and redirect your user accordingly.

The following example uses clerkMiddleware() to redirect authenticated users who haven't completed onboarding to the /onboarding route. This is a convenience redirect, not an access boundary.

Important

Enforce authentication and the onboardingComplete check at the resource, in the page, Route Handler, or Server Action that reads or mutates onboarding-gated data. See the Enforce onboarding at the resource section.

Important

If you're using Next.js ≤15, name your file middleware.ts instead of proxy.ts. The code itself remains the same; only the filename changes.

proxy.ts
import { clerkMiddleware } from '@clerk/nextjs/server'
import { NextRequest, NextResponse } from 'next/server'

export default clerkMiddleware(async (auth, req: NextRequest) => {
  const { isAuthenticated, sessionClaims } = await auth()

  // For users visiting /onboarding, don't try to redirect
  if (req.nextUrl.pathname === '/onboarding') {
    return NextResponse.next()
  }

  // If user doesn't have `onboardingComplete: true` in their publicMetadata,
  // redirect them to the /onboarding route to complete onboarding
  if (isAuthenticated && !sessionClaims?.metadata?.onboardingComplete) {
    return NextResponse.redirect(new URL('/onboarding', req.url))
  }
})

export const config = {
  matcher: [
    // Skip Next.js internals and all static files, unless found in search params
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    // Always run for API routes
    '/(api|trpc)(.*)',
    // Always run for Clerk-specific frontend API routes
    '/__clerk/(.*)',
  ],
}

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.

.env
NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/dashboard
NEXT_PUBLIC_CLERK_SIGN_UP_FORCE_REDIRECT_URL=/onboarding

Enforce onboarding at the resource

The middleware redirect above is a convenience. The enforcement lives at the resource: any page, Route Handler, or Server Action that depends on completed onboarding should check the onboardingComplete claim itself, so that removing the middleware redirect never exposes it.

app/dashboard/page.tsx
import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'

export default async function DashboardPage() {
  const { isAuthenticated, sessionClaims, redirectToSignIn } = await auth()

  // Protect the page from unauthenticated users
  if (!isAuthenticated) return redirectToSignIn()

  // Enforce the onboarding requirement at the resource
  if (!sessionClaims?.metadata?.onboardingComplete) {
    redirect('/onboarding')
  }

  return <p>Dashboard</p>
}

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. For more information, see the guide on 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

Create the /onboarding route with a form that collects the user's onboarding information. This example collects 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.

Quiz

Why is user.reload() called in this form?

app/onboarding/page.tsx
'use client'

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

export default function OnboardingForm() {
  const [error, setError] = React.useState('')
  const { isLoaded, isSignedIn, user } = useUser()
  const router = useRouter()

  // Handle loading state
  if (!isLoaded) return <h1>Loading...</h1>

  if (!isSignedIn) {
    // Add logic to handle unauthenticated users
    // This example renders a UI but you could also redirect to the sign-in page instead
    return <h1>You must be signed in to view this page</h1>
  }

  if (user?.publicMetadata?.onboardingComplete === true) {
    // Add logic to handle users who have already completed onboarding
    // This example renders a UI but you could also redirect to the homepage instead
    return <h1>You have already completed onboarding</h1>
  }

  const handleSubmit = async (formData: FormData) => {
    const res = await completeOnboarding(formData)
    if (res?.message) {
      // Forces a token refresh and refreshes the `User` object
      await user?.reload()
      router.push('/')
    }
    if (res?.error) {
      setError(res?.error)
    }
  }
  return (
    <div>
      <h1>Welcome</h1>
      <form action={handleSubmit}>
        <div>
          <label>Application Name</label>
          <p>Enter the name of your application.</p>
          <input type="text" name="applicationName" required />
        </div>

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

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.
app/onboarding/_actions.ts
'use server'

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

export const completeOnboarding = async (formData: FormData) => {
  const { isAuthenticated, userId } = await auth()

  if (!isAuthenticated) {
    return { message: 'No signed-in user' }
  }

  const client = await clerkClient()

  try {
    const res = await client.users.updateUserMetadata(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.

Feedback

What did you think of this content?

Last updated on