Skip to content

Authentication

Cloudwerk provides a comprehensive authentication system through the @cloudwerk/auth package. It supports multiple providers (OAuth, credentials, email, WebAuthn), flexible session strategies, role-based access control, multi-tenancy, and rate limiting.

  1. Install the auth package:

    Terminal window
    pnpm add @cloudwerk/auth
  2. Create your auth configuration:

    // app/auth/config.ts
    import { defineAuthConfig } from '@cloudwerk/auth/convention'
    export default defineAuthConfig({
    basePath: '/auth',
    session: {
    strategy: 'database',
    maxAge: 30 * 24 * 60 * 60, // 30 days
    },
    })
  3. Add a provider:

    // app/auth/providers/github.ts
    import { defineProvider, github } from '@cloudwerk/auth/convention'
    export default defineProvider(
    github({
    clientId: process.env.GITHUB_CLIENT_ID!,
    clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    })
    )
  4. Use auth helpers in your routes:

    // app/dashboard/page.tsx
    import { requireAuth } from '@cloudwerk/auth'
    export async function loader() {
    const user = requireAuth() // Redirects if not logged in
    return { user }
    }
    export default function DashboardPage({ user }) {
    return <h1>Welcome, {user.name}</h1>
    }

Auth configuration is defined using convention files in the app/auth/ directory:

  • Directoryapp/auth/
    • config.ts # Main auth configuration
    • callbacks.ts # Lifecycle callbacks
    • pages.ts # Custom auth page paths
    • rbac.ts # Role and permission definitions
    • Directoryproviders/
      • github.ts # OAuth provider
      • google.ts # OIDC provider
      • credentials.ts # Email/password
      • email.ts # Magic link

Cloudwerk includes pre-built OAuth providers for popular services:

// app/auth/providers/github.ts
import { defineProvider, github } from '@cloudwerk/auth/convention'
export default defineProvider(
github({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
scope: 'read:user user:email', // Optional custom scope
})
)

Create a custom OAuth 2.0 provider:

// app/auth/providers/custom.ts
import { defineProvider, createOAuth2Provider } from '@cloudwerk/auth'
export default defineProvider(
createOAuth2Provider({
id: 'custom',
name: 'Custom Provider',
clientId: process.env.CUSTOM_CLIENT_ID!,
clientSecret: process.env.CUSTOM_CLIENT_SECRET!,
authorization: 'https://auth.example.com/oauth/authorize',
token: 'https://auth.example.com/oauth/token',
userinfo: 'https://api.example.com/user',
scope: 'read:user',
profile(profile) {
return {
id: profile.id,
email: profile.email,
name: profile.name,
image: profile.avatar_url,
}
},
})
)

Email/password authentication with customizable validation:

// app/auth/providers/credentials.ts
import { defineProvider, credentials, verifyPassword } from '@cloudwerk/auth'
import { db } from '@cloudwerk/core/bindings'
export default defineProvider(
credentials({
credentials: {
email: {
label: 'Email',
type: 'email',
placeholder: '[email protected]',
required: true,
},
password: {
label: 'Password',
type: 'password',
required: true,
},
},
async authorize(creds) {
const user = await db
.prepare('SELECT * FROM users WHERE email = ?')
.bind(creds.email)
.first()
if (!user) return null
const valid = await verifyPassword(creds.password, user.password_hash)
if (!valid) return null
return {
id: user.id,
email: user.email,
name: user.name,
emailVerified: user.email_verified_at
? new Date(user.email_verified_at)
: null,
}
},
})
)

Passwordless authentication via email:

// app/auth/providers/email.ts
import { defineProvider, email } from '@cloudwerk/auth'
export default defineProvider(
email({
maxAge: 24 * 60 * 60, // 24 hours
async sendVerificationRequest({ identifier, url }) {
await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: identifier,
subject: 'Sign in to MyApp',
html: `<a href="${url}">Click here to sign in</a>`,
}),
})
},
})
)

Modern passwordless authentication with passkeys:

// app/auth/providers/passkey.ts
import { defineProvider, passkey } from '@cloudwerk/auth'
export default defineProvider(
passkey({
rpName: 'My App',
rpId: 'myapp.com',
origin: 'https://myapp.com',
authenticatorAttachment: 'platform',
userVerification: 'preferred',
})
)

