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 ifservice.tsis 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.
- Authorization —
hasSubscription,requireSubscription,requirePlan, andcheckPlanLimitto 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.