# How to Add a Custom Onboarding Flow to Your App — Copy-Paste Prompts Included

- Tool: App Features
- Last updated: July 2026

## TL;DR

A custom onboarding flow needs four pieces: a multi-step form (react-hook-form + Zod), a progress indicator, a Supabase table to store answers, and a completion gate that skips returning users. With Lovable or V0 you can ship a working 5-step onboarding in 4–8 hours. Running cost is $0–25/mo depending on user volume. The hardest part — translating onboarding answers into a personalised dashboard — always needs at least one follow-up prompt.

## What a Custom Onboarding Flow Actually Is

A custom onboarding flow is a multi-step sequence that runs once, immediately after a new user signs up, and uses their answers to personalise the app before they reach the dashboard. A wellness app asks about fitness goals. A project management tool asks about team size and workflow style. An e-commerce app asks about product preferences. The flow must do three things correctly: gate behind authentication, never show twice to returning users, and write its answers into a data model that actually drives the personalised experience — not just store them and forget them. The UI is straightforward; the completion gate and the personalisation writer are where builds go wrong.

## Anatomy of the Feature

Eight components that span form state management, database persistence, server-side gating, and personalisation output. AI tools generate the form UI reliably; the completion gate and personalisation writer need explicit prompting.

- **Step controller** (ui): A React state machine that manages which step is currently active, whether forward navigation is gated by validation, and back navigation history. Implemented with useState (step index) plus Framer Motion AnimatePresence for slide transitions between steps. XState is an option for flows with complex conditional branching, but a simple index is sufficient for linear flows.
- **Multi-step form** (ui): react-hook-form with a Zod validation schema per step. FormProvider wraps the entire flow — never unmount it between steps. Each step is a separate component that uses useFormContext() to read and write its fields. Validation runs on every step transition; the Next button is disabled if the current step's schema does not pass.
- **Progress bar** (ui): shadcn/ui Progress component or a custom horizontal stepper showing filled circles for completed steps, the active step highlighted, and hollow circles for upcoming steps. Animated transition (Framer Motion layoutId or CSS width transition) between steps gives clear visual feedback that progress is happening.
- **User profile onboarding table** (data): Supabase table onboarding_responses stores every answer with user_id as the primary key. Columns are typed to match the input: goal text, experience_level text with a check constraint, preferred_topics text[], notification_frequency text, skipped_steps text[] for tracking skips, and completed_at timestamptz that the completion gate reads. RLS allows only the authenticated user to access their own row.
- **Completion gate** (backend): Middleware or a server component that checks onboarding_responses.completed_at for the current user before rendering any authenticated page. Implemented in Next.js as middleware.ts using @supabase/ssr's createServerClient to read the auth cookie server-side — not client-side after hydration. If completed_at is null, redirect to /onboarding. Set a lightweight cookie (onboarding_complete=true) immediately after completion as a fast-path gate.
- **Personalisation writer** (backend): A Supabase Edge Function or Next.js Server Action that fires on final step submission. Reads onboarding_responses for the user and writes derived preferences to user_settings — dashboard_layout, preferred_topics, notification_frequency — which the dashboard query filters by. This is the bridge between 'we collected data' and 'the app actually feels personalised'.
- **Welcome animation** (ui): Lottie animation via lottie-react or a Framer Motion confetti burst triggered on the final step confirmation. Plays for 1.5–2 seconds before auto-redirecting to the dashboard. Marks the transition from 'new user' to 'setup complete' with a moment of positive reinforcement.
- **FlutterFlow equivalent** (ui): In FlutterFlow, the onboarding flow is a PageView widget where each page corresponds to one onboarding step. App State variables store the step index and the collected answers. On the final page, a Chain of Actions writes the data to a Firebase Firestore onboarding_data document under the user's UID and navigates to the home page.

## Data model

Two tables cover onboarding collection and user settings. Run this in the Supabase SQL editor after setting up your project:

```sql
create table public.onboarding_responses (
  user_id uuid references auth.users(id) on delete cascade primary key,
  goal text,
  experience_level text check (experience_level in ('beginner', 'intermediate', 'advanced')),
  preferred_topics text[],
  notification_frequency text default 'daily',
  skipped_steps text[] default '{}',
  completed_at timestamptz,
  updated_at timestamptz default now()
);

alter table public.onboarding_responses enable row level security;

create policy "Users can manage own onboarding"
  on public.onboarding_responses
  for all
  using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

create table public.user_settings (
  user_id uuid references auth.users(id) on delete cascade primary key,
  dashboard_layout text default 'default',
  preferred_topics text[],
  notification_frequency text default 'daily',
  onboarding_version int default 1,
  updated_at timestamptz default now()
);

alter table public.user_settings enable row level security;

create policy "Users can manage own settings"
  on public.user_settings
  for all
  using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

create index onboarding_completion_idx
  on public.onboarding_responses (user_id, completed_at)
  where completed_at is not null;
```

