Onboarding isn’t a nice-to-have.
It’s the first impression. The tutorial. The sales pitch. The trust-building moment. And it’s the one thing that determines whether your users stay... or leave.
Before launching my AI app (learnflow AI) publicly, I rebuilt the onboarding flow three separate times.
Each time, I had to tear down assumptions, rewrite logic, and rethink the entire first-time experience.
Why? Because every version failed to answer a simple question:
"Why should I care about this tool — and how do I win here?"
Here’s the full story with mistakes included.
Attempt #1: Instant Start — But No Guidance
The Idea
Make it feel frictionless. Land on dashboard → see a clear call to action → create an AI tutor → start learning.
The Flow
- After signing up, users landed on the dashboard
- Top action was a big “Create Tutor” button
- No walkthrough, no context, no orientation
The Problem
Users didn’t know what a tutor meant or what it would do.
- "Create a tutor for what?"
- "Where do I go next?"
- "Is this real-time AI or not?"
Diagram: V1 Onboarding Flow
User signs up ↳ Sees pricing table (Kinde hosted) ↳ Chooses Free or Pro ↳ Lands on Dashboard ↳ Sees “Create Tutor” button ↳ No context, no help, no clear goal
What I Learned
Removing friction doesn’t remove confusion. Fewer steps ≠ better onboarding.
Attempt #2: Prebuilt Tutors and Progress Hints
The Idea
People need momentum. Don’t drop them into a blank screen.
The Flow
- Dashboard now featured popular public tutors from other users
- Each tutor had a “Start Session” CTA
- Above the fold: "Create Your First Tutor" prominently shown
- Once a session started, users saw their credit count and a soft upgrade CTA
Code: Displaying Public Tutors
<section className="px-4 lg:px-6 space-y-4 mt-10 sm:mt-0"> <h1 className="text-xl sm:text-3xl">Popular Tutors</h1> <section className="home-section"> {companions?.map((companion) => ( <CompanionCard key={companion._id as Id<"companions">} {...companion} color={getSubjectColor(companion.subject)} /> ))} </section> </section>
UX Wins:
- Users could explore before committing
- Visual interest replaced an empty dashboard
- The call-to-action was goal-oriented, not abstract
Diagram: V2 Onboarding Flow
User signs up ↳ Sees pricing table (Kinde hosted) ↳ Chooses Free or Pro ↳ Lands on Dashboard ↳ Sees public tutors + "Create Tutor" prompt ↳ Starts session or creates own
What I Learned
Show. Don’t tell. Give users somewhere to go even if they’re not ready to create yet.
Attempt #3: Embedded Guidance and Upgrade Nudges
The Idea
Prompt users to either create a new tutor through a stepper UI form.
The Flow
- Tutor creation form was redesigned into a guided stepper
- Each field explained with short text (subject, voice style, duration, etc.)
- After creation, users land in a session room — not back on dashboard
- A sticky banner appears after 2 sessions: "You have 8 credits left. Upgrade now →"
Key Goals
UX Goal | Strategy |
---|---|
Show value fast | Highlight working examples (prebuilt tutors) |
Guide action | Stepper UI for building a new tutor |
Build habit | Credits, limits, and upgrade prompts |
Convex Logic: Onboarding State
export const setOnboardingStep = mutation({ args: { userId: v.id("users"), step: v.string() }, handler: async (ctx, args) => { await ctx.db.patch(args.userId, { onboardingStep: args.step }); }, });
Diagram: Final Flow
Outcome: Ready to Launch
With onboarding v3, users finally:
- Understood what tutors were
- Saw working examples instantly
- Were guided into the learning experience
- Encountered clear, upgrade-aware nudges
Billing: Powered by Kinde
We use Kinde for authentication, and crucially, early plan selection.
What Happens on Signup:
- User signs up →
- Redirected to Kinde-hosted pricing table
- Selects plan (
free
orpro
) - Kinde injects this into their session metadata
const { getUser } = getKindeServerSession(); const user = await getUser(); const plan = user?.user_metadata?.plan || "free";
No backend billing setup, no custom checkout — just Kinde’s prebuilt logic + Stripe.
Credits + Feature Locking
Learnflow AI gives 10 free voice sessions (1 per credit).
Convex schema:
users: defineTable({ email: v.string(), plan: v.optional(v.string()), credits: v.optional(v.number()), })
On each session, you deduct credits:
const creditCost = user.plan === "pro" ? 0 : 1; if (user.credits < creditCost) { throw new Error("Out of credits. Upgrade to continue."); } await ctx.db.patch(user._id, { credits: user.credits - creditCost, });
Readiness to Launch: Final Checklist
✅ First-time experience includes guidance and call-to-action
✅ Public tutors surface value even without engagement
✅ Tutor builder guides user through creation with context
✅ Kinde pricing table ensures early plan selection
✅ Credits and plans synced across Kinde and Convex
✅ Sticky upgrade CTA appears after early usage
✅ Convex tracks onboarding progress, allows segmentation
What I’d Tell Past Me
- You can’t onboard users into ambiguity. Make the value visible.
- Let people explore before committing. Curiosity converts.
- Your database needs onboarding logic. Don’t hardcode onboarding state.
- Don’t hide pricing behind clicks. Make plan choice part of the signup journey.
- Nudge softly. You don’t need a paywall popup — a subtle reminder works better.
Final Thoughts
Most onboarding advice is about simplifying. But sometimes you don’t need less — you need more structure.
If users don’t get why they’re here, what they can do, or what’s next — they’ll bounce.
Rebuilding onboarding 3 times forced me to design for:
- Context, not guesswork
- Guided actions, not open-ended prompts
- Real upgrade paths, not buried links
In the end, I didn’t just build better onboarding.
I built a product that was ready to meet users where they are.
Top comments (0)