npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ciphercross/nestjs-stripe

v1.2.0

Published

NestJS Stripe integration package with Checkout-first and Connect Express support.

Readme

@ciphercross/nestjs-stripe

NestJS Stripe integration package with Checkout-first and Connect Express support.

Installation

npm install @ciphercross/nestjs-stripe stripe

Environment Variables

STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_API_VERSION=2024-06-20
DEFAULT_CURRENCY=usd

Quick Start

1. Import Core Module

import { StripeCoreModule } from '@ciphercross/nestjs-stripe';

@Module({
  imports: [
    StripeCoreModule.forRoot({
      secretKey: process.env.STRIPE_SECRET_KEY,
      webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
      apiVersion: process.env.STRIPE_API_VERSION || '2024-06-20',
      defaultCurrency: process.env.DEFAULT_CURRENCY || 'usd',
      productName: 'MyApp',
      environment: process.env.NODE_ENV === 'production' ? 'prod' : 'dev',
      webhookPath: '/stripe/webhook',
    }),
  ],
})
export class AppModule {}

2. Raw Body Setup (Critical for Webhooks)

Important: Do NOT use JSON parser on the webhook route, or configure raw body parser BEFORE JSON parser.

For Express:

import { json } from 'express';

app.use('/stripe/webhook', express.raw({ type: 'application/json' }));
app.use(json());

For Fastify, use the provided middleware or configure accordingly.

3. Implement Storage Adapters

import { StripeCustomerStorage } from '@ciphercross/nestjs-stripe';

@Injectable()
export class PrismaStripeCustomerStorage implements StripeCustomerStorage {
  constructor(private readonly prisma: PrismaService) {}

  async getCustomerId(subjectId: string): Promise<string | null> {
    const user = await this.prisma.user.findUnique({
      where: { id: subjectId },
      select: { stripeCustomerId: true },
    });
    return user?.stripeCustomerId ?? null;
  }

  async setCustomerId(subjectId: string, customerId: string): Promise<void> {
    await this.prisma.user.update({
      where: { id: subjectId },
      data: { stripeCustomerId: customerId },
    });
  }
}

4. Import Feature Modules

import {
  StripeCheckoutModule,
  StripeSubscriptionsModule,
  StripeConnectModule,
} from '@ciphercross/nestjs-stripe';

@Module({
  imports: [
    StripeCheckoutModule.forRoot({
      customerStorage: {
        provide: 'STRIPE_CUSTOMER_STORAGE',
        useClass: PrismaStripeCustomerStorage,
      },
    }),
    StripeSubscriptionsModule.forRoot({
      customerStorage: {
        provide: 'STRIPE_CUSTOMER_STORAGE',
        useClass: PrismaStripeCustomerStorage,
      },
      subscriptionSync: {
        provide: 'STRIPE_SUBSCRIPTION_SYNC',
        useClass: PrismaSubscriptionSync,
      },
    }),
    StripeConnectModule.forRoot({
      connectStorage: {
        provide: 'STRIPE_CONNECT_STORAGE',
        useClass: PrismaStripeConnectStorage,
      },
    }),
  ],
})
export class PaymentsModule {}

Usage Examples

One-Time Checkout

import { StripeCheckoutService, CreateCheckoutOneTimeDto } from '@ciphercross/nestjs-stripe';

@Controller('payments')
export class PaymentsController {
  constructor(private readonly stripeCheckout: StripeCheckoutService) {}

  @Post('checkout')
  async createCheckout(@Body() dto: CreateCheckoutOneTimeDto) {
    const session = await this.stripeCheckout.createOneTimeSession(dto);
    return { url: session.url, id: session.id };
  }
}

Example with application fee (for marketplace/connect scenarios):

@Post('checkout-with-fee')
async createCheckoutWithFee(@Body() dto: CreateCheckoutOneTimeDto) {
  // For marketplace: charge customer $100, keep $5 as platform fee
  const session = await this.stripeCheckout.createOneTimeSession({
    ...dto,
    connectedAccountId: 'acct_xxx', // Connected account to receive funds
    applicationFeeAmount: 500, // $5.00 in cents (5% commission)
  });
  return { url: session.url, id: session.id };
}

Subscription Checkout

import { StripeSubscriptionsService, CreateCheckoutSubscriptionDto } from '@ciphercross/nestjs-stripe';

@Controller('subscriptions')
export class SubscriptionsController {
  constructor(private readonly stripeSubscriptions: StripeSubscriptionsService) {}

  @Post('checkout')
  async createSubCheckout(@Body() dto: CreateCheckoutSubscriptionDto) {
    const session = await this.stripeSubscriptions.createSubscriptionCheckoutSession(dto);
    return { url: session.url, id: session.id };
  }
}

Billing Portal

