Skip to content

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.

┌─────────────┐ 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.

Autumn supports three feature types, all cached locally:

TypeBehaviorExample
BooleanOn/off gate — feature is available or notCustom branding, priority support
MeteredQuota-based — tracks usage against a limitAI messages, API calls
Credit systemMetered features that consume creditsToken-based AI usage

When a new organization is created, the billingProvision BetterAuth plugin automatically provisions a customer in Autumn:

  1. Org created via /organization/create
  2. Plugin after-hook fires, schedules syncCustomerFromAutumn
  3. Calls Autumn customers.get_or_create with the org ID
  4. 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.

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.

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 — boolean

This query reads entirely from the local Convex cache — no Autumn API call.

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 features follow a check-then-track pattern: verify quota before the action, report usage after success.

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

The AI module maps each supported model to a feature ID in @red/backend-contract/ai/models:

packages/backend-contract/src/ai/models.ts
{ 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.

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 period
  • usage — amount consumed
  • remaininggranted - usage
  • unlimited — if true, no cap

The frontend renders progress bars with color coding (red at 90%+ usage).

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 checkout
window.location.href = paymentUrl
// Cancel at end of billing cycle (default)
await convex.action(
api.modules.core.billing.billing_api.cancelSubscription,
{ planId: "pro" }
)
// Cancel immediately
await convex.action(
api.modules.core.billing.billing_api.cancelSubscription,
{ planId: "pro", cancelAction: "cancel_immediately" }
)
// Undo cancellation
await convex.action(
api.modules.core.billing.billing_api.uncancelSubscription,
{ planId: "pro" }
)

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

Plans configured as one-off billing in Autumn appear as purchasable add-ons (credit packs, one-time features). They use the same checkout action.

Billing actions are org-scoped with two permission levels:

Rolebilling:readbilling:manage
owneryesyes
adminyesno
memberyesno

All org members can view plans and usage. Only owners can subscribe, cancel, or purchase add-ons.

Global admins have additional capabilities:

ActionDescription
adminSyncPlansSync all plans and features from Autumn to local cache
adminSyncCustomerRe-sync a specific org’s billing data
adminGetFeatureUsageView usage for any organization
adminListAllPlansList all plans (including inactive)
adminGetOrgBillingView 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.

Autumn is the source of truth. Local Convex tables are a cache:

TableSynced when
billingPlansAdmin clicks “Sync from Autumn”
billingFeaturesAdmin clicks “Sync from Autumn”
billingCustomersOrg 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.

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.

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.

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:

  1. Add svix to root package.json and an AUTUMN_WEBHOOK_SECRET Convex env var.
  2. Register a /autumn-webhook route in http.ts that verifies the Svix signature and schedules syncCustomerFromAutumn on customer.products.updated.
  3. Optionally drop the watchCheckoutSync schedule from the checkout action — the webhook supersedes it.
  4. 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.

VariableRequiredDescription
AUTUMN_SECRET_KEYyesBearer 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.

FilePurpose
packages/backend/src/convex/lib/core/autumn_client.tsAutumn API client (check, track, attach, portal)
packages/backend/src/convex/modules/core/billing/billing_api.tsPublic, authenticated, and admin endpoints
packages/backend/src/convex/modules/core/billing/billing_internal.tsSync logic and internal mutations
packages/backend/src/convex/modules/core/billing/billing_access.tsHelper for entitlement checks in backend functions
packages/backend/src/convex/modules/core/billing/billing_table.tsSchema for billingCustomers, billingPlans, billingFeatures
packages/backend/src/convex/lib/core/auth/billing_provision.tsBetterAuth plugin for org creation provisioning
packages/web-shell/src/components/billing-guard.tsxDeclarative feature gating component
packages/web-shell/src/components/settings/tabs/org-billing-tab.tsxUser-facing billing tab with usage and plans
packages/backend-contract/src/ai/models.tsModel → feature ID mapping used by the AI module