Skip to content

One-time licence · Next.js + Supabase + Stripe

Everything you need to ship a SaaS this weekend.

Auth, Stripe subscriptions, a credit ledger, emails, and the pages around them — wired together and ready to deploy. And the part every other kit skips: the billing and ledger paths ship with a test suite that runs against a real Postgres, because the bugs worth catching are the ones a mock cannot reproduce.

A real deployment against a real Postgres. Sign up with an email, start a run, and watch the credits move into a hold and back out. Stripe runs in test mode, so nothing you do there charges anything.

Framework
Next.js App Router, TypeScript strict
Database
Supabase Postgres, RLS on every table
Payments
Stripe Checkout, portal, webhooks
Tests
Vitest, plus SQL against a real Postgres

01The problem

Billing bugs do not crash. They move money quietly.

Most subscription starter kits ship a webhook handler that was tested by clicking through Stripe once. That is enough to look correct and nowhere near enough to be correct. Four failures account for almost all of it, and none of them raise an error.

  1. 01The webhook arrives twice

    Stripe retries any delivery it did not get a 2xx for, and it retries on its own schedule. A handler with no idempotency record grants the credits again. The customer paid once and is holding twice the balance, and nothing in the app looks broken.

  2. 02Two requests spend the same credits

    Both read a balance of 10, both check that 8 is affordable, both write. The account ends at 2 instead of refusing the second request. Read-then-write in application code cannot prevent this at any level of care; only a conditional update inside the database can.

  3. 03A hold is never released

    A long job reserves credits and the process dies before it settles. The credits are neither spent nor spendable. Without something watching for holds that stopped reporting activity, they stay stranded until a human is told.

  4. 04The balance and the ledger disagree

    Every entry looks right and the stored total is still wrong, because one path wrote the balance without writing an entry. With no reconciliation pass, the first person to notice the drift is a customer.

02In the box

Everything in the repository, and what checks it.

Auth
Email and password, Google OAuth, SSR sessions, RLS on every table, profile row on signup
Covered byRoute-guard and policy tests
Subscriptions
Stripe Checkout, customer portal, idempotent webhook handler, plan gating
Covered byReplayed webhook fixtures
Credit ledger
Append-only entries, atomic check-and-decrement, reserve to settle to release holds, idle reaper, admin grants, reconciliation
Covered byConcurrency tests on a real Postgres
Metered endpoint
One credit-gated model call wired through the full hold lifecycle
Covered byFailure-path tests
Email
Resend transactional templates for welcome and receipt
Covered byTemplate + no-op tests
Docs
Setup, deploy guides for Vercel and Render, env reference, prompts written for coding agents
Covered byNot tested

Two rows say “not tested” because they are not. A kit that claims full coverage of everything is telling you nothing about the parts that matter.

03The ledger

Reserve, settle, release. One transaction each.

Long jobs cannot charge up front and cannot charge at the end. Up front over-bills every failure; at the end lets a user start work they cannot pay for. A hold does neither: the credits leave the spendable balance when the job starts, and what the job did not use comes back when it finishes.

supabase/migrations/0003_ledger.sqlsql
-- Reserve before the job starts. The balance check and the decrement are the
-- same UPDATE ... WHERE, so two concurrent callers can never both pass it.
select ledger_reserve(p_user => :user_id, p_cost => 40) as run_id;

-- Claim the right to deliver. Exactly one caller wins; the loser throws its
-- output away instead of charging the user twice for duplicated work.
select ledger_begin_delivery(p_run => :run_id);

-- Delivered. Charges the reservation and writes an append-only entry carrying
-- the balance it produced, so any single row can be audited on its own.
select * from ledger_settle(p_run => :run_id);

-- Or it failed. Returns the whole hold with the reason on the entry. Runs that
-- stop heartbeating are released by the reaper without anyone noticing first.
select * from ledger_release(p_run => :run_id, p_reason => 'provider_failed');
  • Application code never writes a balance. Every movement goes through a SECURITY DEFINER function, so there is one place to audit and one place to test.
  • Entries are append-only and carry the balance they produced, so a single row can be checked without replaying the table.
  • A reconciliation query re-derives every balance from its entries. If it disagrees with the stored value, that is a failing test rather than a support ticket.

None of that is worth taking on trust. Open the demo and run one: the credits leave your spendable balance, the run shows its id, its heartbeat and the deadline the reaper is holding it to, and the entry it writes carries the balance it produced.

04Questions

What this is, and what it is not.

Can I try it before buying?
Yes, and you should. There is a live deployment of this exact codebase at /demo: sign up with an email, start a run, and watch credits move into a hold and back out of a real Postgres. Stripe runs there in test mode, so nothing charges anything.
What do I actually get?
Access to a private Git repository holding the complete application: the Next.js app, the SQL migrations, the money-path test suite and the setup docs. You clone it, rename it, and it is your codebase from that point on.
What this is not.
It is not a hosted service, a no-code builder, an admin panel generator, or a CRM. It does not write your product. It removes the weeks you would otherwise spend building auth, billing and a credit system you do not fully trust.
Do I need my own Stripe and Supabase accounts?
Yes, and a Resend account if you want transactional email. All three have free tiers that cover development. Every variable the app reads is listed in .env.example with a note on what breaks when it is wrong.
Can I use it without the credit ledger?
Yes. The ledger is a self-contained migration plus the routes that call it. Subscriptions work without it. It is the reason to buy this kit rather than another one, but nothing forces you to use it.
How long do updates last?
One year: stack major versions and security bumps. The repository is yours permanently either way. A stated year beats a “forever” nobody in this category has kept.
Refunds.
Fourteen days, no questions. Repository access is revoked when a refund is issued.
Licence.
One developer, unlimited products, client work included. Reselling the kit itself is not permitted.
Is there support?
No SLA. Bugs against the kit get fixed and you get the fix. Questions about your own architecture get answered when there is time.

Start from a codebase you can trust with money.

One-time purchase. Fourteen-day refunds, no questions asked.

Not on sale yet — the store is not set up, so this button does nothing, and says so rather than pretending otherwise. The running instance is live today: try the demo.