Product · Seat-based billing

A SvelteKit SaaS starter where seat billing is a seam, a gate, and a set of tests — not a month

All three kits ship the billing architecture: a provider-neutral adapter, seat limits enforced in exactly one place, subscription states that block new seats, errors your UI can branch on, and tests for the unhappy paths. The payment provider adapter is the one thing you write. That is stated up front, not in the small print.

The short answer

What ships: the adapter interface every part of the app consumes, a single seat-limit gate at invite acceptance, five subscription states, machine-readable billing errors, and 357 / 390 / 325 tests covering plan changes, seat boundaries and adapter failures.

What does not ship: the adapter that talks to your payment provider. The bundled mock reports every org as active on a plan of MOCK_PLAN_SEATS (default 3) and returns null from createCheckoutUrl, because there is nothing external to link to. Each kit documents the four things a real adapter has to do.

So: if "billing" on your list means "the seat logic, the states, the failure modes and the tests are done", buy any of the three. If it means "a customer can enter a card today", nobody here can promise you that yet, and a page that pretended otherwise would waste your week.

What the billing layer actually does

Read the last column before choosing a kit. The guarantee is not uniform across the three databases, and a comparison that hid that would cost you a race condition in production.

AreaWhat is shippedWhere the guarantee holds
The integration seamA BillingAdapter interface the product consumes everywhere. Services and routes never import a payment SDK; the implementation is swapped in one file.Holds identically on all three kits. This is the part that does not change whichever one you buy.
Seat limit enforcementOne gate, at invite acceptance: the app checks subscription state and used seats, and refuses with an actionable error naming the fix. Inviting past your seat count is allowed on purpose — the limit surfaces when the person tries to join.Holds on all three. It is one function, and it is the only place seat limits are decided.
Subscription states that block new seatsFive states — none, active, trialing, past_due, canceled. Only active and trialing let anybody join. A past-due or canceled org is refused with a message that says to update billing.Holds on all three. It is the behaviour that stops a lapsed customer from adding users for free.
Machine-readable errorsBillingError carries a stable code (seat_limit, subscription_required, and on the Supabase kit also no_plan and adapter_error), so your UI branches on the code instead of matching message strings.Holds on all three. The Supabase kit adds two codes on top, so your error handling is a superset if you pick it.
Atomicity of the seat checkOn the Postgres kit, invite acceptance runs as one transaction: a per-org row lock, then the seat check, then the single-use claim, then the membership, then the audit row.POSTGRES ONLY. The guarantee is Postgres-only by construction — SQLite has one writer, Cloudflare D1 offers batch() atomicity but no interactive transactions, and PostgREST has no multi-statement transaction without an RPC. On those, seat limits need a database-side constraint or an RPC instead.
The payment provider adapterNot shipped. The mock reports every org as active on a plan of MOCK_PLAN_SEATS (default 3) and returns null from createCheckoutUrl, because there is nothing external to link to.This is the gap, and it is the same in all three kits. Each kit documents exactly what a real adapter has to do — map provider webhooks to a local subscription row, create a checkout session, add a webhook route that verifies a signature, and never trust a client-side success redirect for an entitlement.
Sales tax and merchant of recordCheckout is designed to run through a merchant of record, so the provider is the legal seller and handles tax — you are not registering as a merchant in every country your customers are in.A design decision, documented per kit. Which provider you pick stays your call, and the product does not depend on the choice.

Why pay for this instead of writing it

The decisions are already made, and they are the decisions that hurt

Where the limit is enforced, which subscription states block a join, what an error looks like, why checkout goes through a merchant of record, and why a client-side success redirect is never an entitlement. Those are not hard to write once you know them — they are hard because each one is a question you have to ask first, and the wrong default is discovered by a customer.

Every state is a test, and the tests already exist

Seat limits, plan changes, adapter failures and boundary conditions are covered in the suites all three kits ship, alongside a 95% coverage floor the build enforces. The cases that matter here are the unhappy ones: an org with no plan, an org past due, a downgrade below current usage, an adapter that throws. Those are exactly the rows that get skipped when you write billing yourself.

The adapter is one file, not a refactor

Each kit has a single wiring point for the adapter, and the services that consume it are unchanged when you swap it. The remaining work is the provider adapter itself, and the kit tells you what it has to do.

