Stripe-backed payments provider for Beignet applications.
This package adapts Stripe to the provider-neutral PaymentsPort from
@beignet/core/payments. Use it for hosted checkout, billing portal sessions,
refunds, and verified webhook ingestion. Keep plans, prices, subscriptions,
and billing authorization in your app's features/billing feature. Use
@beignet/core/entitlements for the app-facing product access decision shape.
For non-payment Stripe events, use @beignet/webhooks-stripe with
the generic @beignet/core/webhooks primitive instead.
createStripePaymentsProvider(...) returns the stable
StripePaymentsProvider type. StripePaymentsConfig describes its validated
config; the Zod schema remains internal.
bun add @beignet/provider-payments-stripe stripe @beignet/core
Set the server-side Stripe environment variables:
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_PUBLISHABLE_KEY is optional and exposed only through the Stripe escape
hatch. Never send STRIPE_SECRET_KEY or STRIPE_WEBHOOK_SECRET to the browser.
beignet doctor --strict checks that installed Stripe payments providers are
registered in server/providers.ts and that STRIPE_SECRET_KEY and
STRIPE_WEBHOOK_SECRET are present in app env examples or config.
// server/providers.ts
import { createStripePaymentsProvider } from "@beignet/provider-payments-stripe";
export const providers = [
// other providers...
createStripePaymentsProvider(),
] as const;
Add the payments port to your app ports:
// ports/index.ts
import type { PaymentsPort } from "@beignet/core/payments";
export type AppPorts = {
payments: PaymentsPort;
// other ports...
};
Use createStripePaymentsProvider(...) when you want to pass config directly
instead of reading STRIPE_* env vars. Options override env-derived values:
import { createStripePaymentsProvider } from "@beignet/provider-payments-stripe";
export const providers = [
createStripePaymentsProvider({
secretKey: secrets.stripeSecretKey,
webhookSecret: secrets.stripeWebhookSecret,
publishableKey: "pk_live_...",
}),
] as const;
Calling createStripePaymentsProvider() with no options uses the env-backed
configuration.
If the provider contributes the port at startup, list payments as deferred in
infra/port-wiring.ts.
The provider also exposes ctx.ports.stripe.client as an escape hatch for
Stripe operations the stable Beignet port does not model.
The provider contributes:
ctx.ports.payments, the standard Beignet PaymentsPortctx.ports.stripe, an escape hatch with the Stripe client and publishable keyFor a full app-owned billing slice, start with beignet make payments, run
beignet db generate and beignet db migrate, then swap the generated local
memory payments provider to createStripePaymentsProvider() when Stripe env vars are
configured.
Generated billing slices use memory payments for local development. Before using Stripe in a deployed app:
BILLING_TEAM_PRICE_ID.STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, and optional
STRIPE_PUBLISHABLE_KEY for the target environment.createMemoryPaymentsProvider() in server/providers.ts with
createStripePaymentsProvider()./api/webhooks/payments and
subscribe to checkout, subscription, and invoice events.bun beignet doctor --strict and smoke a test-mode checkout.See https://beignetjs.com/payments for the full cutover and webhook operations guide.
import { requireTenant } from "@beignet/core/ports";
import { env } from "@/lib/env";
import { useCase } from "@/lib/use-case";
export const createCheckoutSession = useCase
.command("billing.createCheckoutSession")
.run(async ({ ctx }) => {
const tenant = requireTenant(ctx);
return ctx.ports.payments.createCheckoutSession({
mode: "subscription",
lineItems: [{ priceId: "price_pro" }],
successUrl: `${env.APP_URL}/billing/success`,
cancelUrl: `${env.APP_URL}/billing`,
clientReferenceId: tenant.id,
metadata: { tenantId: tenant.id },
});
});
Treat the checkout URL as the start of a provider workflow, not fulfillment. Grant access from verified webhooks after the provider confirms payment or subscription state.
Avoid tenant-wide provider idempotency keys for checkout creation. Use Beignet HTTP idempotency on the route, or pass a per-request provider idempotency key when your app explicitly carries one through to the use case.
Webhook routes must read the raw body before parsing JSON so Stripe can verify
the signature. In Next.js apps, use createPaymentWebhookRoute(...) from
@beignet/next:
// app/api/webhooks/payments/route.ts
import { createPaymentWebhookRoute } from "@beignet/next";
import { handlePaymentWebhookUseCase } from "@/features/billing/use-cases";
import { getServer } from "@/server";
export const runtime = "nodejs";
export const { POST } = createPaymentWebhookRoute({
server: getServer,
handle: async ({ ctx, event }) => {
await handlePaymentWebhookUseCase.run({ ctx, input: event });
return {
status: 200,
body: { received: true },
};
},
});
This is the canonical route for Beignet billing flows. It calls
ctx.ports.payments.verifyWebhook(...) and passes your handler a normalized
PaymentWebhookEvent; you do not need a defineWebhook(...) catalog or
@beignet/webhooks-stripe for the generated payments slice.
The helper defaults to Stripe's stripe-signature header. The billing use case
should key idempotency on the provider event ID, update app-owned billing state
in a Unit of Work, record domain events, and use outbox/jobs/notifications for
post-commit effects.
Valid but unhandled Stripe event types should usually be acknowledged as no-ops after verification. Handler exceptions return 500 from the route helper so Stripe can retry; invalid signatures and malformed payloads return 400.
For local webhook testing, forward Stripe CLI events to the same route:
stripe listen --forward-to http://localhost:3000/api/webhooks/payments
Set STRIPE_WEBHOOK_SECRET to the whsec_... printed by that command for the
local app process. Dashboard endpoint secrets, live-mode secrets, and local
Stripe CLI secrets are different values.
The provider records custom provider events through the payments watcher:
payments.checkout.createpayments.checkout.createdpayments.checkout.failedpayments.portal.createpayments.portal.createdpayments.portal.failedpayments.refund.createpayments.refund.createdpayments.refund.failedpayments.webhook.verifiedpayments.webhook.failedDevtools displays these in the first-class payments view.
Stripe API failures throw provider errors from the corresponding payments port
operation. Invalid webhook signatures and malformed webhook payloads return 400
from createPaymentWebhookRoute(...); handler failures return 500 so Stripe can
retry. Verified but unhandled Stripe events should usually be acknowledged
after app code decides they are not relevant.
Generated billing slices start with the memory payments provider from
@beignet/core/payments so local development and tests do not need Stripe
credentials. Queue memory webhook events in tests and switch
server/providers.ts to createStripePaymentsProvider() only when the target
environment has STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET.
whsec_... value for each deployed route or local
Stripe CLI session.beignet doctor --strict as part of cutover so memory payments wiring and
missing Stripe env/config are caught before deployment.