StarterApp Docs
Billing

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

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 with ctx.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

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