# How OpenSEO's Billing System Uses Autumn for Subscriptions: A Technical Deep Dive

> Discover how OpenSEO integrates Autumn for subscription management. Learn about feature flags, credit balances, and seamless customer provisioning with this technical deep dive.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: deep-dive
- Published: 2026-08-05

---

**OpenSEO leverages the Autumn SaaS platform to manage subscription feature flags and credit balances through a lazy-loaded façade that wraps the `autumn-js` SDK, enabling customer provisioning, usage tracking, and webhook-based subscription synchronization.**

OpenSEO relies on Autumn's subscription infrastructure to handle everything from paid plan validation to credit-based API consumption. The integration centers on a lightweight abstraction layer that defers loading the ~450KB SDK until runtime, ensuring minimal cold-start impact on Cloudflare Workers. By examining the source code in `src/server/billing/`, we can see exactly how customer lifecycle management and usage metering interact with Autumn's API.

## The Lazy-Loaded Autumn Façade

To keep Cloudflare Worker cold-start times low, OpenSEO implements a lazy-loading pattern in [`src/server/billing/autumn.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/autumn.ts). Rather than importing the ~450KB `autumn-js` SDK at the top of the file, the code dynamically imports it only when first invoked.

The `loadAutumn()` function initializes the SDK with the `AUTUMN_SECRET_KEY` environment variable and custom retry configuration:

```typescript
// src/server/billing/autumn.ts
function loadAutumn(): Promise<Autumn> {
  return (autumnPromise ??= import("autumn-js").then(
    ({ Autumn }) =>
      new Autumn({
        secretKey: () => getRequiredEnvValue("AUTUMN_SECRET_KEY"),
        retryConfig: { … }
      })
  ));
}

export const autumn = {
  check: (...args) => loadAutumn().then(c => c.check(...args)),
  track: (...args) => loadAutumn().then(c => c.track(...args)),
  customers: {
    getOrCreate: (...args) => loadAutumn().then(c => c.customers.getOrCreate(...args)),
  },
};

```

This façade exposes three critical methods: **`check`** for entitlement verification, **`track`** for usage metering, and **`customers.getOrCreate`** for customer provisioning. The [`vite-plugin-lean-worker-bundle.ts`](https://github.com/every-app/open-seo/blob/main/vite-plugin-lean-worker-bundle.ts) configuration explicitly excludes the heavy Autumn SDK from the initial bundle, reinforcing this lazy-load strategy.

## Customer Provisioning and Cloudflare KV Caching

When an organization first makes a request, the `getOrCreateOrganizationCustomer` function in [`src/server/billing/subscription.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/subscription.ts) ensures a corresponding customer exists in Autumn. To avoid redundant network calls, the result is cached in Cloudflare KV for 24 hours using the `customerEnsuredKey` pattern.

```typescript
// src/server/billing/subscription.ts
export async function getOrCreateOrganizationCustomer(context) {
  const cacheKey = customerEnsuredKey(context.organizationId);
  if (await env.KV.get(cacheKey)) return { id: context.organizationId };
  
  const customer = await autumn.customers.getOrCreate({
    customerId: context.organizationId,
    email: context.userEmail,
  });
  
  await env.KV.put(cacheKey, "1", { expirationTtl: CUSTOMER_ENSURED_TTL_SECONDS });
  return { id: customer.id };
}

```

