The first real backend decision I made on Learnflow AI wasn’t about speed, performance, or even voice logic.
It was about simplicity.
From the start, I knew two things:
- Every user should be on a plan (Free or Pro)
- Every plan should define exactly what features the user could access
But here’s the problem: Most tools split these concerns across two completely different services. One API for auth. Another for billing. A third for usage tracking (usually the database). You end up stitching together logic just to answer a question as basic as:
"Can this user start a session right now?"
Learnflow AI isn’t a typical SaaS app. It’s usage-based, voice-powered, and session-restricted. And to make the experience smooth, I didn’t want 5 lookups across 3 APIs.
So I made a bet:
What if billing and access came from the same source of truth?
Why This Matters (Even for Small Teams)
AI apps aren’t just about signing in and clicking around. They come with real costs.
For Learnflow AI, every session spins up a voice assistant. The longer the call, the higher the cost.
So:
- Every user needs usage limits
- Those limits need to reflect their pricing tier
- And the app needs to enforce them in real-time
That meant I had to build a flow that looked like this:
If the auth and billing systems lived separately, this would have been brittle, slow, and error-prone.
So instead, I chose a system that merged them.
The Stack That Let Me Pull This Off
- Kinde for both auth and plan metadata (+ usage tracking)
- Convex for real-time database to store sessions
- Vapi for voice session orchestration
Why Kinde?
Most auth providers return a token and a user ID. That’s it.
Kinde goes further: it lets you store plan info inside the user metadata.
That means when a user logs in, I can:
- Show or hide features
- Enforce limits
- Trigger upgrade prompts
All without hitting a separate billing API.
Walkthrough: One API Call, Full Access Logic
When a user signs in, here’s what happens under the hood:
1. User Picks a Plan on Signup
Kinde supports hosted pricing tables. So when a user signs up, they see:
- Free Plan
- Pro Plan ($)
Once they select a plan, Kinde adds that to their metadata, which I go on to store into the user’s table in my database.
No need to run a webhook or background sync.
2. Metadata Comes with Every Session
Now, anytime the user logs in, their plan info is bundled in.
So inside Convex, I can write logic like:
export const canStartSession = query({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
const user = await ctx.db.get(args.userId);
const plan = user.plan;
const credits = user.credits || 0;
if (plan === "free" && credits <= 0) {
return { allowed: false, reason: "out_of_credits" };
}
return { allowed: true };
}
});
That’s it.
One call. Full access check.
What Went Wrong (Before This Pattern)
Mistake 1: Treating Auth and Billing as Separate
My first implementation used a separate billing API and tried to sync plan data into Convex via webhook.
Problems:
- Race conditions
- Out-of-date plan info
- Complex error handling if sync failed
Mistake 2: Not Storing Plan Info in Session
Early users would sign up for Pro, but get treated like Free for a few minutes. Why?
Because the app didn’t read billing metadata until a background sync completed.
Friction. Confusion. Drop-off.
Mistake 3: Hidden Usage Until Failure
Before adding credit counters, users had no idea they were near their limit. So the session would just fail one day.
How I Fixed It
- Used Kinde's metadata API as the billing source of truth
- Moved plan info into session
- Showed credit counters in the UI
UI Changes:
- Dashboard header:
You have 3 sessions left
- Tutor page:
This tutor requires Pro
- Modal on failure:
You're out of credits. Upgrade to continue.
Full Flow
Why This Pattern Scales
Even though Learnflow AI is small today, this access logic will scale to thousands of users.
Why?
- No custom billing backend
- No race conditions
- Every plan decision is portable
Plus, plan upgrades are reflected immediately.
What You Can Steal from This
If you're building an AI app with usage-based pricing:
- Choose an auth system that lets you store plan metadata
- Use one backend query to decide access
- Reflect usage visually, early, and often
Don't treat billing as a separate concern.
It is product logic.
Final Thought
It’s easy to over-engineer early.
You start imagining scale. Edge cases. Payment fails. Rate limits.
But in the first version, users just want to know:
- Can I use this?
- What am I allowed to do?
- What happens when I hit the limit?
If you answer those clearly, you win trust.
That’s what this pattern gave me: clarity by default.
Auth and billing in one call.
A pattern worth betting on.
Built with:
- Kinde (Auth + Billing + Metadata + Usage metering)
- Convex (Backend)
- Vapi (Voice sessions)
- Next.js App Router
Top comments (1)
I had no idea kinde offered this feature, saw it on clerk and I was surprised, auth and billing in one place is a really cool feature. I’d try it this weekend