@Post('portal')
async portal(@Body() dto: CreateBillingPortalDto) {
  const session = await this.stripeSubscriptions.createBillingPortalSession(
    dto.subjectId,
    dto.returnUrl,
  );
  return { url: session.url };
}

Connect Onboarding

import { StripeConnectService, CreateAccountLinkDto } from '@ciphercross/nestjs-stripe';

@Controller('connect')
export class ConnectController {
  constructor(private readonly stripeConnect: StripeConnectService) {}

  @Post('onboard')
  async onboard(@Body() dto: CreateAccountLinkDto) {
    const link = await this.stripeConnect.createOnboardingLink(
      dto.subjectId,
      dto.refreshUrl,
      dto.returnUrl,
    );
    return { url: link.url };
  }
}

Webhook Events

The webhook endpoint is automatically registered at /stripe/webhook (or your configured path).

Supported Events

The package automatically handles the following webhook events:

  • checkout.session.completed - Fired when a checkout session is completed
  • customer.subscription.created - Fired when a subscription is created
  • customer.subscription.updated - Fired when a subscription is updated
  • customer.subscription.deleted - Fired when a subscription is deleted
  • invoice.paid - Fired when an invoice payment succeeds
  • invoice.payment_failed - Fired when an invoice payment fails
  • account.updated - Fired when a Connect account is updated
  • payment_intent.succeeded - Fired when a payment intent succeeds
  • payment_intent.payment_failed - Fired when a payment intent fails
  • payout.paid - Fired when a payout is paid
  • payout.failed - Fired when a payout fails

Webhook Event Handling

Events are automatically dispatched to registered handlers. To handle events in your project:

  1. For one-time purchases: Implement your own logic in CheckoutSessionCompletedHandler or extend it:
@Injectable()
export class CustomCheckoutHandler extends CheckoutSessionCompletedHandler {
  constructor(
    private readonly ordersService: OrdersService,
    customerStorage: StripeCustomerStorage,
  ) {
    super(customerStorage);
  }

  async handle(event: Stripe.Event): Promise<void> {
    await super.handle(event);
    
    const session = event.data.object as Stripe.Checkout.Session;
    const orderId = session.metadata?.cc_order_id;
    
    if (orderId) {
      await this.ordersService.markPaid(orderId, session.id);
    }
  }
}
  1. For subscriptions: Implement StripeSubscriptionSync:
@Injectable()
export class PrismaSubscriptionSync implements StripeSubscriptionSync {
  constructor(private readonly prisma: PrismaService) {}

  async onCheckoutCompleted(session: Stripe.Checkout.Session): Promise<void> {
    // Handle subscription checkout completion
    const subjectId = session.metadata?.cc_subject_id;
    const plan = session.metadata?.cc_plan;
    // Save initial subscription state
  }

  async onSubscriptionEvent(event: Stripe.Event): Promise<void> {
    const subscription = event.data.object as Stripe.Subscription;
    const subjectId = subscription.metadata?.cc_subject_id;
    
    await this.prisma.user.update({
      where: { id: subjectId },
      data: {
        subscriptionStatus: subscription.status,
        subscriptionCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
        subscriptionCancelAtPeriodEnd: subscription.cancel_at_period_end,
      },
    });
  }

  async onInvoiceEvent(event: Stripe.Event): Promise<void> {
    const invoice = event.data.object as Stripe.Invoice;
    
    if (event.type === 'invoice.paid') {
      // Grant access, activate subscription
    } else if (event.type === 'invoice.payment_failed') {
      // Revoke access, notify user
    }
  }
}

Metadata Convention

All Stripe objects use standardized metadata keys with cc_ prefix:

  • cc_project - Project name
  • cc_env - Environment (dev/prod)
  • cc_subject_id - User or business ID
  • cc_subject_type - 'user' | 'business'
  • cc_order_id - Order ID for one-time purchases
  • cc_purpose - 'purchase' | 'subscription' | 'connect'
  • cc_plan - Plan identifier (e.g., 'PLUS_MONTHLY')
  • cc_source - 'mobile' | 'web' | 'admin'

Use buildStripeMetadata() helper to ensure consistency.

Idempotency

The package automatically generates idempotency keys for all operations to prevent duplicate payments:

  • One-time checkout: cc:checkout:purchase:<orderId>
  • Subscription checkout: cc:checkout:sub:<subjectId>:<priceId>
  • Connect account: cc:connect:account:<subjectId>

All Stripe API calls use these idempotency keys to ensure operations are idempotent.

Advanced Usage

Using Metadata Helper

Always use buildStripeMetadata() to ensure consistent metadata:

import { buildStripeMetadata, StripeMetadataKeys } from '@ciphercross/nestjs-stripe';

const metadata = buildStripeMetadata({
  project: 'MyApp',
  env: 'prod',
  subjectId: userId,
  subjectType: 'user',
  purpose: 'purchase',
  orderId: order.id,
  source: 'web',
});

