Skip to content
All posts

SaaS kit: Stripe Customer Portal for self-serve plan changes and cancels

DaveOTF Kit
8 min read
SaaS kit: Stripe Customer Portal for self-serve plan changes and cancels

Self-serve billing is not a settings screen you invent under deadline. It is the moment a paying customer changes plan, updates a card, or cancels without filing a ticket — and your product either keeps entitlement honest or drifts into manual spreadsheets. Stripe's Customer Portal exists so you do not rebuild that dashboard. Scope: the kit ships no Stripe Checkout, webhook handler, or billing. Add them via the Payments customization guide; this walkthrough starts after Checkout is in place. After you add Checkout and a purchase webhook to your SaaS app, the next step is wiring a portal session from a privileged route and using webhooks to keep workspace access aligned with the Stripe subscription.

This post is the BOFU how-to for that spine on OTF's SaaS Dashboard kit. It is not the live demo tour at Click-by-click tour of saas.otf-kit.dev, not the greenfield path at How to build a SaaS app with AI, and not the generic billing-ownership piece at Auth and billing you don't hand-roll.

What the kit ships and what you add before the portal

OTF lists the SaaS Dashboard kit on https://otf-kit.dev/templates/saas-dashboard and in the templates catalog. Standalone price is $99; it is also in the Everything Bundle on https://otf-kit.dev/pricing. Live proof before you buy sits at https://saas.otf-kit.dev. Public product claims that matter for this angle:

  • Operator screens for dashboard, analytics, issues, projects, teams, inbox, and settings
  • Auth session tables plus workspace, teams, projects, and issues in schema you migrate
  • The kit includes no billing; its customization guide shows how to add Checkout and a webhook.
  • Agent handoff files (CLAUDE.md, .cursorrules, and prompts under ai/prompts/) for extending the repo; product billing remains an implementation you add yourself

Checkout gets someone onto a plan. The Customer Portal is the ongoing surface: upgrades, downgrades, payment methods, invoices, and cancellation rules you configure once in Stripe. Stripe documents that flow as configure the portal, create a portal session, redirect to the session URL, then listen for subscription and payment-method webhooks (Integrate the customer portal).

Do not treat "Settings" chrome in the kit as a finished billing console. Settings can host the Manage billing button. Stripe hosts the portal.

Authenticated app opens Stripe Customer Portal then returns — Luna, Byte, and Nova with a portal key

Configure the portal before you write a route

Stripe's integration guide is explicit: register products and prices, then use the Dashboard (or the Billing Portal Configuration API) to decide what customers may do — invoice history, payment-method updates, subscription cancel, and whether plan changes are allowed. If customers can upgrade or downgrade, you must also set a product catalog the portal can present (Integrate the customer portal).

Practical order on an owned SaaS repo:

  1. Add and test Checkout plus the purchase webhook using the kit's Payments guide. Idempotency matters on Stripe retries — see Stripe sent the same purchase webhook 4 times.
  2. In the Stripe Dashboard Customer portal settings, enable only the actions you are ready to honor in your app (cancel at period end vs immediate, plan changes, payment methods).
  3. Preview the portal against a test customer before any production toggle. Stripe documents a read-only Preview and a per-customer "Open customer portal" action in sandboxes.
  4. Mirror live-mode configuration separately. Stripe keeps distinct portal configurations for live mode and each sandbox; changing one does not update the other.

If you skip the catalog while enabling plan changes, the portal cannot show honest upgrade targets. That is a configuration bug, not an app bug.

12 production screens. Login and database wired.

The SaaS Dashboard Kit includes login, a database, and a working dashboard. Live demo at saas.otf-kit.dev.

See the live demo

Create a portal session from a privileged route

Stripe's entry point is a Billing Portal Session. Your authenticated "Manage billing" control should POST to your own server. The server looks up the Stripe Customer id for the signed-in workspace member, creates a session with that customer and a return_url back into your app, then redirects to the short-lived url on the session object (Integrate the customer portal; Create a portal session).

Illustrative shape only — implement a server-side portal route in your app after adding Checkout; the kit does not ship a Stripe route:

// illustrative — privileged route after session auth
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function createBillingPortalSession(opts: {
  stripeCustomerId: string;
  returnUrl: string; // absolute URL into your signed-in settings page
}) {
  const session = await stripe.billingPortal.sessions.create({
    customer: opts.stripeCustomerId,
    return_url: opts.returnUrl,
  });
  return session.url; // redirect the browser here — never invent a portal URL
}

