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

@madebylars.com/mbl-payment

v1.0.1

Published

A portable Nuxt 4 payments module with Stripe support and a provider abstraction layer

Readme

nuxt-payments

npm version npm downloads License Nuxt

A portable Nuxt 4 payments module with full TypeScript support. Encapsulates all payment provider logic into a self-contained, reusable module — Stripe ships out of the box, and the provider abstraction layer makes it straightforward to add PayPal, Paddle, or any other provider later.

Features

  • Stripe support — checkout sessions, subscriptions, customer portal, webhook handling
  • Provider abstraction — swap or extend providers without touching module internals
  • Auto-imported composablesuseCheckout(), useSubscription(), usePayments()
  • Stripe.js client plugin$stripe available everywhere via usePayments()
  • Webhook signature verification — Stripe's constructEvent under the hood
  • Nuxt server hooks — react to payment events from anywhere in your app
  • Zod-validated routes — all API endpoints validate inputs at the boundary
  • No UI — logic and API only; bring your own components

Installation

npm install @madebylars.com/mbl-payment

Add the module to nuxt.config.ts:

export default defineNuxtConfig({
  modules: ['@madebylars.com/mbl-payment'],
  payments: {
    defaultProvider: 'stripe',
    providers: {
      stripe: {
        secretKey: process.env.STRIPE_SECRET_KEY,
        webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
        publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
      },
    },
  },
})

Environment variables

STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PUBLISHABLE_KEY=pk_live_...

Nuxt's built-in runtime config override also works — prefix any key with NUXT_:

NUXT_PAYMENTS_STRIPE_SECRET_KEY=sk_live_...

Server API

All routes are registered automatically. No extra setup needed.

POST /api/payments/checkout

Creates a Stripe Checkout session. Either priceId (single-item shorthand) or items (multi-item cart) must be provided.

Body

| Field | Type | Required | Default | |---|---|---|---| | priceId | string | One of priceId / items | — | | items | LineItem[] | One of priceId / items | — | | successUrl | string (URL) | Yes | — | | cancelUrl | string (URL) | Yes | — | | mode | 'payment' \| 'subscription' | No | 'payment' | | customerId | string | No | — | | quantity | number | No (only with priceId) | 1 | | metadata | Record<string, string> | No | — |

LineItem

| Field | Type | Required | Default | |---|---|---|---| | priceId | string | Yes | — | | quantity | number | No | 1 |

ResponseCheckoutSession

{
  "id": "cs_live_...",
  "url": "https://checkout.stripe.com/...",
  "status": "open",
  "customerId": "cus_..."
}

POST /api/payments/webhook

Receives and verifies Stripe webhook events. Point your Stripe dashboard webhook to this endpoint.

Headers required: stripe-signature

After verification, the module emits a Nuxt server hook so your app can react (see Server hooks).


GET /api/payments/portal?customerId=cus_...

Returns a Stripe Billing Portal URL for the given customer.

Query

| Field | Type | Required | |---|---|---| | customerId | string | Yes |

Response

{ "url": "https://billing.stripe.com/..." }

POST /api/payments/cancel

Cancels an active subscription immediately.

Body

| Field | Type | Required | |---|---|---| | subscriptionId | string | Yes |

Response

{ "cancelled": true }

Composables

All three composables are auto-imported — no import statement needed.

useCheckout()

const { createSession, redirectToCheckout, loading, error } = useCheckout()

// Single item — priceId shorthand
await redirectToCheckout({
  priceId: 'price_...',
  successUrl: 'https://example.com/success',
  cancelUrl: 'https://example.com/cancel',
})

// Multi-item cart
await redirectToCheckout({
  items: [
    { priceId: 'price_abc', quantity: 2 },
    { priceId: 'price_xyz', quantity: 1 },
  ],
  successUrl: 'https://example.com/success',
  cancelUrl: 'https://example.com/cancel',
})

// Or get the session back without redirecting
const session = await createSession({ priceId: 'price_...', successUrl: '...', cancelUrl: '...' })

