DEV Community

Cover image for The Unseen Cost of Speed: What I’d change if I rebuilt my AI SaaS today
Shola Jegede
Shola Jegede Subscriber

Posted on • Edited on

The Unseen Cost of Speed: What I’d change if I rebuilt my AI SaaS today

When you build fast, you feel unstoppable. Weekend sprints. Prototype by Friday, live by Sunday. That was my entire approach with Learnflow AI.

And on the surface? It worked.

People signed up. Voice sessions ran smoothly. The tech stack (Next.js + Convex + Kinde + Vapi) held its own.

But a few weeks in, the cracks started to show.

Not in the UI. Not in the feature set.

But underneath.

In the assumptions I baked into my backend. In the flows I hardcoded to get a "demo" feel. In how little room I left for edge cases, upgrades, re-authentications, and returning users.

This is the post I wish I read before shipping that MVP.

It’s not about the speed. It’s about what gets left behind.

How It Started: Learnflow AI in One Weekend

The idea was simple:

Let anyone create a voice-based AI tutor that felt custom and responsive.

Stack

Within 48 hours, I had it working:

  • Sign up
  • Pick plan (via Kinde)
  • Land on dashboard
  • Click "Create Tutor"
  • Click "Start Session"

It worked. Mostly.

What I Didn't See Coming

There’s a cost to optimizing for "works now."

Here were the blind spots that crept up:

1. Onboarding State Wasn't Persistent

I had a stepper for onboarding users through their first tutor creation. It showed once. But if they refreshed or came back later?

They started from scratch.

2. Upgrade Logic Was Too Far Removed

Kinde hosted my pricing table, and plan info was saved to user metadata. But in-app logic had to manually read and check that. I had upgrade prompts that didn’t appear at the right time, or sometimes, not at all.

3. Session Tracking Wasn’t Modular

I tracked session starts and deducted credits... but not always consistently. Retry flows, dropped connections, or users switching devices would cause edge case issues.

How I Fixed It: Layer by Layer

1. Onboarding State Tracking via Convex

Instead of local state or front-end-only logic, I moved onboarding step tracking to the backend.

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 });
  },
});
Enter fullscreen mode Exit fullscreen mode

Now, no matter where a user logs in from, I can:

  • Resume onboarding
  • Branch logic based on their last seen step
  • Trigger upgrade nudges at the right moment

Diagram: Before vs After - Onboarding Flow

2. Credit Enforcement as Middleware

Before:

  • I checked user credits manually inside button click handlers
  • Edge cases like double calls or dropped sessions weren’t covered

Now:

  • I wrapped credit checks inside a dedicated mutation
  • That mutation runs before any session can start
export const canStartSession = query({
  args: { userId: v.id("users") },
  handler: async (ctx, args) => {
    const user = await ctx.db.get(args.userId);
    const isPro = user?.plan === "pro";
    const hasCredits = user?.credits > 0;
    return isPro || hasCredits;
  },
});
Enter fullscreen mode Exit fullscreen mode

Used in frontend:

const allowed = await api.sessions.canStartSession({ userId });
if (!allowed) return showUpgradeModal();
Enter fullscreen mode Exit fullscreen mode

Diagram: Credit Check Flow

3. Modular Session Tracking

Old pattern:

  • startSession just updated credits
  • But didn’t store metadata: which tutor, when, from which device

New pattern:

export const addSession = mutation({
  args: {
    userId: v.id("users"),
    companionId: v.id("companions"),
    timestamp: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    await ctx.db.insert("sessions", {
      userId: args.userId,
      companionId: args.companionId,
    });
  },
});
Enter fullscreen mode Exit fullscreen mode

This lets me:

  • See per-user history
  • Trigger upgrade nudges based on behavior
  • Visualize usage per tutor

Better Architecture Patterns I Adopted

Feature Flags

Instead of if (user.plan === "pro"), I now use:

function hasFeature(user, feature) {
  if (user.plan === "pro") return true;
  return freePlanFeatures.includes(feature);
}
Enter fullscreen mode Exit fullscreen mode

Cleaner. Easier to maintain. Centralized.

Event Hooks

I wanted better separation between action and reaction.

Instead of hardcoding showUpgradeModal() after every failed session start, I used an event emitter pattern:

events.on("session-blocked", () => {
  showUpgradeModal();
});
Enter fullscreen mode Exit fullscreen mode

That means I can swap UI reactions later without touching business logic.

Composable Logic: Credit + Plan

Rather than spreading plan logic in dozens of places, I now compose it.

function canUseFeature(user, feature) {
  if (user.plan === "pro") return true;
  if (!user.credits || user.credits <= 0) return false;
  return freePlanFeatures.includes(feature);
}
Enter fullscreen mode Exit fullscreen mode

Kinde: What Worked, What Didn't

✅ What Worked

  • Hosted pricing pages = instant flow
  • Metadata sync (plan info in session)
  • Easy plan switching between free and pro

⚠️ What Needed Work

  • Kinde metadata isn’t real-time (had to sync manually on plan switch)
  • No built-in usage enforcement
  • Needed my own gating via Convex queries

Still, Kinde handled the identity layer well. I just had to build the enforcement layer.

What I'd Do Differently (If Starting Today)

  1. Design credit flows from day one
    • Users don’t read docs
    • If usage isn’t visible, no one upgrades
  2. Track onboarding + session state in backend
    • Frontend-only state is fragile
  3. Make upgrade logic reactive, not static
    • Trigger prompts based on what users do, not what page they’re on
  4. Modularize credit checks
    • Never rely on frontend logic alone
  5. Plan for role-based UI early
    • Gating with plan tiers helps prevent overuse

Final Thoughts

MVP speed is great.

But every speed-up becomes a tradeoff you’ll have to clean up.

That’s not a failure. It’s just how startups work.

If you're where I was a few weeks ago:

  • Launching something fast
  • Trying to stay lean
  • Unsure how deep to go in pricing logic

Just remember:

Ship fast, but make space for your future self.

Make credit systems, upgrade prompts, and session enforcement composable.

Make onboarding state persistent.

And make it so that your second-time users aren’t starting from scratch.

Got questions or shipping your own AI MVP?

Drop a comment or DM me. I'm building Learnflow AI in public, powered by Kinde, Convex, Vapi, and lessons like these.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.