@xenterprises/fastify-xstripe
v1.1.1
Published
Fastify plugin for Stripe webhooks with simplified, testable handlers for subscription events.
Downloads
9
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
