Product · Cloudflare edge stack

A multi-tenant SvelteKit SaaS starter for Cloudflare D1 — and an honest account of what the port costs you

Organizations, invitations, role-based access, seat billing, an append-only audit log and failed-login rate limiting — $79 once, on your own Cloudflare account. Read the next paragraph before you buy; it is the part most starter pages leave out.

The short answer

The starter you buy runs on better-sqlite3, not D1. A D1 port exists and is running live on Cloudflare Pages today atmulti-tenant-starter.verdantstack-site.pages.dev/— it is the same product, and it is what the link above opens.

Getting from the kit to D1 is a driver swap plus four call-site edits, tabulated below file by file, not a rewrite. What it does not carry over is the test suite: those tests run against :memory: SQLite and were not ported, so the D1 path is something you verify with npm run check,npm run build and a smoke run.

If your answer is "I need this on Workers and I will own the verification", this is the right product. If your answer is "I need the D1 path already under test", it is not — and no product here can honestly promise you that yet.

What the D1 port actually changes

Every row below is transcribed from the D1 port's own change log, which ships with the kit and is the document the port was built from — read it before you start the work. The third column is the part that decides whether this is an afternoon or a week.

AreaThe kit you buyThe D1 demoWhat you actually have to do
Database driverdrizzle-orm/better-sqlite3drizzle-orm/d1 (`DrizzleD1Database`)Swap one import. The D1 driver file is ~50 lines: `setDb(d1)` memoises the handle, `getDb()` throws a named error if the binding is missing.
How the app gets the handlecreateDb(file) opens a local file or :memory:setDb(d1) called per request from hooks.server.ts, reading platform.env.DBOne line in hooks.server.ts. On Workers the binding is a stateless RPC facade, so there is no pool to size — that part is simpler than Node, not harder.
MigrationsChecked-in Drizzle SQL applied at boot by migrate()wrangler d1 migrations apply — no runtime migrationRun migrations from the CLI, not from the app. The kit already documents this: boot-time migrate() races when several instances start at once, and that race is worse against a remote database.
Password hashingnode:crypto scryptPBKDF2-HMAC-SHA256 via Web Crypto, 50,000 iterations, self-describing hash stringThe one substitution you cannot avoid. Workers has no node:crypto, so scrypt is unavailable; PBKDF2 is the Web-Crypto-native stand-in. Iterations are tuned to the free plan CPU budget — raise PASSWORD_ITERATIONS on a paid plan.
Randomness + SHA-256randomBytes / createHash, synchronouscrypto.getRandomValues / crypto.subtle.digest, asynchronousMechanical: `await` the now-async sha256 at its call sites.
Environment variablesprocess.env.*A getEnv() helper that is safe on Workers, defaults preservedSmall indirection so the same code reads on both runtimes.
Adapter@sveltejs/adapter-auto@sveltejs/adapter-cloudflarePin the platform adapter for a production build.
Test suite357 Vitest tests against :memory: SQLiteNOT ported — validated by npm run check + npm run build + a local D1 smoke runSee "What this does not give you". This is the sharpest edge on this page and it is not hidden.

The service layer is untouched by all of this. It was already async and framework-free, taking a Db, which is the whole reason a driver swap is this small rather than a port of the application.

Why $79 beats rebuilding it

Not because the components are hard. They are not. You are buying the parts that only look easy until a customer finds the edge case in week three:

  • 357 automated tests already written for exactly this surface — invite expiry and single-use claims, the rank-escalation attempts, the cross-org isolation cases, seat-limit boundaries, audit history. That number is the single source of truth inpackage.json and the build checks every page that quotes it against it.
  • A coverage floor the build enforces — 95% on statements, branches, functions and lines, set invitest.config.ts. A test suite you cannot let rot is worth more than a larger one you can.
  • A release record, not a promise. 0.2.14, released 2026-10-04, with 14 dated releases in this kit and 39 across the three (153 logged changes). The changelog is public and the counts on this page are parsed from those files at build time — they cannot drift from them without failing the build.
  • A documented RBAC invariant set — owner above admin above member, grants never reach your own rank, the last owner can neither leave nor be removed. These are the rules an enterprise pilot asks about in week one, and they are enforced server-side on every request rather than hidden in the UI.
  • A 30-day refund, so the decision costs you a month of evenings rather than a year of guessing.

If your honest position is "I would build this myself in a weekend", then you should — the parts above are legible code and the docs are public. Buy the version with the tests when the tests are the part you would rather not write.

What you get on any target

Organizations and membership

Create, join, leave; single-use invite links with an expiry and an atomic claim; ownership transfer. Tenant isolation is enforced in the service layer on every mutating call, not in a view.

Role-based access control

Three roles with a strict hierarchy and a capability matrix. A member's screen shows no admin controls and a hand-crafted POST to promote the owner still comes back 403 — that is verified on the live demo, not asserted here.

