DEV Community

masato
masato

Posted on • Originally published at masatoman.net

Usage-Based Billing for Indie Hackers 2026: Implement Stripe Meter API in 5 Files

TL;DR

Flat-rate pricing forces a dilemma: too cheap and heavy users drain your margin; too expensive and light users churn. Usage-based billing (metered pricing) ties revenue to the value you deliver.

Here's the 5-file implementation using Stripe Meter API + Supabase + Next.js:

File Role
Stripe Dashboard Create Meter + metered Price
supabase/migrations/add_usage_events.sql Store usage locally for real-time display
app/api/usage/record/route.ts Record events to both Stripe and Supabase
app/api/stripe/webhook/route.ts Handle invoice.payment_succeeded
components/UsageMeter.tsx Show real-time usage gauge in dashboard

Step 0: Create a Stripe Meter

In Stripe Dashboard → Billing → Meters → Create meter:

Field Example
Meter name ai_tokens
Event name ai_token_consumed
Aggregation SUM

Then create a metered Price under Products. Set Billing type to Per unit and link it to your Meter.


Step 1: Supabase Usage Table

-- supabase/migrations/add_usage_events.sql
CREATE TABLE IF NOT EXISTS usage_events (
  id           uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id      uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  event_type   text NOT NULL,
  quantity     integer NOT NULL DEFAULT 1,
  metadata     jsonb,
  created_at   timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX ON usage_events (user_id, event_type, created_at DESC);

CREATE OR REPLACE VIEW usage_monthly AS
SELECT
  user_id,
  event_type,
  date_trunc('month', created_at) AS month,
  SUM(quantity) AS total_quantity
FROM usage_events
GROUP BY user_id, event_type, date_trunc('month', created_at);
Enter fullscreen mode Exit fullscreen mode

Why Supabase too? Stripe Meter is aggregated for billing — not great for real-time UI. Supabase lets you show used: 47,200 / 100,000 tokens in the dashboard without extra API calls.


Step 2: Record Usage (Stripe + Supabase)

// app/api/usage/record/route.ts
import { stripe } from '@/lib/stripe'
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'

export async function POST(req: Request) {
  const supabase = createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

  const { eventType, quantity, metadata } = await req.json()

  const { data: profile } = await supabase
    .from('profiles')
    .select('stripe_customer_id')
    .eq('id', user.id)
    .single()

  await Promise.all([
    supabase.from('usage_events').insert({ user_id: user.id, event_type: eventType, quantity, metadata }),
    profile?.stripe_customer_id
      ? stripe.billing.meterEvents.create({
          event_name: eventType,
          payload: {
            stripe_customer_id: profile.stripe_customer_id,
            value: String(quantity),
          },
        })
      : Promise.resolve(),
  ])

  return NextResponse.json({ ok: true })
}
Enter fullscreen mode Exit fullscreen mode

Call this from your AI feature route — fire-and-forget so it doesn't block the response:

// After your AI call resolves
const tokensUsed = completion.usage?.total_tokens ?? 0
fetch('/api/usage/record', {
  method: 'POST',
  body: JSON.stringify({ eventType: 'ai_token_consumed', quantity: tokensUsed }),
})
Enter fullscreen mode Exit fullscreen mode

Step 3: Handle Invoice Webhook

// app/api/stripe/webhook/route.ts (metered billing section)
case 'invoice.payment_succeeded': {
  const invoice = event.data.object as Stripe.Invoice
  const customerId = invoice.customer as string

  const { data: profile } = await supabase
    .from('profiles')
    .select('id')
    .eq('stripe_customer_id', customerId)
    .single()

  if (!profile) break

  await supabase.from('billing_cycles').upsert({
    user_id: profile.id,
    period_start: new Date(invoice.period_start! * 1000).toISOString(),
    period_end:   new Date(invoice.period_end!   * 1000).toISOString(),
    amount_paid:  invoice.amount_paid,
  })
  break
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Real-Time Usage Meter Component

// components/UsageMeter.tsx
'use client'
import { createClient } from '@/lib/supabase/client'
import { useEffect, useState } from 'react'

export function UsageMeter({
  userId, eventType, limit, label,
}: {
  userId: string; eventType: string; limit?: number; label?: string
}) {
  const [used, setUsed] = useState(0)
  const supabase = createClient()

  useEffect(() => {
    const monthStart = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString()
    supabase
      .from('usage_events')
      .select('quantity')
      .eq('user_id', userId).eq('event_type', eventType).gte('created_at', monthStart)
      .then(({ data }) => setUsed((data ?? []).reduce((s, r) => s + r.quantity, 0)))
  }, [userId, eventType, supabase])

  const pct = limit ? Math.min((used / limit) * 100, 100) : null

  return (
    <div className="space-y-1">
      <div className="flex justify-between text-sm text-gray-600">
        <span>{label ?? eventType}</span>
        <span>{used.toLocaleString()}{limit ? ` / ${limit.toLocaleString()}` : ''}</span>
      </div>
      {pct != null && (
        <div className="h-2 w-full rounded bg-gray-200">
          <div
            className={`h-2 rounded ${pct >= 90 ? 'bg-red-500' : 'bg-blue-500'}`}
            style={{ width: `${pct}%` }}
          />
        </div>
      )}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

When Usage-Based Billing Makes Sense

Product Type Fit
AI-powered features (tokens, API calls) ✅ Cost-aligned
Storage / bandwidth dependent ✅ Cost-aligned
Uniform usage patterns ⚠️ Flat rate is simpler
B2C individual users ⚠️ Some users dislike unpredictable bills
B2B team usage ✅ Usage scales with team value

Practical hybrid for indie hackers: Base fee (e.g., $10/month) + overage charges. This avoids Stripe's minimum invoice amount issue and gives users a predictable floor.


Full design rationale and Japanese-language version at: masatoman.net/articles/indie-dev-usage-based-billing-2026

Top comments (0)