*Source:* [`subscription.ts`](https://github.com/every-app/open-seo/blob/main/subscription.ts) lines 34-64

If the KV cache hit succeeds, the function returns immediately without calling Autumn's API. This optimization is crucial for high-traffic organizations where every request triggers billing context loading.

## Checking Plan Entitlements

OpenSEO represents paid capabilities as Autumn feature flags. Two critical checks determine access levels:

- **`customerHasPaidPlan`**: Verifies access using `AUTUMN_PAID_PLAN_FEATURE_ID`
- **`customerHasManagedAccess`**: Verifies access using `AUTUMN_MANAGED_ACCESS_FEATURE_ID`

Both functions forward to `autumn.check` and read the boolean `allowed` property:

```typescript
// src/server/billing/subscription.ts
export async function customerHasPaidPlan(customerId) {
  const result = await autumn.check({ 
    customerId, 
    featureId: AUTUMN_PAID_PLAN_FEATURE_ID 
  });
  return result.allowed;
}

```

*Source:* [`subscription.ts`](https://github.com/every-app/open-seo/blob/main/subscription.ts) lines 66-72

This pattern allows OpenSEO to gate features like advanced SEO reporting or managed access capabilities without maintaining subscription state locally.

## Credit Balance Management and Gating

OpenSEO implements a dual-balance credit system for usage-based billing:

1. **`AUTUMN_SEO_DATA_BALANCE_FEATURE_ID`**: Monthly free credits included with the subscription
2. **`AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID`**: Purchased top-up credits

Before consuming resources (such as DataForSEO API calls or LLM requests), the `checkUsageCreditsDepleted` function in [`subscription.ts`](https://github.com/every-app/open-seo/blob/main/subscription.ts) reads both balances via `autumn.check`. If the combined remaining amount is ≤ 0, the request is rejected. To avoid false positives from stale balance data, the code performs a double-read on the full customer object.

*Source:* [`subscription.ts`](https://github.com/every-app/open-seo/blob/main/subscription.ts) lines 84-112

The conversion rate between USD costs and Autumn credits is governed by `AUTUMN_SEO_DATA_CREDITS_PER_USD`, allowing precise cost attribution for variable API expenses.

## Recording Usage with autumn.track

When a request consumes resources, OpenSEO converts the USD cost to credits and deducts them using `autumn.track`. The `trackUsageCreditSpend` function respects a custom retry policy (`AUTUMN_TRACK_RETRY_OPTIONS`) that retries only on HTTP 429 responses to prevent double-charging during transient rate limits.

```typescript
// src/server/billing/subscription.ts
await autumn.track(
  {
    customerId: args.customerId,
    featureId: AUTUMN_SEO_DATA_BALANCE_FEATURE_ID,
    value: monthlyDeduct,
    properties: { … }
  },
  AUTUMN_TRACK_RETRY_OPTIONS,
);

```

*Source:* [`subscription.ts`](https://github.com/every-app/open-seo/blob/main/subscription.ts) lines 32-45

The `properties` object includes metadata about the specific SEO operation, enabling detailed usage analytics within the Autumn dashboard.

## Handling Subscription Changes via Webhooks

Autumn sends `billing.updated` events to `/api/autumn/webhook` when subscription states change. The handler in [`src/server/billing/autumn-webhook.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/autumn-webhook.ts) performs three critical operations:

1. **Verifies the Svix signature** to ensure webhook authenticity
2. **Validates the payload** structure
3. **Calls `syncAutumnCustomerStatus`** to reconcile the local database with Autumn's customer object

*Source:* [`autumn-webhook.ts`](https://github.com/every-app/open-seo/blob/main/autumn-webhook.ts) lines 7-55

The `syncAutumnCustomerStatus` function (located in [`src/server/billing/customer-status-sync.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/customer-status-sync.ts)) updates local records to reflect plan upgrades, downgrades, or credit purchases without requiring manual intervention.

## End-to-End Billing Flow

A typical request flows through the Autumn integration in this sequence:

1. **Customer Resolution**: `getOrCreateOrganizationCustomer` ensures the Autumn customer exists (cached in KV)
2. **Entitlement Check**: `customerHasPaidPlan` or `customerHasManagedAccess` queries feature flags
3. **Credit Validation**: `checkUsageCreditsDepleted` verifies sufficient monthly or top-up credits remain
4. **Usage Tracking**: `trackUsageCreditSpend` records consumption via `autumn.track` with retry logic
5. **State Synchronization**: Webhook handlers update local state when Autumn broadcasts billing changes

## Summary

- **Lazy-loaded façade**: The `autumn-js` SDK is dynamically imported via [`src/server/billing/autumn.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/autumn.ts) to minimize cold-start impact on Cloudflare Workers
- **Aggressive caching**: Customer existence is cached in Cloudflare KV for 24 hours to reduce API calls
- **Feature flagging**: Plan entitlements are checked via `autumn.check` using specific feature IDs like `AUTUMN_PAID_PLAN_FEATURE_ID`
- **Dual-balance system**: Monthly credits and top-up credits are tracked separately to support usage-based billing
- **Safe usage tracking**: `autumn.track` calls retry only on HTTP 429 to prevent double-charging
- **Webhook synchronization**: Svix-signed webhooks trigger `syncAutumnCustomerStatus` to keep local subscription data accurate

## Frequently Asked Questions

### What is Autumn and why does OpenSEO use it?

Autumn is a SaaS subscription management platform that handles feature entitlements, usage metering, and billing webhooks. OpenSEO uses it to avoid building custom subscription logic, instead delegating customer provisioning, credit balance tracking, and plan enforcement to Autumn's API.

### How does OpenSEO prevent double-charging when tracking usage?

The `trackUsageCreditSpend` function passes `AUTUMN_TRACK_RETRY_OPTIONS` to `autumn.track`, which configures the SDK to retry only on HTTP 429 (rate limit) responses. This prevents duplicate charges that could occur if retries happened on network errors or 5xx responses after the server-side operation already succeeded.

### What happens when an organization runs out of credits?

Before processing expensive operations, `checkUsageCreditsDepleted` queries both the monthly and top-up credit balances via `autumn.check`. If the combined remaining credits are zero or negative, the function returns early and blocks the request, prompting the user to purchase additional top-up credits.

### How does the webhook integration keep subscription data in sync?

When a user upgrades their plan or purchases credits, Autumn sends a `billing.updated` webhook to [`src/server/billing/autumn-webhook.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/autumn-webhook.ts). The handler verifies the Svix signature, extracts the `customer_id`, and invokes `syncAutumnCustomerStatus` to update the local database, ensuring OpenSEO's internal state matches Autumn's source of truth within seconds.