The partial index on completed_at speeds up the completion gate query — it only scans rows that have actually finished onboarding, which is most rows once the app has been live for a week.

## Build paths

### Lovable — fit 4/10, 5–8 hours

Lovable generates multi-step forms with react-hook-form reliably. Best when onboarding is part of a full Lovable app with Supabase auth already connected.

1. Connect Lovable Cloud so Supabase auth and a PostgreSQL database are automatically provisioned for your project
2. Open Cloud tab → Secrets and confirm your project is connected — Lovable will use this for the onboarding_responses table
3. Paste the prompt below, listing each step's question and input type explicitly
4. After the first build, test the back button on every step to confirm form values persist — if they clear, send: 'Wrap all steps in a persistent FormProvider and use CSS visibility to hide inactive steps instead of unmounting them'
5. Send a follow-up prompt: 'After onboarding completes, write preferred_topics and notification_frequency from onboarding_responses into user_settings' — this is the personalisation writer Lovable often skips
6. Publish and sign up as a new user, complete onboarding, then refresh — confirm you are not redirected back to /onboarding

Starter prompt:

```
Build a custom onboarding flow for new users. The flow has 5 steps: Step 1 — 'What is your main goal?' (4 multiple-choice cards: 'Learn faster', 'Track progress', 'Build habits', 'Collaborate with a team'); Step 2 — 'What is your experience level?' (3 options: Beginner, Intermediate, Advanced); Step 3 — 'Which topics interest you?' (checkbox list of 8 topics, user selects any); Step 4 — 'How often would you like reminders?' (radio: Daily, Weekly, Never) with a Skip button; Step 5 — completion screen with a Framer Motion confetti animation and 'Let's go' button. Requirements: use react-hook-form with a Zod schema per step that gates the Next button; wrap all steps in a persistent FormProvider — never unmount it; show a progress bar ('Step X of 5') at the top of every step; include a Back button that restores the previous step's answers; record skipped steps in the skipped_steps array; on final submission, upsert to Supabase onboarding_responses table (goal, experience_level, preferred_topics, notification_frequency, completed_at, skipped_steps) and also write preferred_topics and notification_frequency to user_settings; use a middleware.ts check that redirects authenticated users with a null completed_at back to /onboarding and lets completed users through; animate step transitions with Framer Motion slide-in from right; single-column mobile-first layout with full-width buttons on each step.
```

Limitations:

- Lovable's 70% problem is common here: the personalisation writer (writing onboarding answers to user_settings) and the completion gate middleware are frequently absent from the first build — plan for explicit follow-up prompts
- The welcome animation on the final step often needs a second iteration to look polished
- Complex branching flows (show step 4 only if step 2 = 'advanced') require careful multi-step prompting; Lovable tends to linearise conditional logic

### V0 — fit 4/10, 4–6 hours

V0 generates the highest-quality step UI and progress indicator. Best when you want to own the Supabase setup and care about pixel-perfect form layouts.

1. Paste the prompt below into V0, specifying each step's question and input type
2. In the Vars panel, add NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY
3. Run the SQL schema from this page in your Supabase SQL editor to create onboarding_responses and user_settings tables
4. Add SUPABASE_SERVICE_ROLE_KEY as a server-side env var in Vercel Dashboard (needed for middleware to call Supabase with the service role)
5. Publish and test the completion gate by signing in with a completed user — confirm they reach the dashboard, not /onboarding

Starter prompt:

```
Build a multi-step onboarding flow in Next.js App Router with the following steps: Step 1 — 'What is your main goal?' (4 large card options); Step 2 — 'What is your experience level?' (3 options: Beginner, Intermediate, Advanced); Step 3 — 'Which topics interest you?' (checkbox grid of 8 items, any number selectable); Step 4 — 'How often would you like reminders?' (radio options: Daily, Weekly, Never) — this step has a Skip button that records 'step-4' in skipped_steps and advances; Step 5 — success screen with a Framer Motion confetti burst and a 'Go to Dashboard' button. Use react-hook-form with Zod validation per step; wrap all steps in a persistent FormProvider; show a stepper progress bar at the top ('Step X of 5') with animated fill; include a Back button that restores previous answers. On completion, upsert to Supabase table onboarding_responses (user_id from auth.uid(), goal, experience_level, preferred_topics, notification_frequency, skipped_steps, completed_at = now()). Also write a Server Action that reads those answers and upserts to user_settings (preferred_topics, notification_frequency, onboarding_version=1). Add a middleware.ts at the root that uses @supabase/ssr createServerClient to check onboarding_responses.completed_at for the current user — redirect to /onboarding if null, allow through if set. Mobile-first layout: single column, min-h-screen, full-width buttons.
```

