Overview
Stripe billing integration with feature gating, subscriptions, and usage tracking
The billing system integrates Stripe ↗ for payment processing through UseAutumn ↗. UseAutumn handles subscriptions, feature gating, and usage metering while you maintain full control through simple TypeScript APIs.
Pro Tip
Define pricing tiers and features in autumn.config.ts
, then enforce access with ctx.check()
and ctx.track()
in your Convex functions. UseAutumn manages Stripe products, webhooks, and customer sync automatically.
Architecture
The billing system consists of three integrated layers:
Configuration Layer
autumn.config.ts
defines your pricing tiers, features, and limits using the atmn
SDK. This is your single source of truth.
import { feature, product, featureItem, priceItem } from "atmn";
// Define features
export const dashboard = feature({
id: "dashboard",
name: "Dashboard Access",
type: "boolean",
});
export const apiCalls = feature({
id: "api_calls",
name: "API Calls",
type: "single_use",
});
// Define products
export const paid = product({
id: "starterapp-demo-paid",
name: "Paid",
items: [
featureItem({
feature_id: apiCalls.id,
included_usage: 10_000,
interval: "month",
}),
featureItem({
feature_id: dashboard.id,
included_usage: 1,
}),
priceItem({
price: 10,
interval: "month",
}),
],
});
Convex Integration Layer
convex/autumn.ts
initializes the UseAutumn plugin and exports billing functions. All billing operations happen through Convex actions with automatic auth context.
import { Autumn } from "autumn-js";
import { components } from "./_generated/api";
export const autumn = new Autumn(components.autumn, {
secretKey: process.env.AUTUMN_SECRET_KEY,
identify: async (ctx) => {
const user = await ctx.auth.getUserIdentity();
if (!user) return null;
return {
customerId: user.subject,
customerData: {
name: user.name,
email: user.email,
},
};
},
});
export const { check, track } = autumn.api();
Client Integration Layer
packages/app-shell/src/providers/autumn-provider.tsx
connects React components to billing state. The provider wires UseAutumn's React hooks to your Convex backend.
import { useServices } from '~/lib/providers/services-context';
const { billing } = useServices();
const { customer } = billing.useCustomer();
// Trigger checkout
await billing.checkout({
productId: 'starterapp-demo-paid',
successUrl: window.location.origin + '/dashboard/billing',
});
Core Concepts
Direct userId Mapping
BetterAuth userId
= Autumn customerId
. No complex identity resolution.
Server-Side Gating Only
Never check features client-side. All access control happens in Convex actions.
Single Source of Truth
autumn.config.ts
defines everything. Push changes with npx atmn push
.
Zero Stripe Imports
Never import stripe
directly. UseAutumn handles all Stripe operations.
Configuration Workflow
Always Push After Changes
Billing APIs won't reflect your changes until you push the configuration to UseAutumn's servers.
Every time you modify autumn.config.ts
, run:
npx atmn push
This synchronizes your local configuration with UseAutumn's infrastructure. The push command:
- Validates your feature and product definitions
- Creates or updates Stripe products and prices
- Enables your Convex functions to enforce the new limits
# 1. Modify autumn.config.ts
# 2. Push configuration
npx atmn push
# 3. Test billing in development
pnpm dev
# 1. Push from CI/CD after merging
npx atmn push --env production
# 2. Deploy Convex schema changes
npx convex deploy --prod
# 3. Monitor rollout
# UseAutumn propagates changes within seconds
Identity Mapping
The integration uses direct userId mapping for simplicity:
// In convex/autumn.ts identify function
return {
customerId: user.subject, // BetterAuth userId
customerData: {
name: user.name,
email: user.email,
},
};
This means:
- Server: Use
ctx.viewerId
directly withctx.check()
in Convex actions - Client: UseAutumn automatically resolves the current user via Convex auth
- Minimal state: The
billingCustomers
table caches Autumn customer IDs for fast lookups, but sync happens automatically
Documentation Structure
Feature Gating
Control access based on subscription plans. Server-side checks in Convex actions and Next.js pages.
Subscriptions
Recurring billing with subscription tiers. Checkout flows and billing portal integration.
Usage-Based Billing
Track consumption and charge based on actual usage. Credits, metering, and overage patterns.
One-Time Payments
Process single purchases without subscriptions. Digital products and add-ons.
AI Patterns
LLM-optimized billing context. How AI assistants understand and implement billing features.
Quick Start
Configure Products
Edit autumn.config.ts
to define your pricing tiers and features.
export const premium = product({
id: "starterapp-demo-premium",
name: "Premium",
items: [
featureItem({
feature_id: apiCalls.id,
included_usage: 50_000,
interval: "month",
}),
priceItem({ price: 25, interval: "month" }),
],
});
Push Configuration
npx atmn push
Gate a Feature
import { userAction } from "./_helpers/builders";
export const sendMessage = userAction({
args: { body: v.string() },
handler: async (ctx, args) => {
// Check feature access
const result = await ctx.check({ featureId: "messages" });
if (!result.data?.allowed) {
throw new AppError("PLAN_REQUIRED", "Upgrade to send messages");
}
// Track usage
await ctx.track({ featureId: "messages", value: 1 });
// Perform the action
return { success: true };
},
});
Trigger Checkout
'use client';
import { useServices } from '~/lib/providers/services-context';
export function UpgradeButton() {
const { billing } = useServices();
return (
<button
onClick={() => billing.checkout({
productId: 'starterapp-demo-premium',
successUrl: window.location.origin + '/dashboard',
})}
>
Upgrade to Premium
</button>
);
}