// Access metadata keys
const orderId = session.metadata[StripeMetadataKeys.orderId];

Money Utilities

Convert between major and minor units:

import { toMinorUnits, fromMinorUnits } from '@ciphercross/nestjs-stripe';

const cents = toMinorUnits(10.50, 'usd'); // 1050
const dollars = fromMinorUnits(1050, 'usd'); // 10.50

Error Handling

import {
  isStripeError,
  getStripeErrorMessage,
  isRateLimitError,
} from '@ciphercross/nestjs-stripe';

try {
  await stripe.checkout.sessions.create(...);
} catch (error) {
  if (isRateLimitError(error)) {
    // Retry with exponential backoff
  } else if (isStripeError(error)) {
    console.error(getStripeErrorMessage(error));
  }
}

Complete Wallet Top-Up Example

import {
  StripeCheckoutService,
  CreateCheckoutOneTimeDto,
  SubjectType,
  Source,
  toMinorUnits,
} from '@ciphercross/nestjs-stripe';

@Controller('wallet')
export class WalletController {
  constructor(private readonly checkoutService: StripeCheckoutService) {}

  @Post('top-up')
  async topUpWallet(
    @Body('userId') userId: string,
    @Body('amount') amount: number, // e.g., 100 for $100
    @Body('email') email?: string,
  ) {
    const amountInCents = toMinorUnits(amount, 'usd');
    
    const session = await this.checkoutService.createOneTimeSession({
      subjectId: userId,
      subjectType: SubjectType.USER,
      orderId: `topup-${Date.now()}`,
      successUrl: 'https://app.com/wallet/success',
      cancelUrl: 'https://app.com/wallet/cancel',
      customerEmail: email,
      source: Source.WEB,
      lineItems: [
        {
          name: 'Wallet Top-Up',
          description: `Top-up wallet with $${amount}`,
          unitAmount: amountInCents,
          quantity: 1,
          currency: 'usd',
        },
      ],
    });

    return { url: session.url, sessionId: session.id };
  }
}

Connect Account Status

const account = await stripeConnect.getAccountStatus(businessId);

if (account?.charges_enabled && account?.payouts_enabled) {
  // Account is fully onboarded
}

Transfers to Connected Accounts

import { StripeTransfersService, CreateTransferDto } from '@ciphercross/nestjs-stripe';

@Controller('transfers')
export class TransfersController {
  constructor(private readonly transfersService: StripeTransfersService) {}

  @Post()
  async createTransfer(@Body() dto: CreateTransferDto) {
    const transfer = await this.transfersService.createTransfer({
      destinationAccountId: dto.destinationAccountId,
      amount: dto.amount, // in cents
      currency: dto.currency || 'usd',
      description: dto.description,
      transferGroup: dto.transferGroup, // Optional: group related transfers
      metadata: dto.metadata,
    });

    return { transferId: transfer.id, status: transfer.status };
  }

  @Get(':id')
  async getTransfer(@Param('id') transferId: string) {
    return await this.transfersService.getTransfer(transferId);
  }

  @Get()
  async listTransfers(@Query('destination') destinationAccountId?: string) {
    return await this.transfersService.listTransfers({
      destination: destinationAccountId,
      limit: 10,
    });
  }
}

Payouts (Standard and Instant)

import { StripePayoutsService, CreatePayoutDto } from '@ciphercross/nestjs-stripe';

@Controller('payouts')
export class PayoutsController {
  constructor(private readonly payoutsService: StripePayoutsService) {}

  @Post()
  async createPayout(@Body() dto: CreatePayoutDto) {
    const payout = await this.payoutsService.createPayout({
      accountId: dto.accountId, // Connected account ID
      amount: dto.amount, // in cents
      currency: dto.currency || 'usd',
      description: dto.description,
      statementDescriptor: dto.statementDescriptor,
      instant: dto.instant || false, // true for instant payouts
    });

    return {
      payoutId: payout.id,
      status: payout.status,
      arrivalDate: payout.arrival_date,
      method: payout.method, // 'standard' or 'instant'
    };
  }

  @Get(':id')
  async getPayout(
    @Param('id') payoutId: string,
    @Query('accountId') accountId?: string,
  ) {
    return await this.payoutsService.getPayout(payoutId, accountId);
  }

  @Get()
  async listPayouts(
    @Query('accountId') accountId?: string,
    @Query('status') status?: string,
  ) {
    return await this.payoutsService.listPayouts(accountId, {
      status: status as any,
      limit: 10,
    });
  }

  @Post(':id/cancel')
  async cancelPayout(
    @Param('id') payoutId: string,
    @Query('accountId') accountId?: string,
  ) {
    return await this.payoutsService.cancelPayout(payoutId, accountId);
  }
}

Dashboard Login Links