Seat billing seam

A BillingAdapter interface with seat limits enforced in exactly one place — invite acceptance — so the limit surfaces as an actionable error instead of a failed signup. The provider adapter is the one thing you write; see the billing page for the full scope.

Append-only audit log

Who did what, queryable, with no update or delete path in the code. On D1 the audit writes ride along with the same batch as the change they describe.

Auth and sessions

scrypt with database-backed, revocable sessions and hashed tokens — on the D1 port, PBKDF2 because Workers has no scrypt. Failed logins are rate limited before any password hashing happens.

Built for coding agents

AGENTS.md, a generated TypeDoc API reference andllms.txt ship in the box, so Claude Code or Cursor start with the file map and the public API already in context.

What this does not give you

The section every starter page omits. If any of these is a dealbreaker, find out now rather than after the payment clears.

D1 has no interactive transactions

D1 offers batch() atomicity, not interactive transactions or `FOR UPDATE`. The Postgres kit takes a per-org row lock around invite acceptance so the seat check and the claim cannot interleave; that guarantee does not exist on D1. You enforce seat limits with a database-side constraint or an RPC instead. Our Postgres kit documents this limit explicitly in its own architecture notes.

Your password hashing is PBKDF2, not scrypt

That is forced by the runtime, not a choice. It is a defensible KDF, but if your threat model or your compliance checklist names scrypt or argon2, D1 is the wrong target and the Node or Postgres kit is the right buy.

The rate limiter is per-process, so per-isolate

The failed-login limiter lives in memory. On a multi-isolate Pages runtime the effective cap is per isolate, not global. The demo README documents this and records the observed behaviour against the live demo: the fifth wrong password in a burst is refused. Do not read the limiter as a global lockout policy.

The port carries no tests

The 357 tests run against `:memory:` SQLite. They were not ported to a D1 emulator, so what you are buying is a test suite for the logic and a driver swap you verify yourself with check + build + a smoke run. If you need the D1 path under test, that is work you do after buying, and it is the honest headline of this page.

Cloudflare Pages has a read-only filesystem

SQLite cannot simply be pointed at local disk there. The kit deployment guide says so in one line: SQLite must live on a persistent backend (R2 through a Worker), or you swap to D1 or Postgres. If you swap to D1, this page is the plan.

Pricing, stated plainly

$79USD, one-time. Not a subscription.
  • One licence — unlimited projects, commercial use permitted, lifetime updates included.
  • 30-day full refund. Read the licence.
  • 357 automated tests, with a 95% coverage floor enforced by the build.
  • Live demo — the same product on Cloudflare Pages + D1, seeded, resetting daily.
  • Full source delivered privately after purchase; the public repository is documentation only.
  • Building for a client or a team of 3+?Team Licence $249 — 3+ seats, still unlimited projects, invoiced individually.
Get the kit — $79

Which kit is which

KitDatabaseTestsBuy
Multi-tenant SvelteKit StarterSQLite (better-sqlite3) + Drizzle — D1 port documented and demoed357$79
SvelteKit + Supabase StarterSupabase — managed Auth, RLS and realtime390$99
SvelteKit + Postgres StarterAny Postgres (Drizzle + postgres.js), provider-neutral325$79

All three share the same orgs → members → invites → RBAC → billing → audit service layer. Only the driver differs, which is exactly why the D1 swap above is four files rather than a port.

FAQ

Does the kit I buy ship a D1 driver?

No. The Multi-tenant SvelteKit Starter ships better-sqlite3 + Drizzle. A D1 port exists and runs live — it is what the public demo is — and it is a driver swap plus four call-site edits, tabulated above. Nothing about the D1 path is hidden from you; you just have to make the swap.

Is the live demo the real product?

It is the same product running: multi-tenancy, invites, RBAC, seat billing, append-only audit, failed-login rate limiting. It is a pre-built binary served from a Pages preview branch, with the seed data resetting daily, so the data you see is demo data rather than a customer database.

What if I outgrow D1?

The service layer takes a `Db` and imports nothing from SvelteKit, so the tenancy logic does not know which engine is behind it. The domain services — orgs, members, invites — are the same code that runs on SQLite and on Postgres today. Moving engine is a driver swap in the repo, not a rewrite of your business logic.

Is there a Cloudflare-specific guide on the site?

Not yet. This page is the Cloudflare entry point; the depth is in the kit documents linked at the bottom, which cover SQLite in production, the single-writer ceiling, migrations, and the multi-instance cases where SQLite stops being the right answer.

Can I use it if I have not decided on Cloudflare?

Yes — that is the SQLite/Node kit, which runs on any Node host, Docker, Fly, Railway or a VPS with a persistent volume. Cloudflare is one target for it, not a requirement.

Read the row you care about

Deployment and the Cloudflare question

The parts the D1 port touches

Buying guides

Still deciding

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.