HazelJS Payment Package
@hazeljs/payment provides multi-provider payment integration for HazelJS — Stripe, PayPal, Paddle, or custom gateways through one provider-agnostic API.
Quick Reference
- Purpose:
@hazeljs/paymentprovides a provider-agnostic payment API for checkout sessions, customers, subscriptions, and webhooks — supporting Stripe (built-in) with a pluggable interface for PayPal, Paddle, and custom gateways. - When to use: Use
@hazeljs/paymentwhen a HazelJS application needs payment processing. Supports switching or mixing payment providers without code changes. - Key concepts:
PaymentModule,PaymentService, checkout sessions, customer management, subscriptions, webhook handling, provider-agnostic API, Stripe provider. - Dependencies:
@hazeljs/core,stripe(for Stripe provider). - Common patterns: Register
PaymentModule.forRoot({ provider: 'stripe', apiKey })→ usePaymentServiceto create checkout sessions, manage customers, handle webhooks. - Common mistakes: Not verifying webhook signatures (security risk); hardcoding API keys; not handling payment failures and retries.
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
Recipes
Recipe: Stripe Checkout Session
// File: src/payment/payment.controller.ts
import { Controller, Post, Body, UseGuards, Req } from '@hazeljs/core';
import { AuthGuard } from '@hazeljs/auth';
import { PaymentService } from '@hazeljs/payment';
@Controller('payment')
@UseGuards(AuthGuard)
export class PaymentController {
constructor(private readonly payment: PaymentService) {}
@Post('checkout')
async createCheckout(@Req() req: any, @Body() body: { priceId: string }) {
return this.payment.createCheckoutSession({
customerId: req.user.stripeCustomerId,
lineItems: [{ price: body.priceId, quantity: 1 }],
successUrl: `${process.env.APP_URL}/success`,
cancelUrl: `${process.env.APP_URL}/cancel`,
});
}
}
Recipe: Handle Stripe Webhooks
// File: src/payment/webhook.controller.ts
import { Controller, Post, Req, Res, Headers } from '@hazeljs/core';
import { PaymentService } from '@hazeljs/payment';
import { Request, Response } from 'express';
@Controller('payment')
export class WebhookController {
constructor(private readonly payment: PaymentService) {}
@Post('webhook')
async handleWebhook(
@Req() req: Request,
@Res() res: Response,
@Headers('stripe-signature') signature: string,
) {
const event = this.payment.verifyWebhook(req.body, signature);
switch (event.type) {
case 'checkout.session.completed':
// Activate subscription or deliver product
break;
case 'invoice.payment_failed':
// Notify user of failed payment
break;
}
res.status(200).json({ received: true });
}
}
Related Resources
- Auth Package – Route protection and user association
- Config Package – Secret management for payment keys
- Webhook Package – Webhook handling (if available)