npm.io
0.0.25 • Published 12h ago

@beignet/provider-payments-stripe

Licence
MIT
Version
0.0.25
Deps
1
Size
54 kB
Vulns
0
Weekly
0

@beignet/provider-payments-stripe

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/provider-webhooks-stripe with the generic @beignet/core/webhooks primitive instead.

Install

bun add @beignet/provider-payments-stripe stripe @beignet/core

Configure

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.

Wire the provider

// server/providers.ts
import { stripePaymentsProvider } from "@beignet/provider-payments-stripe";

export const providers = [
  // other providers...
  stripePaymentsProvider,
] 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...
};

If the provider contributes the port at startup, list payments as deferred in infra/app-ports.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 PaymentsPort
  • ctx.ports.stripe, an escape hatch with the Stripe client and publishable key

For 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 stripePaymentsProvider when Stripe env vars are configured.

Cut over from memory payments

Generated billing slices use memory payments for local development. Before using Stripe in a deployed app:

  1. Create the Stripe product and price, then set your app's billing price env var, such as BILLING_TEAM_PRICE_ID.
  2. Set STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, and optional STRIPE_PUBLISHABLE_KEY for the target environment.
  3. Replace createMemoryPaymentsProvider() in server/providers.ts with stripePaymentsProvider.
  4. Deploy billing migrations before accepting live webhook traffic.
  5. Configure the Stripe webhook endpoint at /api/webhooks/payments and subscribe to checkout, subscription, and invoice events.
  6. Run bun beignet doctor --strict and smoke a test-mode checkout.

See https://beignetjs.com/payments for the full cutover and webhook operations guide.

Create checkout

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.

Verify webhooks

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 { server } from "@/server";

export const runtime = "nodejs";

export const { POST } = createPaymentWebhookRoute({
  server,
  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/provider-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.

Instrumentation

The provider records custom provider events through the payments watcher:

  • payments.checkout.create
  • payments.checkout.created
  • payments.checkout.failed
  • payments.portal.create
  • payments.portal.created
  • payments.portal.failed
  • payments.refund.create
  • payments.refund.created
  • payments.refund.failed
  • payments.webhook.verified
  • payments.webhook.failed

Devtools displays these in the first-class payments view.

Failure behavior

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.

Local and tests

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 stripePaymentsProvider only when the target environment has STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET.

Deployment notes

  • Deploy billing migrations before accepting live Stripe webhooks.
  • Keep test-mode and live-mode secrets separate.
  • Use the endpoint-specific whsec_... value for each deployed route or local Stripe CLI session.
  • Keep long-running fulfillment behind Beignet jobs, outbox delivery, or listeners after the verified webhook is recorded.
  • Use beignet doctor --strict as part of cutover so memory payments wiring and missing Stripe env/config are caught before deployment.

Keywords