Auditing the money path comes free

The same transaction that claims a seat writes the audit row. When a customer asks "who added this user and when", the answer is in a table with no update or delete path, not in your email.

Which kit — the billing is the same in all three

KitDatabaseTestsBillingBuy
SvelteKit + Postgres StarterAny Postgres — provider-neutral325Strongest seat guarantee — the seat check and the invite claim run in one transaction under a per-org row lock. Postgres-only by construction.$79
SvelteKit + Supabase StarterSupabase (managed Auth, RLS, realtime)390Same seam and one gate; the adapter interface adds no_plan and adapter_error codes on top.$99
Multi-tenant SvelteKit StarterSQLite — one file, no database to run357Same seam and one gate. A single writer serialises writes, so a concurrent double-accept cannot interleave either.$79

One-time price, one licence, unlimited projects, commercial use permitted, lifetime updates, 30-day refund. A Team Licence is $249 for 3+ seats, invoiced individually.

Buy

Postgres

325 tests · provider-neutral · strongest seat guarantee

$79

Get the kit — $79

Supabase

390 tests · managed auth and RLS

$99

Get the kit — $99

SQLite / D1

357 tests · no database to run

$79

Get the kit — $79

Prefer one decision and the docs? All three kits compared ·pricing ·licence. Demos: SQLite · Supabase · Postgres.

Risks of choosing this

A buyer who finds these after paying is a refund. A buyer who finds them before paying is a customer.

No real payment adapter ships — you write it

This is the one thing on this page that could surprise you, so it is not buried. The seam, the enforcement gate, the states, the errors and the tests are all in the box; the adapter that talks to Lemon Squeezy, Paddle, Stripe or your merchant of record is not. Budget a focused day for it, plus a day to get a real webhook firing. If you would rather not, that is a legitimate reason to choose a product that ships it — and we do not have one.

The mock is permissive on purpose

Out of the box every organization looks active with 3 seats and checkout returns null. That is what makes the test suite hermetic, and it is also why the kit looks like it "works" before you have wired anything. Do not ship with the mock still installed.

Seat enforcement is not billing enforcement

The kit stops a 4th member joining a 3-seat plan. It does not charge anyone, retry a failed card, dunning a lapsed subscription or issue a refund — those are provider-side jobs. `past_due` is handled in the sense that it blocks new seats; what happens to the customer relationship is yours.

The strongest atomicity guarantee is Postgres-only

If you deploy on Cloudflare D1 you lose the row lock around invite acceptance, and with it the guarantee that two simultaneous accepts cannot both pass a seat check. The kit documents the fallback — enforce it with a database-side constraint or an RPC. If concurrent invites at the limit are a scenario you expect, this is the detail to design around.

FAQ

Does the kit include Stripe?

No. It includes the interface a payment provider plugs into, plus a mock implementation. The design assumes a merchant of record — a provider that acts as the legal seller and handles sales tax — rather than raw card processing, and the provider is swappable by design.

Why is the limit enforced when someone accepts an invite instead of when you send one?

Because that is where the seat is actually consumed. Blocking at invite creation means an owner cannot pre-invite a colleague before upgrading, and the failure lands on the wrong person. The kit allows the invite and refuses the acceptance with a message that says to upgrade or free a seat.

Can I use usage-based billing instead of seats?

The seam is the seam — the adapter returns a subscription state, so metering is an implementation of that interface rather than a change to the app. Each kit also ships a usage-billing pattern document for metered features. The enforcement gate itself is seat-shaped, which is the common case for B2B teams.

Which kit should I buy for billing specifically?

The billing code is the same in all three, so pick on the database and auth instead. If you want a seat guarantee that holds under concurrent invites, choose the Postgres kit — that atomicity is Postgres-only by construction. If you want Supabase Auth and RLS, choose the Supabase kit.

Is there a refund if it is not what I expected?

Thirty days, full refund, no questions. The price is one-time and the licence covers unlimited projects with lifetime updates.

Read the implementation

The billing design is documented in the open, so you can judge it before paying. Every link below is a page on this site, not a gated document.

The billing pattern

Get in touch

Questions about the product, team licenses, or anything else? We'll aim to respond within 48 hours.

Max 2000 characters

Stored in our own database — no third party. Deleted on request.