@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 containerFor 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().