import { StripeConnectService } from '@ciphercross/nestjs-stripe';

@Controller('connect')
export class ConnectController {
  constructor(private readonly connectService: StripeConnectService) {}

  @Post('dashboard-link')
  async getDashboardLink(@Body('subjectId') subjectId: string) {
    const { url } = await this.connectService.createDashboardLoginLink(subjectId);
    return { url };
  }

  @Post('dashboard-link/:accountId')
  async getDashboardLinkById(@Param('accountId') accountId: string) {
    const { url } = await this.connectService.createDashboardLoginLinkById(accountId);
    return { url };
  }
}

Payment Intent Webhooks

import {
  PaymentIntentEventsHandler,
  PaymentIntentEventHandler,
} from '@ciphercross/nestjs-stripe';

@Injectable()
export class CustomPaymentIntentHandler implements PaymentIntentEventHandler {
  constructor(
    private readonly paymentIntentHandler: PaymentIntentEventsHandler,
    private readonly walletService: WalletService,
  ) {}

  onModuleInit() {
    // Register this handler
    this.paymentIntentHandler.registerHandler(this);
  }

  async onPaymentIntentSucceeded(event: Stripe.Event): Promise<void> {
    const paymentIntent = event.data.object as Stripe.PaymentIntent;
    const subjectId = paymentIntent.metadata?.cc_subject_id;

    if (subjectId) {
      // Update wallet balance after successful payment
      await this.walletService.addFunds(
        subjectId,
        paymentIntent.amount,
        paymentIntent.currency,
      );
    }
  }

  async onPaymentIntentFailed(event: Stripe.Event): Promise<void> {
    const paymentIntent = event.data.object as Stripe.PaymentIntent;
    const subjectId = paymentIntent.metadata?.cc_subject_id;

    if (subjectId) {
      // Handle failed payment
      await this.walletService.notifyPaymentFailed(subjectId, paymentIntent.id);
    }
  }
}

Payout Webhooks

import {
  PayoutEventsHandler,
  PayoutEventHandler,
} from '@ciphercross/nestjs-stripe';

@Injectable()
export class CustomPayoutHandler implements PayoutEventHandler {
  constructor(
    private readonly payoutHandler: PayoutEventsHandler,
    private readonly withdrawalService: WithdrawalService,
  ) {}

  onModuleInit() {
    // Register this handler
    this.payoutHandler.registerHandler(this);
  }

  async onPayoutPaid(event: Stripe.Event): Promise<void> {
    const payout = event.data.object as Stripe.Payout;
    
    // Mark withdrawal as completed
    await this.withdrawalService.markCompleted(
      payout.id,
      payout.amount,
      payout.arrival_date,
    );
  }

  async onPayoutFailed(event: Stripe.Event): Promise<void> {
    const payout = event.data.object as Stripe.Payout;
    
    // Handle failed payout - refund to business wallet
    await this.withdrawalService.handleFailed(
      payout.id,
      payout.failure_code,
      payout.failure_message,
    );
  }
}

Module Configuration

Async Configuration

All modules support async configuration:

StripeCoreModule.forRootAsync({
  useFactory: (configService: ConfigService) => ({
    secretKey: configService.get('STRIPE_SECRET_KEY'),
    webhookSecret: configService.get('STRIPE_WEBHOOK_SECRET'),
    apiVersion: configService.get('STRIPE_API_VERSION'),
    defaultCurrency: configService.get('DEFAULT_CURRENCY'),
    productName: configService.get('PRODUCT_NAME'),
    environment: configService.get('NODE_ENV') === 'production' ? 'prod' : 'dev',
  }),
  inject: [ConfigService],
}),

Best Practices

  1. Always verify webhook signatures: The package does this automatically, but ensure raw body is configured correctly.

  2. Use webhooks as source of truth: Don't rely on redirect URLs for payment confirmation. Always verify via webhooks.

  3. Implement idempotency in your handlers: Even though Stripe provides idempotency, your handlers should also be idempotent.

  4. Store event IDs: Consider implementing an event store to prevent processing duplicate webhook events.

  5. Handle failures gracefully: Implement retry logic for failed webhook processing.

  6. Use metadata consistently: Always use buildStripeMetadata() to ensure consistent metadata across your application.

Troubleshooting

Webhook signature verification fails

  • Ensure raw body middleware is configured BEFORE JSON parser
  • Verify STRIPE_WEBHOOK_SECRET matches your Stripe dashboard webhook secret
  • Check that the request body is not being modified by other middleware

Customer not found errors

  • Ensure StripeCustomerStorage is properly implemented and registered
  • Verify that customer IDs are being stored after customer creation

Subscription sync not working

  • Ensure StripeSubscriptionSync is properly implemented and registered
  • Check that webhook events are being received (check Stripe dashboard)
  • Verify event handlers are registered in the dispatcher

License

MIT