Limitations:

- V0 generates the frontend UI accurately but does not auto-provision Supabase — you run the schema manually in the Supabase SQL editor
- The middleware.ts completion gate may need one revision pass: V0 sometimes uses the deprecated @supabase/auth-helpers package instead of the current @supabase/ssr
- Server Actions for the personalisation writer may not be generated unless explicitly requested — include it in the prompt

### Flutterflow — fit 4/10, 5–8 hours

FlutterFlow's native PageView widget is designed for exactly this pattern. Right choice when building an iOS or Android app where onboarding runs on a phone screen.

1. In FlutterFlow, create an Onboarding page group and add a PageView widget as the root widget
2. Add one child page per onboarding step inside the PageView; disable swipe navigation so only the Next button advances
3. Create App State variables for step index (int), and one variable per onboarding answer (goalText, experienceLevel, preferredTopics list, notificationFrequency)
4. On each Next button, add an Update App State action to save the current step's answer, followed by Control PageView > Next Page
5. On the final step's submit button, add a Firestore Write Document action writing all App State variables to an onboarding_data collection under the current user's UID, then navigate to the Home page
6. Add a Conditional Widget on the Home page that checks the Firestore onboarding_data document — if it does not exist, navigate back to the Onboarding page

Limitations:

- Conditional branching (show step 4 only if step 2 answer = 'advanced') requires custom Dart actions — FlutterFlow's visual condition builder handles simple visibility toggles but not conditional step insertion into a PageView
- The Supabase backend integration path (instead of Firebase Firestore) requires custom API calls rather than FlutterFlow's native Firestore actions
- Restart-onboarding logic (deleting the onboarding_data document and resetting App State) needs a custom Dart action for the delete step

### Custom — fit 5/10, 1–2 weeks

Full control over branching flows, A/B testing, step-level analytics, CRM integration, and re-onboarding campaigns for feature launches.

1. Model the onboarding flow as a directed graph: nodes are steps, edges are transitions with conditions; render whichever step the user's current position in the graph points to
2. Integrate a feature flag service (PostHog or LaunchDarkly) to A/B test different step sequences and measure activation rates per variant
3. Fire a PostHog or Mixpanel event on every step transition (onboarding_step_completed, onboarding_step_skipped) to build a funnel analysis identifying drop-off steps
4. Write a CRM integration (HubSpot or Salesforce API call from a Next.js Server Action) that creates a contact with onboarding data as lead qualification properties
5. Build a re-onboarding flow that increments onboarding_version in user_settings — users who completed version 1 are shown a shorter 2-step version 2 flow when a major feature launches

Limitations:

- A/B testing onboarding flows requires careful session isolation to prevent users from seeing different variants across devices
- CRM integration adds significant complexity to the final submission step — consider it a phase-2 addition after the core flow is proven

## Gotchas

- **Returning users are sent back to onboarding on every login** — The completion gate checks onboarding_responses.completed_at for the current user, but in Next.js middleware the Supabase session is not available until after the client hydrates. The middleware fires first, sees no session, cannot check the DB, and defaults to redirecting to /onboarding — catching every user on every page load. Fix: In middleware.ts, use @supabase/ssr's createServerClient to read the Supabase auth cookie directly from the request headers — this works before hydration. Set a lightweight server cookie (onboarding_complete=1) immediately after onboarding finishes and read that cookie in middleware as a fast-path gate, falling back to the DB check only when the cookie is absent.
- **Back button returns to step 1 instead of the previous step** — When AI tools generate each onboarding step as a separate route (/onboarding/step-1, /onboarding/step-2 etc.), clicking the browser's back button triggers router history and navigates between routes — which unmounts the current step and resets react-hook-form state. The user loses all answers and lands on step 1. Fix: Use a single /onboarding route with step index managed entirely in React state (not the router). If deep-linking to a specific step is needed for development, use a query parameter (?step=2) but never rely on it for navigation — state is the source of truth for all transitions.
- **Form data disappears when navigating back and forward** — If each step component is conditionally rendered (unmounted when not visible), react-hook-form's register loses the field value on unmount. Navigating from step 3 back to step 2 then forward again shows blank fields — the user has to re-enter their answer. Fix: Wrap all step components in a persistent FormProvider from the very first step and keep all step components mounted throughout the flow. Use CSS (display: none or visibility: hidden) to hide inactive steps — never unmount them. This ensures react-hook-form's internal store preserves all values across navigations.
- **The onboarding completion screen flashes briefly on page load for all users** — The completion gate is an async Supabase query. Before it resolves, the app renders its default state — which is the onboarding flow. Authenticated users who have completed onboarding see the first step flicker for a split second on every page load. Fix: Show a full-screen loading skeleton while the onboarding status is being fetched. Only render the onboarding step content after the query confirms the user has not completed onboarding. For the fastest gate, use the server-side middleware approach with the completion cookie — this resolves before the page renders, eliminating the flash entirely.

