Introducing @hazeljs/payment: One API, Any Provider
Multi-provider payment integration for HazelJS — Stripe first, with one API for checkout, customers, subscriptions, and webhooks. Add PayPal or custom gateways without changing your code.
We're shipping @hazeljs/payment — a multi-provider payment package for HazelJS. Use Stripe today; plug in PayPal, Paddle, or your own gateway tomorrow, with one interface and no vendor lock-in.
Why a payment package?
Most apps need payments at some point: one-time purchases, subscriptions, or both. The usual approach is to wire the Stripe SDK (or PayPal, Paddle, etc.) directly into your controllers. That works, but when you want to support a second provider, run A/B tests, or switch gateways, you end up with if (provider === 'stripe') scattered everywhere and duplicated logic for checkout, customers, and webhooks.
We wanted a single API that works across providers. Same methods for creating checkout sessions, managing customers, listing subscriptions, and handling webhooks — so your business logic stays provider-agnostic and you can add or swap providers by configuration.
What's in the box
One API, many providers
- PaymentService —
createCheckoutSession(),createCustomer(),getCustomer(),listSubscriptions(),getCheckoutSession(),parseWebhookEvent(). Pass an optional provider name to use a specific gateway; otherwise the default provider is used. - PaymentModule — Register with
PaymentModule.forRoot({ stripe: { secretKey, webhookSecret }, providers: { mygateway: myProvider }, defaultProvider: 'stripe' }). The conveniencestripeoption creates StripePaymentProvider for you;providerslets you add custom or third-party implementations. - PaymentController (optional) —
POST /payment/checkout-sessionandPOST /payment/webhook/:providerso you can create sessions and receive webhooks without writing route handlers yourself. Webhook routes require the raw request body for signature verification.
Stripe first
Stripe is the first built-in provider. Configure it with your secret key and webhook secret (or env vars), and you get:
- Checkout sessions for one-time payments and subscriptions (including trials)
- Customer create/retrieve
- Subscription listing
- Webhook verification and event parsing
You can still use the raw Stripe client when you need something provider-specific: paymentService.getProvider<StripePaymentProvider>('stripe').getClient().
Extensible
Implement the PaymentProvider interface (createCheckoutSession, createCustomer, getCustomer, listSubscriptions, getCheckoutSession, isWebhookConfigured, parseWebhookEvent), instantiate your provider, and register it in forRoot({ providers: { paypal: new MyPayPalProvider(config) } }). Your app code keeps calling paymentService.createCheckoutSession(options, 'paypal') — no branching on provider names in business logic.
Quick start (Stripe)
1. Install and register the module
npm install @hazeljs/paymentimport { HazelApp } from '@hazeljs/core';
import { PaymentModule } from '@hazeljs/payment';
const app = new HazelApp({
modules: [
PaymentModule.forRoot({
stripe: {
secretKey: process.env.STRIPE_SECRET_KEY,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
},
}),
],
});2. Create a checkout session
const result = await paymentService.createCheckoutSession({
successUrl: 'https://yourapp.com/success',
cancelUrl: 'https://yourapp.com/cancel',
customerEmail: 'user@example.com',
clientReferenceId: userId,
lineItems: [{
priceData: {
currency: 'usd',
unitAmount: 1999,
productData: { name: 'Premium Plan', description: 'Monthly access' },
},
quantity: 1,
}],
});
// Redirect user to result.url3. Subscriptions and customers — Use subscription: { priceId, quantity, trialPeriodDays } for subscription checkouts, and createCustomer() / getCustomer() to attach payments to users. Store the provider customer ID in your DB and pass it as customerId when creating sessions.
4. Webhooks — Point Stripe (or your provider) at POST /payment/webhook/stripe. Ensure the route receives the raw body. Then in your app:
const event = paymentService.parseWebhookEvent('stripe', req.rawBody, req.headers['stripe-signature']);
// Handle event.type: checkout.session.completed, customer.subscription.updated, etc.Multiple providers in one app
You can register Stripe and one or more custom providers and choose per request:
PaymentModule.forRoot({
defaultProvider: 'stripe',
stripe: { secretKey: '...', webhookSecret: '...' },
providers: {
paypal: new MyPayPalProvider(paypalConfig),
},
});
// Later:
await paymentService.createCheckoutSession(options, 'stripe');
await paymentService.createCheckoutSession(options, 'paypal');Webhook URLs are per provider: /payment/webhook/stripe, /payment/webhook/paypal, etc.
When to use it
Good fit: SaaS billing, one-time purchases, subscriptions, and any app that wants a single payment API with the option to support multiple gateways or switch providers without rewriting business logic.
Docs: Payment package — full API, provider interface, and how to add your own gateway.
What's next
We'll keep improving the payment package based on feedback. If you need a second provider (e.g. PayPal) out of the box, open an issue or PR on GitHub. Until then, the PaymentProvider interface is small and well-defined so you can implement any gateway you need.
— The HazelJS Team