For a complete passkey implementation guide, see Passkey Setup.

Cloudwerk supports two session strategies:

Server-side sessions stored in Cloudflare KV:

// app/auth/config.ts
import { defineAuthConfig } from '@cloudwerk/auth/convention'
export default defineAuthConfig({
session: {
strategy: 'database',
maxAge: 30 * 24 * 60 * 60, // 30 days
updateAge: 24 * 60 * 60, // Refresh once per day
},
})

Add the KV binding to wrangler.toml:

[[kv_namespaces]]
binding = "AUTH_SESSIONS"
id = "your-kv-namespace-id"

Customize session cookies:

export default defineAuthConfig({
cookies: {
sessionToken: {
name: '__Secure-session',
options: {
secure: true,
httpOnly: true,
sameSite: 'lax',
path: '/',
},
},
},
})

Protect entire route segments with middleware:

// app/dashboard/middleware.ts
import { authMiddleware } from '@cloudwerk/auth/middleware'
export const middleware = authMiddleware({
unauthenticatedRedirect: '/login',
})

Use context helpers in loaders and handlers:

import {
getUser,
getSession,
isAuthenticated,
requireAuth,
} from '@cloudwerk/auth'
// Get user (returns null if not authenticated)
export async function loader() {
const user = getUser()
return { user }
}
// Require authentication (redirects if not authenticated)
export async function loader() {
const user = requireAuth()
return { user }
}
// Require authentication with error instead of redirect
export async function GET() {
const user = requireAuth({ throwError: true })
return json({ user })
}
// Check authentication status
export async function loader() {
if (isAuthenticated()) {
return { user: getUser() }
}
return { user: null }
}

For JSON APIs, return errors instead of redirects:

// app/api/profile/route.ts
import { requireAuth } from '@cloudwerk/auth'
import { json } from '@cloudwerk/core'
export async function GET() {
const user = requireAuth({ throwError: true })
return json({ user })
}

Create role and permission definitions:

// app/auth/rbac.ts
import { defineRBAC } from '@cloudwerk/auth/convention'
export default defineRBAC({
roles: [
{
id: 'admin',
name: 'Administrator',
permissions: ['*'], // Full access
description: 'Full system access',
},
{
id: 'editor',
name: 'Editor',
permissions: [
'posts:create',
'posts:read',
'posts:update',
'posts:delete:own', // Own resources only
'media:*',
],
},
{
id: 'viewer',
name: 'Viewer',
permissions: ['posts:read', 'media:read'],
},
],
defaultRole: 'viewer',
hierarchy: {
editor: ['viewer'], // Editor inherits viewer permissions
},
})
import { hasRole, hasPermission, requireRole, requirePermission } from '@cloudwerk/auth'
// Check role
if (hasRole('admin')) {
// Admin-only logic
}
// Check permission
if (hasPermission('posts:delete')) {
// Can delete any post
}
// Require role (throws ForbiddenError if lacking)
export async function DELETE(request, { params }) {
requireRole('admin')
await deleteUser(params.id)
return json({ success: true })
}
// Require permission
export async function POST(request) {
requirePermission('posts:create')
// Create post...
}

Protect routes by role:

// app/admin/middleware.ts
import { authMiddleware } from '@cloudwerk/auth/middleware'
export const middleware = authMiddleware({
role: 'admin',
unauthorizedRedirect: '/forbidden',
})
// Multiple roles (any of)
export const middleware = authMiddleware({
roles: ['admin', 'moderator'],
})

For complex authorization logic:

// app/api/posts/[id]/middleware.ts
import { authMiddleware } from '@cloudwerk/auth/middleware'
export const middleware = authMiddleware({
async authorize(user, request) {
const url = new URL(request.url)
const postId = url.pathname.split('/').pop()
// Check if user owns the post
const post = await getPost(postId)
return post.authorId === user.id || user.roles?.includes('admin')
},
})

Multi-tenancy allows a single application to serve multiple customers (tenants) with isolated data. Common patterns include:

  • Subdomain-based: acme.yourapp.com → tenant “acme”
  • Path-based: yourapp.com/acme/dashboard → tenant “acme”
  • Header-based: Custom X-Tenant-ID header

