Onboarding
Customize the web onboarding flow with typed schema, step configuration, and filters.
What It Does
Onboarding is driven by a typed QuickForm schema and step config, then extended through filters for app-specific behavior (organization creation, custom routes, extra inputs, and submit side effects).
When To Use
- You want to collect additional user or organization data right after sign-up.
- You need app-specific onboarding steps without forking core onboarding pages.
Prerequisites
- Onboarding schema and step config in
config/onboarding.config.tsx. - Dashboard onboarding routes mounted.
- Filter initialization running in app providers/hooks.
This page describes the standard kit integration path; adapt app-specific paths and config names when your project differs.
How To Use
Core files
Add new fields to onboarding
Extend the onboarding schema.
Add your new field in apps/dashboard/config/onboarding.config.tsx:
export const onboardingSchema = {
...onboardingUserSchema,
userRole: z.enum(['designer', 'programmer', 'product_manager', 'tester', 'marketer']),
userJobTitle: z.string().min(2, 'Job title is required.'),
};Add the field to a step config.
export const onboardingStepsConfig: QuickFormStepConfig<OnboardingSchema>[] = [
{
type: 'step',
label: 'User',
settings: [
{ type: 'text', slug: 'userName', label: 'Name' },
{ type: 'text', slug: 'userJobTitle', label: 'Job title' },
],
},
];Provide default values for the new field.
Update defaultValues in apps/dashboard/app/onboarding/page.client.tsx:
defaultValues={{
userName: user?.name ?? 'Unknown',
userJobTitle: '',
}}Persist the new field in the correct submit path.
- If the field belongs to
public.user, extendkit/auth/src/router/update-user.ts(updateUserSchema+updateUserAction). - If the field belongs to another table or feature, persist it in the
on_onboarding_submitfilter.
clientTrpc.updateUser.fetch(data) only persists fields defined in kit/auth/src/router/update-user.ts. Extra onboarding fields must be saved through your own onboarding submit filter.
Add a new onboarding step
Declare step fields in schema.
Every step field must exist in the onboarding schema (or be injected by get_onboarding_schema).
Append a typed step in onboardingStepsConfig.
Use QuickFormStepConfig<OnboardingSchema>[] so slug values stay type-safe:
{
type: 'step',
label: 'Team',
settings: [
{
type: 'question_select',
slug: 'teamSize',
question: 'How large is your team?',
answers: [
{ value: '1', label: 'Just me' },
{ value: '2-10', label: '2-10' },
],
},
],
}Add async validation with canGoNext when needed.
For example, validate unique values (slug, subdomain, etc.) before moving to the next step.
Extend onboarding through filters
| Filter | Use |
|---|---|
get_onboarding_schema | Add new fields to the global onboarding schema |
get_onboarding_steps_config | Inject additional steps into the stepper |
get_onboarding_extra_inputs | Register custom QuickForm input components |
on_onboarding_submit | Persist non-user onboarding data and customize redirect URL |
render_onboarding_path | Render dedicated forms for /onboarding/{path} routes |
In apps/dashboard/hooks/use-filters.ts, call package onboarding filters (for example useOrgFilters) so these hooks are enqueued.
Add a dedicated onboarding path (/onboarding/{path})
Use render_onboarding_path to return { config, inputs, onSubmit } for each path:
const renderCustomPath: FilterCallback<'render_onboarding_path'> = (value, options) => {
switch (options.onboardingPath) {
case 'organization':
return {
config,
inputs,
onSubmit: handleSubmit,
};
default:
return null;
}
};If your filter returns null, app/onboarding/[onboardingPath]/page.client.tsx calls notFound(), so the route becomes a 404.
Filter API
Onboarding extensibility is driven by a filter pipeline shared across auth and organization packages.
| Filter | Parameters | Return | Registered By (package file) | Initialized In (app entrypoint) | Environment |
|---|---|---|---|---|---|
get_onboarding_schema | {} | QuickFormSchemaMap | kit/organization/src/shared/filters/use-filters/use-onboarding-filters.tsx | apps/dashboard/hooks/use-filters.ts (useOrgFilters) | client |
get_onboarding_steps_config | { clientTrpc: TrpcClientWithQuery<Router<unknown>> } | QuickFormStepConfig<QuickFormSchemaMap>[] | kit/organization/src/shared/filters/use-filters/use-onboarding-filters.tsx | apps/dashboard/hooks/use-filters.ts (useOrgFilters) | client |
get_onboarding_extra_inputs | {} | SettingsInputsBase | kit/organization/src/shared/filters/use-filters/use-onboarding-filters.tsx | apps/dashboard/hooks/use-filters.ts (useOrgFilters) | client |
render_onboarding_path | { onboardingPath: string; clientTrpc: TrpcClientWithQuery<Router<unknown>>; queryClient: QueryClient; defaultSchema: QuickFormSchemaMap; defaultSteps: QuickFormStepConfig<QuickFormSchemaMap>[] } | null | { config: QuickFormConfig<QuickFormSchemaMap>; inputs: SettingsInputsBase; onSubmit: (data: unknown) => Promise<void> } | kit/organization/src/shared/filters/use-filters/use-onboarding-filters.tsx | apps/dashboard/hooks/use-filters.ts (useOrgFilters) | client |
on_onboarding_submit | { data: Record<string, unknown>; clientTrpc: TrpcClientWithQuery<Router<unknown>>; queryClient: QueryClient } | string | kit/organization/src/shared/filters/use-filters/use-onboarding-filters.tsx | apps/dashboard/hooks/use-filters.ts (useOrgFilters) | client |
server_redirect_onboarding | {} | string | null | kit/organization/src/www/filters/server-filters.ts | apps/dashboard/lib/init-server-filters.ts (initOrgServerFilters) | server |
- Keep
useOrgFilters({ orgConfig })inapps/dashboard/hooks/use-filters.ts. - Keep
initOrgServerFilters({ orgConfig })inapps/dashboard/lib/init-server-filters.ts.
MCP Context
capability: onboarding_flow entrypoints: - apps/dashboard/config/onboarding.config.tsx - apps/dashboard/app/onboarding/page.tsx - apps/dashboard/app/onboarding/page.client.tsx - apps/dashboard/app/onboarding/[onboardingPath]/page.client.tsx - apps/dashboard/hooks/use-filters.ts - apps/dashboard/lib/init-server-filters.ts - kit/organization/src/shared/filters/use-filters/use-onboarding-filters.tsx - kit/organization/src/www/filters/server-filters.ts - kit/auth/src/router/update-user.ts inputs: - onboarding_form_values outputs: - updated_user_profile - optional organization/bootstrap data constraints: - flow should run before user has completedOnboarding side_effects: - updates user data and redirects - may create organization and refresh org membership queries
Agent Recipe
- Add new fields to
onboardingSchema. - Place fields in
onboardingStepsConfig(or inject them throughget_onboarding_steps_config). - Ensure default values exist in onboarding client pages.
- Persist fields through
updateUser(user table) oron_onboarding_submit(feature-specific tables). - Keep
completedOnboardinggating in server onboarding page.
Troubleshooting
- If onboarding never appears, verify
completedOnboardingchecks and onboarding redirects. - If a custom field renders but is not saved, verify whether it should be persisted in
updateUseroron_onboarding_submit. - If
/onboarding/{path}returns 404, verifyrender_onboarding_pathreturns config for that exact path. - If an async step validation never runs, verify it is declared in the step
canGoNextfunction.
Related
Render typed React email templates from @kit/email-templates.
Subscriptions, checkout, invoices, and wallet flows with @kit/billing.
How is this guide?
Last updated on 3/27/2026