| Return | Type | Description | |---|---|---| | createSession | (options: CheckoutOptions) => Promise<CheckoutSession \| null> | Creates a checkout session | | redirectToCheckout | (options: CheckoutOptions) => Promise<void> | Creates a session and redirects the browser | | loading | Ref<boolean> | True while the request is in flight | | error | Ref<Error \| null> | Set if the request failed |


useSubscription()

const { subscribe, cancel, currentSubscription, loading, error } = useSubscription()

// Start a Stripe Checkout flow for a subscription
await subscribe({
  priceId: 'price_...',
  customerId: 'cus_...',
  successUrl: 'https://example.com/success',
  cancelUrl: 'https://example.com/cancel',
})

// Cancel an active subscription
await cancel('sub_...')

| Return | Type | Description | |---|---|---| | subscribe | (options) => Promise<void> | Opens Stripe Checkout in subscription mode | | cancel | (subscriptionId: string) => Promise<void> | Cancels the subscription immediately | | currentSubscription | Ref<Subscription \| null> | Reactive subscription state (cleared on cancel) | | loading | Ref<boolean> | True while a request is in flight | | error | Ref<Error \| null> | Set if a request failed |


usePayments()

const { stripe, getPortalUrl } = usePayments()

// Raw Stripe.js instance — use for custom payment flows
stripe?.elements(...)

// Redirect to Stripe Billing Portal
const url = await getPortalUrl('cus_...')
if (url) window.location.href = url

| Return | Type | Description | |---|---|---| | stripe | Stripe \| null | Initialized Stripe.js instance | | getPortalUrl | (customerId: string) => Promise<string \| null> | Fetches the Billing Portal URL |


Server hooks

After each verified webhook event, the module calls a Nitro hook your server plugins can listen to. Register listeners in server/plugins/payments.ts:

// server/plugins/payments.ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('payments:checkout:completed', async (data) => {
    const session = data as Stripe.Checkout.Session
    // provision access, send welcome email, etc.
  })

  nitroApp.hooks.hook('payments:subscription:created', async (data) => { /* ... */ })
  nitroApp.hooks.hook('payments:subscription:updated', async (data) => { /* ... */ })
  nitroApp.hooks.hook('payments:subscription:cancelled', async (data) => { /* ... */ })
  nitroApp.hooks.hook('payments:payment:failed', async (data) => { /* ... */ })
})

| Hook | Stripe event | |---|---| | payments:checkout:completed | checkout.session.completed | | payments:subscription:created | customer.subscription.created | | payments:subscription:updated | customer.subscription.updated | | payments:subscription:cancelled | customer.subscription.deleted | | payments:payment:failed | invoice.payment_failed |


TypeScript

All types are exported from the module root:

import type {
  LineItem,
  CheckoutOptions,
  CheckoutSession,
  SubscriptionOptions,
  Subscription,
  RawWebhookEvent,
  WebhookResult,
  PaymentProvider,
  PaymentsModuleOptions,
} from '@madebylars.com/mbl-payment'

Adding a provider

  1. Create src/runtime/providers/myprovider/index.ts and implement the PaymentProvider interface:
import type { PaymentProvider } from '@madebylars.com/mbl-payment'

export function createMyProvider(): PaymentProvider {
  return {
    name: 'myprovider',
    async createCheckout(options) { /* ... */ },
    async createSubscription(options) { /* ... */ },
    async handleWebhook(event) { /* ... */ },
    async cancelSubscription(id) { /* ... */ },
  }
}
  1. Add a branch to src/runtime/server/utils/providerFactory.ts:
if (payments.defaultProvider === 'myprovider') {
  return createMyProvider()
}
  1. Extend PaymentsModuleOptions.providers in src/types.ts with any new config fields.

Nothing else needs to change — the composables and API routes delegate to whatever provider is active.


Contribution

# Install dependencies
npm install

# Generate type stubs and prepare the playground
npm run dev:prepare

# Develop with the playground
npm run dev

# Build the playground
npm run dev:build

# Run ESLint
npm run lint

# Run Vitest
npm run test
npm run test:watch

# Release a new version
npm run release