@xenterprises/fastify-xstripe
v1.0.0
Published
Fastify plugin for Stripe webhooks with simplified, testable handlers for subscription events.
Readme
xStripe
Fastify v5 plugin for simplified, testable Stripe webhook handling. Focuses on subscription lifecycle management with clean, readable code.
Requirements
- Fastify v5.0.0+
- Node.js v20+
Features
- Simple webhook handling - Clean event-based architecture
- Built-in handlers - 20+ default handlers for common events
- Easy to test - Handlers are pure functions
- Type-safe - Full TypeScript support
- Readable - Clear, self-documenting code
- Flexible - Override any default handler
- Production-ready - Signature verification, error handling, logging
Installation
npm install @xenterprises/fastify-xstripe fastify@5Quick Start
import Fastify from 'fastify';
import xStripe from '@xenterprises/fastify-xstripe';
const fastify = Fastify({ logger: true });
await fastify.register(xStripe, {
apiKey: process.env.STRIPE_API_KEY,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
webhookPath: '/stripe/webhook', // Optional, defaults to /stripe/webhook
});
await fastify.listen({ port: 3000 });Custom Handlers
Override default handlers with your business logic:
await fastify.register(xStripe, {
apiKey: process.env.STRIPE_API_KEY,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
handlers: {
// Handle new subscription
'customer.subscription.created': async (event, fastify, stripe) => {
const subscription = event.data.object;
// Update your database
await fastify.prisma.user.update({
where: { stripeCustomerId: subscription.customer },
data: {
subscriptionId: subscription.id,
subscriptionStatus: subscription.status,
planId: subscription.items.data[0]?.price.id,
},
});
// Send welcome email
await fastify.email.send(
subscription.customer.email,
'Welcome to Premium!',
'<h1>Thanks for subscribing!</h1>'
);
},
// Handle subscription cancellation
'customer.subscription.deleted': async (event, fastify, stripe) => {
const subscription = event.data.object;
// Revoke access
await fastify.prisma.user.update({
where: { stripeSubscriptionId: subscription.id },
data: {
subscriptionStatus: 'canceled',
hasAccess: false,
},
});
},
// Handle failed payment
'invoice.payment_failed': async (event, fastify, stripe) => {
const invoice = event.data.object;
// Send payment failure email
const customer = await stripe.customers.retrieve(invoice.customer);
await fastify.email.send(
customer.email,
'Payment Failed',
'<p>Please update your payment method.</p>'
);
},
},
});Handler Function Signature
All handlers receive three parameters:
async function handler(event, fastify, stripe) {
// event - The Stripe webhook event object
// fastify - The Fastify instance (access to decorators)
// stripe - The Stripe client instance
}Supported Events
Subscription Events
customer.subscription.created- New subscriptioncustomer.subscription.updated- Subscription changedcustomer.subscription.deleted- Subscription canceledcustomer.subscription.trial_will_end- Trial ending in 3 dayscustomer.subscription.paused- Subscription pausedcustomer.subscription.resumed- Subscription resumed
Invoice Events
invoice.created- Invoice createdinvoice.finalized- Invoice ready for paymentinvoice.paid- Payment succeededinvoice.payment_failed- Payment failedinvoice.upcoming- Upcoming charge notification
Payment Events
payment_intent.succeeded- Payment successfulpayment_intent.payment_failed- Payment failed
Customer Events
customer.created- New customercustomer.updated- Customer details changedcustomer.deleted- Customer deleted
Payment Method Events
payment_method.attached- Payment method addedpayment_method.detached- Payment method removed
Checkout Events
checkout.session.completed- Checkout completedcheckout.session.expired- Checkout session expired
Testing Webhooks Locally
1. Use Stripe CLI
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login to Stripe
stripe login
# Forward webhooks to your local server
stripe listen --forward-to localhost:3000/stripe/webhook
# Trigger test events
stripe trigger customer.subscription.created
stripe trigger invoice.payment_failed2. Test Handlers Directly
Since handlers are pure functions, they're easy to test:
import { test } from 'node:test';
import assert from 'node:assert';
test('subscription.created handler', async () => {
const mockEvent = {
type: 'customer.subscription.created',
data: {
object: {
id: 'sub_123',
customer: 'cus_123',
status: 'active',
},
},
};
const mockFastify = {
log: { info: () => {} },
prisma: {
user: {
update: async (data) => {
assert.equal(data.where.stripeCustomerId, 'cus_123');
return {};
},
},
},
};
const mockStripe = {};
await handlers['customer.subscription.created'](
mockEvent,
mockFastify,
mockStripe
);
});Common Patterns
Update Database on Subscription Change
'customer.subscription.updated': async (event, fastify, stripe) => {
const subscription = event.data.object;
const previous = event.data.previous_attributes || {};
// Check what changed
if ('status' in previous) {
await fastify.prisma.user.update({
where: { stripeSubscriptionId: subscription.id },
data: { subscriptionStatus: subscription.status },
});
// Handle specific status changes
if (subscription.status === 'past_due') {
// Send payment reminder
}
}
}Send Notification Emails
'customer.subscription.trial_will_end': async (event, fastify, stripe) => {
const subscription = event.data.object;
const customer = await stripe.customers.retrieve(subscription.customer);
await fastify.email.send(
customer.email,
'Your trial ends soon!',
'<p>Convert to a paid plan to keep access.</p>'
);
}Track Failed Payments
'invoice.payment_failed': async (event, fastify, stripe) => {
const invoice = event.data.object;
await fastify.prisma.user.update({
where: { stripeSubscriptionId: invoice.subscription },
data: {
failedPaymentCount: { increment: 1 },
lastFailedPayment: new Date(),
},
});
// After 3 failed payments, suspend account
const user = await fastify.prisma.user.findUnique({
where: { stripeSubscriptionId: invoice.subscription },
});
if (user.failedPaymentCount >= 3) {
await fastify.prisma.user.update({
where: { id: user.id },
data: { accountSuspended: true },
});
}
}Configuration Options
| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| apiKey | string | Yes | - | Stripe API key |
| webhookSecret | string | No | - | Stripe webhook signing secret |
| webhookPath | string | No | /stripe/webhook | Webhook endpoint path |
| handlers | object | No | {} | Custom event handlers |
| apiVersion | string | No | 2024-11-20.acacia | Stripe API version |
Security
- Signature Verification: All webhooks are verified using Stripe's signature
- Raw Body Required: Plugin automatically handles raw body parsing
- Error Isolation: Handler errors don't prevent webhook acknowledgment
- Logging: All events and errors are logged
Best Practices
- Always acknowledge webhooks quickly - Do heavy processing async
- Make handlers idempotent - Stripe may send events multiple times
- Log everything - Use structured logging for debugging
- Test with Stripe CLI - Test all event types before production
- Monitor failed handlers - Set up alerts for handler errors
Environment Variables
STRIPE_API_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...Integration with Other xPlugins
Works seamlessly with other x-series plugins:
await fastify.register(xStripe, { /* ... */ });
await fastify.register(xTwilio, { /* ... */ }); // Send SMS notifications
await fastify.register(xConfig, { /* ... */ }); // Email notifications
// In your handlers:
'invoice.payment_failed': async (event, fastify, stripe) => {
// Send email via xConfig/SendGrid
await fastify.email.send(/* ... */);
// Send SMS via xTwilio
await fastify.sms.send(/* ... */);
}License
ISC
