Auth
@delightstack/auth is full-stack authentication for SvelteKit apps on Cloudflare Workers —
email/password, magic links, OAuth providers, multi-org, invitations, permissions, and even an
OAuth 2.0 server, all backed by a single Durable Object.
Features
Section titled “Features”- Email/password with Argon2id (WASM) hashing, password reset, and change.
- Magic links & email codes — passwordless sign-in via short-lived JWT links and/or typed one-time codes.
- Passkeys (WebAuthn) — phishing-resistant, usernameless sign-in with Touch ID, Face ID, or security keys; zero config needed alongside other sign-in methods.
- OAuth providers — Google, GitHub, or any OAuth 2.0 provider; link multiple to one account.
- Multi-organization — users belong to many orgs with bitwise-encoded permissions; org switching, member management, and invitations built in.
- Reactive
AuthClient— Svelte 5 runes, auto-refresh, typed API methods. - Route guards —
requireAuth,requireOrg,requirePermission,requireEntitlement. - OAuth 2.0 server — be a provider: app registration, auth codes, access/refresh tokens.
Install
Section titled “Install”pnpm add @delightstack/auth| Import | Use |
|---|---|
@delightstack/auth/worker | AuthDatabaseServer — the Durable Object class |
@delightstack/auth/server | defineAuthConfig, createAuthHandle, server types |
@delightstack/auth/sveltekit | route guards + cookie helpers |
@delightstack/auth/client | AuthClient — reactive Svelte 5 client |
1. Define your config
Section titled “1. Define your config”import { defineAuthConfig } from '@delightstack/auth/server';
export const authConfig = defineAuthConfig({ secret: env.JWT_KEY_SECRET, issuer: 'my-app', permissions: ['org:read', 'org:write', 'org:admin'] as const, entitlements: ['premium'] as const, email: { sendEmail: async ({ to, subject, html, link, type }) => { // send via Resend / SES / etc. }, }, hooks: { onSignUp: async ({ result, method }) => { /* welcome email, default resources, … */ }, },});2. Register the Durable Object
Section titled “2. Register the Durable Object”In your backend Worker, subclass the DO to inject the config, then declare it in wrangler.toml
(see Architecture):
import { AuthDatabaseServer as BaseAuth } from '@delightstack/auth/worker';import { authConfig } from './auth.config';
export class AuthDatabaseServer extends BaseAuth { constructor(ctx, env) { super(ctx, env, authConfig); }}3. Wire the SvelteKit handle
Section titled “3. Wire the SvelteKit handle”import { createAuthHandle } from '@delightstack/auth/server';import { authConfig } from '$lib/auth.config';
export const handle = createAuthHandle({ config: authConfig, getAuthServer: (event) => { const namespace = event.platform.env.AUTH; return namespace.get(namespace.idFromName('main')); },});The handle extracts and auto-refreshes the JWT, resolves the active org, populates event.locals,
serves /api/auth/* routes, and enforces CSRF on mutations.
4. Guard routes
Section titled “4. Guard routes”import { createAuthGuards } from '@delightstack/auth/sveltekit';
export const { requireAuth, requireOrg, requirePermission, requireEntitlement } = createAuthGuards({ permissions: ['org:read', 'org:write', 'org:admin', 'org:owner'] as const, });
// +page.server.tsimport { requirePermission } from '$lib/auth.guards';
export const load = requirePermission('org:admin', ({ locals }) => { return { user: locals.user }; // redirects if signed out, /403 if missing permission});5. The reactive client
Section titled “5. The reactive client”<script> // Hydrated in +layout.ts with `new AuthClient(data.auth)` from locals.auth_client_data const { data } = $props(); const auth = data.auth;</script>
{#if auth.signed_in} <p>Hello, {auth.name}</p> <button onclick={() => auth.signOut()}>Sign out</button>{/if}Organizations & ownership
Section titled “Organizations & ownership”Users can belong to many orgs. Each org has exactly one owner (the user in the org row’s
owner_id) plus members whose abilities come from their bitwise-encoded permissions. The org
API routes are authorization-aware: renaming an org or managing members requires the admin
permission (org_admin_permission, default 'org:admin' — the owner always qualifies), while
deleting an org or transferring ownership requires being the current owner.
// Transfer ownership — only the current owner can do thisawait auth.transferOrgOwnership(org_id, new_owner_user_id);
// Other org management (see the README for the full list)await auth.updateOrg(org_id, { name: 'Renamed' }); // admins or the ownerawait auth.updateOrgUserPermission(org_id, user_id, permission); // adminsawait auth.removeOrgUser(org_id, user_id); // admins; members may remove themselvesawait auth.deleteOrg(org_id); // owner onlyOn transfer, the new owner is granted the admin permission (and org membership if needed). The previous owner keeps their existing permissions — demote or remove them afterwards if that’s what your app wants.