The tenant resolver middleware runs before your routes, identifying the tenant from the request and making it available throughout your application.

Configure how tenants are identified:

// app/middleware.ts
import { createTenantResolver, createD1TenantStorage } from '@cloudwerk/auth/tenant'
const storage = createD1TenantStorage(env.DB)
const resolver = createTenantResolver(storage, {
strategy: 'subdomain',
baseDomain: 'myapp.com',
})
export const middleware = async (request, next) => {
const { tenant } = await resolver.require(request)
setContext('tenant', tenant)
return next()
}
StrategyExampleDescription
subdomainacme.myapp.comTenant from subdomain
path/t/acme/dashboardTenant from URL path
headerX-Tenant-ID: acmeTenant from request header
cookietenant=acmeTenant from cookie

Protect auth endpoints from brute force attacks:

// app/api/auth/login/middleware.ts
import {
createLoginRateLimiter,
createFixedWindowStorage,
} from '@cloudwerk/auth/rate-limit'
const storage = createFixedWindowStorage(env.RATE_LIMIT_KV)
const loginLimiter = createLoginRateLimiter(storage, {
limit: 5, // 5 attempts
window: 900, // per 15 minutes
})
export const middleware = async (request, next) => {
const { response } = await loginLimiter.check(request)
if (response) return response // Rate limited
return next()
}
LimiterDefaultDescription
createLoginRateLimiter5 per 15 minLogin attempts by IP + email
createPasswordResetRateLimiter3 per hourPassword reset by IP
createEmailVerificationRateLimiter5 per hourEmail verification by IP
import { signIn, signOut, getSession } from '@cloudwerk/auth/client'
// Sign in with provider
await signIn('github')
// Sign in with credentials
await signIn('credentials', {
password: 'password',
redirectTo: '/dashboard',
})
// Sign out
await signOut({ redirectTo: '/' })
// Get current session
const session = await getSession()
if (session) {
console.log('Logged in as', session.user.email)
}
import { createAuthStore } from '@cloudwerk/auth/client'
const authStore = createAuthStore()
// Subscribe to auth state changes
authStore.subscribe((state) => {
console.log('Auth state:', state.status, state.user)
})
// Get current state
const { user, status } = authStore.getState()

Here’s a complete example of using auth state in a React component:

// components/AuthStatus.tsx
'use client'
import { createAuthStore, signIn, signOut } from '@cloudwerk/auth/client'
import { useEffect, useState } from 'react'
const authStore = createAuthStore()
export function AuthStatus() {
const [session, setSession] = useState(authStore.getSession())
useEffect(() => {
// Subscribe to auth changes (e.g., after sign-in/sign-out)
return authStore.subscribe((state) => {
setSession(state.session)
})
}, [])
if (!session) {
return <button onClick={() => signIn('github')}>Sign In</button>
}
return (
<div>
<span>Welcome, {session.user.name}</span>
<button onClick={() => signOut()}>Sign Out</button>
</div>
)
}

Customize the auth flow with lifecycle callbacks:

// app/auth/callbacks.ts
import { defineAuthCallbacks } from '@cloudwerk/auth/convention'
export default defineAuthCallbacks({
async signIn({ user, account, profile }) {
// Called when user signs in
// Return false to deny sign-in
return true
},
async session({ session, user }) {
// Customize session data
session.user.role = user.role
return session
},
async jwt({ token, user }) {
// Customize JWT token (jwt strategy only)
if (user) {
token.role = user.role
}
return token
},
})
import { hashPassword, verifyPassword, generateToken } from '@cloudwerk/auth'
// Hash password for storage
const hash = await hashPassword('user_password')
// Verify password
const isValid = await verifyPassword('user_password', hash)
// Generate secure token
const token = await generateToken() // 32-byte random token

Auth errors can be caught and handled:

import {
UnauthenticatedError,
ForbiddenError,
InvalidCredentialsError,
} from '@cloudwerk/auth'
try {
const user = requireAuth()
} catch (error) {
if (error instanceof UnauthenticatedError) {
return redirect('/login')
}
if (error instanceof ForbiddenError) {
return json({ error: 'Access denied' }, { status: 403 })
}
throw error
}