@vynxc/better-stripe
v0.1.0
Published
Extended Stripe plugin for Better Auth with one-time payment support
Readme
better-stripe
Extended Stripe plugin for Better Auth with one-time payment support.
Fork of @better-auth/stripe v1.5.6 with one-time payment functionality adapted from better-auth#4892.
Features
- All existing
@better-auth/stripesubscription features (upgrade, cancel, restore, billing portal, seat-based billing, organizations) - One-time payments — create checkout sessions for single purchases, track payment status, list payment history
- Per-product
onPaymentCompletecallbacks - Promotion codes and automatic tax support
- 16 automated tests including integration tests with real Stripe webhook signature verification
Installation
npm install better-stripe stripePeer dependencies: better-auth, @better-auth/core, better-call, stripe (v18-20)
Quick Start
Server
import { betterAuth } from "better-auth";
import Stripe from "stripe";
import { stripe } from "better-stripe";
const auth = betterAuth({
// ...your config
plugins: [
stripe({
stripeClient: new Stripe(process.env.STRIPE_SECRET_KEY!),
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
createCustomerOnSignUp: true,
// One-time payments
payments: {
enabled: true,
products: [
{
name: "lifetime-access",
priceId: "price_xxx", // from Stripe dashboard
onPaymentComplete: async ({ payment, product }) => {
console.log(`Payment ${payment.id} completed for ${product.name}`);
// Grant access, send email, etc.
},
},
],
successUrl: "/thank-you",
cancelUrl: "/pricing",
},
// Subscriptions (optional, same as @better-auth/stripe)
subscription: {
enabled: true,
plans: [
{ name: "starter", priceId: "price_starter" },
{ name: "pro", priceId: "price_pro" },
],
},
}),
],
});Client
import { createAuthClient } from "better-auth/client";
import { stripeClient } from "better-stripe/client";
const client = createAuthClient({
plugins: [
stripeClient({
subscription: true,
payments: true,
}),
],
});Webhook Endpoint
Add the webhook route to your Stripe dashboard or use the CLI for local development:
https://your-app.com/api/auth/stripe/webhook# Local development
stripe listen --forward-to http://localhost:3000/api/auth/stripe/webhookAPI Reference
Payment Endpoints
POST /payment/create-session
Create a Stripe Checkout session for a one-time payment.
const { data } = await client.payment.createSession({
productName: "lifetime-access",
successUrl: "/thank-you", // optional, uses plugin default
cancelUrl: "/pricing", // optional, uses plugin default
quantity: 1, // optional, default 1
metadata: { coupon: "SAVE10" }, // optional
disableRedirect: false, // optional, default false
});
// data.url — Stripe Checkout URL (redirect user here)
// data.sessionId — Stripe session ID
// data.paymentId — database payment record ID
// data.redirect — whether to auto-redirectGET /payment/status
Get the status of a payment. Automatically syncs with Stripe if the payment hasn't succeeded yet.
const { data } = await client.payment.status({
query: { paymentId: "xxx" },
});
// or by session ID:
const { data } = await client.payment.status({
query: { sessionId: "cs_test_xxx" },
});
// data.status — "requires_payment_method" | "succeeded" | "canceled" | ...
// data.amount — amount in cents
// data.currency — "usd"
// data.stripePaymentIntentId — Stripe PaymentIntent IDGET /payment/list
List payments for the authenticated user.
const { data } = await client.payment.list({
query: {
status: "succeeded", // optional filter
limit: 10, // optional, default 10
offset: 0, // optional, default 0
},
});
// data — array of Payment objectsSubscription Endpoints
All subscription endpoints from @better-auth/stripe are preserved:
| Endpoint | Method | Description |
|----------|--------|-------------|
| /subscription/upgrade | POST | Create or upgrade a subscription |
| /subscription/cancel | POST | Cancel a subscription |
| /subscription/restore | POST | Restore a canceled subscription |
| /subscription/list | GET | List active subscriptions |
| /subscription/billing-portal | POST | Create a billing portal session |
| /subscription/success | GET | Handle post-checkout redirect |
| /stripe/webhook | POST | Handle Stripe webhook events |
Configuration
Plugin Options
stripe({
// Required
stripeClient: Stripe, // Stripe SDK instance
stripeWebhookSecret: string, // Webhook signing secret
// Customer management
createCustomerOnSignUp?: boolean, // Auto-create Stripe customer on signup
onCustomerCreate?: (data, ctx) => Promise<void>,
getCustomerCreateParams?: (user, ctx) => Promise<Partial<Stripe.CustomerCreateParams>>,
// One-time payments
payments?: {
enabled: boolean,
products: StripeProduct[] | (() => StripeProduct[] | Promise<StripeProduct[]>),
requireEmailVerification?: boolean, // default false
successUrl?: string,
cancelUrl?: string,
allowPromotionCodes?: boolean, // default false
automaticTax?: boolean, // default false
authorizeReference?: (data, ctx) => Promise<boolean>,
getCheckoutSessionParams?: (data, ctx) => Promise<{ params?, options? }>,
},
// Subscriptions
subscription?: {
enabled: boolean,
plans: StripePlan[] | (() => StripePlan[] | Promise<StripePlan[]>),
requireEmailVerification?: boolean,
onSubscriptionComplete?: (data, ctx) => Promise<void>,
onSubscriptionUpdate?: (data) => Promise<void>,
onSubscriptionCancel?: (data) => Promise<void>,
onSubscriptionCreated?: (data) => Promise<void>,
onSubscriptionDeleted?: (data) => Promise<void>,
authorizeReference?: (data, ctx) => Promise<boolean>,
getCheckoutSessionParams?: (data, req, ctx) => Promise<{ params?, options? }>,
},
// Organizations (requires better-auth organization plugin)
organization?: {
enabled: true,
getCustomerCreateParams?: (org, ctx) => Promise<Partial<Stripe.CustomerCreateParams>>,
onCustomerCreate?: (data, ctx) => Promise<void>,
},
// Global
onEvent?: (event: Stripe.Event) => Promise<void>,
})Product Configuration
{
name: "lifetime-access", // required — used to reference the product
priceId: "price_xxx", // Stripe price ID (use this or lookupKey)
lookupKey: "lifetime_key", // alternative to priceId
description: "One-time access", // optional
group: "premium", // optional — for categorizing products
metadata: { tier: "gold" }, // optional
onPaymentComplete: async ({ event, stripeSession, payment, product }, ctx) => {
// Called after successful payment
// payment.id, payment.amount, payment.currency, payment.referenceId
},
}Database Schema
The plugin adds a payment table (when payments.enabled: true):
| Column | Type | Description |
|--------|------|-------------|
| id | string | Primary key |
| product | string | Product name |
| referenceId | string | User ID or custom reference |
| stripeCustomerId | string | Stripe customer ID |
| stripeSessionId | string | Checkout session ID |
| stripePaymentIntentId | string | Payment intent ID (set after payment) |
| priceId | string | Stripe price ID |
| status | string | Payment status |
| amount | number | Amount in cents (set after payment) |
| currency | string | Currency code (default: "usd") |
| metadata | string | JSON metadata |
The existing subscription and user tables from @better-auth/stripe are also included when their respective features are enabled.
Webhook Events
The plugin handles these Stripe webhook events:
| Event | Handler |
|-------|---------|
| checkout.session.completed | Updates payment/subscription records, calls onPaymentComplete or onSubscriptionComplete |
| customer.subscription.created | Creates subscription record |
| customer.subscription.updated | Updates subscription, handles cancellations and trial transitions |
| customer.subscription.deleted | Marks subscription as canceled |
All other events are passed to onEvent if configured.
Testing
Run Tests
npm run test:runThe test suite includes:
- Unit tests (
test/payment.test.ts) — 11 tests with mocked Stripe client - Integration tests (
test/integration.test.ts) — 5 tests with real Stripe webhook signature verification usinggenerateTestHeaderString
Dev Server
For manual testing with the Stripe CLI:
STRIPE_SECRET_KEY=sk_test_... \
STRIPE_WEBHOOK_SECRET=whsec_... \
STRIPE_PRICE_ID=price_... \
npm run dev:serverThen in another terminal:
stripe listen --forward-to http://localhost:3333/api/auth/stripe/webhookDifferences from @better-auth/stripe
| Feature | @better-auth/stripe | better-stripe |
|---------|----------------------|-----------------|
| Subscriptions | Yes | Yes |
| One-time payments | No | Yes |
| POST /payment/create-session | - | Yes |
| GET /payment/status | - | Yes |
| GET /payment/list | - | Yes |
| payment table | - | Yes |
| onPaymentComplete callback | - | Yes |
| Promotion codes (payments) | - | Yes |
| Automatic tax (payments) | - | Yes |
License
MIT
