Jan 30, 2024
Roy Anger
Leverage Clerk’s customizable session tokens, publicMetadata and Next’s Middleware to create a robust onboarding experience within a few lines of code.
As part of your onboarding flow, you may want to collect extra information from your user and use it to drive your application state. Let’s walk through a quick example using Next.js and TypeScript to show you how simple implementing an onboarding flow can be.
In this guide, you will learn how to:
To see a working example, check out our sample demonstration app here
The examples below have been pared down to the bare minimum to enable you to easily customize them to your needs, you can build them with the Clerk + Next Quickstart
Let’s get started!
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. [Learn More
First, navigate to Sessions in your Clerk Dashboard and click the ‘Edit’ button. In the modal that opens, there will be a window where you can augment your session token with custom claims.
In there, add the following and hit save:
1{2"metadata": "{{user.public_metadata}}"3}
If you haven’t already, we can make the public metadata type information accessible to our application by adding the following to src/types/globals.d.ts
:
1export { };23declare global {4interface CustomJwtSessionClaims {5metadata: {6onboardingComplete?: boolean;7};8}9}
We have just added custom data to our session token in the Clerk Dashboard and made those claims accessible to our app. Next, we’ll use clerkMiddleware
to redirect the user based on onboardingComplete
status.
Clerk's authMiddleware()
allows you ton configure access to your routes with fine grained control. You can also retrieve claims directly from the session and redirect your user accordingly. [Learn More]
Add the code sample below to your src/middleware.ts
file:
1import { authMiddleware } from "@clerk/nextjs";2import { redirectToSignIn } from "@clerk/nextjs/server";3import { NextRequest, NextResponse } from "next/server";45// This example protects all routes including api/trpc routes6// Please edit this to allow other routes to be public as needed.7// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware8export default authMiddleware({9publicRoutes: ["/"],10afterAuth: async (auth, req: NextRequest) => {11const { userId, sessionClaims } = auth1213// For user visiting /onboarding, don't try and redirect14if (userId && req.nextUrl.pathname === "/onboarding") {15return NextResponse.next();16}1718// User isn't signed in and the route is private -- redirect to sign-in19if (!userId && !auth.isPublicRoute) return redirectToSignIn({ returnBackUrl: req.url })2021// Catch users who doesn't have `onboardingComplete: true` in PublicMetata22// Redirect them to the /onboading out to complete onboarding23if (userId && !sessionClaims?.metadata?.onboardingComplete) {24const onboardingUrl = new URL("/onboarding", req.url);25return NextResponse.redirect(onboardingUrl)26}2728// User is logged in and the route is protected - let them view.29if (userId && !auth.isPublicRoute) return NextResponse.next()3031// If the route is public, anyone can view it.32if (auth.isPublicRoute) return NextResponse.next()3334}35});3637export const config = {38matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],39};
Next, create a layout.tsx
file in src/app/onboarding
and add the following code to the file. This logic could go in the Middleware, but by adding to the layout.tsx
to the route the logic remains in one place. This file can also be expanded to handle multiple steps, if multiple steps are required for an onboarding flow.
1import { auth } from "@clerk/nextjs/server"2import { redirect } from "next/navigation"34export default function RootLayout({5children,6}: {7children: React.ReactNode8}) {910// Check if a user has completed onboarding11// If yes, redirect them to /dashboard12if (auth().sessionClaims?.metadata.onboardingComplete === true) {13redirect("/dashboard")14}1516return <>{children}</>17}
Now that we have the logic for where to direct the user, we’ll need a way to track their onboarding status and note it on their session, let’s dig into that now!
Updating a user's publicMetadata
as they progress through the flow will allow us to recognize when they have successfully completed their onboarding and, per the logic above, are now able to access the application. [Learn More]
To do this you need:
publicMetadata
First, add a method in your backend, that will be called on form submission and update the user’s publicMetadata
accordingly. The example below uses the clerkClient
wrapper to interact with the Backend API.
Under src/app/onboarding/_actions.ts
add the following code snippet:
1"use server";23import { auth, clerkClient } from "@clerk/nextjs/server";45export const completeOnboarding = async (formData: FormData) => {6const { userId } = auth();78if (!userId) {9return { message: "No Logged In User" };10}1112try {13const res = await clerkClient.users.updateUser(userId, {14publicMetadata: {15onboardingComplete: true,16applicationName: formData.get("applicationName"),17applicationType: formData.get("applicationType"),18},19});20return { message: res.publicMetadata };21} catch (err) {22return { "error": 'There was an error updating the user metadata.' };23}24};
Now that we have a method to securely update our user’s publicMetadata
we can call this server action from a client side form.
With the backend updateUser method in place, we’ll add a basic page that contains a form to complete the onboarding process.
This example form that will capture an application name (applicationName) and application type of either B2C or B2B (applicationType). 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.
To implement this logic, insert the following into your src/app/onboarding/page.tsx
:
1"use client";23import * as React from "react";4import { useUser } from "@clerk/nextjs";5import { useRouter } from "next/navigation";6import { completeOnboarding } from "./_actions";78export default function OnboardingComponent() {9const [error, setError] = React.useState("");10const { user } = useUser();11const router = useRouter();1213const handleSubmit = async (formData: FormData) => {14const res = await completeOnboarding(formData);15if (res?.message) {16await user?.reload();17router.push("/dashboard");18}19if (res?.error) {20setError(res?.error);21}22};23return (24<div>25<h1>Welcome</h1>26<form action={handleSubmit}>27<div>28<label>Application Name</label>29<p>Enter the name of your application.</p>30<input type="text" name="applicationName" required />31</div>3233<div>34<label>Application Type</label>35<p>Describe the type of your application.</p>36<input type="text" name="applicationType" required />37</div>38{error && <p className="text-red-600">Error: {error}</p>}39<button type="submit">Submit</button>40</form>41</div>42);43}
Your onboarding flow is now complete! 🎉 New users who haven’t yet onboarded will now land on your /onboarding
page and, once they have completed onboarding, will be sent through to the dashboard. By using Clerk, which already handles user authentication, we were able to simplify the process of creating a custom user onboarding flow as well.
Start completely free for up to 10,000 monthly active users and up to 100 monthly active orgs. No credit card required.
Learn more about our transparent per-user costs to estimate how much your company could save by implementing Clerk.
The latest news and updates from Clerk, sent to your inbox.