Features
PreviousNext

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.

How To Use

Core files

onboarding.config.tsx
page.tsx
page.client.tsx

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, extend kit/auth/src/router/update-user.ts (updateUserSchema + updateUserAction).
  • If the field belongs to another table or feature, persist it in the on_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

FilterUse
get_onboarding_schemaAdd new fields to the global onboarding schema
get_onboarding_steps_configInject additional steps into the stepper
get_onboarding_extra_inputsRegister custom QuickForm input components
on_onboarding_submitPersist non-user onboarding data and customize redirect URL
render_onboarding_pathRender 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.

FilterParametersReturnRegistered By (package file)Initialized In (app entrypoint)Environment
get_onboarding_schema{}QuickFormSchemaMapkit/organization/src/shared/filters/use-filters/use-onboarding-filters.tsxapps/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.tsxapps/dashboard/hooks/use-filters.ts (useOrgFilters)client
get_onboarding_extra_inputs{}SettingsInputsBasekit/organization/src/shared/filters/use-filters/use-onboarding-filters.tsxapps/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.tsxapps/dashboard/hooks/use-filters.ts (useOrgFilters)client
on_onboarding_submit{ data: Record<string, unknown>; clientTrpc: TrpcClientWithQuery<Router<unknown>>; queryClient: QueryClient }stringkit/organization/src/shared/filters/use-filters/use-onboarding-filters.tsxapps/dashboard/hooks/use-filters.ts (useOrgFilters)client
server_redirect_onboarding{}string | nullkit/organization/src/www/filters/server-filters.tsapps/dashboard/lib/init-server-filters.ts (initOrgServerFilters)server

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

  1. Add new fields to onboardingSchema.
  2. Place fields in onboardingStepsConfig (or inject them through get_onboarding_steps_config).
  3. Ensure default values exist in onboarding client pages.
  4. Persist fields through updateUser (user table) or on_onboarding_submit (feature-specific tables).
  5. Keep completedOnboarding gating in server onboarding page.

Troubleshooting

  • If onboarding never appears, verify completedOnboarding checks and onboarding redirects.
  • If a custom field renders but is not saved, verify whether it should be persisted in updateUser or on_onboarding_submit.
  • If /onboarding/{path} returns 404, verify render_onboarding_path returns config for that exact path.
  • If an async step validation never runs, verify it is declared in the step canGoNext function.

How is this guide?

Last updated on 3/27/2026