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:
- Add custom claims to your session token
- Configure your Middleware to read session data
- 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(opens in a new tab).
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:
- Navigate to the Clerk Dashboard(opens in a new tab).
- In the navigation sidebar, select Sessions.
- In the Customize session token section, select the Edit button.
- 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}}" }
- Select Save.
To get auto-complete and prevent TypeScript errors when working with custom session claims, you can define a global type.
- In your application's root folder, add a
types
directory. - Inside of the
types
directory, add aglobals.d.ts
file. - Create the
CustomJwtSessionClaims
interface and declare it globally. - Add the custom claims to the
CustomJwtSessionClaims
interface.
For this guide, your globals.d.ts
file should look like this:
types/globals.d.tsexport {}; declare global { interface CustomJwtSessionClaims { metadata: { onboardingComplete?: boolean; }; } }
Configure your Middleware to read session data
Clerk's authMiddleware()
(opens in a new tab) 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 authMiddleware()
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. This is so that any user visiting your application is forced to authenticate, and then forced to onboard. You can customize the publicRoutes
array to include any routes that should be accessible to all users, even unauthenticated ones.
src/middleware.tsimport { authMiddleware } from "@clerk/nextjs"; import { redirectToSignIn } from "@clerk/nextjs/server"; import { NextRequest, NextResponse } from "next/server"; export default authMiddleware({ // Define which routes are public publicRoutes: [], afterAuth: async (auth, req: NextRequest) => { const { userId, sessionClaims } = auth; // For users visiting /onboarding, don't try to redirect if (userId && req.nextUrl.pathname === "/onboarding") { return NextResponse.next(); } // If the user isn't signed in and the route is private, redirect to sign-in if (!userId && !auth.isPublicRoute) 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 && !auth.isPublicRoute) return NextResponse.next(); // If the route is public, anyone can view it. if (auth.isPublicRoute) return NextResponse.next(); }, }); 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.
- In your
/app
directory, create an/onboarding
folder. - In your
/onboarding
directory, create alayout.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.
src/app/onboarding/layout.tsximport { auth } from "@clerk/nextjs/server"; import { redirect } from "next/navigation"; export default function RootLayout({ children, }: { children: React.ReactNode; }) { if (auth().sessionClaims?.metadata.onboardingComplete === true) { redirect("/"); } return <>{children}</>; }
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.
- In your
/onboarding
directory, create apage.tsx
file. - 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(); 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.
- In your
/onboarding
directory, create an_actions.ts
file. - 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 theclerkClient
wrapper to interact with the Backend API and update the user'spublicMetadata
.
"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.
Last updated on April 9, 2024