@roostjs/billing Guides

Task-oriented instructions for Stripe integration, subscriptions, webhooks, and metered billing.

How to configure Stripe credentials

Add your Stripe keys to .dev.vars for local development and to the Cloudflare dashboard for production.

STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
import { BillingServiceProvider } from '@roostjs/billing';

app.register(BillingServiceProvider);
// BillingProvider is now available in the container

For production, add secrets via the Cloudflare dashboard under Workers → Settings → Variables. See the environment guide for the full secrets workflow.

How to create a customer and subscription

Resolve BillingProviderToken from the container and call createCustomer, then subscribe. Store the returned IDs on your user record.

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

async function subscribeUser(userId: string, priceId: string) {
  const billing = container.resolve(BillingProviderToken);
  const user = await User.findOrFail(userId);

  // Create Stripe customer if not yet created
  if (!user.attributes.stripeCustomerId) {
    const customer = await billing.createCustomer({
      name: user.attributes.name,
      email: user.attributes.email,
      metadata: { userId },
    });
    user.attributes.stripeCustomerId = customer.providerId;
    await user.save();
  }

  // Subscribe to a plan
  const subscription = await billing.subscribe({
    customerId: user.attributes.stripeCustomerId,
    priceId,               // e.g. 'price_pro_monthly' from Stripe dashboard
    trialDays: 14,
  });

  user.attributes.stripeSubscriptionId = subscription.subscriptionId;
  await user.save();

  return subscription;
}

How to handle Stripe webhooks

Use parseWebhookEvent to verify the signature and parse the event. Return 200 immediately — process asynchronously if the handler is slow.

import { createFileRoute } from '@tanstack/react-router';
import { BillingProviderToken } from '@roostjs/billing';

export const Route = createFileRoute('/api/webhooks/stripe')({ component: () => null });

export async function POST(request: Request) {
  const billing = container.resolve(BillingProviderToken);

  let event;
  try {
    event = await billing.parseWebhookEvent(request, env.STRIPE_WEBHOOK_SECRET);
  } catch {
    return new Response('Invalid signature', { status: 400 });
  }

  switch (event.type) {
    case 'customer.subscription.created':
    case 'customer.subscription.updated': {
      const sub = event.data.object;
      await User.where('stripeSubscriptionId', sub.id)
        .first()
        .then((user) => {
          if (user) {
            user.attributes.subscriptionStatus = sub.status;
            return user.save();
          }
        });
      break;
    }
    case 'customer.subscription.deleted': {
      // Handle cancellation
      break;
    }
    case 'invoice.payment_failed': {
      // Notify user of payment failure
      break;
    }
  }

  return new Response('OK', { status: 200 });
}

Register the webhook endpoint URL in the Stripe dashboard under Developers → Webhooks. Use stripe listen --forward-to localhost:8787/api/webhooks/stripe for local testing.

How to gate routes by subscription status

Add SubscribedMiddleware or OnTrialMiddleware to routes that require an active subscription or trial.

import { SubscribedMiddleware, OnTrialMiddleware } from '@roostjs/billing';

// Require an active subscription
app.useMiddleware(SubscribedMiddleware);

// Or require an active trial
app.useMiddleware(OnTrialMiddleware);

Users without a matching subscription receive a 402 Payment Required response. To redirect instead, wrap the check in a custom middleware:

async function requireSubscription(request: Request, next: Handler): Promise<Response> {
  const user = await sessionManager.resolveUser(request);
  const billing = container.resolve(BillingProviderToken);
  const status = await billing.getSubscriptionStatus(user.stripeSubscriptionId);

  if (!['active', 'trialing'].includes(status)) {
    return Response.redirect('/billing/upgrade', 302);
  }

  return next(request);
}

How to implement metered billing

Report usage to Stripe after each billable action using reportUsage with the subscription item ID from the subscription object.

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

async function runAiQuery(userId: string, query: string) {
  const user = await User.findOrFail(userId);
  const billing = container.resolve(BillingProviderToken);

  // Run the billable action
  const result = await aiService.run(query);

  // Report one unit of usage
  await billing.reportUsage({
    subscriptionItemId: user.attributes.stripeSubscriptionItemId,
    quantity: 1,
    timestamp: Math.floor(Date.now() / 1000),
  });

  return result;
}

The subscriptionItemId (format: si_...) is the metered item within the subscription. Store it when you first create the subscription — it's available in the subscription object returned by subscribe().