## Best practices

- Keep the total step count to 5 or fewer — each additional step reduces completion rates; move non-essential questions to the user profile page instead
- Use a persistent FormProvider that wraps all steps and stays mounted throughout the flow — never unmount steps, use CSS to hide inactive ones
- Implement the completion gate in Next.js middleware using @supabase/ssr so returning users never see the onboarding screen, even for a flash
- Ask only for what you will use: every onboarding answer must drive a visible personalisation in the dashboard — if you can't point to where an answer is used, remove the step
- Record skipped steps in skipped_steps[] even when skip is allowed — this data reveals which questions users find irrelevant and is essential for onboarding iteration
- Show a specific time estimate on the first step ('Takes about 2 minutes') — it meaningfully reduces abandonment for users who are uncertain about the commitment
- Animate step transitions with a slide-in from the right (forward) and slide-out to the right (back) — this directional motion reinforces the sense of progress

## Frequently asked questions

### How do I prevent users from repeating onboarding after they've completed it?

Use a server-side completion gate in Next.js middleware.ts. With @supabase/ssr's createServerClient, you can read the user's auth cookie and query onboarding_responses.completed_at before the page renders. If it's set, allow through. If null, redirect to /onboarding. For speed, also set a lightweight cookie immediately after completion — middleware reads the cookie first and only hits the database when the cookie is absent.

### Can I let users go back and change their answers?

Yes — wrap all steps in a persistent react-hook-form FormProvider that never unmounts. Navigate backwards by decrementing the step index in React state; the FormProvider preserves all entered values. If you conditionally render steps (mounting/unmounting), react-hook-form loses the values on unmount and the user sees blank fields on return.

### How do I skip optional steps?

Add a Skip button to steps you designate as non-critical. On click, advance the step index without validating the current step's fields, and append the step's ID to the skipped_steps array in your form state. When you save onboarding_responses, write the skipped_steps array to the database so you can analyse which questions users find irrelevant.

### Can onboarding steps branch based on earlier answers?

Simple branching (show/hide a single step based on a previous answer) works with conditional rendering of that step component. Complex branching — where the entire sequence of remaining steps changes based on a user segment — requires modelling the flow as a directed graph. This is significantly harder than a linear flow and usually warrants custom development.

### How do I track where users drop off in onboarding?

Fire an analytics event (PostHog, Mixpanel, or a Supabase insert) each time a user completes or skips a step. Include the step ID and a timestamp. Build a funnel view showing completion rate per step — a sharp drop between step 2 and step 3 tells you exactly which question is losing people. PostHog's free tier (1M events/mo) is sufficient for most apps at early stage.

### Can I personalise the dashboard based on onboarding answers?

Yes, and this is the entire point. After the final step, run a Server Action or Edge Function that reads onboarding_responses and writes to user_settings — preferred_topics, dashboard_layout, notification_frequency. Your dashboard query then filters by user_settings. If you skip this step, you collected data with no effect — which users notice immediately.

### How many onboarding steps is too many?

Research consistently shows drop-off increases significantly after step 5. Aim for 3–5 steps covering only the questions that directly drive personalisation. Move everything else — detailed profile info, notification preferences, billing — to the user profile settings page where users can fill it in at their own pace.

### How do I handle onboarding for users who signed up via social login?

Social login (Google, GitHub) does not affect onboarding — the flow triggers based on whether onboarding_responses.completed_at is null, regardless of how the user authenticated. On the first page load after any sign-up method, the completion gate checks the database and redirects new social login users to /onboarding exactly the same as email-signup users.

---

Source: https://www.rapidevelopers.com/app-features/custom-onboarding-flow
© RapidDev — https://www.rapidevelopers.com/app-features/custom-onboarding-flow
