How OpenSEO's Billing System Uses Autumn for Subscriptions: A Technical Deep Dive
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. 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:
// 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 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 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.
// 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 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 usingAUTUMN_PAID_PLAN_FEATURE_IDcustomerHasManagedAccess: Verifies access usingAUTUMN_MANAGED_ACCESS_FEATURE_ID
Both functions forward to autumn.check and read the boolean allowed property:
// 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 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:
AUTUMN_SEO_DATA_BALANCE_FEATURE_ID: Monthly free credits included with the subscriptionAUTUMN_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 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 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.
// 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 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 performs three critical operations:
- Verifies the Svix signature to ensure webhook authenticity
- Validates the payload structure
- Calls
syncAutumnCustomerStatusto reconcile the local database with Autumn's customer object
Source: autumn-webhook.ts lines 7-55
The syncAutumnCustomerStatus function (located in 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:
- Customer Resolution:
getOrCreateOrganizationCustomerensures the Autumn customer exists (cached in KV) - Entitlement Check:
customerHasPaidPlanorcustomerHasManagedAccessqueries feature flags - Credit Validation:
checkUsageCreditsDepletedverifies sufficient monthly or top-up credits remain - Usage Tracking:
trackUsageCreditSpendrecords consumption viaautumn.trackwith retry logic - State Synchronization: Webhook handlers update local state when Autumn broadcasts billing changes
Summary
- Lazy-loaded façade: The
autumn-jsSDK is dynamically imported viasrc/server/billing/autumn.tsto 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.checkusing specific feature IDs likeAUTUMN_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.trackcalls retry only on HTTP 429 to prevent double-charging - Webhook synchronization: Svix-signed webhooks trigger
syncAutumnCustomerStatusto 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. 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →