Payment Package

npm downloads

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 with stripe: { secretKey, webhookSecret } in PaymentModule.forRoot().
  • Extensible — Implement the PaymentProvider interface to add PayPal, Paddle, or custom gateways and register them alongside Stripe.
  • Optional controller — Ready-made POST /payment/checkout-session and POST /payment/webhook/:provider when 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 --> OtherAPI

Key components

  1. PaymentService — Delegates to the default or named provider: createCheckoutSession(), createCustomer(), getCustomer(), listSubscriptions(), getCheckoutSession(), parseWebhookEvent(), getProvider(), getProviderNames().
  2. PaymentModuleforRoot({ stripe?, providers?, defaultProvider? }); convenience stripe option creates StripePaymentProvider; providers accepts custom provider instances.
  3. PaymentController — Optional: POST /payment/checkout-session (body may include provider), POST /payment/webhook/:provider (requires raw body for signature verification).
  4. PaymentProvider — Interface to implement for new gateways; all providers return the same generic types (Customer, CheckoutSessionInfo, CreateCheckoutSessionResult, etc.).

Key features

FeatureDescription
Provider-agnostic APISame methods work across Stripe, custom providers
Stripe first-classStripePaymentProvider with checkout, customers, subscriptions, webhooks
Multiple providersRegister several; use defaultProvider or pass provider name per call
WebhooksparseWebhookEvent(providerName, payload, signature); controller route per provider
Optional controllerCheckout 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_... or sk_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

MethodDescription
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