Onboarding
Easily add onboarding to your authentication flow.
The Onboarding plugin allows you to create multi-step onboarding flows for new users. It automatically tracks completion status, enforces step requirements, and integrates seamlessly with your authentication flow.
Features
- Multi-step onboarding flows with custom validation
- Automatic completion tracking per user
- Required step enforcement before marking onboarding complete
- One-time step protection to prevent duplicate completions
- Built-in presets for common onboarding scenarios
- Client-side integration with automatic redirects
Installation
Install the plugin
npm install @better-auth-extended/onboarding
Add the plugin to your auth config
To use the Onboarding plugin, add it to your auth config.
import { betterAuth } from "better-auth";
import {
onboarding,
createOnboardingStep,
} from "@better-auth-extended/onboarding";
import { z } from "zod";
export const auth = betterAuth({
// ... other config options
plugins: [
onboarding({
steps: {
legalConsent: createOnboardingStep({
input: z.object({
tosAccepted: z.boolean(),
privacyPolicyAccepted: z.boolean(),
marketingConsent: z.boolean().optional().default(false),
}),
async handler(ctx) {
const { tosAccepted, privacyPolicyAccepted, marketingConsent } =
ctx.body;
if (!tosAccepted || !privacyPolicyAccepted) {
// Don't mark step as completed
throw ctx.error("UNAVAILABLE_FOR_LEGAL_REASONS");
}
},
required: true,
once: true,
}),
profile: createOnboardingStep({
input: z.object({
bio: z.string().optional(),
gender: z.enum(["m", "f", "d"]).optional(),
dateOfBirth: z.date().optional(),
}),
async handler(ctx) {
// Create or update user profile
const profile = await createProfile(ctx.body);
return profile;
},
}),
},
completionStep: "profile",
}),
],
});
Add the client plugin
Include the client plugin in your auth client instance.
import { createAuthClient } from "better-auth/client";
import { onboardingClient } from "@better-auth-extended/onboarding/client";
import type { auth } from "./your/path"; // Import as type
const authClient = createAuthClient({
plugins: [
onboardingClient<typeof auth>({
onOnboardingRedirect: () => {
window.location.href = "/onboarding";
},
}),
],
});
Run migrations
This plugin adds additional fields to the user table. Click here to see the schema
npx @better-auth/cli migrate
or generate
npx @better-auth/cli generate
Usage
The Onboarding plugin provides several endpoints to manage the onboarding flow. Users can check if they need onboarding, complete steps, and verify their progress.
Check if user needs onboarding
Use the shouldOnboard
function to check if a user needs to complete onboarding.
const { data, error } = await authClient.onboarding.shouldOnboard();
Complete onboarding step
Use the onboardingStep
function to complete a specific onboarding step. The step name is derived from your step configuration
const { data, error } = await authClient.onboarding.step.profile({ bio, gender, dateOfBirth,});
Prop | Description | Type |
---|---|---|
bio? | Example property | string |
gender? | Example property | "m" | "f" | "d" |
dateOfBirth? | Example property | Date |
Check step access
Use the canAccessOnboardingStep
function to check if a user can access a specific step. This is useful for preventing access to steps that shouldn't be available.
const data = await auth.api.canAccessOnboardingStepLegalConsent();
Skip Onboarding Step
For optional completion steps, users can skip them if they're not required. This is useful when the completion step is optional but you still want to mark onboarding as complete.
This is only available for optional completion steps
const { data, error } = await authClient.onboarding.skipStep.myCompletionStep();
Handle onboarding redirects
The plugin automatically handles onboarding redirects when users sign up or get their session. Configure the redirect behavior in the client plugin.
onboardingClient<typeof auth>({
onOnboardingRedirect: () => {
// Custom redirect logic
window.location.href = "/onboarding";
},
});
Defining steps
Steps are defined using the createOnboardingStep
function. Each step requires a handler function and can include input validation and completion rules.
import { createOnboardingStep } from "@better-auth-extended/onboarding";
const step = createOnboardingStep({
async handler(ctx) {
// process step
},
// ... other configuration
});
Options
- input:
ZodType
- Zod schema for request body validation. - handler:
<R>(ctx: GenericEndpointContext) => R | Promise<R>
- Function that processes the step. - once:
boolean
- Iftrue
, step can only be completed once. (default:true
) - required:
boolean
- Iftrue
, step must be completed before onboarding is done. (default:false
) - requireHeaders:
boolean
- Iftrue
, headers are required in context. - requireRequest:
boolean
- Iftrue
, request object is required. - cloneRequest:
boolean
- Clone the request object from router.
Example
import { createOnboardingStep } from "@better-auth-extended/onboarding";
import { z } from "zod";
const preferencesStep = createOnboardingStep({
input: z.object({
theme: z.enum(["light", "dark", "system"]).optional().default("system"),
notifications: z.boolean().optional().default(false),
}),
async handler(ctx) {
const { theme, notifications } = ctx.body;
const preferences = await ctx.context.adapter.update({
model: "preferences",
where: [
{
field: "userId",
value: ctx.context.session.id,
},
],
update: {
theme,
notifications,
},
});
return preferences;
},
once: false,
});
Presets
Setup New Password
Prompt the user to enter and confirm a new password.
This this particularly useful to applications where the user was given a temporary password.
import { setupNewPasswordStep } from "@better-auth-extended/onboarding/presets";
onboarding({
steps: {
newPassword: setupNewPasswordStep({
required: true,
passwordSchema: {
minLength: 12,
maxLength: 128,
},
}),
},
completionStep: "newPassword",
});
Setup 2FA
Allows the user to enable two-factor authentication.
import { setup2FAStep } from "@better-auth-extended/onboarding/presets";
onboarding({
steps: {
twoFactor: setup2FAStep({
required: true,
}),
},
completionStep: "twoFactor",
});
Schema
The plugin adds additional fields to the user
table.
Table Name: user
Field Name | Type | Key | Description |
---|---|---|---|
shouldOnboard | boolean | Whether the user needs to complete onboarding | |
completedSteps | string | JSON string array of completed step IDs |
Options
steps: Record<string, OnboardingStep>
- Object mapping step IDs to step configurations. Each step defines the input validation, handler function, and completion rules.
completionStep: keyof Steps
- the step ID that marks onboarding as complete. Once this step is completed, the user's shouldOnboard
field is set to false
.
autoEnableOnSignUp: boolean
| (ctx: GenericEndpointContext) => Promise<boolean> | boolean
- Whether to automatically enable onboarding for new users during sign up. (default: true
)
secondaryStorage: boolean
- Whether to use secondary storage instead of the database. (default: false
)
schema: Custom schema configuration for renaming fields or adding additional configuration.
Best Practises
1. Progressive Disclosure
Break down onboarding into logical, digestible steps:
const steps = {
welcome: createOnboardingStep({
/* welcome step */
}),
profile: createOnboardingStep({
/* basic profile */
}),
preferences: createOnboardingStep({
/* user preferences */
}),
verification: createOnboardingStep({
/* email/phone verification */
}),
};
2. Input Validation
Always validate user input with Zod schemas:
createOnboardingStep({
input: z
.object({
email: z.email("Invalid email address"),
phone: z.regex(/^\+?[\d\s-()]+$/, "Invalid phone number"),
})
.refine((data) => data.email || data.phone, {
message: "Either email or phone is required",
path: ["email"],
}),
async handler(ctx) {
// ...
},
});
3. Required vs Optional Steps
Use the required
flag to distinguish between essential and optional steps:
const steps = {
terms: createOnboardingStep({ required: true }), // Must complete
profile: createOnboardingStep({ required: false }), // Optional
preferences: createOnboardingStep(), // Optional
};
4. One-Time vs Repeatable Steps
Use the once
flag to distinguish between one-time and repeatable steps:
const steps = {
terms: createOnboardingStep(), // Submit only once
profile: createOnboardingStep({ once: true }), // Submit only once
preferences: createOnboardingStep({ once: false }), // Allow multiple submits
};
5. Secondary Storage
If applicable, use secondary storage instead of the main database.
onboarding({
secondaryStorage: true,
});
How is this guide?