Logo FormaUI

FormaUI

All providers

Stripe

A complete, drop-in Stripe integration for Next.js App Router projects. It covers the full billing lifecycle — hosted Checkout, recurring subscriptions, plan upgrades/downgrades, cancellation, the Customer Portal, refunds, and plan-based authorization — with no UI. Copy the folder, set your environment variables, define your plans, and every piece is already wired to Next.js: Server Actions, a ready-to-copy webhook route, and typed React hooks for Client Components.

Dependencies

  • stripe: official Stripe Node SDK, used for every call to the Stripe API and for webhook signature verification.
  • zod: validates every input before it reaches the Stripe SDK (checkout, subscription changes, refunds, customer lookup).
  • server-only: fails the build if service.ts is ever imported from a Client Component, keeping the Secret Key out of the browser bundle.

Folder structure

payments/stripe/
  actions.ts          // "use server" — public API: import this from Server Components, forms, and Server Actions
  service.ts           // internal — raw logic against the Stripe SDK, never imported from components
  client.ts             // client helpers — call actions.ts directly and handle the resulting redirect
  hooks.ts               // React hooks (useCheckout, useCustomerPortal) with loading/error state
  schemas.ts              // Zod validation for every input
  types.ts                  // shared types (Plan, PaymentsSubscription, PaymentsCustomer, PaymentsError, ...)
  constants.ts               // technical config — metadata keys, env var names, default routes, API version
  plans.ts                     // plan catalog, Price IDs, and per-plan limits — the file you actually edit
  webhooks/
    handler.ts                  // internal — signature verification + event dispatch
    route.ts                     // copy as-is to app/api/webhooks/stripe/route.ts
  env.example
  setup.md

How the files fit together

actions.ts is the entry point. Every public operation — creating a Checkout session, opening the Customer Portal, changing or canceling a subscription, issuing a refund, checking whether a user has access — is exported here as a Server Action. This is the only file you should import from a Server Component, a <form action>, or another Server Action.

service.ts holds the raw implementation: it talks to the Stripe SDK, maps Stripe objects to the module's own types, and normalizes every error into a PaymentsError. It's marked internal on purpose — actions.ts and webhooks/handler.ts are its only consumers. Nothing here is safe to call from a component directly (it holds the Stripe Secret Key).

client.ts contains two small helpers (startCheckout, openCustomerPortal) that call the Server Actions in actions.ts and redirect the browser to the URL Stripe returns. No fetch, no API route — Next.js handles the client → server call transparently.

hooks.ts wraps those two helpers in useCheckout() and useCustomerPortal(), adding isLoading/error state so a button in a Client Component doesn't need to manage that by hand.

What actions.ts covers

  • Checkout — create a hosted Checkout session for a one-time payment or a recurring subscription, resolving/creating the Stripe Customer automatically.
  • Customer Portal — create a session of Stripe's hosted portal, where customers manage their payment method, invoices, and cancellation themselves.
  • Subscription management — read the current subscription, upgrade/downgrade plans (with automatic proration), cancel (at period end or immediately), and resume a subscription pending cancellation.
  • Refunds — full or partial refunds against a PaymentIntent.
  • Customer lookup — get or create a customer by user ID, fetch a customer by Stripe ID, or look one up without creating it.
  • AuthorizationhasSubscription, requireSubscription, requirePlan, and checkPlanLimit to gate Server Actions, Server Components, and per-plan feature limits.

Usage examples

Start a checkout from a Server Action

import { createCheckoutSession } from "@/payments/stripe/actions";
import { redirect } from "next/navigation";

async function subscribe(userId: string) {
  "use server";
  const { url } = await createCheckoutSession({ userId, planId: "pro" });
  redirect(url);
}

Start a checkout from a Client Component

"use client";
import { useCheckout } from "@/payments/stripe/hooks";

export function SubscribeButton({ userId }: { userId: string }) {
  const { checkout, isLoading } = useCheckout();

  return (
    <button onClick={() => checkout({ userId, planId: "pro" })} disabled={isLoading}>
      {isLoading ? "Redirecting..." : "Subscribe to Pro"}
    </button>
  );
}

Open the Customer Portal

"use client";
import { useCustomerPortal } from "@/payments/stripe/hooks";

export function ManageBillingButton({ customerId }: { customerId: string }) {
  const { openPortal, isLoading } = useCustomerPortal();

  return (
    <button onClick={() => openPortal({ customerId })} disabled={isLoading}>
      Manage subscription
    </button>
  );
}

Gate a Server Action behind a plan

import { requirePlan } from "@/payments/stripe/actions";

async function generateReport(userId: string) {
  "use server";
  await requirePlan({ userId, planId: "team" });
  // protected logic
}

Check subscription status in a Server Component

import { hasSubscription } from "@/payments/stripe/actions";

export default async function DashboardPage({ userId }: { userId: string }) {
  const subscribed = await hasSubscription(userId);
  // ...
}

Upgrade/downgrade a plan

import { updateSubscription } from "@/payments/stripe/actions";

await updateSubscription({ subscriptionId, planId: "team" });

Cancel and resume a subscription

import { cancelSubscription, resumeSubscription } from "@/payments/stripe/actions";

await cancelSubscription({ subscriptionId }); // cancels at period end by default
await resumeSubscription({ subscriptionId });

Issue a refund

import { createRefund } from "@/payments/stripe/actions";

await createRefund({ paymentIntentId, reason: "requested_by_customer" });

Check a plan limit

import { checkPlanLimit } from "@/payments/stripe/actions";

const { withinLimit, limit } = await checkPlanLimit(userId, "projects", currentProjectCount);

Webhook

webhooks/route.ts is copied as-is to app/api/webhooks/stripe/route.ts. Signature verification, parsing, and event dispatch are already handled — you only fill in the TODOs to sync events (subscription created/updated/canceled, payment failed, refunded) with your database.

What you can build with this module

This module gives you everything needed to run subscription billing for a SaaS product: a self-serve pricing page that starts Checkout, a billing settings page backed by the Customer Portal, plan-gated features and API routes via requirePlan/requireSubscription, usage limits per plan (seats, projects, API calls) via checkPlanLimit, and a support/refunds flow via createRefund. Pair it with an auth boilerplate (for userId) and any database to persist the webhook events, and you have a production-ready billing system without writing any Stripe integration code by hand.