Payment Package
The @hazeljs/payment package provides multi-provider payment integration for HazelJS. Use Stripe today; plug in PayPal, Paddle, or your own gateway with one interface. Checkout sessions, customers, subscriptions, and webhooks are exposed through a provider-agnostic API.
Purpose
Adding payments to an app usually means choosing one provider (Stripe, PayPal, etc.) and coupling your code to its SDK. Switching providers or supporting multiple gateways leads to duplicated logic and provider-specific conditionals. The @hazeljs/payment package addresses this by:
- One API, many providers — Same methods for checkout, customers, subscriptions, and webhooks. Use the default provider or pass a provider name when calling the service.
- Stripe included — First-class support via
StripePaymentProvider; configure withstripe: { secretKey, webhookSecret }inPaymentModule.forRoot(). - Extensible — Implement the
PaymentProviderinterface to add PayPal, Paddle, or custom gateways and register them alongside Stripe. - Optional controller — Ready-made
POST /payment/checkout-sessionandPOST /payment/webhook/:providerwhen you use the module.
Architecture
flowchart TB
subgraph App [Your App]
PaymentModule
PaymentService
PaymentController
end
subgraph Providers [Providers]
Stripe[StripePaymentProvider]
Custom[Custom Provider]
end
subgraph External [External]
StripeAPI[Stripe API]
OtherAPI[Other Gateway]
end
PaymentModule --> PaymentService
PaymentController --> PaymentService
PaymentService --> Stripe
PaymentService --> Custom
Stripe --> StripeAPI
Custom --> OtherAPIKey components
- PaymentService — Delegates to the default or named provider:
createCheckoutSession(),createCustomer(),getCustomer(),listSubscriptions(),getCheckoutSession(),parseWebhookEvent(),getProvider(),getProviderNames(). - PaymentModule —
forRoot({ stripe?, providers?, defaultProvider? }); conveniencestripeoption createsStripePaymentProvider;providersaccepts custom provider instances. - PaymentController — Optional:
POST /payment/checkout-session(body may includeprovider),POST /payment/webhook/:provider(requires raw body for signature verification). - PaymentProvider — Interface to implement for new gateways; all providers return the same generic types (
Customer,CheckoutSessionInfo,CreateCheckoutSessionResult, etc.).
Key features
| Feature | Description |
|---|---|
| Provider-agnostic API | Same methods work across Stripe, custom providers |
| Stripe first-class | StripePaymentProvider with checkout, customers, subscriptions, webhooks |
| Multiple providers | Register several; use defaultProvider or pass provider name per call |
| Webhooks | parseWebhookEvent(providerName, payload, signature); controller route per provider |
| Optional controller | Checkout session creation and webhook endpoint out of the box |
Installation
npm install @hazeljs/payment
For Stripe, set (or pass in code):
- STRIPE_SECRET_KEY — e.g.
sk_test_...orsk_live_... - STRIPE_WEBHOOK_SECRET — e.g.
whsec_...for webhook signature verification
Quick start (Stripe)
1. Register the module
import { 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
import { PaymentService } from '@hazeljs/payment';
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.url
3. Subscriptions
const result = await paymentService.createCheckoutSession({
successUrl: 'https://yourapp.com/success',
cancelUrl: 'https://yourapp.com/cancel',
customerId: stripeCustomerId,
subscription: {
priceId: 'price_xxx',
quantity: 1,
trialPeriodDays: 14,
},
});
4. Customers
const customer = await paymentService.createCustomer({
email: 'user@example.com',
name: 'Jane Doe',
metadata: { userId: 'your-internal-id' },
});
5. Webhooks
The controller exposes POST /payment/webhook/:provider (e.g. POST /payment/webhook/stripe). You must pass the raw request body to this route so the provider can verify the signature.
Handle events in your app:
const event = paymentService.parseWebhookEvent(
'stripe',
req.rawBody,
req.headers['stripe-signature']
);
if (event && typeof event === 'object' && 'type' in event) {
switch ((event as { type: string }).type) {
case 'checkout.session.completed':
// Fulfill order, grant access
break;
case 'customer.subscription.updated':
// Update subscription in your DB
break;
}
}
For Stripe you can type the event:
import type { StripeWebhookEvent } from '@hazeljs/payment';
const event = paymentService.parseWebhookEvent('stripe', body, sig) as StripeWebhookEvent;
Multiple providers
Register Stripe and custom providers, and optionally set a default:
import { PaymentModule, StripePaymentProvider, type PaymentProvider } from '@hazeljs/payment';
const myProvider: PaymentProvider = new MyPaymentProvider(config);
PaymentModule.forRoot({
defaultProvider: 'stripe',
stripe: { secretKey: '...', webhookSecret: '...' },
providers: {
mygateway: myProvider,
},
});
Use a specific provider when creating a session or handling webhooks:
await paymentService.createCheckoutSession(options, 'stripe');
await paymentService.createCheckoutSession(options, 'mygateway');
// Webhook URL: POST /payment/webhook/stripe or POST /payment/webhook/mygateway
Stripe-specific API (e.g. raw Stripe client):
import { PaymentService, StripePaymentProvider, STRIPE_PROVIDER_NAME } from '@hazeljs/payment';
const stripe = paymentService.getProvider<StripePaymentProvider>(STRIPE_PROVIDER_NAME);
const client = stripe.getClient(); // Stripe SDK instance
Adding a new provider
Implement the PaymentProvider interface and register it in forRoot({ providers: { name: instance } }):
import type { PaymentProvider } from '@hazeljs/payment';
import type {
CreateCheckoutSessionOptions,
CreateCheckoutSessionResult,
CreateCustomerOptions,
Customer,
CheckoutSessionInfo,
SubscriptionStatusFilter,
} from '@hazeljs/payment';
export class MyPaymentProvider implements PaymentProvider {
readonly name = 'mygateway';
async createCheckoutSession(options: CreateCheckoutSessionOptions): Promise<CreateCheckoutSessionResult> {
// Call your gateway API, return { sessionId, url }.
}
async createCustomer(options: CreateCustomerOptions): Promise<Customer> {
// Create customer in gateway; return { id, email, name?, metadata? }.
}
async getCustomer(customerId: string): Promise<Customer | null> {
// Retrieve and map to Customer.
}
async listSubscriptions(customerId: string, status?: SubscriptionStatusFilter) {
// Return { data: Subscription[] }.
}
async getCheckoutSession(sessionId: string): Promise<CheckoutSessionInfo> {
// Return { id, url, customerId?, subscriptionId?, status? }.
}
isWebhookConfigured(): boolean {
return Boolean(this.webhookSecret);
}
parseWebhookEvent(payload: string | Buffer, signature: string): unknown {
// Verify signature and return parsed event.
}
}
API summary
| Method | Description |
|---|---|
createCheckoutSession(options, provider?) | Create checkout session; returns { sessionId, url } |
createCustomer(options, provider?) | Create a customer |
getCustomer(customerId, provider?) | Retrieve a customer |
listSubscriptions(customerId, status?, provider?) | List subscriptions |
getCheckoutSession(sessionId, provider?) | Retrieve session (e.g. after redirect) |
parseWebhookEvent(providerName, payload, signature) | Verify and parse webhook event |
getProvider(name) | Get provider instance (e.g. for Stripe client) |
getProviderNames() | List registered provider names |
When to use it
Good fit: SaaS billing, one-time purchases, subscriptions, and any app that needs a single payment API with the option to support multiple gateways (Stripe now, others later) or use provider-specific features via getProvider().
Less ideal: Pure invoicing or complex billing logic that does not map to checkout sessions and webhooks; consider a dedicated billing service alongside this package.
What's next?
- Auth Package — Protect payment routes and associate customers with authenticated users
- Config Package — Load Stripe keys and webhook secrets from environment or config files