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.

dashboard / roles
permissions · 6/6
dashboard:read
members:read
members:invite
roles:create
roles:assign_permissions
org:update
@RequirePermission('members:invite')

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.
members / invitations 4
  • Admin accepted
  • Member pending
  • Member revoked
  • 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 \ permissiondashboard:readmembers:readmembers:inviteroles:assign_permissionsorg:updateorg: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.

TypeScript 6 · strict · superforms + Zod · in-process Hono under /api

  • 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

Skip the first month.
Start on the part that's yours.