@roostjs/billing

Abstract billing provider interface with a Stripe adapter. Implemented using only fetch for Cloudflare Worker compatibility — no Node.js SDK.

Installation

bun add @roostjs/billing

Configuration

Required environment variables:

STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

Register the service provider:

import { BillingServiceProvider } from '@roostjs/billing';
app.register(BillingServiceProvider);

Resolve the provider via the BillingProviderToken symbol:

import { BillingProviderToken } from '@roostjs/billing';
const billing = container.resolve(BillingProviderToken);

BillingProvider Interface

The abstract interface implemented by StripeProvider (and FakeBillingProvider). Resolve via BillingProviderToken to stay provider-agnostic.

createCustomer(params: CreateCustomerParams): Promise<CreateCustomerResult>

Create a billing customer record. Returns the provider-assigned customer ID.

subscribe(params: SubscribeParams): Promise<SubscribeResult>

Create a subscription for a customer on the given price.

cancelSubscription(subscriptionId: string): Promise<void>

Cancel a subscription. The subscription remains active until the end of the billing period.

resumeSubscription(subscriptionId: string): Promise<void>

Resume a cancelled subscription before the billing period ends.

swapSubscription(subscriptionId: string, newPriceId: string): Promise<SubscribeResult>

Change an existing subscription to a different price. Prorates immediately.

getSubscriptionStatus(subscriptionId: string): Promise<SubscriptionStatus>

Returns the current status string. Possible values: 'active', 'trialing', 'past_due', 'canceled', 'incomplete', 'incomplete_expired', 'paused'.

createCheckoutSession(params: CheckoutSessionParams): Promise<CheckoutSessionResult>

Create a hosted checkout session. The result contains a url to redirect the user to.

createPortalSession(params: PortalSessionParams): Promise<PortalSessionResult>

Create a customer portal session. The result contains a url for the portal.

reportUsage(params: UsageRecordParams): Promise<void>

Report metered usage for a subscription item.

parseWebhookEvent(request: Request, webhookSecret: string): Promise<WebhookEvent>

Verify the webhook signature using the Web Crypto API and parse the event payload. Throws if the signature is invalid.

StripeProvider

Concrete implementation of BillingProvider using Stripe's REST API via fetch. Registered by BillingServiceProvider.

StripeClient

Low-level HTTP client for the Stripe REST API. Used internally by StripeProvider. Import directly when you need raw Stripe API access beyond what BillingProvider exposes.

verifyStripeWebhook

verifyStripeWebhook(request: Request, secret: string): Promise<WebhookEvent>

Verify a Stripe webhook signature using the Web Crypto API and return the parsed event. Throws WebhookVerificationError if the signature is invalid or missing.

FakeBillingProvider

In-memory implementation of BillingProvider for use in tests. Records all calls and returns synthetic results without network requests.

import { FakeBillingProvider } from '@roostjs/billing';

const fake = new FakeBillingProvider();
const customer = await fake.createCustomer({ name: 'Test', email: 'test@example.com' });

Use the Billing helper object to enable/disable the fake globally:

import { Billing } from '@roostjs/billing';

const fake = Billing.fake();   // enable fake mode, returns FakeBillingProvider
Billing.restore();             // disable fake mode

SubscribedMiddleware

Middleware that gates access to users with an active subscription. Returns 402 Payment Required if the user has no active subscription.

import { SubscribedMiddleware } from '@roostjs/billing';
app.useMiddleware(SubscribedMiddleware);

OnTrialMiddleware

Middleware that gates access to users currently on a trial subscription.

import { OnTrialMiddleware } from '@roostjs/billing';
app.useMiddleware(OnTrialMiddleware);

Types

interface CreateCustomerParams {
  name: string;
  email: string;
  metadata?: Record<string, string>;
}

interface CreateCustomerResult {
  providerId: string;
}

interface SubscribeParams {
  customerId: string;
  priceId: string;
  trialDays?: number;
  metadata?: Record<string, string>;
}

interface SubscribeResult {
  subscriptionId: string;
  status: SubscriptionStatus;
  currentPeriodEnd: string;
}

interface CheckoutSessionParams {
  customerId: string;
  priceId: string;
  successUrl: string;
  cancelUrl: string;
  trialDays?: number;
}

interface CheckoutSessionResult {
  sessionId: string;
  url: string;
}

interface PortalSessionParams {
  customerId: string;
  returnUrl: string;
}

interface PortalSessionResult {
  url: string;
}

interface UsageRecordParams {
  subscriptionItemId: string;
  quantity: number;
  timestamp: number;
}

interface WebhookEvent {
  type: string;
  data: unknown;
}

type SubscriptionStatus =
  | 'active'
  | 'trialing'
  | 'past_due'
  | 'canceled'
  | 'incomplete'
  | 'incomplete_expired'
  | 'paused';