Payments that just work.
mbl-payment wraps Stripe behind a provider abstraction — checkout sessions, subscriptions, billing portal, and webhook handling all wired up automatically. Drop it in and move on.
Install
npm install @madebylars.com/mbl-paymentWhat you get
Stripe checkout sessions
Single-item priceId shorthand or multi-item cart — POST /api/payments/checkout handles both. Zod-validated at the boundary.
Subscriptions
Start, cancel, and track Stripe subscriptions via useSubscription(). Trial days, customer IDs, and reactive subscription state included.
Billing portal
GET /api/payments/portal returns a Stripe Billing Portal URL so customers can self-serve plan changes and invoices.
Webhook handling
POST /api/payments/webhook verifies Stripe signatures and fires Nitro hooks your server plugins can listen to — no middleware wiring needed.
Provider abstraction
Stripe ships out of the box. Implement PaymentProvider and swap in PayPal, Paddle, or any other provider without touching module internals.
Auto-imported composables
useCheckout(), useSubscription(), usePayments() — all available without imports. Stripe.js is loaded client-side via a plugin.
Configuration
// 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,
},
},
},
})useCheckout()
<script setup>
const { redirectToCheckout, createSession, 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: '...' })
</script>useSubscription()
<script setup>
const { subscribe, cancel, currentSubscription, loading, error } = useSubscription()
// Open Stripe Checkout in subscription mode
await subscribe({
priceId: 'price_...',
customerId: 'cus_...',
successUrl: 'https://example.com/success',
cancelUrl: 'https://example.com/cancel',
trialDays: 14,
})
// Cancel an active subscription immediately
await cancel('sub_...')
</script>
<template>
<div v-if="currentSubscription">
Active until {{ new Date(currentSubscription.currentPeriodEnd * 1000).toLocaleDateString() }}
</div>
</template>usePayments()
<script setup>
const { stripe, getPortalUrl } = usePayments()
// Redirect to Stripe Billing Portal
const url = await getPortalUrl('cus_...')
if (url) window.location.href = url
// Raw Stripe.js instance for custom flows
stripe?.elements({ clientSecret: '...' })
</script>Server hooks
After each verified webhook, the module fires a Nitro hook your server plugins can listen to.
// server/plugins/payments.ts
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('payments:checkout:completed', async (data) => {
// 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) => { /* ... */ })
})Server API
| Route | Description |
|---|---|
| POST /api/payments/checkout | Create a Stripe Checkout session |
| POST /api/payments/webhook | Receive and verify Stripe webhook events |
| GET /api/payments/portal | Get a Stripe Billing Portal URL |
| POST /api/payments/cancel | Cancel an active subscription immediately |