9.7 KiB
How Flowglad is Integrated
This document explains the Flowglad billing integration in this codebase.
Overview
Flowglad handles usage-based billing with metered subscriptions. Users can:
- Use the app for free with limited requests
- Subscribe to a paid plan for more usage
- Top up credits when needed
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Frontend │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ useBilling() │ │ Pricing UI │ │
│ │ (Flowglad hook) │ │ (checkout) │ │
│ └────────┬────────┘ └────────┬────────┘ │
└───────────┼──────────────────────┼──────────────────────────────┘
│ │
▼ ▼
┌───────────────────────┐ ┌───────────────────────┐
│ /api/flowglad/* │ │ /api/usage-events │
│ (billing endpoints) │ │ (record usage) │
└───────────┬───────────┘ └───────────┬───────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Flowglad API │
│ - Customer management │
│ - Subscriptions │
│ - Usage metering │
│ - Checkout sessions │
└─────────────────────────────────────────────────────────────────┘
File Structure
packages/web/src/
├── lib/
│ ├── flowglad.ts # FlowgladServer initialization
│ ├── billing.ts # Usage checking & recording logic
│ └── billing-helpers.ts # Utility functions for pricing/usage
└── routes/api/
├── flowglad/
│ └── $.ts # Catch-all route for Flowglad API
└── usage-events.ts # Record usage events
Core Files
1. lib/flowglad.ts - Server Initialization
Creates a FlowgladServer instance for a specific user:
import { FlowgladServer } from "@flowglad/server"
import { db } from "@/db/connection"
import { users } from "@/db/schema"
import { eq } from "drizzle-orm"
export const flowglad = (customerExternalId: string) => {
const env = getEnv()
if (!env.FLOWGLAD_SECRET_KEY) {
return null
}
return new FlowgladServer({
apiKey: env.FLOWGLAD_SECRET_KEY,
customerExternalId, // Maps to user.id
getCustomerDetails: async (externalId: string) => {
// Fetch user details from database
const user = await db().query.users.findFirst({
where: eq(users.id, externalId),
})
if (!user) {
throw new Error(`User not found: ${externalId}`)
}
return {
email: user.email,
name: user.name ?? undefined,
}
},
})
}
Key Points:
- Takes
customerExternalIdwhich is the user's ID from better-auth - Fetches customer details (email, name) from database when needed
- Returns
nullifFLOWGLAD_SECRET_KEYis not configured
2. routes/api/flowglad/$.ts - API Route Handler
Proxies requests to Flowglad for billing operations:
export const Route = createFileRoute("/api/flowglad/$")({
server: {
handlers: {
GET: async ({ request, params }) => {
const userId = await getUserId(request)
if (!userId) {
return json({ error: "Unauthorized" }, 401)
}
const flowgladServer = flowglad(userId)
// ... handle request
},
POST: async ({ request, params }) => {
// Same pattern for POST
},
},
},
})
Endpoints available:
GET /api/flowglad/billing- Get user's billing infoPOST /api/flowglad/checkout- Create checkout session- etc.
3. routes/api/usage-events.ts - Record Usage
Records usage events after AI requests:
// POST /api/usage-events
// Body: { usageMeterSlug: "ai_requests", amount: 1 }
const usageEvent = await flowgladServer.createUsageEvent({
subscriptionId: currentSubscription.id,
priceSlug: usagePrice.slug,
amount,
transactionId: finalTransactionId, // For idempotency
})
4. lib/billing.ts - Usage Logic
Implements billing business logic:
// Usage limits by tier
const GUEST_FREE_REQUESTS = 5 // No auth required
const AUTH_FREE_REQUESTS_DAILY = 20 // Authenticated, no subscription
const PAID_PLAN_REQUESTS = 1000 // Pro plan per billing period
// Meter slug (must match Flowglad dashboard)
export const AI_REQUESTS_METER = "ai_requests"
// Key functions:
export async function checkUsageAllowed(request: Request): Promise<UsageCheckResult>
export async function recordUsage(request: Request, amount?: number): Promise<void>
export async function getBillingSummary(request: Request): Promise<BillingSummary>
5. lib/billing-helpers.ts - Utilities
Helper functions for working with Flowglad data:
// Find usage price by meter slug
findUsagePriceByMeterSlug(usageMeterSlug, pricingModel)
// Compute total usage credits from subscription
computeUsageTotal(usageMeterSlug, currentSubscription, pricingModel)
// Check if plan is default/free
isDefaultPlanBySlug(pricingModel, priceSlug)
Usage Flow
1. User Makes AI Request
// In /api/chat/ai.ts
const usage = await checkUsageAllowed(request)
if (!usage.allowed) {
return new Response("Usage limit reached", { status: 429 })
}
// ... process AI request ...
// Record usage after success
await recordUsage(request, 1)
2. Frontend Checks Billing
// Using Flowglad React hook
import { useBilling } from "@flowglad/react" // or @flowglad/nextjs
function Dashboard() {
const billing = useBilling()
if (!billing.loaded) return <Loading />
const usage = billing.checkUsageBalance("ai_requests")
const remaining = usage?.availableBalance ?? 0
return <div>Remaining: {remaining}</div>
}
3. User Upgrades
// Create checkout session
const handleUpgrade = async () => {
await billing.createCheckoutSession({
priceSlug: "pro_monthly",
successUrl: `${window.location.origin}/`,
cancelUrl: window.location.href,
quantity: 1,
autoRedirect: true,
})
}
Flowglad Dashboard Setup
1. Create Usage Meter
In Flowglad dashboard, create a usage meter:
- Slug:
ai_requests - Name: "AI Requests"
- Type: Sum
- Reset: Per billing period
2. Create Products & Prices
Free Plan (default):
- Default: Yes
- Price: $0/month
Pro Plan:
- Name: "Pro"
- Price slug:
pro_monthly - Amount: $7.99/month
- Add usage price for
ai_requestsmeter with 1000 included credits
3. Get API Key
- Go to Settings → API Keys
- Create a secret key (starts with
sk_) - Add to environment:
# Local development
echo "FLOWGLAD_SECRET_KEY=sk_test_xxx" >> packages/web/.env
# Production (Cloudflare)
wrangler secret put FLOWGLAD_SECRET_KEY
Environment Variables
# Required for Flowglad
FLOWGLAD_SECRET_KEY=sk_live_xxx # or sk_test_xxx for testing
Testing Locally
- Set up Flowglad account and get test API key
- Add to
.env:FLOWGLAD_SECRET_KEY=sk_test_xxx - Create products/prices in Flowglad dashboard
- Run the app and test:
- Sign up → user becomes Flowglad customer
- Make AI requests → usage is tracked
- Hit limit → upgrade prompt shown
- Subscribe → checkout flow
- More requests → usage deducted from subscription
Debugging
Check if Flowglad is configured
curl 'http://localhost:3000/api/flowglad/billing' -H 'Cookie: ...'
# Error: "Flowglad not configured" → FLOWGLAD_SECRET_KEY not set
# Error: "Unauthorized" → User not logged in
# Success: Returns billing data with subscriptions, usage, etc.
Check usage balance
const billing = await flowglad(userId).getBilling()
const usage = billing.checkUsageBalance("ai_requests")
console.log("Available:", usage?.availableBalance)
Record test usage
curl -X POST 'http://localhost:3000/api/usage-events' \
-H 'Content-Type: application/json' \
-H 'Cookie: ...' \
-d '{"usageMeterSlug": "ai_requests", "amount": 1}'
Common Issues
"Customer not found"
The user doesn't have a Flowglad customer record yet. This is created automatically when:
- User first accesses billing endpoints
getCustomerDetailsis called
"No active subscription found"
User needs to subscribe to a plan. Show them the pricing page.
"Usage price not found for meter"
The ai_requests meter exists but no usage price is attached to it in your pricing model. Add a usage price in Flowglad dashboard.