Skip to content

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.

  • 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 guardsrequireAuth, requireOrg, requirePermission, requireEntitlement.
  • OAuth 2.0 server — be a provider: app registration, auth codes, access/refresh tokens.
Terminal window
pnpm add @delightstack/auth
ImportUse
@delightstack/auth/workerAuthDatabaseServer — the Durable Object class
@delightstack/auth/serverdefineAuthConfig, createAuthHandle, server types
@delightstack/auth/sveltekitroute guards + cookie helpers
@delightstack/auth/clientAuthClient — reactive Svelte 5 client
src/lib/auth.config.ts
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, … */
},
},
});

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);
}
}
hooks.server.ts
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.

src/lib/auth.guards.ts
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.ts
import { requirePermission } from '$lib/auth.guards';
export const load = requirePermission('org:admin', ({ locals }) => {
return { user: locals.user }; // redirects if signed out, /403 if missing permission
});
<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}

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 this
await 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 owner
await auth.updateOrgUserPermission(org_id, user_id, permission); // admins
await auth.removeOrgUser(org_id, user_id); // admins; members may remove themselves
await auth.deleteOrg(org_id); // owner only

On 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.