DEV Community

Cover image for Auth and Billing in One API Call: A Pattern Worth Betting On
Shola Jegede
Shola Jegede Subscriber

Posted on

Auth and Billing in One API Call: A Pattern Worth Betting On

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:

First flow

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 };
  }
});

Enter fullscreen mode Exit fullscreen mode

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

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)

Collapse
 
dorathy profile image
Dorathy John

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