Hard rules that survive agent edits:

  • Authenticate before creating a session. A portal session is a capability grant for that Stripe Customer.
  • Persist the Stripe Customer id at Checkout time (or when the subscription is first created) on a server-owned table. Do not ask the browser to supply a customer id.
  • Redirect only to session.url returned by Stripe. Do not hardcode a portal hostname or paste a fake Checkout pay link into samples.
  • Keep secret keys and webhook signing secrets on the server. The kit's customization guide shows how to add a Checkout route and webhook. Keep portal session creation server-side too.

Optional: pass a specific configuration id when different customer cohorts need different portal features. Stripe documents that override on session create; otherwise the default Dashboard configuration applies.

Webhook pulses for update, cancel, and card land as green checks on entitlement — Dex and Nova

Webhooks keep entitlement aligned with the portal

The portal UI is not your source of truth. Stripe is. When a customer upgrades, downgrades, cancels, or changes a default payment method, your app must react through webhooks. Stripe's portal guide lists the events you should care about, including:

  • customer.subscription.updated — plan or quantity changes; grant or adjust product access from the price on the subscription items
  • customer.subscription.deleted — revoke access when the subscription ends
  • Cancel-at-period-end flows — listen for updates where cancellation is scheduled, then revoke only when the period actually ends (or when deleted fires), depending on how you configured the portal
  • payment_method.attached / payment_method.detached and customer.updated — keep local billing profile fields in sync; do not treat billing email as a login credential (Integrate the customer portal)

After you add Checkout and its purchase webhook to the kit, process portal events in your own handler:

  1. Verify the Stripe signature on every event.
  2. Deduplicate by event id so retries do not double-apply entitlement (same lesson as the purchase idempotency post).
  3. Update workspace plan / seat / status columns from the subscription object — not from what the success page guessed.
  4. Keep agent prompts pointed at "extend the webhook handler," not "add a second billing table in chat."

If an agent proposes a client-only "cancel" button that flips a local canceled flag without Stripe, reject it. Local flags drift. Portal + webhooks do not.

Where to add a Manage billing button

Public screen inventory for the SaaS kit includes Settings surfaces for profile, workspace, notifications, and appearance. That is the natural home for a Manage billing control that hits your portal-session route. The operator stays inside your product until redirect; Stripe renders plan change, card update, invoice history, and cancel UX; return_url brings them back to the same settings area.

Buyer checklist after clone:

  • After implementing Checkout and the webhook, configure the Stripe secret key and webhook signing secret in your environment. The kit does not ship a billing configuration.
  • After implementing Checkout, verify that your flow creates a Customer and Subscription in Stripe test mode.
  • Add a server-side field or table for stripe_customer_id (and a subscription id if needed); the kit does not include a billing row.
  • Add the authenticated portal-session route and a single Settings CTA
  • Enable portal features in test mode, click through upgrade/cancel, watch webhook rows update entitlement
  • Only then copy configuration to live mode and point production webhooks at the same handler shape

Agent session tip: load CLAUDE.md / .cursorrules and ask: "After Checkout and its webhook are implemented, add a Manage billing button that creates a Billing Portal Session for the workspace's Stripe Customer and redirects to session.url." Configure the Stripe product catalog before implementing plan choices.

When to buy the kit versus staying on Checkout-only glue

Stay on a thin Checkout script when you have no recurring customers yet and cancellation is still a reply to an email. Consider the SaaS kit as a starting repo if you are prepared to add Checkout and a Customer Portal, and most of these are true:

  • Operators already live in dashboard / issues / projects / teams screens you intend to keep
  • Subscriptions are the commercial model, not one-off payments
  • You want plan changes and card updates without building a billing UI
  • Webhook-confirmed entitlement must match what Stripe shows for the Customer
  • Maintenance happens in Cursor or Claude Code on a repository you own, with agent memory next to the code

Sandboxes remain useful for spikes. After implementation, the durable seam is: Checkout for acquisition, Customer Portal for self-serve lifecycle, webhooks for access — all on privileged routes in a repo you can migrate.

Product page: SaaS Dashboard kit. Pricing: https://otf-kit.dev/pricing. For the broader greenfield framing, read How to build a SaaS app with AI. For webhook retry discipline, keep Stripe webhook idempotency open beside this page.

Sources

kitsbackendtemplates
OTF SaaS Dashboard Kit

Ship the product, not the setup.

  • 12 production screens — auth, team, analytics, and settings
  • Login and a real database are wired in
  • AI configs pre-tuned so your agent extends instead of regenerates
Free starter

Ship your first screen tonight — free starter in your inbox

The quickstart, the SDK repo and the live kit demos, on this screen and in your inbox.

No spam. Unsubscribe any time.

Free with your email

Get the free AI configs pack

The AI files every kit ships with. Drop them in and your AI tool knows your project.

  • CLAUDE.mdProject context
  • .cursorrulesRules for Cursor
  • prompts/Task prompts

No spam. Unsubscribe any time.