Cloudflare Workers · SvelteKit · D1
Clone it once.
Never rebuild auth, orgs, or RBAC again.
Tenet is a multi-tenant starter for Cloudflare Workers — email & password with verification, Google OAuth, organizations with email invites, and permission-slug RBAC. Wired through Better Auth, D1, and Cloudflare Email Sending. One Worker runs the app and an in-process Hono API.
No global users.role. Every endpoint gated by a resource:action slug.
The layer nobody wants to write twice
Every new SaaS rewrites the same first month.
It always starts the same way. Sign-up, a verification email, password reset, then invites — a month of plumbing before a single feature ships. Authorization gets bolted on as it goes: a global users.role column and if checks that can't tell one tenant from the next.
Tenet treats identity as the platform, not the first sprint. Roles live inside each organization; access is a set of permission slugs checked at the edge of every handler.
// what you keep writingif (user.role === 'admin') allow();// leaks across tenants, can't grow with the product// what Tenet gives you@RequirePermission('members:invite')async invite() { /* … */ }01 — Authentication
Sign-in is already done.
Two ways in, both production-shaped. Email accounts require a verified address before they reach the dashboard, and every transactional message is sent by Cloudflare — no Resend, no SMTP relay.
Email & password
- Password sign-up with required verification
- Password reset over email
- Verify + reset mail via env.EMAIL.send()
GET /verify-email?token=…
Google OAuth
- One-click Google sign-in
- Enabled the moment creds are set
- Sessions verified out of the box
GET /api/auth/callback/google
02 — Organizations
Multi-tenant from the first commit.
Create an organization and you're its Owner. Invite people by email with a role — anything but Owner. Members can belong to several orgs and switch between them; the dashboard and API follow the active tenant.
- Invitations move through pending → accepted, or land on expired / revoked.
- Invitees accept at /accept-invite?token=…, signing in or up on the way through.
- Remove members freely — except the last Owner, which is protected.
- dana@acme.dev Admin accepted
- priya@acme.dev Member pending
- contractor@x.io Member revoked
- old-lead@acme.dev Admin expired
03 — RBAC · the point of the whole thing
Permissions are the product.
Authorization isn't a role name in an if — it's a set of permission slugs, resolved per organization and enforced on the API with @RequirePermission().
| role \ permission | dashboard:read | members:read | members:invite | roles:assign_permissions | org:update | org:delete |
|---|---|---|---|---|---|---|
| Owner protected · all | ||||||
| Admin full catalog | ||||||
| Member 2 granted |
Owner & Admin are seeded and locked. Toggle a Member cell to grant a slug — the same edit as the dashboard's role editor.
Slugs, not names
Permissions are atomic resource:action strings. Guards check the slug, never a role name.
System vs custom
A seeded system catalog ships with the starter; each org can define its own custom permissions.
Per-organization roles
Roles belong to an org. Assign or detach permissions in the dashboard — no redeploy.
04 — Stack
One Worker. App and API in-process.
The Hono API mounts inside the SvelteKit app under /api — no second deploy, no network hop between front end and back end. Nothing here is chosen to sound impressive; it's chosen to run on Workers.
- SvelteKit 2 app + routes
- Svelte 5 runes
- Tailwind 4 styling
- Hono /api router
- @wilt/core DI modules
- Drizzle ORM schema + queries
- Cloudflare D1 SQLite
- Better Auth sessions + org
- R2 file storage
- Vitest 4 tests
- pnpm workspace
- Workers runtime
Honest scope
What you get — and what you don't.
In the box
- Clone-and-go identity: email, Google, verified sessions
- Organizations, email invites, membership & switcher
- Dashboard IAM — roles, permissions, assignment
- A permission-gated resource pattern to copy
Not in v1
- Stripe, subscriptions, usage metering
- SAML / SSO, magic links, passkeys
- SCIM provisioning
- A real vertical domain (CRM, LMS, …)
git clone → your next SaaS