Build SaaS Billing + Tax Core on nextjs-prisma-postgres
Production Stripe Billing core for a seat-based SaaS — plans, seats, proration, dunning, idempotent webhooks, Stripe Tax, invoices, customer portal — as a receipt-backed build pack for your own coding agent.
Stripe subscription billing looks simple until you hit proration, dunning, tax registration, and webhooks that arrive late, twice, or out of order — the exact edge cases that turn a weekend project into a support-ticket generator. This pack is a receipt-verified Next.js + Prisma + Stripe billing spine — plans, seats, proration, 14-day dunning grace, idempotent webhooks, Stripe Tax, hosted invoices, and a restricted customer portal — that your own coding agent builds in a couple of sessions instead of you researching Stripe's subscription-lifecycle traps one production incident at a time. The receipt requires 16 acceptance checks to pass against a pinned stack (Node 24, Next 16, Prisma 7, Stripe API `2026-06-24.dahlia`, Postgres 17) on a named model matrix before this pack is listed — not a claim that it "always works."
Example use cases
- **An indie B2B SaaS replacing a "charge everyone the same flat fee" MVP** with real per-seat billing (add seats now, prorated; remove seats at renewal, never mid-cycle) before it can credibly pitch a 20-seat enterprise buyer.
- **An agency building its third or fourth client SaaS this year**, tired of re-deriving Stripe's subscription-schedule rules (one active schedule per subscription, `current_period_end`'s move onto the item object) from scratch on every engagement.
- **A pre-launch startup whose first paying customers are EU businesses**, needing Stripe Tax's automatic calculation and B2B reverse-charge tax-ID collection correct on day one rather than bolted on after a tax authority notices.
- **A solo developer who inherited a hand-rolled webhook handler** that silently double-charged a customer during a downgrade, and wants the replacement built on a documented idempotency and out-of-order-safety design instead of another ad hoc fix.
Scale envelope
The shipped architecture is one Railway-hosted Postgres 17 instance plus one Next.js app instance, with Stripe as the system of record and the database used purely as a webhook-maintained cache (D1) of two small tables: one `Subscription` row per account, one `WebhookEvent` row per processed event (D2).
- **What it comfortably handles:** engineering estimate, not a load-tested benchmark — on the order of **1,000–10,000 subscribing accounts**. The reasoning: writes happen only on billing *events* (checkout, plan/seat change, renewal, payment failure), not on page views or API traffic — a seat-based B2B SaaS typically generates a handful of subscription-lifecycle events per account per billing cycle. Even at 10,000 accounts averaging 3 events/month, that's roughly 30,000 short, single-row transactional writes a month (an average of one every couple of minutes, with bursts around renewal dates) — well within what a single small Postgres instance handles for this shape of write, and each webhook is one short interactive transaction (D3/D4), not a long-running job.
- **First ceiling:** connection count on the single Postgres instance, and it shows up sooner from **customer-facing API/page traffic** (`/billing` loads, checkout/portal/invoice route calls) than from webhook volume, if you scale the Next.js app horizontally to more than one instance for that traffic. Postgres's own default is `max_connections = 100`; Prisma's own pooling guidance caps a single instance's pool around `(2 × vCPUs) + 1`, so a few app instances fit comfortably before you'd need to change anything — but it's a real, named ceiling, not an infinite one.
**Upgrade path:** a connection pooler (e.g. PgBouncer, or Railway's own pooling) in front of Postgres, then a vertical Postgres upgrade (Railway's larger Postgres plans) — no application code changes required, since D1 already treats the database as a pure cache with nothing that assumes a specific connection count.
- **Second, later ceiling:** the webhook route processes each event synchronously inside the request handler (D3/D4) with no queue in front of it — correct for the event volumes above, since Stripe already retries failed (non-200) deliveries with its own backoff, so the app doesn't need to build retry infrastructure. If your account volume gets large enough that Stripe's own delivery-attempts view shows sustained backlog or latency, the upgrade path is to make the webhook handler acknowledge fast and enqueue the actual DB write to a background worker — Stripe's own recommendation for high-volume endpoints, and deliberately out of this pack's scope.
**What it costs to skip the pack (modelled, not measured):** building this billing spine yourself the way most people actually work — one agent on a top-tier model, no cheap-tier routing, no parallel subagents, no prompt-caching discipline — models out at roughly **$100–390 in API tokens, likely around $195**. That's this pack's own measured build volume repriced at Opus-class rates, multiplied by an exploration factor for the 20 numbered decisions in `ARCHITECTURE.md`; the pack-guided build itself measured ≈$7 of tokens per verification run (August 2026 rates), because tasks route down to cheaper tiers. Honest reading: at the likely figure that clears the $149 price, but at the low end it doesn't — treat this as roughly break-even on tokens. The reason to buy is that the four bugs named in the table above are billing bugs, and billing bugs are found by customers.
What you get
A running Next.js service that is the complete billing spine of a seat-based B2B SaaS, with Stripe as the system of record.
**Features**
- Plan catalog (flat + per-seat tiers), synced to Stripe Products/Prices by an idempotent setup script keyed on `lookup_key` — no hardcoded price IDs anywhere.
- Subscribe via hosted Stripe Checkout with Stripe Tax: automatic tax calculation, B2B tax-ID collection (reverse charge), required billing address.
- Seats: add seats now (prorated, charged immediately), remove seats at renewal (never mid-cycle refunds), guarded against dropping below seats in use or above plan max.
- Plan changes: upgrades apply and invoice immediately (`always_invoice`, payment failure blocks the upgrade); downgrades apply at period end via a two-phase subscription schedule.
- Dunning: webhook-driven billing state machine with a 14-day read-only grace window computed at request time — no cron, no drift; Stripe Smart Retries does payment recovery.
- Webhooks: signature-verified, DB-enforced idempotent, out-of-order-safe processor for a deliberately minimal event set — including churn-and-return (a returning customer's new subscription cleanly replaces the canceled one). Honest limit: Stripe's `event.created` has 1-second resolution, so two distinct events inside the same second apply last-writer-wins; a same-second inversion cannot corrupt Stripe (the local row is a display/entitlement cache) and self-heals on the next subscription event.
- Invoices: history from Stripe with hosted PDF links (nothing stored locally).
- Customer portal: payment-method update, invoice history, cancel-at-period-end — plan switching deliberately disabled so your proration policy stays enforced in code.
- Acceptance suite + `verify.sh`: 16 receipt-grade checks (including network-free behavior probes of every billing route), exit 0 = correct build.
- Ops runbook for everything code can't do: Smart Retries settings, tax registrations, webhook endpoint, live smoke test.
**Screens / surfaces**
- `/billing` — plan, seat count with change controls, billing-state banner (active / read-only grace / blocked), invoice list, portal button (Tailwind 4 + shadcn/ui, pinned).
- Stripe-hosted Checkout (tax, tax IDs, addresses handled by Stripe).
- Stripe-hosted customer portal (restricted feature set).
- API: `POST /api/billing/{checkout,plan,seats,portal}`, `GET /api/billing/invoices`, `POST /api/stripe/webhook`.
How it works
01
Buy the pack
Instant download: PRD, architecture decisions, task graph, acceptance tests, scaffold, and per-phase prompts.
02
Feed it to your agent
Claude Code or Cursor builds inside the pinned scaffold — no context needed beyond the pack itself.
03
Verify
Run the pack's acceptance script. It checks the same things our verification harness checked.
nextjs-prisma-postgres versions lock
| node | 24.18.1 |
| next | 16.2.12 |
| react | 19.2.8 |
| react-dom | 19.2.8 |
| stripe | 22.3.0 |
| stripe-api-version | 2026-06-24.dahlia |
| prisma | 7.9.1 |
| @prisma/client | 7.9.1 |
| @prisma/adapter-pg | 7.9.1 |
| typescript | 5.9.3 |
| vitest | 4.1.10 |
| tailwindcss | 4.3.3 |
| @tailwindcss/postcss | 4.3.3 |
| shadcn | 4.13.1 |
| tailwind-merge | 3.6.0 |
| clsx | 2.1.1 |
| class-variance-authority | 0.7.1 |
| @types/node | 24.3.0 |
| @types/react | 19.2.2 |
| @types/react-dom | 19.2.2 |
| postgres | 17.10 |
Prescribed services
- Railway — Railway referral credits
Disclosure: some links on this page are affiliate links. We may earn a commission if you sign up through them, at no extra cost to you. We only link to services the pack actually verified against.