Billing
Billing is powered by Autumn and scoped entirely to organizations — each org has one billing customer record that tracks its subscription, plan, and feature usage.
All pricing, plans, and features must be configured in the Autumn dashboard. RED does not define pricing in code — Autumn is the single source of truth. After configuring your plans and features in Autumn, sync the data to RED via the admin panel. This means you can adjust pricing, add features, or create new plans without any code changes or redeployment.
The starter kit ships with two demo billing features (standard_messages and premium_messages) mapped to the AI chat models. Replace these with your own features in Autumn to match your product’s billing model.
Architecture
Section titled “Architecture”┌─────────────┐ sync ┌──────────────┐ API calls ┌────────┐│ Autumn │ ────────────→ │ Convex cache │ ←───────────── │ Autumn ││ Dashboard │ (plans, │ billingPlans │ (check, │ API ││ (configure) │ features) │ billingFeatures│ track, │ │└─────────────┘ │ billingCustomers│ attach) └────────┘ └──────────────┘ ↑ Convex queries (fast, local) │ ┌──────────────┐ │ Frontend │ │ BillingGuard │ │ Billing tab │ └──────────────┘Local cache (billingPlans, billingFeatures, billingCustomers) serves all read queries — no API call needed for entitlement checks. Autumn API is only called for mutations (checkout, usage tracking) and admin sync operations.
Feature types
Section titled “Feature types”Autumn supports three feature types, all cached locally:
| Type | Behavior | Example |
|---|---|---|
| Boolean | On/off gate — feature is available or not | Custom branding, priority support |
| Metered | Quota-based — tracks usage against a limit | AI messages, API calls |
| Credit system | Metered features that consume credits | Token-based AI usage |
Organization provisioning
Section titled “Organization provisioning”When a new organization is created, the billingProvision BetterAuth plugin automatically provisions a customer in Autumn:
- Org created via
/organization/create - Plugin after-hook fires, schedules
syncCustomerFromAutumn - Calls Autumn
customers.get_or_createwith the org ID - Customer data cached in
billingCustomers
This is non-blocking — the org is usable immediately while provisioning happens in the background. A lazy provisioning fallback also exists: if getFeatureUsage is called for an org without a customer record, it provisions on demand.
Entitlement checks
Section titled “Entitlement checks”Frontend — BillingGuard
Section titled “Frontend — BillingGuard”Gate UI features declaratively with the BillingGuard component:
import { BillingGuard } from "@red/web-shell/billing-guard"
<BillingGuard featureId="custom_branding"> <BrandingEditor /></BillingGuard>When the feature is not available, BillingGuard renders a default upgrade prompt (or a custom fallback if provided). The upgrade prompt opens the org billing tab.
Frontend — checkFeature query
Section titled “Frontend — checkFeature query”For programmatic checks in components:
import { api } from "@red/backend/api"import { useQuery } from "convex/react"
const check = useQuery(api.modules.core.billing.billing_api.checkFeature, { featureId: "premium_messages"})
// check.allowed — boolean// check.included — number | null (null = unlimited)// check.unlimited — booleanThis query reads entirely from the local Convex cache — no Autumn API call.
Backend — getOrgPlanItems
Section titled “Backend — getOrgPlanItems”For entitlement checks inside Convex functions:
import { getOrgPlanItems, isFeatureAvailable } from "../billing/billing_access"
const items = await getOrgPlanItems(ctx, orgId)const canUseFeature = isFeatureAvailable( items.find((i) => i.feature_id === "custom_branding"))Metered usage tracking
Section titled “Metered usage tracking”Metered features follow a check-then-track pattern: verify quota before the action, report usage after success.
Quota check (before action)
Section titled “Quota check (before action)”import { autumnCheck } from "../../lib/core/autumn_client"
const { allowed, balance } = await autumnCheck(orgId, "standard_messages", { sendEvent: false // don't consume, just check})
if (!allowed) throw appError("FORBIDDEN", "Quota exceeded")Usage report (after success)
Section titled “Usage report (after success)”Usage is reported asynchronously via the scheduler to avoid blocking the main action:
await ctx.scheduler.runAfter( 0, internal.modules.core.billing.billing_internal.trackAndSync, { organizationId: orgId, featureId: "standard_messages", value: 1 })trackAndSync calls Autumn’s balances.track endpoint to increment usage by the given value.
Real-world example: AI messages
Section titled “Real-world example: AI messages”The AI module maps each supported model to a feature ID in @red/backend-contract/ai/models:
{ value: "openai/gpt-oss-120b", featureId: "standard_messages" }{ value: "minimax/minimax-m2.7", featureId: "premium_messages" }{ value: "moonshotai/kimi-k2.5", featureId: "premium_messages" }Before running an agent turn, the handler checks quota against the selected model’s feature. On successful generation, it schedules a usage increment of 1.
Displaying usage
Section titled “Displaying usage”The billing tab calls getFeatureUsage to fetch live usage data from Autumn for all metered features on the org’s plan:
import { api } from "@red/backend/api"
const usage = await convex.action( api.modules.core.billing.billing_api.getFeatureUsage, {})
// Returns array:// [{ featureId, name, display, granted, remaining, usage, unlimited }]Each entry includes:
granted— total allocation for the billing periodusage— amount consumedremaining—granted - usageunlimited— if true, no cap
The frontend renders progress bars with color coding (red at 90%+ usage).
Plan management
Section titled “Plan management”Checkout
Section titled “Checkout”Initiates a subscription to a plan. Autumn handles the payment flow:
const { paymentUrl } = await convex.action( api.modules.core.billing.billing_api.checkout, { planId: "pro", successUrl: "/settings?tab=billing" })
// Redirect user to paymentUrl for Stripe checkoutwindow.location.href = paymentUrlCancel / uncancel
Section titled “Cancel / uncancel”// Cancel at end of billing cycle (default)await convex.action( api.modules.core.billing.billing_api.cancelSubscription, { planId: "pro" })
// Cancel immediatelyawait convex.action( api.modules.core.billing.billing_api.cancelSubscription, { planId: "pro", cancelAction: "cancel_immediately" })
// Undo cancellationawait convex.action( api.modules.core.billing.billing_api.uncancelSubscription, { planId: "pro" })Self-service portal
Section titled “Self-service portal”Open the Autumn billing portal for payment method management and invoice history:
const { url } = await convex.action( api.modules.core.billing.billing_api.openBillingPortal, { returnUrl: window.location.href })Add-ons
Section titled “Add-ons”Plans configured as one-off billing in Autumn appear as purchasable add-ons (credit packs, one-time features). They use the same checkout action.
Permissions
Section titled “Permissions”Billing actions are org-scoped with two permission levels:
| Role | billing:read | billing:manage |
|---|---|---|
| owner | yes | yes |
| admin | yes | no |
| member | yes | no |
All org members can view plans and usage. Only owners can subscribe, cancel, or purchase add-ons.
Admin operations
Section titled “Admin operations”Global admins have additional capabilities:
| Action | Description |
|---|---|
adminSyncPlans | Sync all plans and features from Autumn to local cache |
adminSyncCustomer | Re-sync a specific org’s billing data |
adminGetFeatureUsage | View usage for any organization |
adminListAllPlans | List all plans (including inactive) |
adminGetOrgBilling | View any org’s billing details |
The admin billing tab shows all plans with their entitlements (boolean, metered, credit) and a sync button. The admin org detail page shows the org’s subscription status, usage bars, and subscription history.
Data sync
Section titled “Data sync”Autumn is the source of truth. Local Convex tables are a cache:
| Table | Synced when |
|---|---|
billingPlans | Admin clicks “Sync from Autumn” |
billingFeatures | Admin clicks “Sync from Autumn” |
billingCustomers | Org creation, post-checkout watcher, cancel/uncancel, daily cron, admin sync |
The sync process fetches data from Autumn, upserts locally, and prunes stale records. Plan items are enriched with feature metadata (type, name, display labels) during sync.
Post-checkout convergence
Section titled “Post-checkout convergence”Stripe → Autumn payment settlement is asynchronous: when a user pays and lands back in the app, Autumn may not have processed Stripe’s invoice.paid webhook yet, so an immediate sync would capture stale state.
The checkout action schedules watchCheckoutSync (in billing_internal.ts) which polls Autumn with backoff [3s, 20s, 60s, 120s, 300s] — five attempts spread over ~8 minutes. Each tick fetches the customer and upserts; the watcher exits the moment the active subscription’s plan_id matches the requested plan. Convex query reactivity means the UI flips from “Free” to “Pro” the moment the row is written, no frontend polling needed.
Daily cron
Section titled “Daily cron”billing-customer-sync (in crons.ts) runs at 03:00 UTC and re-pulls every billingCustomers row from Autumn. It catches lifecycle drift the watcher can’t see — monthly renewals, portal-initiated cancellations, past_due, expired. Customers are scheduled 100ms apart to stay friendly to Autumn’s per-customer locks.
Future: Autumn webhook (optional upgrade)
Section titled “Future: Autumn webhook (optional upgrade)”Autumn ships a customer.products.updated webhook (currently in beta, contact support@useautumn.com to enable) that pushes plan changes in near-real-time via Svix. To swap pull → push:
- Add
svixto rootpackage.jsonand anAUTUMN_WEBHOOK_SECRETConvex env var. - Register a
/autumn-webhookroute inhttp.tsthat verifies the Svix signature and schedulessyncCustomerFromAutumnoncustomer.products.updated. - Optionally drop the
watchCheckoutSyncschedule from thecheckoutaction — the webhook supersedes it. - The daily cron can stay as a safety net or be removed.
The pull-based default avoids the beta-access dependency, so the boilerplate works out of the box.
Environment setup
Section titled “Environment setup”| Variable | Required | Description |
|---|---|---|
AUTUMN_SECRET_KEY | yes | Bearer token for the Autumn API |
Plans, features, and pricing are configured entirely in the Autumn dashboard — no code changes needed. After configuring, run the admin sync to update the local cache.
Key files
Section titled “Key files”| File | Purpose |
|---|---|
packages/backend/src/convex/lib/core/autumn_client.ts | Autumn API client (check, track, attach, portal) |
packages/backend/src/convex/modules/core/billing/billing_api.ts | Public, authenticated, and admin endpoints |
packages/backend/src/convex/modules/core/billing/billing_internal.ts | Sync logic and internal mutations |
packages/backend/src/convex/modules/core/billing/billing_access.ts | Helper for entitlement checks in backend functions |
packages/backend/src/convex/modules/core/billing/billing_table.ts | Schema for billingCustomers, billingPlans, billingFeatures |
packages/backend/src/convex/lib/core/auth/billing_provision.ts | BetterAuth plugin for org creation provisioning |
packages/web-shell/src/components/billing-guard.tsx | Declarative feature gating component |
packages/web-shell/src/components/settings/tabs/org-billing-tab.tsx | User-facing billing tab with usage and plans |
packages/backend-contract/src/ai/models.ts | Model → feature ID mapping used by the AI module |