This is the full developer documentation for cloudwerk # Cloudwerk > A full-stack framework for Cloudflare Workers with file-based routing that compiles to Hono. Pre-Alpha Build for the Edge.\ Ship Without the Complexity. ============================ Cloudwerk is a full-stack framework for Cloudflare Workers. Start simple. Scale infinitely. One codebase. One deploy command. [Get Started ](/getting-started/installation/)[ View on GitHub](https://github.com/squirrelsoft-dev/cloudwerk) Zero cold starts • Global edge deployment • Built on Cloudflare Terminal █ Scroll to explore 1 **Chapter 1** The Landing Page 2 **Chapter 2** Confirmation Emails 3 **Chapter 3** Background Jobs 4 **Chapter 4** Viral Referrals 5 **Chapter 5** Image Uploads 6 **Chapter 6** External Integrations 7 **Chapter 7** Service Extraction 8 **Chapter 8** Admin Dashboard 9 **Complete** Production-Ready ## The Developer Journey Follow a startup's evolution from a simple landing page to a full-stack edge application—adding features one chapter at a time. Scroll to explore Chapter 1 ### The Landing Page You have an idea. Let's validate it. Every great product starts with a landing page. With Cloudwerk, you're production-ready in minutes—not hours. Just create a route, connect your database, and deploy. Routes D1 Database Cloudwerk Project Explorer `app/` `├── page.tsx` `└── api/` `└── waitlist/` `└── route.ts` ⚛️ page.tsx app/page.tsx 📘 route.ts app/api/waitlist/route.ts ```tsx export default function Home() { return (

Coming Soon

); } ``` ```ts import { db } from '@cloudwerk/bindings' export async function POST(request: Request) { const { email } = await request.json() await db.prepare(` INSERT INTO waitlist (email, created_at) VALUES (?, datetime('now')) `).bind(email).run() return Response.json({ success: true }) } ``` Deploy █ Chapter 2 ### Confirmation Emails People are signing up! Let's confirm their interest. Create a reusable email service that lives alongside your app. Services start local—running in the same worker—so there's zero latency and zero configuration. \+ Services (Local) Cloudwerk Project Explorer `app/` `├── page.tsx` `├── api/` `│ └── waitlist/` `│ └── route.ts` `└── services/` `└── email/` `└── service.ts` 📘 service.ts app/services/email/service.ts 📘 route.ts app/api/waitlist/route.ts ```ts import { defineService } from '@cloudwerk/service' interface EmailService { sendWelcome(params: { to: string; position: number }): Promise } export default defineService({ name: 'email', methods: { async sendWelcome({ to, position }) { await fetch('https://api.resend.com/emails', { method: 'POST', headers: { 'Authorization': `Bearer ${env.RESEND_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ to, subject: `You're #${position} on the waitlist!`, html: `

You're in!

You're #${position} in line.

`, }), }) }, }, }) ``` ```ts import { db, services } from '@cloudwerk/bindings' export async function POST(request: Request) { const { email } = await request.json() const result = await db.prepare(` INSERT INTO waitlist (email) VALUES (?) RETURNING id `).bind(email).first() await services.email.sendWelcome({ to: email, position: result.id }) return Response.json({ success: true, position: result.id }) } ``` Chapter 3 ### Don't Block the Response Emails are slowing down signups. Let's queue them. Sending emails inline adds latency. Move them to a queue and respond instantly. Cloudwerk queues are just files—define a handler and start sending. \+ Queues Cloudwerk Project Explorer `app/` `├── page.tsx` `├── api/` `│ └── waitlist/` `│ └── route.ts` `├── services/` `│ └── email/` `│ └── service.ts` `└── queues/` `└── emails.ts` 📘 emails.ts app/queues/emails.ts 📘 route.ts app/api/waitlist/route.ts ```ts import { defineQueue } from '@cloudwerk/queue' import { services } from '@cloudwerk/bindings' interface WelcomeEmail { to: string position: number } export default defineQueue({ name: 'emails', async process(message) { await services.email.sendWelcome(message.body) }, }) ``` ```ts import { db, queues } from '@cloudwerk/bindings' export async function POST(request: Request) { const { email } = await request.json() const result = await db.prepare(` INSERT INTO waitlist (email) VALUES (?) RETURNING id `).bind(email).first() // Queue email — respond instantly! await queues.emails.send({ to: email, position: result.id }) return Response.json({ success: true, position: result.id }) } ``` BEFOREUser waits 800ms (DB + Email API) AFTERUser waits 50ms (DB + Queue) Chapter 4 ### Viral Referrals Let's incentivize sharing with a real-time leaderboard. Durable Objects give you strongly consistent, real-time state at the edge. Perfect for counters, leaderboards, and collaboration. Each user gets their own referral code, and the leaderboard updates live via WebSocket. \+ Durable Objects Real-time WebSocket Cloudwerk Project Explorer `app/` `├── page.tsx` `├── refer/` `│ └── [code]/` `│ └── page.tsx` `├── api/` `│ ├── waitlist/` `│ │ └── route.ts` `│ └── leaderboard/` `│ └── route.ts` `├── services/` `├── queues/` `└── objects/` `└── leaderboard.ts` 📘 leaderboard.ts app/objects/leaderboard.ts 📘 route.ts app/api/leaderboard/route.ts ```ts import { defineDurableObject } from '@cloudwerk/realtime' export default defineDurableObject({ name: 'Leaderboard', methods: { async addReferral(userId: string) { const current = (await this.ctx.storage.get(userId)) ?? 0 await this.ctx.storage.put(userId, current + 1) // Notify all connected clients this.broadcast(JSON.stringify({ type: 'update', userId, count: current + 1 })) return current + 1 }, async getTop(limit = 10) { const all = await this.ctx.storage.list() return [...all.entries()] .sort((a, b) => b[1] - a[1]) .slice(0, limit) }, }, }) ``` ```ts import { durableObjects } from '@cloudwerk/bindings' export async function GET() { const leaderboard = durableObjects.Leaderboard.get('global') const top = await leaderboard.getTop(10) return Response.json(top) } ``` Chapter 5 ### Image Uploads Let users add profile pictures and shareable cards. R2 gives you S3-compatible object storage with zero egress fees. Upload files directly, then queue heavy processing like thumbnails and social cards for background execution. \+ R2 Storage Image Processing Cloudwerk Project Explorer `app/` `├── page.tsx` `├── refer/` `│ └── [code]/` `│ └── page.tsx` `├── api/` `│ ├── waitlist/` `│ │ └── route.ts` `│ ├── leaderboard/` `│ │ └── route.ts` `│ └── upload/` `│ └── route.ts` `├── services/` `│ ├── email/` `│ │ └── service.ts` `│ └── images/` `│ └── service.ts` `├── queues/` `│ ├── emails.ts` `│ └── image-processing.ts` `└── objects/` `└── leaderboard.ts` 📘 route.ts app/api/upload/route.ts 📘 image-processing.ts app/queues/image-processing.ts ```ts import { r2, queues } from '@cloudwerk/bindings' export async function POST(request: Request) { const formData = await request.formData() const file = formData.get('avatar') as File const key = `avatars/${crypto.randomUUID()}` await r2.put(key, file.stream(), { httpMetadata: { contentType: file.type }, }) // Generate thumbnail + social card async await queues.imageProcessing.send({ key, operations: ['thumbnail-200', 'social-card'] }) return Response.json({ key, url: `/cdn/${key}` }) } ``` ```ts import { defineQueue } from '@cloudwerk/queue' import { services } from '@cloudwerk/bindings' export default defineQueue<{ key: string; operations: string[] }>({ name: 'imageProcessing', config: { batchSize: 5, // Process 5 images at a time maxRetries: 3, }, async process(message) { const { key, operations } = message.body for (const op of operations) { await services.images.process(key, op) } }, }) ``` Chapter 6 ### External Integrations Notify our Discord when someone hits a milestone. Triggers let you react to events across your system. When a user hits 10 referrals, fire a webhook to Discord. When a payment succeeds, send a receipt. Clean, decoupled, testable. \+ Event Triggers Webhooks Cloudwerk Project Explorer `app/` `├── page.tsx` `├── refer/` `│ └── [code]/` `│ └── page.tsx` `├── api/` `│ └── ...` `├── services/` `├── queues/` `├── objects/` `└── triggers/` `└── referral-milestone.ts` 📘 referral-milestone.ts app/triggers/referral-milestone.ts 📘 usage.ts emit event ```ts import { defineTrigger } from '@cloudwerk/triggers' import { db } from '@cloudwerk/bindings' export default defineTrigger({ name: 'referral-milestone', event: 'referral.milestone', async handler({ userId, count, email }) { // Notify Discord await fetch(env.DISCORD_WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: `🎉 **${email}** just hit **${count} referrals**!`, }), }) // Award a badge await db.prepare(` INSERT INTO badges (user_id, badge, awarded_at) VALUES (?, ?, datetime('now')) `).bind(userId, `referrals-${count}`).run() }, }) ``` ```ts // In your leaderboard logic import { emit } from '@cloudwerk/triggers' const count = await leaderboard.addReferral(userId) if (count === 10 || count === 50 || count === 100) { await emit('referral.milestone', { userId, count, email }) } ``` Chapter 7 ### Scale for Launch Day Launch is tomorrow. Let's extract heavy services. Your email and image services are getting heavy. Extract them to dedicated workers with a single config change. Same code. Same imports. Cloudwerk handles the service bindings automatically. \+ Service Extraction Service Bindings Cloudwerk Project Explorer `app/` `├── page.tsx` `├── refer/` `│ └── [code]/` `│ └── page.tsx` `├── api/` `│ └── ...` `├── services/` `│ ├── email/` `│ └── images/` `├── queues/` `├── objects/` `└── triggers/` `cloudwerk.config.ts` 📘 cloudwerk.config.ts cloudwerk.config.ts ```ts import { defineConfig } from '@cloudwerk/core' export default defineConfig({ services: { extract: ['email', 'images'], }, }) ``` Deploy █ BEFORE (Local Mode) Main Worker RoutesEmailImagesQueues → AFTER (Extracted) Main Worker RoutesQueuesObjects Email Service Images Service Same code. Same imports. Zero changes to your app. Chapter 8 ### Admin Dashboard Manage your waitlist and send announcements. Add authentication in minutes. Cloudwerk's convention-based auth supports OAuth providers, credentials, passkeys, and role-based access control. Protect routes with a single function call. \+ Authentication OAuth RBAC Cloudwerk Project Explorer `app/` `├── page.tsx` `├── refer/` `│ └── [code]/` `│ └── page.tsx` `├── admin/` `│ ├── layout.tsx` `│ ├── page.tsx` `│ └── announce/` `│ └── route.ts` `├── api/` `│ └── ...` `├── auth/` `│ ├── providers/` `│ │ └── google.ts` `│ └── rbac.ts` `├── services/` `├── queues/` `├── objects/` `└── triggers/` 📘 google.ts app/auth/providers/google.ts 📘 rbac.ts app/auth/rbac.ts ⚛️ layout.tsx app/admin/layout.tsx ```ts import { defineProvider, google } from '@cloudwerk/auth' export default defineProvider( google({ clientId: { env: 'GOOGLE_CLIENT_ID' }, clientSecret: { env: 'GOOGLE_CLIENT_SECRET' }, }) ) ``` ```ts import { defineRBAC } from '@cloudwerk/auth' export default defineRBAC({ roles: { admin: ['*'], user: ['waitlist:read'], }, defaultRole: 'user', }) ``` ```tsx import { requireRole } from '@cloudwerk/auth' export default function AdminLayout({ children }) { requireRole('admin') return (
{children}
) } ``` Chapter 9 ### Your App, Production-Ready The journey is complete. What started as a simple landing page is now a full-stack edge application with real-time features, background processing, file storage, external integrations, and secure admin access. THE FINAL ARCHITECTURE app/ ├── page.tsx Landing page ├── refer/\[code]/ Referral pages ├── admin/ Protected admin (layout + pages) ├── api/ API routes ├── auth/ Google OAuth + RBAC ├── services/ 2 services (extracted) │ ├── email/ → mylaunch-email-service │ └── images/ → mylaunch-images-service ├── queues/ 2 queues │ ├── emails.ts Async email delivery │ └── images.ts Background processing ├── objects/ 1 durable object │ └── leaderboard.ts Real-time referral tracking └── triggers/ 1 trigger └── milestones.ts Discord notifications $ cloudwerk deploy ✓ Deployed 3 workers in 5.3s 🚀 8 Routes•2 Services•2 Queues•1 Durable Object•1 Trigger One codebase. One deploy command. Zero DevOps complexity. ## Everything You Need Cloudwerk provides all the primitives for building full-stack edge applications. 📁 ### File-Based Routing Intuitive routing powered by your file structure. Create page.tsx and route.ts files. ⚡ ### Compiles to Hono Your routes compile to Hono, giving you the performance of one of the fastest frameworks. ☁️ ### Cloudflare Native First-class support for D1, KV, R2, Durable Objects, Queues, and all Cloudflare primitives. 🌍 ### Edge-First Deploy to 300+ edge locations worldwide. Your code runs milliseconds from your users. 🔐 ### Built-in Auth OAuth providers, session management, RBAC, and multi-tenancy out of the box. 📦 ### Service Extraction Start with co-located services, extract to separate Workers when you scale. 📬 ### Queue Integration Type-safe queue consumers with Zod validation, retries, and dead letter queues. ⏰ ### Event Triggers Cron jobs, webhooks, R2 events, email handlers, and more—all with type safety. ## Cloudwerk vs Traditional Architecture | Feature | Cloudwerk | Traditional | | ---------------- | ----------------------- | ---------------------------- | | Deployment | Edge (300+ locations) | Single region | | Cold Start | \~0ms (no containers) | 100ms-10s | | Scaling | Automatic, instant | Manual or delayed auto-scale | | Database | D1 (distributed SQLite) | Centralized Postgres/MySQL | | State Management | Durable Objects | Redis/External store | | Queue Processing | Cloudflare Queues | SQS/RabbitMQ/Redis | | File Storage | R2 (S3-compatible) | S3/GCS | | Auth | Built-in + Workers KV | External service | ## Ready to Build on the Edge? Get started with Cloudwerk in minutes. Create your first edge-native application today. [Get Started ](/getting-started/installation/)[View Examples](/examples/blog/) Terminal ``` $ npx @cloudwerk/create-app my-app $ cd my-app $ cloudwerk dev ✓ Routes compiled (3 routes) ✓ Database ready (D1) ✓ Types generated 🚀 Server running at http://localhost:8787 ``` # 404 > Page not found. Check the URL or try using the search bar. # API Reference > Complete API documentation for Cloudwerk packages. Reference documentation for all Cloudwerk APIs. ## Core [Section titled “Core”](#core) * [Context](/api/context/) - Request context and utilities * [Configuration](/api/configuration/) - Configuration options * [CLI](/api/cli/) - Command-line interface ## Authentication & Security [Section titled “Authentication & Security”](#authentication--security) * [Auth](/api/auth/) - Authentication system * [Security](/api/security/) - Security middleware (CSRF, CSP, headers) * [Bindings](/api/bindings/) - Cloudflare bindings ## Infrastructure [Section titled “Infrastructure”](#infrastructure) * [Durable Objects](/api/durable-objects/) - Stateful compute * [Queues](/api/queues/) - Queue producers and consumers * [Services](/api/services/) - Service bindings * [Triggers](/api/triggers/) - Scheduled tasks ## Media [Section titled “Media”](#media) * [Images](/api/images/) - Image transformations ## Testing [Section titled “Testing”](#testing) * [Testing](/api/testing/) - Testing utilities # Auth API > Complete API reference for @cloudwerk/auth - authentication, sessions, RBAC, multi-tenancy, and rate limiting. The `@cloudwerk/auth` package provides a comprehensive authentication system with providers, sessions, RBAC, multi-tenancy, and rate limiting. ## Installation [Section titled “Installation”](#installation) ```bash pnpm add @cloudwerk/auth ``` ## Configuration API [Section titled “Configuration API”](#configuration-api) ### defineAuthConfig() [Section titled “defineAuthConfig()”](#defineauthconfig) Main configuration function for the auth system. ```typescript import { defineAuthConfig } from '@cloudwerk/auth/convention' export default defineAuthConfig({ basePath?: string, session?: SessionConfig, cookies?: CookieConfig, pages?: PagesConfig, secret?: string, }) ``` #### AuthConfig [Section titled “AuthConfig”](#authconfig) | Property | Type | Default | Description | | ---------- | --------------- | ---------------- | ---------------------------- | | `basePath` | `string` | `'/auth'` | Base path for auth endpoints | | `session` | `SessionConfig` | See below | Session configuration | | `cookies` | `CookieConfig` | See below | Cookie configuration | | `pages` | `PagesConfig` | See below | Custom page paths | | `secret` | `string` | Required for JWT | Secret for signing tokens | #### SessionConfig [Section titled “SessionConfig”](#sessionconfig) ```typescript interface SessionConfig { strategy: 'database' | 'jwt' // Session storage strategy maxAge?: number // Session lifetime in seconds (default: 30 days) updateAge?: number // Refresh interval in seconds (default: 24 hours) } ``` #### CookieConfig [Section titled “CookieConfig”](#cookieconfig) ```typescript interface CookieConfig { sessionToken?: { name?: string // Cookie name (default: 'cloudwerk.session-token') options?: CookieOptions // Cookie options } } interface CookieOptions { secure?: boolean // HTTPS only (default: true in production) httpOnly?: boolean // No JavaScript access (default: true) sameSite?: 'lax' | 'strict' | 'none' // (default: 'lax') path?: string // Cookie path (default: '/') domain?: string // Cookie domain } ``` #### PagesConfig [Section titled “PagesConfig”](#pagesconfig) ```typescript interface PagesConfig { signIn?: string // Sign-in page (default: '/auth/signin') signOut?: string // Sign-out page (default: '/auth/signout') error?: string // Error page (default: '/auth/error') verifyRequest?: string // Email verification page newUser?: string // New user redirect } ``` *** ## Provider API [Section titled “Provider API”](#provider-api) ### defineProvider() [Section titled “defineProvider()”](#defineprovider) Wraps a provider configuration for use in Cloudwerk. ```typescript import { defineProvider } from '@cloudwerk/auth/convention' export default defineProvider(providerConfig) ``` ### OAuth Providers [Section titled “OAuth Providers”](#oauth-providers) #### github() [Section titled “github()”](#github) ```typescript import { github } from '@cloudwerk/auth/convention' github({ clientId: string, clientSecret: string, scope?: string, // Default: 'read:user user:email' }) ``` #### google() [Section titled “google()”](#google) ```typescript import { google } from '@cloudwerk/auth/convention' google({ clientId: string, clientSecret: string, scope?: string, // Default: 'openid email profile' }) ``` #### discord() [Section titled “discord()”](#discord) ```typescript import { discord } from '@cloudwerk/auth/convention' discord({ clientId: string, clientSecret: string, scope?: string, // Default: 'identify email' }) ``` #### apple() [Section titled “apple()”](#apple) ```typescript import { apple } from '@cloudwerk/auth/convention' apple({ clientId: string, clientSecret: string, // Generated from Apple private key scope?: string, }) ``` ### createOAuth2Provider() [Section titled “createOAuth2Provider()”](#createoauth2provider) Create a custom OAuth 2.0 provider. ```typescript import { createOAuth2Provider } from '@cloudwerk/auth' createOAuth2Provider({ id: string, // Unique provider ID name: string, // Display name clientId: string, clientSecret: string, authorization: string, // Authorization endpoint URL token: string, // Token endpoint URL userinfo: string, // Userinfo endpoint URL scope?: string, // OAuth scopes profile: (profile: unknown) => UserProfile, // Profile mapping }) ``` **UserProfile Interface:** ```typescript interface UserProfile { id: string email?: string name?: string image?: string emailVerified?: Date | null } ``` ### credentials() [Section titled “credentials()”](#credentials) Email/password authentication. ```typescript import { credentials } from '@cloudwerk/auth' credentials({ credentials: { [fieldName: string]: { label: string, type: 'text' | 'email' | 'password', placeholder?: string, required?: boolean, } }, authorize: (credentials: Record, ctx: AuthContext) => Promise, }) ``` **Example:** ```typescript credentials({ credentials: { email: { label: 'Email', type: 'email', required: true }, password: { label: 'Password', type: 'password', required: true }, }, async authorize(creds, ctx) { const user = await ctx.env.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 } }, }) ``` ### email() [Section titled “email()”](#email) Magic link / passwordless authentication. ```typescript import { email } from '@cloudwerk/auth' email({ from: string, // Sender email address maxAge?: number, // Link validity in seconds (default: 86400) sendVerificationRequest: (params: EmailParams) => Promise, }) interface EmailParams { identifier: string // Recipient email url: string // Magic link URL token: string // Verification token expires: Date // Expiration time } ``` ### passkey() [Section titled “passkey()”](#passkey) WebAuthn / passkey authentication. ```typescript import { passkey } from '@cloudwerk/auth' passkey({ rpName: string, // Relying party name rpId: string, // Relying party ID (domain) origin: string, // Expected origin authenticatorAttachment?: 'platform' | 'cross-platform', userVerification?: 'required' | 'preferred' | 'discouraged', residentKey?: 'required' | 'preferred' | 'discouraged', }) ``` *** ## Callbacks API [Section titled “Callbacks API”](#callbacks-api) ### defineAuthCallbacks() [Section titled “defineAuthCallbacks()”](#defineauthcallbacks) Define lifecycle callbacks for the auth flow. ```typescript import { defineAuthCallbacks } from '@cloudwerk/auth/convention' export default defineAuthCallbacks({ signIn?: SignInCallback, redirect?: RedirectCallback, session?: SessionCallback, jwt?: JwtCallback, }) ``` #### SignInCallback [Section titled “SignInCallback”](#signincallback) Called when a user signs in. ```typescript type SignInCallback = (params: { user: User, account: Account | null, profile?: Profile, email?: { verificationRequest?: boolean }, credentials?: Record, }) => Awaitable // Return false to deny, string to redirect ``` #### RedirectCallback [Section titled “RedirectCallback”](#redirectcallback) Customize redirect URLs. ```typescript type RedirectCallback = (params: { url: string, baseUrl: string, }) => Awaitable ``` #### SessionCallback [Section titled “SessionCallback”](#sessioncallback) Customize session data. ```typescript type SessionCallback = (params: { session: Session, user: User, token?: JWT, }) => Awaitable ``` **Example:** ```typescript defineAuthCallbacks({ async session({ session, user }) { session.user.id = user.id session.user.role = user.role return session }, }) ``` #### JwtCallback [Section titled “JwtCallback”](#jwtcallback) Customize JWT token (jwt strategy only). ```typescript type JwtCallback = (params: { token: JWT, user?: User, account?: Account, profile?: Profile, trigger?: 'signIn' | 'signUp' | 'update', }) => Awaitable ``` *** ## RBAC API [Section titled “RBAC API”](#rbac-api) ### defineRBAC() [Section titled “defineRBAC()”](#definerbac) Define roles and permissions. ```typescript import { defineRBAC } from '@cloudwerk/auth/convention' export default defineRBAC({ roles: Role[], defaultRole?: string, hierarchy?: Record, }) ``` #### Role [Section titled “Role”](#role) ```typescript interface Role { id: string // Unique role identifier name: string // Display name permissions: string[] // Permission strings description?: string // Optional description } ``` #### Permission Syntax [Section titled “Permission Syntax”](#permission-syntax) | Pattern | Description | | --------------------- | ----------------------- | | `resource:action` | Standard permission | | `resource:*` | All actions on resource | | `*` | Full access (admin) | | `resource:action:own` | Only own resources | **Example:** ```typescript defineRBAC({ roles: [ { id: 'admin', name: 'Administrator', permissions: ['*'], }, { id: 'editor', name: 'Editor', permissions: [ 'posts:create', 'posts:read', 'posts:update', 'posts:delete:own', 'media:*', ], }, { id: 'viewer', name: 'Viewer', permissions: ['posts:read', 'media:read'], }, ], defaultRole: 'viewer', hierarchy: { editor: ['viewer'], // Editor inherits viewer permissions }, }) ``` *** ## Context Helpers [Section titled “Context Helpers”](#context-helpers) ### getUser() [Section titled “getUser()”](#getuser) Get the current authenticated user (returns null if not authenticated). ```typescript import { getUser } from '@cloudwerk/auth' const user = getUser() // Returns: User | null ``` ### getSession() [Section titled “getSession()”](#getsession) Get the current session. ```typescript import { getSession } from '@cloudwerk/auth' const session = getSession() // Returns: Session | null ``` ### isAuthenticated() [Section titled “isAuthenticated()”](#isauthenticated) Check if the current request is authenticated. ```typescript import { isAuthenticated } from '@cloudwerk/auth' if (isAuthenticated()) { // User is logged in } ``` ### requireAuth() [Section titled “requireAuth()”](#requireauth) Require authentication; redirects or throws if not authenticated. ```typescript import { requireAuth } from '@cloudwerk/auth' // Redirects to sign-in page if not authenticated const user = requireAuth() // Throws UnauthenticatedError instead of redirecting const user = requireAuth({ throwError: true }) // Custom redirect URL const user = requireAuth({ redirectTo: '/custom-login' }) ``` ### hasRole() [Section titled “hasRole()”](#hasrole) Check if the user has a specific role. ```typescript import { hasRole } from '@cloudwerk/auth' if (hasRole('admin')) { // User is admin } // Check for any of multiple roles if (hasRole(['admin', 'moderator'])) { // User has admin OR moderator role } ``` ### hasPermission() [Section titled “hasPermission()”](#haspermission) Check if the user has a specific permission. ```typescript import { hasPermission } from '@cloudwerk/auth' if (hasPermission('posts:delete')) { // User can delete posts } ``` ### requireRole() [Section titled “requireRole()”](#requirerole) Require a specific role; throws ForbiddenError if lacking. ```typescript import { requireRole } from '@cloudwerk/auth' requireRole('admin') // Throws if not admin requireRole(['admin', 'moderator']) // Any of these roles ``` ### requirePermission() [Section titled “requirePermission()”](#requirepermission) Require a specific permission; throws ForbiddenError if lacking. ```typescript import { requirePermission } from '@cloudwerk/auth' requirePermission('posts:create') ``` *** ## Middleware API [Section titled “Middleware API”](#middleware-api) ### authMiddleware() [Section titled “authMiddleware()”](#authmiddleware) Create authentication middleware for route protection. ```typescript import { authMiddleware } from '@cloudwerk/auth/middleware' export const middleware = authMiddleware(options) ``` #### AuthMiddlewareOptions [Section titled “AuthMiddlewareOptions”](#authmiddlewareoptions) ```typescript interface AuthMiddlewareOptions { // Authentication requirements required?: boolean // Require authentication (default: true) unauthenticatedRedirect?: string // Redirect URL for unauthenticated // Role requirements role?: string // Required role roles?: string[] // Any of these roles // Permission requirements permission?: string // Required permission permissions?: string[] // Any of these permissions // Custom authorization authorize?: (user: User, request: Request) => Awaitable // Forbidden handling unauthorizedRedirect?: string // Redirect for unauthorized (403) } ``` **Examples:** ```typescript // Require authentication authMiddleware({ unauthenticatedRedirect: '/login', }) // Require specific role authMiddleware({ role: 'admin', unauthorizedRedirect: '/forbidden', }) // Custom authorization logic authMiddleware({ async authorize(user, request) { const url = new URL(request.url) const resourceId = url.pathname.split('/').pop() const resource = await getResource(resourceId) return resource.ownerId === user.id }, }) ``` *** ## Multi-Tenancy API [Section titled “Multi-Tenancy API”](#multi-tenancy-api) ### createTenantResolver() [Section titled “createTenantResolver()”](#createtenantresolver) Create a tenant resolver for multi-tenant applications. ```typescript import { createTenantResolver, createD1TenantStorage } from '@cloudwerk/auth/tenant' const storage = createD1TenantStorage(env.DB) const resolver = createTenantResolver(storage, options) ``` #### TenantResolverOptions [Section titled “TenantResolverOptions”](#tenantresolveroptions) ```typescript interface TenantResolverOptions { strategy: 'subdomain' | 'path' | 'header' | 'cookie' baseDomain?: string // For subdomain strategy pathPrefix?: string // For path strategy (default: '/t/') headerName?: string // For header strategy (default: 'X-Tenant-ID') cookieName?: string // For cookie strategy (default: 'tenant') } ``` #### TenantResolver Methods [Section titled “TenantResolver Methods”](#tenantresolver-methods) ```typescript interface TenantResolver { resolve(request: Request): Promise require(request: Request): Promise<{ tenant: Tenant }> // Throws if not found } ``` ### Storage Adapters [Section titled “Storage Adapters”](#storage-adapters) ```typescript import { createD1TenantStorage, createKVTenantStorage, createMemoryTenantStorage, } from '@cloudwerk/auth/tenant' // D1 storage const storage = createD1TenantStorage(env.DB, { tableName: 'tenants', // Default }) // KV storage const storage = createKVTenantStorage(env.TENANTS_KV, { prefix: 'tenant:', // Default }) // In-memory (for testing) const storage = createMemoryTenantStorage() ``` *** ## Rate Limiting API [Section titled “Rate Limiting API”](#rate-limiting-api) ### createLoginRateLimiter() [Section titled “createLoginRateLimiter()”](#createloginratelimiter) Rate limiter for login attempts. ```typescript import { createLoginRateLimiter, createFixedWindowStorage } from '@cloudwerk/auth/rate-limit' const storage = createFixedWindowStorage(env.RATE_LIMIT_KV) const limiter = createLoginRateLimiter(storage, { limit: 5, // Max attempts window: 900, // Per 15 minutes (in seconds) }) ``` ### createPasswordResetRateLimiter() [Section titled “createPasswordResetRateLimiter()”](#createpasswordresetratelimiter) Rate limiter for password reset requests. ```typescript import { createPasswordResetRateLimiter } from '@cloudwerk/auth/rate-limit' const limiter = createPasswordResetRateLimiter(storage, { limit: 3, // Max attempts window: 3600, // Per hour }) ``` ### createEmailVerificationRateLimiter() [Section titled “createEmailVerificationRateLimiter()”](#createemailverificationratelimiter) Rate limiter for email verification requests. ```typescript import { createEmailVerificationRateLimiter } from '@cloudwerk/auth/rate-limit' const limiter = createEmailVerificationRateLimiter(storage, { limit: 5, // Max attempts window: 3600, // Per hour }) ``` ### Rate Limiter Methods [Section titled “Rate Limiter Methods”](#rate-limiter-methods) ```typescript interface RateLimiter { check(request: Request): Promise } interface RateLimitResult { success: boolean // Whether request is allowed limit: number // Configured limit remaining: number // Remaining requests reset: number // Seconds until reset response?: Response // 429 response if rate limited } ``` ### Storage Implementations [Section titled “Storage Implementations”](#storage-implementations) ```typescript import { createFixedWindowStorage, createSlidingWindowStorage, } from '@cloudwerk/auth/rate-limit' // Fixed window (simpler, less accurate) const storage = createFixedWindowStorage(env.KV) // Sliding window (more accurate, higher storage) const storage = createSlidingWindowStorage(env.KV) ``` *** ## Client API [Section titled “Client API”](#client-api) ### signIn() [Section titled “signIn()”](#signin) Initiate sign-in flow. ```typescript import { signIn } from '@cloudwerk/auth/client' // OAuth provider await signIn('github') await signIn('google', { callbackUrl: '/dashboard' }) // Credentials await signIn('credentials', { email: 'user@example.com', password: 'password', redirectTo: '/dashboard', }) // Email/magic link await signIn('email', { email: 'user@example.com' }) ``` ### signOut() [Section titled “signOut()”](#signout) Sign out the current user. ```typescript import { signOut } from '@cloudwerk/auth/client' await signOut() await signOut({ redirectTo: '/' }) await signOut({ redirect: false }) // Returns session data ``` ### getSession() (Client) [Section titled “getSession() (Client)”](#getsession-client) Get the current session on the client. ```typescript import { getSession } from '@cloudwerk/auth/client' const session = await getSession() if (session) { console.log('Logged in as', session.user.email) } ``` ### createAuthStore() [Section titled “createAuthStore()”](#createauthstore) Create a reactive auth store for frameworks. ```typescript import { createAuthStore } from '@cloudwerk/auth/client' const store = createAuthStore() // Subscribe to changes store.subscribe((state) => { console.log('Auth state:', state.status, state.user) }) // Get current state const { user, status } = store.getState() // Status: 'loading' | 'authenticated' | 'unauthenticated' ``` *** ## Password Utilities [Section titled “Password Utilities”](#password-utilities) ### hashPassword() [Section titled “hashPassword()”](#hashpassword) Hash a password for storage. ```typescript import { hashPassword } from '@cloudwerk/auth' const hash = await hashPassword('user_password') // Store hash in database ``` ### verifyPassword() [Section titled “verifyPassword()”](#verifypassword) Verify a password against a hash. ```typescript import { verifyPassword } from '@cloudwerk/auth' const isValid = await verifyPassword('user_password', storedHash) ``` ### generateToken() [Section titled “generateToken()”](#generatetoken) Generate a secure random token. ```typescript import { generateToken } from '@cloudwerk/auth' const token = await generateToken() // 32 bytes, hex encoded const token = await generateToken(64) // Custom length ``` *** ## Error Classes [Section titled “Error Classes”](#error-classes) ### UnauthenticatedError [Section titled “UnauthenticatedError”](#unauthenticatederror) Thrown when authentication is required but missing. ```typescript import { UnauthenticatedError } from '@cloudwerk/auth' class UnauthenticatedError extends Error { readonly code: 'UNAUTHENTICATED' readonly status: 401 } ``` ### ForbiddenError [Section titled “ForbiddenError”](#forbiddenerror) Thrown when user lacks required role/permission. ```typescript import { ForbiddenError } from '@cloudwerk/auth' class ForbiddenError extends Error { readonly code: 'FORBIDDEN' readonly status: 403 readonly requiredRole?: string readonly requiredPermission?: string } ``` ### InvalidCredentialsError [Section titled “InvalidCredentialsError”](#invalidcredentialserror) Thrown when credentials are invalid. ```typescript import { InvalidCredentialsError } from '@cloudwerk/auth' class InvalidCredentialsError extends Error { readonly code: 'INVALID_CREDENTIALS' readonly status: 401 } ``` ### SessionExpiredError [Section titled “SessionExpiredError”](#sessionexpirederror) Thrown when session has expired. ```typescript import { SessionExpiredError } from '@cloudwerk/auth' class SessionExpiredError extends Error { readonly code: 'SESSION_EXPIRED' readonly status: 401 } ``` *** ## Type Definitions [Section titled “Type Definitions”](#type-definitions) ### User [Section titled “User”](#user) ```typescript interface User { id: string email?: string | null name?: string | null image?: string | null emailVerified?: Date | null role?: string roles?: string[] } ``` ### Session [Section titled “Session”](#session) ```typescript interface Session { user: User expires: Date } ``` ### Account [Section titled “Account”](#account) ```typescript interface Account { provider: string providerAccountId: string type: 'oauth' | 'oidc' | 'email' | 'credentials' | 'webauthn' access_token?: string refresh_token?: string expires_at?: number token_type?: string scope?: string } ``` ### JWT [Section titled “JWT”](#jwt) ```typescript interface JWT { sub: string // Subject (user ID) iat: number // Issued at exp: number // Expires at jti: string // JWT ID [key: string]: unknown // Custom claims } ``` ### Tenant [Section titled “Tenant”](#tenant) ```typescript interface Tenant { id: string slug: string name: string settings?: Record createdAt: Date updatedAt: Date } ``` *** ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Authentication Guide](/guides/authentication/)** - Patterns and best practices * **[Middleware API](/api/context/)** - Advanced middleware patterns * **[Database Guide](/guides/database/)** - User storage with D1 # Bindings Reference > Reference for Cloudflare bindings available in Cloudwerk, including typed proxies for queues, services, and durable objects. Cloudwerk provides seamless access to all Cloudflare bindings. In route handlers, you can import bindings directly for a clean, ergonomic API. ## Importable Bindings (Recommended) [Section titled “Importable Bindings (Recommended)”](#importable-bindings-recommended) The simplest way to access bindings in route handlers is to import them directly: ```typescript // app/api/posts/route.ts import { DB, CACHE } from '@cloudwerk/core/bindings' import { json } from '@cloudwerk/core' export async function GET() { const { results: posts } = await DB.prepare('SELECT * FROM posts').all() return json(posts) } ``` Tip Importable bindings use proxies that resolve from the current request context at access time. This means the binding is always the correct instance for the current request. ### Setup [Section titled “Setup”](#setup) Run the type generator to enable TypeScript autocomplete for your bindings: ```bash cloudwerk bindings generate-types ``` This creates `.cloudwerk/types/` with type definitions for your bindings and updates your `tsconfig.json` to include them. ### Available Exports [Section titled “Available Exports”](#available-exports) The `@cloudwerk/core/bindings` module provides: | Export | Description | | ------------------------------------------ | ---------------------------------------------- | | Named exports (e.g., `DB`, `KV`, `BUCKET`) | Direct access to bindings by name | | `bindings` | Proxy object for dynamic access to any binding | | `getBinding(name)` | Type-safe binding retrieval | | `hasBinding(name)` | Check if a binding exists | | `getBindingNames()` | List all available binding names | | `queues` | Typed queue producers (see below) | | `services` | Typed service RPC proxy (see below) | | `durableObjects` | Typed Durable Object namespaces (see below) | ```typescript import { bindings, getBinding, hasBinding, getBindingNames } from '@cloudwerk/core/bindings' export async function GET() { // Dynamic access const db = bindings.DB as D1Database // Type-safe retrieval const kv = getBinding('CACHE') // Conditional access if (hasBinding('ANALYTICS')) { const analytics = getBinding('ANALYTICS') // Use analytics... } // List all bindings const available = getBindingNames() return json({ availableBindings: available }) } ``` ## Accessing Bindings in Loaders [Section titled “Accessing Bindings in Loaders”](#accessing-bindings-in-loaders) In loader functions, access bindings via the `context` parameter: ```typescript export async function loader({ context }: LoaderArgs) { // Access bindings via context const db = context.db; // D1 Database const kv = context.kv; // KV Namespace const r2 = context.r2; // R2 Bucket const env = context.env; // Environment variables } ``` Note In loaders, you receive the context as a parameter. In route handlers, use importable bindings or `getContext()` to access bindings. *** ## Typed Queue Producers [Section titled “Typed Queue Producers”](#typed-queue-producers) Cloudwerk provides a typed `queues` proxy for sending messages to queue consumers defined in `app/queues/`. ```typescript import { queues } from '@cloudwerk/core/bindings' // Send to email queue (defined in app/queues/email.ts) await queues.email.send({ to: 'user@example.com', subject: 'Welcome!', body: 'Thanks for signing up.', }) // Send with delay const delayedMessage = { to: 'user@example.com', subject: 'Reminder', body: 'Follow up' } await queues.email.send(delayedMessage, { delaySeconds: 60 }) // Batch send await queues.notifications.sendBatch([ { userId: '1', type: 'email', message: 'Hello' }, { userId: '2', type: 'push', message: 'Hello' }, ]) ``` ### Queue Utilities [Section titled “Queue Utilities”](#queue-utilities) ```typescript import { queues, getQueue, hasQueue, getQueueNames } from '@cloudwerk/core/bindings' // Type-safe queue retrieval interface EmailMessage { to: string subject: string body: string } const emailQueue = getQueue('email') // Check if queue exists if (hasQueue('analytics')) { await queues.analytics.send({ event: 'pageview' }) } // List all queues const available = getQueueNames() // ['email', 'notifications', 'analytics'] ``` ### Queue Interface [Section titled “Queue Interface”](#queue-interface) ```typescript interface Queue { send(message: T, options?: SendOptions): Promise sendBatch(messages: T[], options?: SendOptions): Promise } interface SendOptions { delaySeconds?: number // 0-43200 (max 12 hours) contentType?: 'json' | 'text' | 'bytes' | 'v8' } ``` Tip Run `cloudwerk queues generate-types` to generate TypeScript definitions for your queue message types. *** ## Typed Services Proxy [Section titled “Typed Services Proxy”](#typed-services-proxy) Cloudwerk provides a typed `services` proxy for calling service methods defined in `app/services/`. ```typescript import { services } from '@cloudwerk/core/bindings' // Call email service (defined in app/services/email/service.ts) const result = await services.email.send({ to: 'user@example.com', subject: 'Hello', body: '

Welcome!

', }) // Call payment service const checkout = await services.payments.createCheckout({ customerId: 'cus_123', items: [{ priceId: 'price_123', quantity: 1 }], successUrl: '/success', cancelUrl: '/cancel', }) ``` ### Service Utilities [Section titled “Service Utilities”](#service-utilities) ```typescript import { services, getService, hasService, getServiceNames } from '@cloudwerk/core/bindings' // Type-safe service retrieval interface EmailService { send(params: { to: string; subject: string; body: string }): Promise<{ success: boolean }> } const email = getService('email') // Check if service exists if (hasService('analytics')) { await services.analytics.track({ event: 'signup' }) } // List all services const available = getServiceNames() // ['email', 'payments', 'cache'] ``` ### Transparent Routing [Section titled “Transparent Routing”](#transparent-routing) The services proxy automatically routes calls based on the configured mode: ```typescript // Your code (identical for both modes) await services.email.send({ to: '...' }) // Local mode: Direct function call (no latency) // Extracted mode: RPC via Cloudflare service binding ``` Tip Run `cloudwerk services generate-types` to generate TypeScript definitions for your service methods. *** ## Typed Durable Objects Proxy [Section titled “Typed Durable Objects Proxy”](#typed-durable-objects-proxy) Cloudwerk provides a typed `durableObjects` proxy for accessing Durable Objects defined in `app/objects/`. ```typescript import { durableObjects } from '@cloudwerk/core/bindings' // Get a counter DO by name const counter = durableObjects.counter.get('user-123') // Call RPC methods directly const count = await counter.increment(5) const current = await counter.getCount() await counter.reset() // Get by unique ID const uniqueId = durableObjects.counter.newUniqueId() const newCounter = durableObjects.counter.getById(uniqueId) ``` ### Durable Object Utilities [Section titled “Durable Object Utilities”](#durable-object-utilities) ```typescript import { durableObjects, getDurableObject, hasDurableObject } from '@cloudwerk/core/bindings' // Type-safe DO retrieval interface CounterDO { increment(amount?: number): Promise getCount(): Promise } const counterNs = getDurableObject('counter') // Check if DO exists if (hasDurableObject('game')) { const game = durableObjects.game.get('room-abc') } ``` ### Namespace Interface [Section titled “Namespace Interface”](#namespace-interface) ```typescript interface DurableObjectNamespaceProxy { get(name: string): T // Get by name (deterministic) getById(id: DurableObjectId): T // Get by unique ID newUniqueId(): DurableObjectId // Generate unique ID idFromName(name: string): DurableObjectId idFromString(hexId: string): DurableObjectId } ``` Tip Run `cloudwerk objects generate-types` to generate TypeScript definitions for your Durable Object methods. *** ## D1 Database [Section titled “D1 Database”](#d1-database) Cloudflare D1 is a serverless SQLite database. ### Configuration [Section titled “Configuration”](#configuration) ```toml # wrangler.toml [[d1_databases]] binding = "DB" database_name = "my-database" database_id = "your-database-id" ``` ### Usage [Section titled “Usage”](#usage) ```typescript // Query builder (recommended) const users = await context.db .selectFrom('users') .where('status', '=', 'active') .execute(); // Raw queries const result = await context.env.DB .prepare('SELECT * FROM users WHERE id = ?') .bind(userId) .first(); // Batch queries const results = await context.env.DB.batch([ context.env.DB.prepare('SELECT * FROM users'), context.env.DB.prepare('SELECT * FROM posts'), ]); ``` ### Types [Section titled “Types”](#types) ```typescript interface D1Database { prepare(query: string): D1PreparedStatement; batch(statements: D1PreparedStatement[]): Promise[]>; run(query: string): Promise; } interface D1PreparedStatement { bind(...values: unknown[]): D1PreparedStatement; first(column?: string): Promise; all(): Promise>; run(): Promise; } ``` ## KV Namespace [Section titled “KV Namespace”](#kv-namespace) Cloudflare Workers KV provides key-value storage. ### Configuration [Section titled “Configuration”](#configuration-1) ```toml # wrangler.toml [[kv_namespaces]] binding = "KV" id = "your-kv-id" ``` ### Usage [Section titled “Usage”](#usage-1) ```typescript // Get value const value = await context.kv.get('key'); const jsonValue = await context.kv.get('key', 'json'); const streamValue = await context.kv.get('key', 'stream'); // Set value await context.kv.put('key', 'value'); await context.kv.put('key', JSON.stringify(data)); // With expiration await context.kv.put('key', 'value', { expirationTtl: 3600, // 1 hour in seconds }); // With metadata await context.kv.put('key', 'value', { metadata: { createdAt: Date.now() }, }); // Delete await context.kv.delete('key'); // List keys const keys = await context.kv.list(); const prefixedKeys = await context.kv.list({ prefix: 'user:' }); ``` ### Types [Section titled “Types”](#types-1) ```typescript interface KVNamespace { get(key: string, type?: 'text'): Promise; get(key: string, type: 'json'): Promise; get(key: string, type: 'arrayBuffer'): Promise; get(key: string, type: 'stream'): Promise; put(key: string, value: string | ArrayBuffer | ReadableStream, options?: KVPutOptions): Promise; delete(key: string): Promise; list(options?: KVListOptions): Promise; } ``` ## R2 Object Storage [Section titled “R2 Object Storage”](#r2-object-storage) Cloudflare R2 provides S3-compatible object storage. ### Configuration [Section titled “Configuration”](#configuration-2) ```toml # wrangler.toml [[r2_buckets]] binding = "R2" bucket_name = "my-bucket" ``` ### Usage [Section titled “Usage”](#usage-2) ```typescript // Get object const object = await context.r2.get('path/to/file.txt'); if (object) { const text = await object.text(); const arrayBuffer = await object.arrayBuffer(); const blob = await object.blob(); } // Put object await context.r2.put('path/to/file.txt', 'Hello, World!'); await context.r2.put('path/to/file.txt', fileStream, { httpMetadata: { contentType: 'text/plain', }, }); // Delete object await context.r2.delete('path/to/file.txt'); // List objects const objects = await context.r2.list(); const prefixedObjects = await context.r2.list({ prefix: 'uploads/', limit: 100, }); // Head (metadata only) const head = await context.r2.head('path/to/file.txt'); ``` ### Types [Section titled “Types”](#types-2) ```typescript interface R2Bucket { get(key: string): Promise; put(key: string, value: ReadableStream | ArrayBuffer | string, options?: R2PutOptions): Promise; delete(key: string): Promise; list(options?: R2ListOptions): Promise; head(key: string): Promise; } ``` ## Environment Variables [Section titled “Environment Variables”](#environment-variables) Access environment variables and secrets. ### Configuration [Section titled “Configuration”](#configuration-3) ```toml # wrangler.toml [vars] ENVIRONMENT = "production" API_URL = "https://api.example.com" ``` Set secrets: ```bash wrangler secret put API_KEY ``` ### Usage [Section titled “Usage”](#usage-3) ```typescript // Access variables const environment = context.env.ENVIRONMENT; const apiUrl = context.env.API_URL; const apiKey = context.env.API_KEY; // Secret ``` Caution Never log or expose secrets. Use `wrangler secret` for sensitive values. ## Vectorize [Section titled “Vectorize”](#vectorize) Vector database for AI applications. ### Configuration [Section titled “Configuration”](#configuration-4) ```toml # wrangler.toml [[vectorize]] binding = "VECTORIZE" index_name = "my-index" ``` ### Usage [Section titled “Usage”](#usage-4) ```typescript // Insert vectors await context.env.VECTORIZE.insert([ { id: 'doc-1', values: [0.1, 0.2, 0.3], metadata: { title: 'Doc 1' } }, ]); // Query vectors const results = await context.env.VECTORIZE.query(queryVector, { topK: 10, filter: { category: 'tech' }, }); ``` ## AI [Section titled “AI”](#ai) Cloudflare Workers AI for inference. ### Configuration [Section titled “Configuration”](#configuration-5) ```toml # wrangler.toml [ai] binding = "AI" ``` ### Usage [Section titled “Usage”](#usage-5) ```typescript // Text generation const response = await context.env.AI.run('@cf/meta/llama-2-7b-chat-int8', { prompt: 'Hello!', }); // Embeddings const embeddings = await context.env.AI.run('@cf/baai/bge-base-en-v1.5', { text: 'Hello, world!', }); // Image classification const result = await context.env.AI.run('@cf/microsoft/resnet-50', { image: imageArrayBuffer, }); ``` ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Queues API](/api/queues/)** - Queue consumer reference * **[Services API](/api/services/)** - Service extraction reference * **[Durable Objects API](/api/durable-objects/)** - Durable Object reference * **[Database Guide](/guides/database/)** - D1 in depth * **[Authentication](/guides/authentication/)** - Using KV for sessions # CLI Reference > Complete reference for Cloudwerk CLI commands. The Cloudwerk CLI provides commands for development, building, and deployment. ## Installation [Section titled “Installation”](#installation) The CLI is included with `@cloudwerk/cli`: ```bash pnpm add -D @cloudwerk/cli ``` ## Commands [Section titled “Commands”](#commands) ### `cloudwerk dev` [Section titled “cloudwerk dev”](#cloudwerk-dev) Start the development server with hot reload. ```bash cloudwerk dev [path] [options] ``` **Options:** | Option | Description | Default | | -------------- | ---------------------- | ------------- | | `--port, -p` | Port number | `8787` | | `--host, -H` | Host to bind | `localhost` | | `--config, -c` | Path to config file | Auto-detected | | `--verbose` | Enable verbose logging | `false` | **Examples:** ```bash # Start dev server cloudwerk dev # Custom port cloudwerk dev --port 3000 # All interfaces cloudwerk dev --host 0.0.0.0 # With custom config cloudwerk dev --config cloudwerk.staging.ts ``` ### `cloudwerk build` [Section titled “cloudwerk build”](#cloudwerk-build) Build the application for production. ```bash cloudwerk build [path] [options] ``` **Options:** | Option | Description | Default | | -------------- | ---------------------------------------------------- | ------------- | | `--output, -o` | Output directory | `./dist` | | `--ssg` | Generate static pages for `rendering: static` routes | `false` | | `--minify` | Minify bundles | `true` | | `--no-minify` | Disable minification | - | | `--sourcemap` | Generate source maps | `false` | | `--config, -c` | Path to config file | Auto-detected | | `--verbose` | Enable verbose logging | `false` | **Examples:** ```bash # Production build cloudwerk build # With bundle analysis cloudwerk build --sourcemap # Development build cloudwerk build --no-minify # Static site generation cloudwerk build --ssg ``` ### `cloudwerk deploy` [Section titled “cloudwerk deploy”](#cloudwerk-deploy) Deploy to Cloudflare Workers. ```bash cloudwerk deploy [path] [options] ``` **Options:** | Option | Description | Default | | -------------- | ------------------------- | ------------- | | `--env, -e` | Environment to deploy to | - | | `--dry-run` | Preview without deploying | `false` | | `--skip-build` | Skip the build step | `false` | | `--config, -c` | Path to config file | Auto-detected | | `--verbose` | Enable verbose logging | `false` | **Examples:** ```bash # Deploy to production cloudwerk deploy # Deploy to staging cloudwerk deploy --env staging # Preview deployment cloudwerk deploy --dry-run ``` ### `cloudwerk config` [Section titled “cloudwerk config”](#cloudwerk-config) Manage Cloudwerk configuration. ```bash cloudwerk config [options] ``` **Subcommands:** | Command | Description | | ------------------- | ------------------------- | | `get ` | Get a configuration value | | `set ` | Set a configuration value | **Examples:** ```bash # Get config value cloudwerk config get appDir # Set config value cloudwerk config set dev.port 3000 ``` ### `cloudwerk bindings` [Section titled “cloudwerk bindings”](#cloudwerk-bindings) Manage Cloudflare bindings (D1, KV, R2, Queues, etc.). ```bash cloudwerk bindings [options] cloudwerk bindings [options] ``` Running without a subcommand shows an overview of all configured bindings. **Subcommands:** | Command | Description | | ---------------- | ------------------------------------------------- | | `add [type]` | Add a new binding (d1, kv, r2, queue, do, secret) | | `remove [name]` | Remove a binding | | `update [name]` | Update an existing binding | | `generate-types` | Regenerate TypeScript type definitions | **Examples:** ```bash # Show bindings overview cloudwerk bindings # Add D1 database cloudwerk bindings add d1 # Add KV namespace cloudwerk bindings add kv # Remove a binding cloudwerk bindings remove --force # Regenerate types cloudwerk bindings generate-types ``` ### `cloudwerk triggers` [Section titled “cloudwerk triggers”](#cloudwerk-triggers) Manage Cloudwerk triggers (scheduled, queue, R2, webhook, etc.). ```bash cloudwerk triggers [options] cloudwerk triggers [options] ``` Running without a subcommand shows an overview of all triggers. **Subcommands:** | Command | Description | | ---------- | --------------------------------------------- | | `list` | List all triggers with details | | `validate` | Validate trigger configurations | | `generate` | Regenerate wrangler.toml and TypeScript types | **Examples:** ```bash # Show triggers overview cloudwerk triggers # List all triggers cloudwerk triggers list # Filter by type cloudwerk triggers list --type scheduled # Validate configs cloudwerk triggers validate --strict # Regenerate config cloudwerk triggers generate # Output as JSON cloudwerk triggers list --json ``` ### `cloudwerk objects` [Section titled “cloudwerk objects”](#cloudwerk-objects) Manage Cloudwerk Durable Objects. ```bash cloudwerk objects [options] cloudwerk objects [options] ``` Running without a subcommand shows an overview of all durable objects. **Subcommands:** | Command | Description | | ------------- | ------------------------------------------------- | | `list` | List all durable objects with details | | `info ` | Show durable object details | | `migrations` | Show migration history for SQLite durable objects | | `generate` | Regenerate wrangler.toml and TypeScript types | **Examples:** ```bash # Show objects overview cloudwerk objects # List all durable objects cloudwerk objects list # Show details for specific object cloudwerk objects info Counter # View migrations cloudwerk objects migrations # Regenerate config cloudwerk objects generate ``` ### `cloudwerk services` [Section titled “cloudwerk services”](#cloudwerk-services) Manage Cloudwerk services (RPC bindings). ```bash cloudwerk services [options] cloudwerk services [options] ``` Running without a subcommand shows an overview of all services. **Subcommands:** | Command | Description | | ---------------- | -------------------------------------------- | | `list` | List all services with details | | `info ` | Show service details | | `extract ` | Extract service to separate Worker | | `inline ` | Convert extracted service back to local mode | | `deploy ` | Deploy extracted service | | `status` | Show status of all services | **Examples:** ```bash # Show services overview cloudwerk services # List all services cloudwerk services list # Show details for specific service cloudwerk services info email # Extract service to separate Worker cloudwerk services extract payments # Convert back to local mode cloudwerk services inline payments # Deploy extracted service cloudwerk services deploy payments --env staging # Show all services status cloudwerk services status ``` ## Global Options [Section titled “Global Options”](#global-options) These options work with all commands: | Option | Description | | --------------- | ---------------- | | `--config, -c` | Config file path | | `--help, -h` | Show help | | `--version, -v` | Show version | | `--verbose` | Verbose output | ## Configuration [Section titled “Configuration”](#configuration) Commands read from `cloudwerk.config.ts` by default. Override with `--config`: ```bash cloudwerk dev --config cloudwerk.production.ts ``` ## Exit Codes [Section titled “Exit Codes”](#exit-codes) | Code | Description | | ---- | ------------------- | | `0` | Success | | `1` | General error | | `2` | Configuration error | | `3` | Build error | | `4` | Deployment error | ## Planned Commands [Section titled “Planned Commands”](#planned-commands) Coming Soon The following commands are planned for future releases: * `cloudwerk generate` — Scaffold pages, layouts, routes, middleware, components * `cloudwerk migrate` — Database migrations (create, up, down, status, reset) * `cloudwerk routes` — Display registered routes * `cloudwerk typecheck` — Run TypeScript type checking * `cloudwerk lint` — Run ESLint with Cloudwerk-specific rules * `cloudwerk test` — Run Vitest tests with Cloudwerk test utilities ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Configuration](/api/configuration/)** - Config file options * **[Deployment Guide](/guides/deployment/)** - Deploy your app * **[Testing](/api/testing/)** - Testing utilities # Configuration > Complete reference for cloudwerk.config.ts configuration options. The `cloudwerk.config.ts` file is the central configuration for your Cloudwerk application. Convention Over Configuration Most Cloudwerk features use **convention-based discovery** and don’t require explicit configuration: * **Routes**: Auto-discovered from `app/` directory * **Triggers**: Auto-discovered from `app/triggers/` * **Services**: Auto-discovered from `app/services/` * **Queues**: Auto-discovered from `app/queues/` * **Durable Objects**: Auto-discovered from `app/objects/` The `cloudwerk.config.ts` file is for customizing defaults, not defining everything manually. ## Basic Configuration [Section titled “Basic Configuration”](#basic-configuration) ```typescript // cloudwerk.config.ts import { defineConfig } from '@cloudwerk/core'; export default defineConfig({ // Configuration options }); ``` ## Options Reference [Section titled “Options Reference”](#options-reference) ### `appDir` [Section titled “appDir”](#appdir) The directory containing your application routes. ```typescript export default defineConfig({ appDir: 'app', // default }); ``` ### `publicDir` [Section titled “publicDir”](#publicdir) The directory for static assets. ```typescript export default defineConfig({ publicDir: 'public', // default }); ``` ### `outDir` [Section titled “outDir”](#outdir) The output directory for builds. ```typescript export default defineConfig({ outDir: '.cloudwerk', // default }); ``` ### `dev` [Section titled “dev”](#dev) Development server configuration. ```typescript export default defineConfig({ dev: { port: 8787, // default host: 'localhost', // default https: false, // Enable HTTPS locally open: true, // Open browser on start }, }); ``` ### `build` [Section titled “build”](#build) Build configuration options. ```typescript export default defineConfig({ build: { minify: true, // Minify output sourcemap: true, // Generate sourcemaps treeshake: true, // Tree shake unused code target: 'es2022', // ES target external: [], // External dependencies }, }); ``` ### `database` [Section titled “database”](#database) D1 database configuration. ```typescript export default defineConfig({ database: { binding: 'DB', // Wrangler binding name migrationsDir: 'migrations', // Migrations directory }, }); ``` ### `auth` [Section titled “auth”](#auth) Authentication configuration. ```typescript export default defineConfig({ auth: { session: { storage: 'kv', // 'kv' | 'durable-object' namespace: 'SESSIONS', // KV namespace binding maxAge: 60 * 60 * 24 * 7, // 7 days in seconds cookie: { name: 'session', httpOnly: true, secure: true, sameSite: 'lax', path: '/', domain: undefined, // Auto-detect }, }, providers: { github: { clientId: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, scopes: ['user:email'], }, google: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, scopes: ['email', 'profile'], }, }, }, }); ``` ### `queues` [Section titled “queues”](#queues) Queue configuration. ```typescript export default defineConfig({ queues: { EMAIL_QUEUE: { handler: './workers/email-queue.ts', }, WEBHOOK_QUEUE: { handler: './workers/webhook-queue.ts', }, }, }); ``` ### `durableObjects` [Section titled “durableObjects”](#durableobjects) Durable Objects configuration. ```typescript export default defineConfig({ durableObjects: { COUNTER: { class: './workers/counter.ts', className: 'Counter', }, CHAT_ROOM: { class: './workers/chat-room.ts', className: 'ChatRoom', }, }, }); ``` ### `triggers` [Section titled “triggers”](#triggers) Cron trigger configuration. ```typescript export default defineConfig({ triggers: { handler: './workers/triggers.ts', }, }); ``` ### `middleware` [Section titled “middleware”](#middleware) Global middleware configuration. ```typescript export default defineConfig({ middleware: { // Apply to all routes global: ['./middleware/logging.ts', './middleware/cors.ts'], // Apply to specific paths paths: { '/api/*': ['./middleware/auth.ts'], '/admin/*': ['./middleware/admin.ts'], }, }, }); ``` ### `rendering` [Section titled “rendering”](#rendering) Server-side rendering configuration. ```typescript export default defineConfig({ rendering: { streaming: true, // Enable streaming SSR renderer: 'hono-jsx', // 'hono-jsx' | 'react' | 'preact' }, }); ``` ### `experimental` [Section titled “experimental”](#experimental) Experimental features. ```typescript export default defineConfig({ experimental: { // Enable experimental features parallelLoaders: true, edgeCaching: true, }, }); ``` ## Full Example [Section titled “Full Example”](#full-example) ```typescript // cloudwerk.config.ts import { defineConfig } from '@cloudwerk/core'; export default defineConfig({ appDir: 'app', publicDir: 'public', outDir: '.cloudwerk', dev: { port: 8787, open: true, }, build: { minify: true, sourcemap: true, treeshake: true, }, database: { binding: 'DB', migrationsDir: 'migrations', }, auth: { session: { storage: 'kv', namespace: 'SESSIONS', maxAge: 60 * 60 * 24 * 7, cookie: { name: 'session', httpOnly: true, secure: true, sameSite: 'lax', }, }, }, queues: { EMAIL_QUEUE: { handler: './workers/email-queue.ts', }, }, durableObjects: { RATE_LIMITER: { class: './workers/rate-limiter.ts', className: 'RateLimiter', }, }, triggers: { handler: './workers/triggers.ts', }, rendering: { streaming: true, renderer: 'hono-jsx', }, }); ``` ## TypeScript Support [Section titled “TypeScript Support”](#typescript-support) The `defineConfig` helper provides full TypeScript support: ```typescript import { defineConfig, type CloudwerkConfig } from '@cloudwerk/core'; // Full type inference export default defineConfig({ // TypeScript will show available options }); // Or define separately const config: CloudwerkConfig = { appDir: 'app', }; export default defineConfig(config); ``` ## Environment-Specific Config [Section titled “Environment-Specific Config”](#environment-specific-config) Use environment variables for environment-specific configuration: ```typescript export default defineConfig({ build: { sourcemap: process.env.NODE_ENV !== 'production', minify: process.env.NODE_ENV === 'production', }, }); ``` Tip Configuration is loaded at build time. Environment variables are resolved during the build process. ## Next Steps [Section titled “Next Steps”](#next-steps) * **[CLI Reference](/api/cli/)** - Command-line interface * **[Context API](/api/context/)** - Runtime context * **[Bindings Reference](/api/bindings/)** - Cloudflare bindings # Context API > Reference for the CloudwerkContext object available in handlers and loaders. The `CloudwerkContext` object provides access to bindings, utilities, and request information throughout your application. ## Overview [Section titled “Overview”](#overview) There are three ways to access context in Cloudwerk: 1. **Importable helpers** (recommended) - Import `params`, `request`, `get`, `set` directly 2. **`getContext()`** - Call to get the full context object 3. **`context` parameter** (loaders only) - Receive context as a function parameter All three methods work anywhere within a request: route handlers, loaders, pages, layouts, and middleware. ```typescript // Importable helpers - work everywhere (recommended) import { params, request, get } from '@cloudwerk/core/context' export async function GET() { const userId = params.id const user = get('user') return json({ userId, user }) } // In a loader - importable helpers or context parameter both work import { params, get } from '@cloudwerk/core/context' export async function loader({ context }: LoaderArgs) { // Option 1: Use importable helpers (recommended) const userId = params.id const user = get('user') // Option 2: Use context parameter const userId2 = context.params.id return { userId, user }; } // getContext() - also works everywhere import { getContext, json } from '@cloudwerk/core' export async function GET() { const ctx = getContext() return json({ userId: ctx.params.id }) } // In middleware import { set } from '@cloudwerk/core/context' export const middleware: Middleware = async (request, next) => { set('user', await validateSession(request)) return next() }; ``` ## Importable Context Helpers [Section titled “Importable Context Helpers”](#importable-context-helpers) Import context helpers directly from `@cloudwerk/core/context`. These work in route handlers, loaders, pages, layouts, and middleware - anywhere within a request: ```typescript import { params, request, get, set, getRequestId } from '@cloudwerk/core/context' export async function GET() { const userId = params.id const authHeader = request.headers.get('Authorization') const user = get('user') // From middleware return json({ userId, requestId: getRequestId() }) } ``` ### Available Exports [Section titled “Available Exports”](#available-exports) | Export | Type | Description | | -------------------- | --------------------------------- | -------------------------------------- | | `params` | `Record` | Route parameters from dynamic segments | | `request` | `Request` | Current request object | | `env` | `Record` | Environment bindings | | `executionCtx` | `ExecutionContext` | For `waitUntil()` background tasks | | `getRequestId()` | `() => string` | Get unique request ID for tracing | | `get(key)` | `(key: string) => T \| undefined` | Get middleware data | | `set(key, value)` | `(key: string, value: T) => void` | Set data for downstream handlers | ### Examples [Section titled “Examples”](#examples) **Access route parameters:** ```typescript import { params } from '@cloudwerk/core/context' // For route /users/[id]/posts/[postId] export async function GET() { const { id, postId } = params return json({ userId: id, postId }) } ``` **Access request data:** ```typescript import { request } from '@cloudwerk/core/context' export async function POST() { const body = await request.json() const contentType = request.headers.get('Content-Type') return json({ received: body }) } ``` **Background tasks with executionCtx:** ```typescript import { executionCtx, request } from '@cloudwerk/core/context' import { json } from '@cloudwerk/core' export async function POST() { const data = await request.json() // Fire-and-forget background task executionCtx.waitUntil( sendAnalytics({ event: 'data_submitted', data }) ) return json({ success: true }) } ``` **Share data between middleware and handlers:** ```typescript // middleware.ts import { set } from '@cloudwerk/core/context' export const middleware: Middleware = async (request, next) => { const user = await validateSession(request) set('user', user) return next() } // route.ts import { get } from '@cloudwerk/core/context' import { json } from '@cloudwerk/core' export async function GET() { const user = get('user') if (!user) { return new Response('Unauthorized', { status: 401 }) } return json({ user }) } ``` ## Properties [Section titled “Properties”](#properties) ### `env` [Section titled “env”](#env) Access to all Cloudflare bindings: ```typescript interface CloudwerkContext { env: { DB: D1Database; // D1 binding KV: KVNamespace; // KV binding R2: R2Bucket; // R2 binding MY_QUEUE: Queue; // Queue binding DURABLE_OBJECT: DurableObjectNamespace; [key: string]: unknown; // Custom bindings }; } ``` ### `db` [Section titled “db”](#db) Query builder for D1 database: ```typescript // Select const users = await context.db .selectFrom('users') .where('status', '=', 'active') .orderBy('created_at', 'desc') .limit(10) .execute(); // Insert const user = await context.db .insertInto('users') .values({ email: 'user@example.com', name: 'John' }) .returning(['id', 'email']) .executeTakeFirst(); // Update await context.db .updateTable('users') .set({ name: 'Jane' }) .where('id', '=', userId) .execute(); // Delete await context.db .deleteFrom('users') .where('id', '=', userId) .execute(); ``` ### `kv` [Section titled “kv”](#kv) KV namespace helper: ```typescript // Get value const value = await context.kv.get('key'); const jsonValue = await context.kv.get('key', 'json'); // Set value await context.kv.put('key', 'value'); await context.kv.put('key', JSON.stringify(data), { expirationTtl: 3600, }); // Delete await context.kv.delete('key'); ``` ### `r2` [Section titled “r2”](#r2) R2 bucket helper: ```typescript // Get object const object = await context.r2.get('path/to/file.txt'); if (object) { const text = await object.text(); } // Put object await context.r2.put('path/to/file.txt', content, { httpMetadata: { contentType: 'text/plain' }, }); // Delete await context.r2.delete('path/to/file.txt'); ``` ### `queues` [Section titled “queues”](#queues) Queue producers: ```typescript // Send message await context.queues.MY_QUEUE.send({ type: 'email', to: 'user@example.com' }); // Send batch await context.queues.MY_QUEUE.sendBatch([ { body: { type: 'email', to: 'user1@example.com' } }, { body: { type: 'email', to: 'user2@example.com' } }, ]); ``` ### `auth` [Section titled “auth”](#auth) Authentication utilities: ```typescript interface AuthContext { // Get current user (null if not authenticated) getUser(): Promise; // Require user (throws RedirectError if not authenticated) requireUser(): Promise; // Session management createSession(data: SessionData): Promise; destroySession(): Promise; getSession(): Promise; // OAuth helpers getOAuthUrl(provider: string): string; exchangeOAuthCode(provider: string, code: string): Promise; getOAuthProfile(provider: string, tokens: OAuthTokens): Promise; } ``` Usage: ```typescript // Get current user const user = await context.auth.getUser(); if (!user) { throw new RedirectError('/login'); } // Or use requireUser const user = await context.auth.requireUser(); // Throws if not authenticated // Create session await context.auth.createSession({ userId: user.id, email: user.email, }); // Destroy session (logout) await context.auth.destroySession(); ``` ### `request` [Section titled “request”](#request) The incoming Request object: ```typescript // Access request properties const url = new URL(context.request.url); const method = context.request.method; const headers = context.request.headers; // Parse body const json = await context.request.json(); const formData = await context.request.formData(); const text = await context.request.text(); ``` ### `waitUntil` [Section titled “waitUntil”](#waituntil) Extend request lifetime for background tasks: ```typescript export async function POST(request: Request, { context }: CloudwerkHandlerContext) { // Respond immediately const response = json({ success: true }); // Continue processing in background context.waitUntil( sendAnalyticsEvent({ type: 'api_call', endpoint: '/api/users' }) ); return response; } ``` ### `cf` [Section titled “cf”](#cf) Cloudflare-specific request properties: ```typescript interface CfProperties { asn: number; // ASN of the incoming request asOrganization: string; // Organization name city: string; // City colo: string; // Cloudflare data center continent: string; // Continent code country: string; // Country code latitude: string; // Latitude longitude: string; // Longitude postalCode: string; // Postal code region: string; // Region/state regionCode: string; // Region code timezone: string; // Timezone tlsVersion: string; // TLS version tlsCipher: string; // TLS cipher } // Usage const { country, city, timezone } = context.cf; console.log(`Request from ${city}, ${country} (${timezone})`); ``` ## Response Helpers [Section titled “Response Helpers”](#response-helpers) ### `json()` [Section titled “json()”](#json) Create JSON response: ```typescript export async function GET(request: Request, { context }: CloudwerkHandlerContext) { return context.json({ message: 'Hello' }); return context.json({ error: 'Not found' }, { status: 404 }); return context.json(data, { status: 201, headers: { 'X-Custom': 'value' }, }); } ``` ### `redirect()` [Section titled “redirect()”](#redirect) Create redirect response: ```typescript return context.redirect('/dashboard'); return context.redirect('/login', 302); // Temporary redirect return context.redirect('https://example.com', 301); // Permanent redirect ``` ### `html()` [Section titled “html()”](#html) Create HTML response: ```typescript return context.html('

Hello, World!

'); return context.html(htmlContent, { status: 200 }); ``` ### `text()` [Section titled “text()”](#text) Create text response: ```typescript return context.text('Hello, World!'); return context.text('Not found', { status: 404 }); ``` ### `stream()` [Section titled “stream()”](#stream) Create streaming response: ```typescript const stream = new ReadableStream({ start(controller) { controller.enqueue('Hello, '); controller.enqueue('World!'); controller.close(); }, }); return context.stream(stream, { headers: { 'Content-Type': 'text/plain' }, }); ``` ## Type Definitions [Section titled “Type Definitions”](#type-definitions) ```typescript interface CloudwerkContext { env: Env; db: DatabaseClient; kv: KVHelper; r2: R2Helper; queues: Record; auth: AuthContext; request: Request; waitUntil: (promise: Promise) => void; cf: CfProperties; // Response helpers json(data: T, init?: ResponseInit): Response; redirect(url: string, status?: number): Response; html(content: string, init?: ResponseInit): Response; text(content: string, init?: ResponseInit): Response; stream(stream: ReadableStream, init?: ResponseInit): Response; } interface CloudwerkHandlerContext { params: Record; context: CloudwerkContext; } interface LoaderArgs { request: Request; params: Record; context: CloudwerkContext; } ``` ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Bindings Reference](/api/bindings/)** - All Cloudflare bindings * **[Authentication Guide](/guides/authentication/)** - Auth patterns * **[Database Guide](/guides/database/)** - D1 database usage # Durable Objects API > Complete API reference for @cloudwerk/durable-object - stateful edge computing with native RPC, storage, and WebSockets. The `@cloudwerk/durable-object` package provides a convention-based system for creating Durable Objects with native RPC methods, SQLite/KV storage, WebSocket support, and alarms. ## Installation [Section titled “Installation”](#installation) ```bash pnpm add @cloudwerk/durable-object ``` ## defineDurableObject() [Section titled “defineDurableObject()”](#definedurableobject) Creates a Durable Object definition with typed state and methods. ```typescript import { defineDurableObject } from '@cloudwerk/durable-object' export default defineDurableObject({ init?: (ctx: DurableObjectContext) => Awaitable, methods?: Record, fetch?: (request: Request, ctx: DurableObjectContext) => Awaitable, webSocketMessage?: (ws: WebSocket, message: string | ArrayBuffer, ctx: DurableObjectContext) => Awaitable, webSocketClose?: (ws: WebSocket, code: number, reason: string, ctx: DurableObjectContext) => Awaitable, webSocketError?: (ws: WebSocket, error: Error, ctx: DurableObjectContext) => Awaitable, alarm?: (ctx: DurableObjectContext) => Awaitable, }) ``` ### Type Parameter [Section titled “Type Parameter”](#type-parameter) The generic type `TState` defines the shape of your Durable Object’s state. ```typescript interface CounterState { count: number lastUpdated: Date } export default defineDurableObject({ // ... }) ``` ### Parameters [Section titled “Parameters”](#parameters) | Parameter | Type | Description | | ------------------ | -------------------------- | ----------------------------------- | | `init` | `Function` | Initialize state when DO is created | | `methods` | `Record` | RPC methods exposed to callers | | `fetch` | `Function` | Handle HTTP requests | | `webSocketMessage` | `Function` | Handle WebSocket messages | | `webSocketClose` | `Function` | Handle WebSocket close events | | `webSocketError` | `Function` | Handle WebSocket errors | | `alarm` | `Function` | Handle scheduled alarms | *** ## DurableObjectContext [Section titled “DurableObjectContext”](#durableobjectcontext) The context object passed to all handlers. ```typescript interface DurableObjectContext { // State management state: TState setState(updates: Partial): void getState(): TState // Storage storage: DurableObjectStorage sql: SqlStorage // WebSocket acceptWebSocket(ws: WebSocket, tags?: string[]): void getWebSockets(tag?: string): WebSocket[] // Alarms setAlarm(time: Date | number): Promise deleteAlarm(): Promise getAlarm(): Promise // Environment env: Env // Execution waitUntil(promise: Promise): void blockConcurrencyWhile(fn: () => Promise): Promise } ``` *** ## Init Handler [Section titled “Init Handler”](#init-handler) Initialize state when the Durable Object is first created. ```typescript export default defineDurableObject({ async init(ctx) { // Load from storage or return defaults const stored = await ctx.storage.get('state') return stored ?? { count: 0, lastUpdated: new Date() } }, }) ``` Tip The `init` handler runs when the DO is first accessed. Use it to load persisted state from storage. *** ## RPC Methods [Section titled “RPC Methods”](#rpc-methods) Methods are exposed as native Cloudflare RPC methods. ```typescript export default defineDurableObject({ init: async () => ({ count: 0, lastUpdated: new Date() }), methods: { async increment(amount: number = 1) { this.state.count += amount this.state.lastUpdated = new Date() await this.ctx.storage.put('state', this.state) return this.state.count }, async decrement(amount: number = 1) { this.state.count -= amount this.state.lastUpdated = new Date() await this.ctx.storage.put('state', this.state) return this.state.count }, async getCount() { return this.state.count }, async reset() { this.state.count = 0 this.state.lastUpdated = new Date() await this.ctx.storage.put('state', this.state) return 0 }, }, }) ``` ### Calling RPC Methods [Section titled “Calling RPC Methods”](#calling-rpc-methods) ```typescript import { durableObjects } from '@cloudwerk/core/bindings' // Get stub by name const counter = durableObjects.counter.get('user-123') // Call methods directly (native RPC) const count = await counter.increment(5) const current = await counter.getCount() await counter.reset() ``` *** ## Storage API [Section titled “Storage API”](#storage-api) ### Key-Value Storage [Section titled “Key-Value Storage”](#key-value-storage) ```typescript interface DurableObjectStorage { // Single operations get(key: string): Promise put(key: string, value: T): Promise delete(key: string): Promise // Batch operations get(keys: string[]): Promise> put(entries: Record): Promise delete(keys: string[]): Promise // List list(options?: StorageListOptions): Promise> // Transactions transaction(fn: () => Promise): Promise // Sync sync(): Promise } interface StorageListOptions { prefix?: string start?: string end?: string limit?: number reverse?: boolean } ``` ### Storage Examples [Section titled “Storage Examples”](#storage-examples) ```typescript methods: { async saveUser(ctx, user: User) { await ctx.storage.put(`user:${user.id}`, user) }, async getUser(ctx, id: string) { return ctx.storage.get(`user:${id}`) }, async getAllUsers(ctx) { const users = await ctx.storage.list({ prefix: 'user:' }) return Array.from(users.values()) }, async deleteUser(ctx, id: string) { return ctx.storage.delete(`user:${id}`) }, async batchSave(ctx, users: User[]) { const entries = Object.fromEntries( users.map(u => [`user:${u.id}`, u]) ) await ctx.storage.put(entries) }, } ``` *** ## SQL Storage [Section titled “SQL Storage”](#sql-storage) Durable Objects support SQLite for relational data. ```typescript interface SqlStorage { exec(query: string, ...bindings: unknown[]): SqlStorageResult prepare(query: string): SqlStatement } interface SqlStatement { bind(...values: unknown[]): SqlStatement first(): T | null all(): T[] run(): SqlRunResult } interface SqlRunResult { changes: number lastRowId: number } ``` ### SQL Examples [Section titled “SQL Examples”](#sql-examples) ```typescript export default defineDurableObject({ async init(ctx) { // Create tables on init ctx.sql.exec(` CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, content TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) `) return { initialized: true } }, methods: { async addMessage(ctx, userId: string, content: string) { const result = ctx.sql.exec( 'INSERT INTO messages (user_id, content) VALUES (?, ?)', userId, content ) return result.lastRowId }, async getMessages(ctx, userId: string, limit: number = 50) { return ctx.sql .prepare('SELECT * FROM messages WHERE user_id = ? ORDER BY created_at DESC LIMIT ?') .bind(userId, limit) .all() }, async deleteMessage(ctx, id: number) { const result = ctx.sql.exec('DELETE FROM messages WHERE id = ?', id) return result.changes > 0 }, }, }) ``` *** ## Transactions [Section titled “Transactions”](#transactions) Ensure atomicity for multiple operations. ```typescript methods: { async transfer(ctx, fromId: string, toId: string, amount: number) { await ctx.storage.transaction(async () => { const from = await ctx.storage.get(`account:${fromId}`) const to = await ctx.storage.get(`account:${toId}`) if (!from || !to) throw new Error('Account not found') if (from.balance < amount) throw new Error('Insufficient funds') from.balance -= amount to.balance += amount await ctx.storage.put({ [`account:${fromId}`]: from, [`account:${toId}`]: to, }) }) }, } ``` *** ## Fetch Handler [Section titled “Fetch Handler”](#fetch-handler) Handle raw HTTP requests to the Durable Object. ```typescript export default defineDurableObject({ async fetch(request, ctx) { const url = new URL(request.url) if (url.pathname === '/ws') { // Upgrade to WebSocket const pair = new WebSocketPair() ctx.acceptWebSocket(pair[1]) return new Response(null, { status: 101, webSocket: pair[0] }) } if (url.pathname === '/state') { return Response.json(ctx.state) } return new Response('Not found', { status: 404 }) }, }) ``` *** ## WebSocket Handlers [Section titled “WebSocket Handlers”](#websocket-handlers) Handle WebSocket connections for real-time features. ```typescript interface ChatState { messages: Message[] } export default defineDurableObject({ init: async () => ({ messages: [] }), async fetch(request, ctx) { const url = new URL(request.url) if (url.pathname === '/ws') { const pair = new WebSocketPair() const userId = url.searchParams.get('userId') // Accept with tags for filtering ctx.acceptWebSocket(pair[1], [userId, 'all']) return new Response(null, { status: 101, webSocket: pair[0] }) } return new Response('Not found', { status: 404 }) }, async webSocketMessage(ws, message, ctx) { const data = JSON.parse(message as string) if (data.type === 'chat') { const msg = { id: crypto.randomUUID(), userId: data.userId, content: data.content, timestamp: new Date(), } ctx.state.messages.push(msg) await ctx.storage.put('messages', ctx.state.messages) // Broadcast to all connected clients for (const client of ctx.getWebSockets('all')) { client.send(JSON.stringify({ type: 'message', data: msg })) } } }, async webSocketClose(ws, code, reason, ctx) { console.log(`WebSocket closed: ${code} ${reason}`) }, async webSocketError(ws, error, ctx) { console.error('WebSocket error:', error) }, }) ``` ### WebSocket Methods [Section titled “WebSocket Methods”](#websocket-methods) | Method | Description | | -------------------------------- | ---------------------------------------------------- | | `ctx.acceptWebSocket(ws, tags?)` | Accept WebSocket with optional tags | | `ctx.getWebSockets(tag?)` | Get connected WebSockets, optionally filtered by tag | *** ## Alarms [Section titled “Alarms”](#alarms) Schedule periodic tasks within the Durable Object. ```typescript export default defineDurableObject({ async init(ctx) { // Schedule first alarm await ctx.setAlarm(Date.now() + 60_000) // 1 minute return { processedCount: 0 } }, async alarm(ctx) { // Process pending work await processPendingTasks(ctx) ctx.setState({ processedCount: ctx.state.processedCount + 1, }) // Schedule next alarm await ctx.setAlarm(Date.now() + 60_000) }, }) ``` ### Alarm Methods [Section titled “Alarm Methods”](#alarm-methods) | Method | Description | | -------------------- | --------------------------------------- | | `ctx.setAlarm(time)` | Schedule alarm (Date or timestamp) | | `ctx.deleteAlarm()` | Cancel scheduled alarm | | `ctx.getAlarm()` | Get scheduled alarm time (null if none) | *** ## Accessing Durable Objects [Section titled “Accessing Durable Objects”](#accessing-durable-objects) ### durableObjects Proxy [Section titled “durableObjects Proxy”](#durableobjects-proxy) ```typescript import { durableObjects } from '@cloudwerk/core/bindings' // Get by name (deterministic ID) const counter = durableObjects.counter.get('user-123') // Get by unique ID const uniqueId = durableObjects.counter.newUniqueId() const counter = durableObjects.counter.getById(uniqueId) // Call RPC methods const count = await counter.increment(5) ``` ### getDurableObject() [Section titled “getDurableObject()”](#getdurableobject) Get a typed Durable Object namespace. ```typescript import { getDurableObject } from '@cloudwerk/core/bindings' interface CounterDO { increment(amount?: number): Promise getCount(): Promise } const counterNs = getDurableObject('counter') const counter = counterNs.get('user-123') const count = await counter.increment(5) ``` ### hasDurableObject() [Section titled “hasDurableObject()”](#hasdurableobject) Check if a Durable Object namespace exists. ```typescript import { hasDurableObject } from '@cloudwerk/core/bindings' if (hasDurableObject('counter')) { const counter = durableObjects.counter.get('user-123') } ``` *** ## Error Handling [Section titled “Error Handling”](#error-handling) ### DurableObjectError [Section titled “DurableObjectError”](#durableobjecterror) Base error class for Durable Object failures. ```typescript import { DurableObjectError } from '@cloudwerk/durable-object' class DurableObjectError extends Error { readonly code: string readonly objectName: string readonly objectId?: string } ``` ### DurableObjectNotFoundError [Section titled “DurableObjectNotFoundError”](#durableobjectnotfounderror) Thrown when accessing a non-existent Durable Object. ```typescript import { DurableObjectNotFoundError } from '@cloudwerk/durable-object' ``` ### DurableObjectMethodError [Section titled “DurableObjectMethodError”](#durableobjectmethoderror) Thrown when an RPC method fails. ```typescript import { DurableObjectMethodError } from '@cloudwerk/durable-object' try { await counter.increment(-1) } catch (error) { if (error instanceof DurableObjectMethodError) { console.error('Method failed:', error.methodName, error.cause) } } ``` *** ## CLI Commands [Section titled “CLI Commands”](#cli-commands) ### List Durable Objects [Section titled “List Durable Objects”](#list-durable-objects) ```bash cloudwerk objects list ``` ### Show Object Details [Section titled “Show Object Details”](#show-object-details) ```bash cloudwerk objects info ``` ### Generate Types [Section titled “Generate Types”](#generate-types) ```bash cloudwerk objects generate-types ``` *** ## Type Definitions [Section titled “Type Definitions”](#type-definitions) ### DurableObjectDefinition [Section titled “DurableObjectDefinition”](#durableobjectdefinition) ```typescript interface DurableObjectDefinition { init?: (ctx: DurableObjectContext) => Awaitable methods?: Record> fetch?: (request: Request, ctx: DurableObjectContext) => Awaitable webSocketMessage?: WebSocketMessageHandler webSocketClose?: WebSocketCloseHandler webSocketError?: WebSocketErrorHandler alarm?: (ctx: DurableObjectContext) => Awaitable } type DurableObjectMethod = ( ctx: DurableObjectContext, ...args: any[] ) => Awaitable ``` ### Generated Class [Section titled “Generated Class”](#generated-class) Cloudwerk generates a DurableObject class from your definition: ```typescript // Auto-generated export class Counter extends DurableObject { async increment(amount?: number): Promise { /* ... */ } async getCount(): Promise { /* ... */ } async reset(): Promise { /* ... */ } } ``` *** ## Wrangler Configuration [Section titled “Wrangler Configuration”](#wrangler-configuration) Cloudwerk auto-generates wrangler configuration: ```toml # wrangler.toml (auto-generated) [durable_objects] bindings = [ { name = "COUNTER", class_name = "Counter" } ] [[migrations]] tag = "v1" new_classes = ["Counter"] ``` *** ## Best Practices [Section titled “Best Practices”](#best-practices) ### State Design [Section titled “State Design”](#state-design) ```typescript // Good: Minimal state, load on demand interface GameState { id: string status: 'waiting' | 'playing' | 'finished' } export default defineDurableObject({ async init(ctx) { return await ctx.storage.get('state') ?? { id: '', status: 'waiting' } }, methods: { async getFullState(ctx) { // Load additional data on demand const players = await ctx.storage.list({ prefix: 'player:' }) return { ...ctx.state, players: Array.from(players.values()) } }, }, }) ``` ### Use Transactions for Consistency [Section titled “Use Transactions for Consistency”](#use-transactions-for-consistency) ```typescript methods: { async atomicUpdate(ctx, updates: Partial) { await ctx.storage.transaction(async () => { const current = await ctx.storage.get('state') await ctx.storage.put('state', { ...current, ...updates }) await ctx.storage.put('lastUpdate', new Date().toISOString()) }) }, } ``` ### Cleanup with Alarms [Section titled “Cleanup with Alarms”](#cleanup-with-alarms) ```typescript async alarm(ctx) { // Clean up stale data const messages = await ctx.storage.list({ prefix: 'msg:' }) const staleThreshold = Date.now() - 24 * 60 * 60 * 1000 // 24 hours const staleKeys = [] for (const [key, msg] of messages) { if (new Date(msg.timestamp).getTime() < staleThreshold) { staleKeys.push(key) } } if (staleKeys.length > 0) { await ctx.storage.delete(staleKeys) } // Schedule next cleanup await ctx.setAlarm(Date.now() + 60 * 60 * 1000) // 1 hour } ``` *** ## Limits [Section titled “Limits”](#limits) | Limit | Value | | ------------------------- | ----------------- | | Storage per object | 10 GB | | KV operations per request | 1000 | | SQL rows per table | Unlimited | | WebSocket connections | 32,768 per object | | Alarm precision | 1 second | *** ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Durable Objects Guide](/guides/durable-objects/)** - Patterns and best practices * **[Services API](/api/services/)** - Service extraction * **[WebSockets Guide](/guides/websockets/)** - Real-time features # Images API > Complete API reference for @cloudwerk/images - Cloudflare Images integration with file-based image definitions, transform pipelines, and CDN-style resizing. The `@cloudwerk/images` package provides integration with Cloudflare’s image services: Hosted Images for storage and delivery, Image Transformer for CDN-style resizing, and the IMAGES binding for transform pipelines. ## Installation [Section titled “Installation”](#installation) ```bash pnpm add @cloudwerk/images ``` ## Cloudflare Image Products [Section titled “Cloudflare Image Products”](#cloudflare-image-products) Cloudflare offers three distinct image services, each suited for different use cases: | Product | Purpose | Requirements | | --------------------- | ---------------------------- | -------------------------------- | | **Hosted Images** | Store & serve images via CDN | Account ID, API token, paid plan | | **Image Transformer** | Resize images via URL params | Zone-level route, paid plan | | **IMAGES Binding** | Transform pipeline in Worker | wrangler.toml binding, paid plan | *** ## Hosted Images [Section titled “Hosted Images”](#hosted-images) ### defineImage() [Section titled “defineImage()”](#defineimage) Creates an image definition with named variants. ```typescript import { defineImage } from '@cloudwerk/images' export default defineImage({ variants: Record, accountId?: string | EnvRef, apiToken?: string | EnvRef, }) ``` #### Parameters [Section titled “Parameters”](#parameters) | Parameter | Type | Description | | ----------- | ------------------------------ | ------------------------------------------------------------ | | `variants` | `Record` | Named transformation variants | | `accountId` | `string \| EnvRef` | Cloudflare account ID (default: `{ env: 'CF_ACCOUNT_ID' }`) | | `apiToken` | `string \| EnvRef` | Cloudflare API token (default: `{ env: 'CF_IMAGES_TOKEN' }`) | #### Returns [Section titled “Returns”](#returns) Returns an `ImageDefinition` object that Cloudwerk registers automatically. ### ImageVariant [Section titled “ImageVariant”](#imagevariant) Transformation options for a variant. ```typescript interface ImageVariant { width?: number // Width in pixels height?: number // Height in pixels fit?: 'cover' | 'contain' | 'scale-down' | 'crop' | 'pad' format?: 'webp' | 'avif' | 'jpeg' | 'png' | 'auto' quality?: number // 1-100 dpr?: number // Device pixel ratio (1-3) gravity?: 'auto' | 'center' | 'top' | 'bottom' | 'left' | 'right' | 'face' sharpen?: number // 0-10 blur?: number // 1-250 brightness?: number // -1 to 1 contrast?: number // -1 to 1 rotate?: 0 | 90 | 180 | 270 metadata?: 'keep' | 'copyright' | 'none' } ``` ### Example [Section titled “Example”](#example) ```typescript // app/images/avatars.ts import { defineImage } from '@cloudwerk/images' export default defineImage({ variants: { thumbnail: { width: 100, height: 100, fit: 'cover' }, profile: { width: 400, height: 400, fit: 'cover' }, large: { width: 800, fit: 'scale-down', quality: 90 }, }, }) ``` ### EnvRef [Section titled “EnvRef”](#envref) Reference environment variables for credentials. ```typescript interface EnvRef { env: string // Environment variable name } // Example export default defineImage({ variants: { /* ... */ }, accountId: { env: 'CLOUDFLARE_ACCOUNT_ID' }, apiToken: { env: 'CLOUDFLARE_IMAGES_TOKEN' }, }) ``` *** ## ImageClient [Section titled “ImageClient”](#imageclient) The image client provides methods for uploading, retrieving, and managing images. ### images Proxy [Section titled “images Proxy”](#images-proxy) Access image clients via the `images` proxy. ```typescript import { images } from '@cloudwerk/core/bindings' // Upload an image const result = await images.avatars.upload(file) // Get variant URL const url = images.avatars.url(result.id, 'thumbnail') ``` ### upload() [Section titled “upload()”](#upload) Upload an image and receive an image ID. ```typescript const result = await images.avatars.upload(file: File | Blob | ArrayBuffer, options?: UploadOptions) ``` #### UploadOptions [Section titled “UploadOptions”](#uploadoptions) ```typescript interface UploadOptions { id?: string // Custom image ID (auto-generated if not provided) metadata?: Record // Custom metadata requireSignedUrls?: boolean // Require signed URLs } ``` #### Returns [Section titled “Returns”](#returns-1) ```typescript interface ImageResult { id: string // Image ID filename?: string // Original filename metadata?: Record // Custom metadata uploaded: Date // Upload timestamp variants: string[] // Available variant names } ``` ### url() [Section titled “url()”](#url) Get the URL for an image variant. ```typescript const url = images.avatars.url(id: string, variant: string): string ``` ### Example [Section titled “Example”](#example-1) ```typescript import { images } from '@cloudwerk/core/bindings' import { json } from '@cloudwerk/core' export async function POST(request: Request) { const formData = await request.formData() const file = formData.get('avatar') as File const result = await images.avatars.upload(file, { metadata: { userId: 'user-123' }, }) return json({ id: result.id, thumbnail: images.avatars.url(result.id, 'thumbnail'), profile: images.avatars.url(result.id, 'profile'), }) } ``` ### getDirectUploadUrl() [Section titled “getDirectUploadUrl()”](#getdirectuploadurl) Generate a URL for direct browser uploads. ```typescript const directUpload = await images.avatars.getDirectUploadUrl(options?: DirectUploadOptions) ``` #### DirectUploadOptions [Section titled “DirectUploadOptions”](#directuploadoptions) ```typescript interface DirectUploadOptions { id?: string // Custom image ID metadata?: Record // Custom metadata requireSignedUrls?: boolean // Require signed URLs expiry?: Date // URL expiration } ``` #### Returns [Section titled “Returns”](#returns-2) ```typescript interface DirectUploadResult { id: string // Image ID uploadUrl: string // URL for browser upload } ``` ### list() [Section titled “list()”](#list) List uploaded images. ```typescript const images = await images.avatars.list(options?: ListOptions) ``` #### ListOptions [Section titled “ListOptions”](#listoptions) ```typescript interface ListOptions { page?: number // Page number (1-based) perPage?: number // Results per page (max 1000) } ``` ### delete() [Section titled “delete()”](#delete) Delete an image. ```typescript await images.avatars.delete(id: string) ``` ### get() [Section titled “get()”](#get) Get image details. ```typescript const details = await images.avatars.get(id: string) ``` *** ## Image Transformer [Section titled “Image Transformer”](#image-transformer) ### createImageTransformer() [Section titled “createImageTransformer()”](#createimagetransformer) Creates a route handler for CDN-style image resizing using Cloudflare’s Image Resizing service. ```typescript import { createImageTransformer } from '@cloudwerk/images' export const GET = createImageTransformer(config?: TransformerConfig) ``` #### TransformerConfig [Section titled “TransformerConfig”](#transformerconfig) ```typescript interface TransformerConfig { allowedOrigins?: string[] // Allowed image source origins presets?: Record // Named presets defaults?: TransformPreset // Default transformations maxWidth?: number // Max width (default: 4096) maxHeight?: number // Max height (default: 4096) cacheControl?: string // Cache header (default: 1 year) allowArbitrary?: boolean // Allow arbitrary params (default: true) validateSource?: (url: URL) => boolean | Promise // Custom validation } ``` #### TransformPreset [Section titled “TransformPreset”](#transformpreset) ```typescript interface TransformPreset { width?: number height?: number fit?: 'cover' | 'contain' | 'scale-down' | 'crop' | 'pad' format?: 'webp' | 'avif' | 'jpeg' | 'png' | 'auto' quality?: number // 1-100 dpr?: number // 1-3 gravity?: 'auto' | 'center' | 'top' | 'bottom' | 'left' | 'right' | 'face' sharpen?: number // 0-10 blur?: number // 1-250 brightness?: number // -1 to 1 contrast?: number // -1 to 1 rotate?: 0 | 90 | 180 | 270 metadata?: 'keep' | 'copyright' | 'none' background?: string // Hex or RGB for padding } ``` ### Example [Section titled “Example”](#example-2) ```typescript // app/cdn/images/[...path]/route.ts import { createImageTransformer } from '@cloudwerk/images' export const GET = createImageTransformer({ allowedOrigins: ['https://images.mysite.com'], presets: { thumbnail: { width: 100, height: 100, fit: 'cover' }, hero: { width: 1920, height: 1080, fit: 'cover' }, }, defaults: { format: 'auto', quality: 85, }, }) ``` ### URL Format [Section titled “URL Format”](#url-format) The transformer parses the source URL from the path and options from query params: ```plaintext /cdn/images/https://images.mysite.com/photo.jpg?preset=thumbnail /cdn/images/https://images.mysite.com/photo.jpg?w=800&h=600&fit=cover ``` #### Query Parameters [Section titled “Query Parameters”](#query-parameters) | Parameter | Alias | Description | | ------------ | ----- | ------------------------ | | `preset` | - | Named preset from config | | `width` | `w` | Width in pixels | | `height` | `h` | Height in pixels | | `fit` | - | Fit mode | | `format` | `f` | Output format | | `quality` | `q` | Quality (1-100) | | `dpr` | - | Device pixel ratio | | `gravity` | - | Crop gravity | | `blur` | - | Blur amount | | `sharpen` | - | Sharpen amount | | `brightness` | - | Brightness adjustment | | `contrast` | - | Contrast adjustment | | `rotate` | - | Rotation degrees | ### variantToPreset() [Section titled “variantToPreset()”](#varianttopreset) Convert an ImageVariant to a TransformPreset for reusing hosted image variants. ```typescript import { variantToPreset, createImageTransformer } from '@cloudwerk/images' import avatars from '../images/avatars' const presets = Object.fromEntries( Object.entries(avatars.variants).map(([name, variant]) => [ name, variantToPreset(variant), ]) ) export const GET = createImageTransformer({ presets }) ``` *** ## IMAGES Binding [Section titled “IMAGES Binding”](#images-binding) The IMAGES binding provides in-Worker image transformation without uploading to Cloudflare Images. ### Configuration [Section titled “Configuration”](#configuration) Add the binding to `wrangler.toml`: ```toml [[images]] binding = "MY_IMAGES" ``` ### CloudflareImagesBinding [Section titled “CloudflareImagesBinding”](#cloudflareimagesbinding) ```typescript interface CloudflareImagesBinding { input(source: Blob | ArrayBuffer | ReadableStream): CloudflareImagesTransformBuilder info(source: Blob | ArrayBuffer | ReadableStream): Promise } interface CloudflareImageInfo { width: number height: number format: string fileSize: number } ``` ### CloudflareImagesTransformBuilder [Section titled “CloudflareImagesTransformBuilder”](#cloudflareimagestransformbuilder) Fluent API for chaining transformations. ```typescript interface CloudflareImagesTransformBuilder { transform(options: CloudflareImageTransformOptions): CloudflareImagesTransformBuilder output(options: CloudflareImageOutputOptions): CloudflareImagesTransformBuilder response(): Promise blob(): Promise arrayBuffer(): Promise stream(): ReadableStream } ``` ### Example [Section titled “Example”](#example-3) ```typescript import { getBinding } from '@cloudwerk/core/bindings' import type { CloudflareImagesBinding } from '@cloudwerk/images' export async function POST(request: Request) { const IMAGES = getBinding('MY_IMAGES') const formData = await request.formData() const file = formData.get('image') as File // Transform the image const response = await IMAGES .input(await file.arrayBuffer()) .transform({ width: 800, rotate: 90 }) .output({ format: 'image/webp', quality: 85 }) .response() return response } ``` ### CloudflareImageTransformOptions [Section titled “CloudflareImageTransformOptions”](#cloudflareimagetransformoptions) ```typescript interface CloudflareImageTransformOptions { width?: number height?: number fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad' gravity?: 'auto' | 'left' | 'right' | 'top' | 'bottom' | 'center' | 'face' quality?: number // 1-100 dpr?: number // 1-3 rotate?: 0 | 90 | 180 | 270 sharpen?: number // 0-10 blur?: number // 1-250 brightness?: number // -1 to 1 contrast?: number // -1 to 1 background?: string // Hex or RGB border?: { color: string; width: number } trim?: { top?: number; right?: number; bottom?: number; left?: number } } ``` ### CloudflareImageOutputOptions [Section titled “CloudflareImageOutputOptions”](#cloudflareimageoutputoptions) ```typescript interface CloudflareImageOutputOptions { format?: 'image/jpeg' | 'image/png' | 'image/webp' | 'image/avif' | 'image/gif' quality?: number // 1-100 metadata?: 'keep' | 'copyright' | 'none' } ``` ### IMAGE\_PRESETS [Section titled “IMAGE\_PRESETS”](#image_presets) Built-in presets for common use cases. ```typescript import { IMAGE_PRESETS } from '@cloudwerk/images' // Available presets IMAGE_PRESETS.thumbnail // 100x100 cover IMAGE_PRESETS.preview // 400x400 contain IMAGE_PRESETS.large // 1200px wide IMAGE_PRESETS.hero // 1920x1080 cover IMAGE_PRESETS.social // 1200x630 (Open Graph) IMAGE_PRESETS.mobile // 640px wide, WebP ``` *** ## Error Classes [Section titled “Error Classes”](#error-classes) ### ImageError [Section titled “ImageError”](#imageerror) Base error class for image operations. ```typescript import { ImageError } from '@cloudwerk/images' class ImageError extends Error { readonly code: string } ``` ### ImageConfigError [Section titled “ImageConfigError”](#imageconfigerror) Invalid image configuration. ```typescript class ImageConfigError extends ImageError { readonly code: 'IMAGE_CONFIG_ERROR' } ``` ### ImageUploadError [Section titled “ImageUploadError”](#imageuploaderror) Image upload failed. ```typescript class ImageUploadError extends ImageError { readonly code: 'IMAGE_UPLOAD_ERROR' readonly status?: number } ``` ### ImageNotFoundError [Section titled “ImageNotFoundError”](#imagenotfounderror) Image not found. ```typescript class ImageNotFoundError extends ImageError { readonly code: 'IMAGE_NOT_FOUND' readonly imageId: string } ``` ### ImageVariantError [Section titled “ImageVariantError”](#imagevarianterror) Invalid variant name. ```typescript class ImageVariantError extends ImageError { readonly code: 'IMAGE_VARIANT_ERROR' readonly variantName: string readonly availableVariants: string[] } ``` ### ImageTransformError [Section titled “ImageTransformError”](#imagetransformerror) Image transformation failed. ```typescript class ImageTransformError extends ImageError { readonly code: string readonly status: number } ``` *** ## Image Utilities [Section titled “Image Utilities”](#image-utilities) ### getImages() [Section titled “getImages()”](#getimages) Get a typed image client by name. ```typescript import { getImages } from '@cloudwerk/core/bindings' const avatars = getImages('avatars') await avatars.upload(file) ``` ### hasImages() [Section titled “hasImages()”](#hasimages) Check if an image definition exists. ```typescript import { hasImages } from '@cloudwerk/core/bindings' if (hasImages('avatars')) { await images.avatars.upload(file) } ``` ### getImagesNames() [Section titled “getImagesNames()”](#getimagesnames) List all available image definition names. ```typescript import { getImagesNames } from '@cloudwerk/core/bindings' const available = getImagesNames() // ['avatars', 'products', 'banners'] ``` *** ## Type Definitions [Section titled “Type Definitions”](#type-definitions) ### ImageDefinition [Section titled “ImageDefinition”](#imagedefinition) ```typescript interface ImageDefinition> { variants: V accountId?: string | EnvRef apiToken?: string | EnvRef } ``` ### ImageClientInterface [Section titled “ImageClientInterface”](#imageclientinterface) ```typescript interface ImageClientInterface { upload(source: File | Blob | ArrayBuffer, options?: UploadOptions): Promise url(id: string, variant: V): string get(id: string): Promise delete(id: string): Promise list(options?: ListOptions): Promise getDirectUploadUrl(options?: DirectUploadOptions): Promise } ``` *** ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Images Guide](/guides/images/)** - Patterns and best practices * **[Bindings API](/api/bindings/)** - Core binding utilities * **[File Conventions](/reference/file-conventions/)** - `app/images/` structure # Queues API > Complete API reference for @cloudwerk/queue - type-safe queue consumers with schema validation and dead letter queues. The `@cloudwerk/queue` package provides a convention-based queue system for processing background jobs with type safety, schema validation, and dead letter queues. ## Installation [Section titled “Installation”](#installation) ```bash pnpm add @cloudwerk/queue ``` ## defineQueue() [Section titled “defineQueue()”](#definequeue) Creates a typed queue consumer. ```typescript import { defineQueue } from '@cloudwerk/queue' export default defineQueue({ process: (message: QueueMessage) => Awaitable, // OR processBatch?: (messages: QueueMessage[]) => Awaitable, schema?: ZodSchema, config?: QueueConfig, onError?: (error: Error, message: QueueMessage) => Awaitable, }) ``` ### Parameters [Section titled “Parameters”](#parameters) | Parameter | Type | Description | | -------------- | ------------- | ------------------------------------------------------- | | `process` | `Function` | Handler for individual messages | | `processBatch` | `Function` | Handler for batch processing (alternative to `process`) | | `schema` | `ZodSchema` | Optional Zod schema for message validation | | `config` | `QueueConfig` | Optional queue configuration | | `onError` | `Function` | Optional error handler | ### Returns [Section titled “Returns”](#returns) Returns a `QueueDefinition` object that Cloudwerk registers automatically. *** ## QueueConfig [Section titled “QueueConfig”](#queueconfig) Configuration options for queue behavior. ```typescript interface QueueConfig { batchSize?: number // Max messages per batch (1-100, default: 10) maxRetries?: number // Max retry attempts (0-100, default: 3) retryDelay?: string // Delay between retries (default: '1m') deadLetterQueue?: string // DLQ for failed messages batchTimeout?: string // Max wait time to fill batch (default: '5s') } ``` ### Duration Format [Section titled “Duration Format”](#duration-format) | Format | Description | | ------- | ----------- | | `'30s'` | 30 seconds | | `'5m'` | 5 minutes | | `'1h'` | 1 hour | ### Example [Section titled “Example”](#example) ```typescript export default defineQueue({ config: { batchSize: 25, maxRetries: 5, retryDelay: '2m', deadLetterQueue: 'orders-dlq', batchTimeout: '10s', }, async process(message) { await processOrder(message.body) message.ack() }, }) ``` *** ## QueueMessage [Section titled “QueueMessage”](#queuemessage) The message object passed to process handlers. ```typescript interface QueueMessage { readonly id: string // Unique message ID readonly body: T // Message payload (validated if schema provided) readonly timestamp: Date // When message was originally sent readonly attempts: number // Current delivery attempt (1-based) ack(): void // Acknowledge successful processing retry(options?: RetryOptions): void // Request retry deadLetter(reason?: string): void // Send to dead letter queue } interface RetryOptions { delaySeconds?: number // Delay before retry } ``` ### ack() [Section titled “ack()”](#ack) Mark the message as successfully processed. The message is removed from the queue. ```typescript async process(message) { await processWork(message.body) message.ack() // Remove from queue } ``` ### retry() [Section titled “retry()”](#retry) Request that the message be redelivered. Use for transient failures. ```typescript async process(message) { try { await processWork(message.body) message.ack() } catch (error) { if (isTransientError(error)) { message.retry() // Default retry delay // OR message.retry({ delaySeconds: 60 }) // Custom delay } } } ``` ### deadLetter() [Section titled “deadLetter()”](#deadletter) Send the message to the dead letter queue. Use for permanent failures. ```typescript async process(message) { try { await processWork(message.body) message.ack() } catch (error) { if (isPermanentError(error)) { message.deadLetter(error.message) } else { message.retry() } } } ``` *** ## Schema Validation [Section titled “Schema Validation”](#schema-validation) Use Zod schemas for runtime message validation. ```typescript import { defineQueue } from '@cloudwerk/queue' import { z } from 'zod' const EmailSchema = z.object({ to: z.string().email(), subject: z.string().min(1), body: z.string(), priority: z.enum(['low', 'normal', 'high']).default('normal'), }) type EmailMessage = z.infer export default defineQueue({ schema: EmailSchema, async process(message) { // message.body is validated and typed const { to, subject, body, priority } = message.body await sendEmail(to, subject, body, priority) message.ack() }, }) ``` Tip When a message fails schema validation, it is automatically sent to the dead letter queue (if configured) or acknowledged to prevent infinite retries. *** ## Batch Processing [Section titled “Batch Processing”](#batch-processing) Process multiple messages together for better throughput. ```typescript export default defineQueue({ config: { batchSize: 100, batchTimeout: '30s', }, async processBatch(messages) { // Process all messages together const events = messages.map(m => m.body) try { await batchInsertEvents(events) // Acknowledge all on success for (const msg of messages) { msg.ack() } } catch (error) { // Retry all on failure for (const msg of messages) { msg.retry() } } }, }) ``` ### Mixed Results [Section titled “Mixed Results”](#mixed-results) Handle individual successes and failures in a batch: ```typescript async processBatch(messages) { const results = await Promise.allSettled( messages.map(async (msg) => { await processOne(msg.body) return msg }) ) for (let i = 0; i < results.length; i++) { const result = results[i] const message = messages[i] if (result.status === 'fulfilled') { message.ack() } else { if (message.attempts >= 3) { message.deadLetter(result.reason.message) } else { message.retry() } } } } ``` *** ## Error Handler [Section titled “Error Handler”](#error-handler) The optional `onError` callback is invoked when message processing fails. ```typescript export default defineQueue({ async process(message) { await processTask(message.body) message.ack() }, async onError(error, message) { console.error(`Task failed (attempt ${message.attempts}):`, error) // Log for investigation await logFailedMessage({ queueMessage: message, error: error.message, stack: error.stack, }) // Alert on repeated failures if (message.attempts >= 3) { await alertOps(`Task ${message.id} failed ${message.attempts} times`) } }, }) ``` *** ## Typed Producers [Section titled “Typed Producers”](#typed-producers) Send messages to queues with type safety. ### queues Proxy [Section titled “queues Proxy”](#queues-proxy) ```typescript import { queues } from '@cloudwerk/core/bindings' // Single message await queues.email.send({ to: 'user@example.com', subject: 'Welcome!', body: 'Thanks for signing up.', }) // With options await queues.email.send(message, { delaySeconds: 60, contentType: 'json', }) ``` ### sendBatch() [Section titled “sendBatch()”](#sendbatch) Send multiple messages at once. ```typescript import { queues } from '@cloudwerk/core/bindings' await queues.notifications.sendBatch([ { userId: '1', type: 'email', message: 'Hello' }, { userId: '2', type: 'push', message: 'Hello' }, ]) // With options await queues.notifications.sendBatch(messages, { delaySeconds: 30, }) ``` ### SendOptions [Section titled “SendOptions”](#sendoptions) ```typescript interface SendOptions { delaySeconds?: number // Delay before processing (0-43200, max 12 hours) contentType?: ContentType // Message encoding } type ContentType = 'json' | 'text' | 'bytes' | 'v8' ``` *** ## Queue Utilities [Section titled “Queue Utilities”](#queue-utilities) ### getQueue() [Section titled “getQueue()”](#getqueue) Get a typed queue producer by name. ```typescript import { getQueue } from '@cloudwerk/core/bindings' interface EmailMessage { to: string subject: string body: string } const emailQueue = getQueue('email') await emailQueue.send({ to: '...', subject: '...', body: '...' }) ``` ### hasQueue() [Section titled “hasQueue()”](#hasqueue) Check if a queue exists. ```typescript import { hasQueue } from '@cloudwerk/core/bindings' if (hasQueue('email')) { await queues.email.send(message) } ``` ### getQueueNames() [Section titled “getQueueNames()”](#getqueuenames) List all available queue names. ```typescript import { getQueueNames } from '@cloudwerk/core/bindings' const available = getQueueNames() // ['email', 'notifications', 'analytics'] ``` *** ## Dead Letter Queue Consumer [Section titled “Dead Letter Queue Consumer”](#dead-letter-queue-consumer) Create a consumer for dead letter messages. ```typescript // app/queues/orders-dlq.ts import { defineQueue } from '@cloudwerk/queue' interface DeadLetterMessage { originalQueue: string originalMessage: T error: string attempts: number failedAt: string } export default defineQueue({ async process(message) { const { originalQueue, originalMessage, error, attempts } = message.body // Store for investigation await db.insert('failed_jobs', { queue: originalQueue, payload: JSON.stringify(originalMessage), error, attempts, failed_at: message.body.failedAt, }) // Alert for critical queues if (originalQueue === 'payments') { await alertOps(`Payment processing failed: ${error}`) } message.ack() }, }) ``` *** ## Testing [Section titled “Testing”](#testing) ### mockQueueMessage() [Section titled “mockQueueMessage()”](#mockqueuemessage) Create mock queue messages for testing. ```typescript import { mockQueueMessage } from '@cloudwerk/queue/testing' const message = mockQueueMessage({ id: 'msg-123', body: { to: 'user@example.com', subject: 'Test' }, attempts: 1, }) ``` ### Testing Example [Section titled “Testing Example”](#testing-example) ```typescript import { describe, it, expect, vi } from 'vitest' import { mockQueueMessage } from '@cloudwerk/queue/testing' import emailQueue from './email' describe('email queue', () => { it('should send email and acknowledge', async () => { const sendEmail = vi.fn().mockResolvedValue(true) const message = mockQueueMessage({ body: { to: 'user@example.com', subject: 'Hello', body: 'Test' }, }) await emailQueue.process(message) expect(message.ack).toHaveBeenCalled() }) it('should retry on transient error', async () => { const message = mockQueueMessage({ body: { to: 'user@example.com', subject: 'Hello', body: 'Test' }, attempts: 1, }) // Simulate transient error vi.mocked(sendEmail).mockRejectedValueOnce(new Error('Network error')) await emailQueue.process(message) expect(message.retry).toHaveBeenCalled() }) }) ``` *** ## Error Classes [Section titled “Error Classes”](#error-classes) ### QueueError [Section titled “QueueError”](#queueerror) Base error class for queue failures. ```typescript import { QueueError } from '@cloudwerk/queue' class QueueError extends Error { readonly code: string readonly queueName: string readonly messageId?: string } ``` ### SchemaValidationError [Section titled “SchemaValidationError”](#schemavalidationerror) Thrown when message fails schema validation. ```typescript import { SchemaValidationError } from '@cloudwerk/queue' class SchemaValidationError extends QueueError { readonly code: 'SCHEMA_VALIDATION_ERROR' readonly issues: ZodIssue[] } ``` ### DeadLetterError [Section titled “DeadLetterError”](#deadlettererror) Thrown when sending to DLQ fails. ```typescript import { DeadLetterError } from '@cloudwerk/queue' class DeadLetterError extends QueueError { readonly code: 'DEAD_LETTER_ERROR' readonly originalError: Error } ``` *** ## CLI Commands [Section titled “CLI Commands”](#cli-commands) ### List Queues [Section titled “List Queues”](#list-queues) ```bash cloudwerk queues list ``` ### Show Queue Details [Section titled “Show Queue Details”](#show-queue-details) ```bash cloudwerk queues info ``` ### Generate Types [Section titled “Generate Types”](#generate-types) Generate TypeScript types for all queues. ```bash cloudwerk queues generate-types ``` This creates `.cloudwerk/types/queues.d.ts` with typed producers. *** ## Type Definitions [Section titled “Type Definitions”](#type-definitions) ### QueueDefinition [Section titled “QueueDefinition”](#queuedefinition) ```typescript interface QueueDefinition { process?: (message: QueueMessage) => Awaitable processBatch?: (messages: QueueMessage[]) => Awaitable schema?: ZodSchema config?: QueueConfig onError?: (error: Error, message: QueueMessage) => Awaitable } ``` ### Queue (Producer) [Section titled “Queue (Producer)”](#queue-producer) ```typescript interface Queue { send(message: T, options?: SendOptions): Promise sendBatch(messages: T[], options?: SendOptions): Promise } ``` ### Generated Types [Section titled “Generated Types”](#generated-types) After running `generate-types`, you get full type safety: ```typescript // .cloudwerk/types/queues.d.ts (auto-generated) declare module '@cloudwerk/core/bindings' { interface CloudwerkQueues { email: Queue notifications: Queue analytics: Queue } } ``` *** ## Limits [Section titled “Limits”](#limits) | Limit | Value | | ----------------- | ------------------------ | | Max batch size | 100 messages | | Max message size | 128 KB | | Max delay | 12 hours (43200 seconds) | | Message retention | 4 days | | Max retries | 100 | *** ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Queues Guide](/guides/queues/)** - Patterns and best practices * **[Triggers API](/api/triggers/)** - Queue trigger handlers * **[Services API](/api/services/)** - Background job services # Security API > Complete API reference for @cloudwerk/security - CSRF protection, security headers, CSP, origin validation, and secure fetch helpers. The `@cloudwerk/security` package provides comprehensive security middleware for Cloudwerk applications, including CSRF protection, security headers, Content Security Policy, origin validation, and client-side helpers. ## Installation [Section titled “Installation”](#installation) ```bash pnpm add @cloudwerk/security ``` *** ## Combined Security Middleware [Section titled “Combined Security Middleware”](#combined-security-middleware) ### securityMiddleware() [Section titled “securityMiddleware()”](#securitymiddleware) All-in-one middleware that composes multiple security protections. ```typescript import { securityMiddleware } from '@cloudwerk/security/middleware' export const middleware = securityMiddleware(options) ``` #### SecurityMiddlewareOptions [Section titled “SecurityMiddlewareOptions”](#securitymiddlewareoptions) | Property | Type | Default | Description | | ---------------- | ---------------------------------- | -------- | ----------------------------------- | | `csrf` | `CSRFMiddlewareOptions \| false` | Enabled | CSRF protection config | | `requestedWith` | `RequestedWithOptions \| false` | Enabled | X-Requested-With validation | | `headers` | `SecurityHeadersOptions \| false` | Enabled | Security headers config | | `csp` | `CSPOptions \| false` | Disabled | Content Security Policy | | `origin` | `OriginValidationOptions \| false` | Disabled | Origin validation config | | `allowedOrigins` | `string[]` | - | Shorthand for origin.allowedOrigins | **Default behavior:** * CSRF protection: **Enabled** * X-Requested-With: **Enabled** (forces CORS preflight) * Security headers: **Enabled** * CSP: **Disabled** (requires app-specific config) * Origin validation: **Disabled** (requires allowedOrigins config) **Examples:** ```typescript // Use all defaults export const middleware = securityMiddleware() // Full configuration export const middleware = securityMiddleware({ allowedOrigins: ['https://myapp.com'], csrf: { excludePaths: ['/api/webhooks/stripe'], }, csp: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", 'https://cdn.example.com'], }, reportOnly: true, }, headers: { frameOptions: 'SAMEORIGIN', }, }) // Disable specific protections export const middleware = securityMiddleware({ csrf: false, requestedWith: false, }) ``` *** ## CSRF Protection [Section titled “CSRF Protection”](#csrf-protection) ### csrfMiddleware() [Section titled “csrfMiddleware()”](#csrfmiddleware) Create CSRF protection middleware using the double-submit cookie pattern. ```typescript import { csrfMiddleware } from '@cloudwerk/security/middleware' export const middleware = csrfMiddleware(options) ``` #### CSRFMiddlewareOptions [Section titled “CSRFMiddlewareOptions”](#csrfmiddlewareoptions) | Property | Type | Default | Description | | --------------- | ---------- | ------------------------------------ | ---------------------------- | | `cookieName` | `string` | `'cloudwerk.csrf-token'` | CSRF cookie name | | `headerName` | `string` | `'X-CSRF-Token'` | CSRF header name | | `formFieldName` | `string` | `'csrf_token'` | Form field name | | `methods` | `string[]` | `['POST', 'PUT', 'PATCH', 'DELETE']` | Methods requiring validation | | `excludePaths` | `string[]` | `[]` | Paths to skip validation | **Example:** ```typescript export const middleware = csrfMiddleware({ excludePaths: ['/api/webhooks/stripe', '/api/webhooks/github'], }) ``` ### generateCsrfToken() [Section titled “generateCsrfToken()”](#generatecsrftoken) Generate a cryptographically secure CSRF token. ```typescript import { generateCsrfToken } from '@cloudwerk/security' const token = generateCsrfToken() // Returns: URL-safe base64 string (43 characters) ``` ### setCsrfCookie() [Section titled “setCsrfCookie()”](#setcsrfcookie) Set a CSRF cookie on a response. ```typescript import { generateCsrfToken, setCsrfCookie } from '@cloudwerk/security' export function GET(request: Request) { const token = generateCsrfToken() const response = new Response(JSON.stringify({ csrfToken: token })) return setCsrfCookie(response, token, options) } ``` #### SetCsrfCookieOptions [Section titled “SetCsrfCookieOptions”](#setcsrfcookieoptions) | Property | Type | Default | Description | | ------------ | ----------------------------- | ------------------------ | ----------------------------- | | `cookieName` | `string` | `'cloudwerk.csrf-token'` | Cookie name | | `path` | `string` | `'/'` | Cookie path | | `httpOnly` | `boolean` | `false` | Must be false for JS access | | `secure` | `boolean` | `true` | HTTPS only | | `sameSite` | `'strict' \| 'lax' \| 'none'` | `'lax'` | SameSite attribute | | `maxAge` | `number` | `86400` | Max age in seconds (24 hours) | ### rotateCsrfToken() [Section titled “rotateCsrfToken()”](#rotatecsrftoken) Rotate the CSRF token after authentication state changes. ```typescript import { rotateCsrfToken } from '@cloudwerk/security' // After successful login export async function handleLogin(request: Request) { const user = await validateCredentials(request) const session = await createSession(user) let response = createAuthResponse(user, session) response = rotateCsrfToken(response) return response } ``` CSRF Token Rotation Always rotate CSRF tokens after successful authentication to prevent session fixation attacks. This binds the CSRF token to the new session. ### verifyCsrfToken() [Section titled “verifyCsrfToken()”](#verifycsrftoken) Verify a CSRF token using timing-safe comparison. ```typescript import { verifyCsrfToken } from '@cloudwerk/security' const isValid = verifyCsrfToken(cookieToken, requestToken) ``` ### Token Extraction Helpers [Section titled “Token Extraction Helpers”](#token-extraction-helpers) ```typescript import { getCsrfTokenFromCookie, getCsrfTokenFromHeader, getCsrfTokenFromFormBody, } from '@cloudwerk/security' // Get from cookie const cookieToken = getCsrfTokenFromCookie(request) // Get from header const headerToken = getCsrfTokenFromHeader(request) // Get from form body (async) const formToken = await getCsrfTokenFromFormBody(request) ``` *** ## Security Headers [Section titled “Security Headers”](#security-headers) ### securityHeadersMiddleware() [Section titled “securityHeadersMiddleware()”](#securityheadersmiddleware) Set standard security headers on responses. ```typescript import { securityHeadersMiddleware } from '@cloudwerk/security/middleware' export const middleware = securityHeadersMiddleware(options) ``` #### SecurityHeadersOptions [Section titled “SecurityHeadersOptions”](#securityheadersoptions) | Property | Type | Default | Description | | ------------------------------ | ---------------------------------------- | ----------------------------------- | --------------------------------- | | `contentTypeOptions` | `'nosniff' \| false` | `'nosniff'` | X-Content-Type-Options | | `frameOptions` | `'DENY' \| 'SAMEORIGIN' \| false` | `'DENY'` | X-Frame-Options | | `referrerPolicy` | `ReferrerPolicy \| false` | `'strict-origin-when-cross-origin'` | Referrer-Policy | | `xssProtection` | `'0' \| '1' \| '1; mode=block' \| false` | `'0'` | X-XSS-Protection | | `permittedCrossDomainPolicies` | `string \| false` | `'none'` | X-Permitted-Cross-Domain-Policies | | `dnsPrefetchControl` | `'on' \| 'off' \| false` | `'off'` | X-DNS-Prefetch-Control | | `crossOriginOpenerPolicy` | `COOP \| false` | - | Cross-Origin-Opener-Policy | | `crossOriginEmbedderPolicy` | `COEP \| false` | - | Cross-Origin-Embedder-Policy | | `crossOriginResourcePolicy` | `CORP \| false` | - | Cross-Origin-Resource-Policy | **Referrer-Policy values:** `'no-referrer'`, `'no-referrer-when-downgrade'`, `'origin'`, `'origin-when-cross-origin'`, `'same-origin'`, `'strict-origin'`, `'strict-origin-when-cross-origin'`, `'unsafe-url'` **Example:** ```typescript export const middleware = securityHeadersMiddleware({ frameOptions: 'SAMEORIGIN', crossOriginOpenerPolicy: 'same-origin', crossOriginEmbedderPolicy: 'require-corp', }) ``` *** ## Content Security Policy [Section titled “Content Security Policy”](#content-security-policy) ### cspMiddleware() [Section titled “cspMiddleware()”](#cspmiddleware) Generate and set Content-Security-Policy headers. ```typescript import { cspMiddleware } from '@cloudwerk/security/middleware' export const middleware = cspMiddleware(options) ``` #### CSPOptions [Section titled “CSPOptions”](#cspoptions) | Property | Type | Default | Description | | ----------------- | --------------- | ------------ | ---------------------------------- | | `directives` | `CSPDirectives` | `{}` | CSP directives | | `reportOnly` | `boolean` | `false` | Use report-only mode | | `useNonce` | `boolean` | `false` | Generate nonces for inline scripts | | `nonceContextKey` | `string` | `'cspNonce'` | Context key for nonce | #### CSPDirectives [Section titled “CSPDirectives”](#cspdirectives) Common directives: | Directive | Description | | ------------------------- | -------------------------------------------------- | | `defaultSrc` | Fallback for other fetch directives | | `scriptSrc` | Valid sources for scripts | | `styleSrc` | Valid sources for stylesheets | | `imgSrc` | Valid sources for images | | `fontSrc` | Valid sources for fonts | | `connectSrc` | Valid sources for fetch, XMLHttpRequest, WebSocket | | `frameSrc` | Valid sources for frames | | `frameAncestors` | Valid parents for embedding | | `formAction` | Valid form submission targets | | `baseUri` | Valid base URIs | | `reportUri` | URI to report violations to | | `upgradeInsecureRequests` | Upgrade HTTP to HTTPS | **Examples:** ```typescript // Basic CSP export const middleware = cspMiddleware({ directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", 'https://cdn.example.com'], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", 'data:', 'https:'], connectSrc: ["'self'", 'https://api.example.com'], }, }) // Report-only mode for testing export const middleware = cspMiddleware({ directives: { defaultSrc: ["'self'"], reportUri: '/api/csp-report', }, reportOnly: true, }) // With nonce generation export const middleware = cspMiddleware({ directives: { scriptSrc: ["'self'"], }, useNonce: true, }) // Access nonce via response header: X-CSP-Nonce ``` ### generateCSPHeader() [Section titled “generateCSPHeader()”](#generatecspheader) Generate a CSP header string from directives. ```typescript import { generateCSPHeader } from '@cloudwerk/security' const csp = generateCSPHeader({ defaultSrc: ["'self'"], scriptSrc: ["'self'", 'https://cdn.example.com'], }) // "default-src 'self'; script-src 'self' https://cdn.example.com" ``` ### generateNonce() [Section titled “generateNonce()”](#generatenonce) Generate a cryptographic nonce for inline scripts. ```typescript import { generateNonce } from '@cloudwerk/security' const nonce = generateNonce() // Use in template: ``` *** ## Origin Validation [Section titled “Origin Validation”](#origin-validation) ### originValidationMiddleware() [Section titled “originValidationMiddleware()”](#originvalidationmiddleware) Validate Origin/Referer headers to prevent cross-origin attacks. ```typescript import { originValidationMiddleware } from '@cloudwerk/security/middleware' export const middleware = originValidationMiddleware(options) ``` #### OriginValidationOptions [Section titled “OriginValidationOptions”](#originvalidationoptions) | Property | Type | Default | Description | | --------------------- | ---------- | ------------------------------------ | ------------------------------------------------ | | `allowedOrigins` | `string[]` | `[]` | Full origin URLs (e.g., `'https://example.com'`) | | `allowedHosts` | `string[]` | `[]` | Hostnames only (e.g., `'example.com'`) | | `allowSubdomains` | `boolean` | `false` | Allow subdomains of allowed hosts | | `methods` | `string[]` | `['POST', 'PUT', 'PATCH', 'DELETE']` | Methods requiring validation | | `excludePaths` | `string[]` | `[]` | Paths to skip validation | | `rejectMissingOrigin` | `boolean` | `true` | Reject requests without Origin header | **Examples:** ```typescript // Allow specific origins export const middleware = originValidationMiddleware({ allowedOrigins: ['https://myapp.com', 'https://admin.myapp.com'], }) // Allow all subdomains export const middleware = originValidationMiddleware({ allowedHosts: ['myapp.com'], allowSubdomains: true, }) // Allows: myapp.com, www.myapp.com, api.myapp.com, etc. // Exclude webhooks export const middleware = originValidationMiddleware({ allowedOrigins: ['https://myapp.com'], excludePaths: ['/api/webhooks'], rejectMissingOrigin: false, // Allow webhooks without Origin }) ``` *** ## X-Requested-With Validation [Section titled “X-Requested-With Validation”](#x-requested-with-validation) ### requestedWithMiddleware() [Section titled “requestedWithMiddleware()”](#requestedwithmiddleware) Require X-Requested-With header to force CORS preflight. ```typescript import { requestedWithMiddleware } from '@cloudwerk/security/middleware' export const middleware = requestedWithMiddleware(options) ``` #### RequestedWithOptions [Section titled “RequestedWithOptions”](#requestedwithoptions) | Property | Type | Default | Description | | --------------- | ---------- | ------------------------------------ | ---------------------------- | | `requiredValue` | `string` | `'XMLHttpRequest'` | Required header value | | `methods` | `string[]` | `['POST', 'PUT', 'PATCH', 'DELETE']` | Methods requiring validation | | `excludePaths` | `string[]` | `[]` | Paths to skip validation | How X-Requested-With Helps The `X-Requested-With` header is a custom header that cannot be set cross-origin without triggering a CORS preflight request. By requiring it on mutation requests, you ensure that cross-origin requests must pass CORS checks, providing defense-in-depth against CSRF attacks. **Example:** ```typescript export const middleware = requestedWithMiddleware({ excludePaths: ['/api/webhooks'], }) ``` *** ## Client API [Section titled “Client API”](#client-api) The client module provides helpers for making secure fetch requests from the browser. ### secureFetch() [Section titled “secureFetch()”](#securefetch) A fetch wrapper that automatically adds CSRF tokens and X-Requested-With headers. ```typescript import { secureFetch } from '@cloudwerk/security/client' // POST request with automatic security headers const response = await secureFetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Alice' }), }) // Works with FormData const response = await secureFetch('/api/upload', { method: 'POST', body: formData, }) // DELETE request const response = await secureFetch(`/api/users/${userId}`, { method: 'DELETE', }) ``` **What secureFetch adds:** * `X-CSRF-Token` header (read from cookie) * `X-Requested-With: XMLHttpRequest` header * `credentials: 'same-origin'` (by default) ### configureSecureFetch() [Section titled “configureSecureFetch()”](#configuresecurefetch) Configure global defaults for secure fetch. ```typescript import { configureSecureFetch } from '@cloudwerk/security/client' configureSecureFetch({ baseUrl: '/api', credentials: 'include', csrfCookieName: 'my-csrf-token', csrfHeaderName: 'X-My-CSRF', }) ``` #### SecureFetchOptions [Section titled “SecureFetchOptions”](#securefetchoptions) | Property | Type | Default | Description | | -------------------- | -------------------- | ------------------------ | --------------------------- | | `csrfCookieName` | `string` | `'cloudwerk.csrf-token'` | CSRF cookie name | | `csrfHeaderName` | `string` | `'X-CSRF-Token'` | CSRF header name | | `requestedWithValue` | `string` | `'XMLHttpRequest'` | X-Requested-With value | | `credentials` | `RequestCredentials` | `'same-origin'` | Credentials mode | | `baseUrl` | `string` | - | Base URL for relative paths | ### Convenience Methods [Section titled “Convenience Methods”](#convenience-methods) ```typescript import { secureGet, securePost, securePut, securePatch, secureDelete, } from '@cloudwerk/security/client' // GET (no CSRF/X-Requested-With needed for GET) const response = await secureGet('/api/users') // POST with auto JSON serialization const response = await securePost('/api/users', { name: 'Alice' }) // PUT const response = await securePut(`/api/users/${id}`, { name: 'Bob' }) // PATCH const response = await securePatch(`/api/users/${id}`, { name: 'Carol' }) // DELETE const response = await secureDelete(`/api/users/${id}`) ``` ### getCsrfToken() [Section titled “getCsrfToken()”](#getcsrftoken) Get the CSRF token from cookies. ```typescript import { getCsrfToken } from '@cloudwerk/security/client' const token = getCsrfToken() // Use in custom fetch calls or forms ``` ### withCsrfToken() [Section titled “withCsrfToken()”](#withcsrftoken) Add CSRF token to a headers object. ```typescript import { withCsrfToken } from '@cloudwerk/security/client' const headers = withCsrfToken({ 'Content-Type': 'application/json', }) fetch('/api/users', { method: 'POST', headers, body: JSON.stringify(data), }) ``` ### csrfInput() [Section titled “csrfInput()”](#csrfinput) Generate a hidden input element with the CSRF token for forms. ```typescript import { csrfInput } from '@cloudwerk/security/client' const formHtml = `
${csrfInput()}
` // Outputs: ``` *** ## Migration from @cloudwerk/auth [Section titled “Migration from @cloudwerk/auth”](#migration-from-cloudwerkauth) CSRF protection has moved from `@cloudwerk/auth` to `@cloudwerk/security`. The old imports continue to work with a deprecation warning. **Before:** ```typescript import { csrfMiddleware, generateCsrfToken } from '@cloudwerk/auth/middleware' ``` **After:** ```typescript import { csrfMiddleware, generateCsrfToken } from '@cloudwerk/security' // or import { csrfMiddleware, generateCsrfToken } from '@cloudwerk/security/middleware' ``` *** ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Security Guide](/guides/security/)** - Best practices and patterns * **[Authentication Guide](/guides/authentication/)** - Auth with security middleware * **[Auth API](/api/auth/)** - Authentication system reference # Services API > Complete API reference for @cloudwerk/service - reusable services with local execution or extraction to separate Workers. The `@cloudwerk/service` package provides a service extraction system for building reusable services that can run locally or be extracted to separate Workers with RPC communication. ## Installation [Section titled “Installation”](#installation) ```bash pnpm add @cloudwerk/service ``` ## defineService() [Section titled “defineService()”](#defineservice) Creates a service definition with methods and optional lifecycle hooks. ```typescript import { defineService } from '@cloudwerk/service' export default defineService({ methods: Record, name?: string, hooks?: ServiceHooks, config?: ServiceExtractionConfig, }) ``` ### Parameters [Section titled “Parameters”](#parameters) | Parameter | Type | Description | | --------- | -------------------------- | ----------------------------------------------- | | `methods` | `Record` | Service methods | | `name` | `string` | Override service name (default: directory name) | | `hooks` | `ServiceHooks` | Lifecycle hooks | | `config` | `ServiceExtractionConfig` | Extraction configuration | ### Returns [Section titled “Returns”](#returns) Returns a `ServiceDefinition` object that Cloudwerk registers automatically. *** ## Service Methods [Section titled “Service Methods”](#service-methods) Methods are async functions with access to `this.env` for bindings. ```typescript export default defineService({ methods: { async send(params: SendParams): Promise { // Access bindings via this.env const response = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { 'Authorization': `Bearer ${this.env.RESEND_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify(params), }) return response.json() }, // Methods can call other methods async sendBatch(emails: SendParams[]): Promise<{ sent: number }> { const results = await Promise.all( emails.map(e => this.send(e)) ) return { sent: results.filter(r => r.success).length } }, }, }) ``` ### Method Context (`this`) [Section titled “Method Context (this)”](#method-context-this) Inside service methods, `this` provides: | Property | Type | Description | | ---------- | ----- | ------------------------------------------ | | `this.env` | `Env` | Environment bindings (D1, KV, R2, secrets) | *** ## ServiceHooks [Section titled “ServiceHooks”](#servicehooks) Lifecycle hooks for cross-cutting concerns. ```typescript interface ServiceHooks { onInit?: () => Awaitable onBefore?: (method: string, args: unknown[]) => Awaitable onAfter?: (method: string, result: unknown) => Awaitable onError?: (method: string, error: Error) => Awaitable } ``` ### Hook Execution Order [Section titled “Hook Execution Order”](#hook-execution-order) 1. **onInit** - Once when service initializes 2. **onBefore** - Before each method call 3. Method execution 4. **onAfter** - On successful completion 5. **onError** - On failure (replaces onAfter) ### onInit() [Section titled “onInit()”](#oninit) Called once when the service is first initialized. ```typescript hooks: { onInit: async () => { console.log('Service initialized') // Initialize connections, load config, etc. }, } ``` ### onBefore() [Section titled “onBefore()”](#onbefore) Called before every method invocation. ```typescript hooks: { onBefore: async (method, args) => { console.log(`[${method}] called with`, args) // Logging, validation, rate limiting, etc. }, } ``` ### onAfter() [Section titled “onAfter()”](#onafter) Called after successful method completion. ```typescript hooks: { onAfter: async (method, result) => { console.log(`[${method}] returned`, result) // Analytics, metrics, caching, etc. }, } ``` ### onError() [Section titled “onError()”](#onerror) Called when a method throws an error. ```typescript hooks: { onError: async (method, error) => { console.error(`[${method}] failed:`, error.message) // Error tracking, alerting, etc. await reportError('service-name', method, error) }, } ``` ### Complete Hooks Example [Section titled “Complete Hooks Example”](#complete-hooks-example) ```typescript export default defineService({ methods: { async processPayment(orderId: string, amount: number) { // Process payment logic return { success: true, transactionId: 'txn_123' } }, }, hooks: { onInit: async () => { console.log('Payment service ready') }, onBefore: async (method, args) => { console.log(JSON.stringify({ service: 'payments', method, args, timestamp: Date.now(), })) }, onAfter: async (method, result) => { await trackMetric('payment_processed', 1) }, onError: async (method, error) => { await alertOps(`Payment ${method} failed: ${error.message}`) }, }, }) ``` *** ## Extraction Configuration [Section titled “Extraction Configuration”](#extraction-configuration) Configure how the service runs when extracted to a separate Worker. ```typescript interface ServiceExtractionConfig { extraction?: { workerName?: string // Name of extracted Worker (default: {name}-service) bindings?: string[] // Required bindings to forward } } ``` ### Example [Section titled “Example”](#example) ```typescript export default defineService({ methods: { async send(params) { // ... }, }, config: { extraction: { workerName: 'email-service', bindings: ['RESEND_API_KEY', 'DB'], }, }, }) ``` *** ## Service Modes [Section titled “Service Modes”](#service-modes) Services can run in three modes configured in `cloudwerk.config.ts`. ### Local Mode [Section titled “Local Mode”](#local-mode) Services run as direct function calls in the main Worker. ```typescript // cloudwerk.config.ts export default defineConfig({ services: { mode: 'local', // All services run locally }, }) ``` **Characteristics:** * No network latency * No serialization overhead * Full access to Worker context * Best for: utilities, low-latency operations ### Extracted Mode [Section titled “Extracted Mode”](#extracted-mode) Services run as separate Workers using Cloudflare service bindings. ```typescript // cloudwerk.config.ts export default defineConfig({ services: { mode: 'extracted', // All services as separate Workers }, }) ``` **Characteristics:** * Isolated resource usage * Independent scaling * Separate deployment * Best for: resource-intensive operations, microservices ### Hybrid Mode [Section titled “Hybrid Mode”](#hybrid-mode) Mix local and extracted services. ```typescript // cloudwerk.config.ts export default defineConfig({ services: { mode: 'hybrid', email: { mode: 'extracted' }, analytics: { mode: 'extracted' }, cache: { mode: 'local' }, utils: { mode: 'local' }, }, }) ``` *** ## Using Services [Section titled “Using Services”](#using-services) ### services Proxy [Section titled “services Proxy”](#services-proxy) Import and use services with type safety. ```typescript import { services } from '@cloudwerk/core/bindings' export async function POST(request: Request) { // Type-safe method calls const result = await services.email.send({ to: 'user@example.com', subject: 'Hello', body: '

Welcome!

', }) return json({ success: true, messageId: result.messageId }) } ``` ### getService() [Section titled “getService()”](#getservice) Get a typed service by name. ```typescript import { getService } from '@cloudwerk/core/bindings' interface EmailService { send(params: { to: string; subject: string; body: string }): Promise<{ success: boolean }> } const email = getService('email') const result = await email.send({ to: '...', subject: '...', body: '...' }) ``` ### hasService() [Section titled “hasService()”](#hasservice) Check if a service exists. ```typescript import { hasService } from '@cloudwerk/core/bindings' if (hasService('email')) { await services.email.send({ ... }) } ``` ### getServiceNames() [Section titled “getServiceNames()”](#getservicenames) List all available service names. ```typescript import { getServiceNames } from '@cloudwerk/core/bindings' const available = getServiceNames() // ['email', 'payments', 'cache'] ``` *** ## RPC Communication [Section titled “RPC Communication”](#rpc-communication) When a service is extracted, Cloudwerk generates a WorkerEntrypoint class. ### Generated Worker [Section titled “Generated Worker”](#generated-worker) ```typescript // Auto-generated: .cloudwerk/extracted/email-service/worker.ts import { WorkerEntrypoint } from 'cloudflare:workers' import service from '../../../app/services/email/service' export class EmailService extends WorkerEntrypoint { async send(...args: unknown[]): Promise { return service.methods.send.apply({ env: this.env }, args) } } ``` ### Service Binding [Section titled “Service Binding”](#service-binding) For extracted services, bindings are auto-configured: ```toml # wrangler.toml (auto-generated) [[services]] binding = "EMAIL_SERVICE" service = "email-service" entrypoint = "EmailService" ``` ### Transparent Routing [Section titled “Transparent Routing”](#transparent-routing) The `services` proxy routes calls automatically: ```typescript // Your code (identical for both modes) await services.email.send({ to: '...' }) // Local mode: Direct function call // Extracted mode: RPC via service binding ``` *** ## Error Handling [Section titled “Error Handling”](#error-handling) ### ServiceNotFoundError [Section titled “ServiceNotFoundError”](#servicenotfounderror) Thrown when accessing a non-existent service. ```typescript import { ServiceNotFoundError } from '@cloudwerk/service' try { await services.nonexistent.method() } catch (error) { if (error instanceof ServiceNotFoundError) { console.error('Service not found:', error.serviceName) } } ``` ### ServiceMethodError [Section titled “ServiceMethodError”](#servicemethoderror) Thrown when a service method fails. ```typescript import { ServiceMethodError } from '@cloudwerk/service' try { await services.email.send({ to: '...' }) } catch (error) { if (error instanceof ServiceMethodError) { console.error('Method failed:', error.serviceName, error.methodName) console.error('Original error:', error.cause) } } ``` ### ServiceConnectionError [Section titled “ServiceConnectionError”](#serviceconnectionerror) Thrown when extracted service is unreachable. ```typescript import { ServiceConnectionError } from '@cloudwerk/service' try { await services.email.send({ to: '...' }) } catch (error) { if (error instanceof ServiceConnectionError) { console.error('Cannot reach service:', error.serviceName) } } ``` *** ## CLI Commands [Section titled “CLI Commands”](#cli-commands) ### List Services [Section titled “List Services”](#list-services) ```bash cloudwerk services list ``` ### Show Service Details [Section titled “Show Service Details”](#show-service-details) ```bash cloudwerk services info ``` ### Extract Service [Section titled “Extract Service”](#extract-service) Extract a service to a separate Worker. ```bash cloudwerk services extract ``` ### Inline Service [Section titled “Inline Service”](#inline-service) Convert an extracted service back to local mode. ```bash cloudwerk services inline ``` ### Deploy Service [Section titled “Deploy Service”](#deploy-service) Deploy an extracted service. ```bash cloudwerk services deploy ``` ### Show All Status [Section titled “Show All Status”](#show-all-status) ```bash cloudwerk services status ``` ### Generate Types [Section titled “Generate Types”](#generate-types) ```bash cloudwerk services generate-types ``` *** ## Type Definitions [Section titled “Type Definitions”](#type-definitions) ### ServiceDefinition [Section titled “ServiceDefinition”](#servicedefinition) ```typescript interface ServiceDefinition { methods: Record name?: string hooks?: ServiceHooks config?: ServiceConfig } type ServiceMethod = (...args: any[]) => Awaitable ``` ### ServiceConfig [Section titled “ServiceConfig”](#serviceconfig) ```typescript interface ServiceConfig { extraction?: { workerName?: string bindings?: string[] } } ``` ### ServiceHooks [Section titled “ServiceHooks”](#servicehooks-1) ```typescript interface ServiceHooks { onInit?: () => Awaitable onBefore?: (method: string, args: unknown[]) => Awaitable onAfter?: (method: string, result: unknown) => Awaitable onError?: (method: string, error: Error) => Awaitable } ``` ### App Config [Section titled “App Config”](#app-config) ```typescript interface CloudwerkConfig { services?: { mode: 'local' | 'extracted' | 'hybrid' [serviceName: string]: { mode: 'local' | 'extracted' } } } ``` *** ## Best Practices [Section titled “Best Practices”](#best-practices) ### When to Extract [Section titled “When to Extract”](#when-to-extract) **Extract to separate Worker when:** * Service makes many external API calls * Service is CPU or memory intensive * Service needs independent scaling * Service should be deployable separately **Keep local when:** * Service is a thin wrapper or utility * Low latency is critical * Service has minimal resource usage * Service is tightly coupled to main app ### Service Design [Section titled “Service Design”](#service-design) ```typescript // Good: Focused, single-domain service export default defineService({ methods: { async sendEmail(params) { /* ... */ }, async sendBatch(emails) { /* ... */ }, async getDeliveryStatus(id) { /* ... */ }, }, }) // Avoid: Mixed concerns export default defineService({ methods: { async sendEmail(params) { /* ... */ }, async processPayment(params) { /* ... */ }, // Different domain async generateReport(params) { /* ... */ }, // Different domain }, }) ``` ### Use Hooks for Cross-Cutting Concerns [Section titled “Use Hooks for Cross-Cutting Concerns”](#use-hooks-for-cross-cutting-concerns) ```typescript // Good: Logging/metrics in hooks export default defineService({ methods: { async processPayment(orderId, amount) { // Pure business logic only return await stripe.charges.create({ amount }) }, }, hooks: { onBefore: async (method, args) => { await trackMetric(`${method}_started`) }, onAfter: async (method, result) => { await trackMetric(`${method}_completed`) }, }, }) ``` *** ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Services Guide](/guides/services/)** - Patterns and best practices * **[Durable Objects API](/api/durable-objects/)** - Stateful services * **[Queues API](/api/queues/)** - Background job processing # Testing > Testing utilities and patterns for Cloudwerk applications. Cloudwerk provides testing utilities built on Vitest for testing your pages, API routes, and Workers. ## Setup [Section titled “Setup”](#setup) ### Install Dependencies [Section titled “Install Dependencies”](#install-dependencies) ```bash pnpm add -D vitest @cloudflare/vitest-pool-workers ``` ### Configuration [Section titled “Configuration”](#configuration) Create `vitest.config.ts`: ```typescript import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { environment: 'miniflare', environmentOptions: { bindings: { // Test bindings }, }, }, }); ``` Or use the Cloudflare pool: ```typescript import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; export default defineWorkersConfig({ test: { poolOptions: { workers: { wrangler: { configPath: './wrangler.toml' }, }, }, }, }); ``` ## Testing API Routes [Section titled “Testing API Routes”](#testing-api-routes) ### Basic Route Tests [Section titled “Basic Route Tests”](#basic-route-tests) ```typescript // app/api/users/route.test.ts import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { createTestContext } from '@cloudwerk/testing'; describe('GET /api/users', () => { let ctx: TestContext; beforeAll(async () => { ctx = await createTestContext(); }); afterAll(async () => { await ctx.cleanup(); }); it('should return users', async () => { const response = await ctx.fetch('/api/users'); expect(response.status).toBe(200); const data = await response.json(); expect(data).toHaveProperty('users'); }); it('should require authentication', async () => { const response = await ctx.fetch('/api/users/me'); expect(response.status).toBe(401); }); }); ``` ### Testing POST Requests [Section titled “Testing POST Requests”](#testing-post-requests) ```typescript describe('POST /api/users', () => { it('should create a user', async () => { const response = await ctx.fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'test@example.com', name: 'Test User', }), }); expect(response.status).toBe(201); const user = await response.json(); expect(user.email).toBe('test@example.com'); }); it('should validate input', async () => { const response = await ctx.fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'invalid' }), }); expect(response.status).toBe(400); const error = await response.json(); expect(error).toHaveProperty('errors'); }); }); ``` ### Testing with Authentication [Section titled “Testing with Authentication”](#testing-with-authentication) ```typescript describe('Authenticated Routes', () => { it('should access protected route with auth', async () => { // Create authenticated context const authCtx = await ctx.withAuth({ userId: 'user-123' }); const response = await authCtx.fetch('/api/users/me'); expect(response.status).toBe(200); const user = await response.json(); expect(user.id).toBe('user-123'); }); }); ``` ## Testing Loaders [Section titled “Testing Loaders”](#testing-loaders) ### Page Loader Tests [Section titled “Page Loader Tests”](#page-loader-tests) ```typescript // app/users/[id]/page.test.ts import { describe, it, expect } from 'vitest'; import { loader } from './page'; import { createMockLoaderArgs } from '@cloudwerk/testing'; describe('User Page Loader', () => { it('should return user data', async () => { const args = createMockLoaderArgs({ params: { id: 'user-123' }, }); // Seed test data await args.context.db .insertInto('users') .values({ id: 'user-123', name: 'Test User', email: 'test@example.com' }) .execute(); const result = await loader(args); expect(result.user).toMatchObject({ id: 'user-123', name: 'Test User', }); }); it('should throw NotFoundError for missing user', async () => { const args = createMockLoaderArgs({ params: { id: 'nonexistent' }, }); await expect(loader(args)).rejects.toThrow('Not found'); }); }); ``` ## Testing Components [Section titled “Testing Components”](#testing-components) ### Component Tests [Section titled “Component Tests”](#component-tests) ```typescript // app/components/UserCard.test.tsx import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import { UserCard } from './UserCard'; describe('UserCard', () => { it('should render user name', () => { render(); expect(screen.getByText('John Doe')).toBeInTheDocument(); expect(screen.getByText('john@example.com')).toBeInTheDocument(); }); it('should handle missing avatar', () => { render(); expect(screen.getByText('J')).toBeInTheDocument(); // Initials fallback }); }); ``` ## Testing Middleware [Section titled “Testing Middleware”](#testing-middleware) ```typescript // app/middleware.test.ts import { describe, it, expect } from 'vitest'; import { middleware } from './middleware'; import { createMockRequest, createMockContext } from '@cloudwerk/testing'; describe('Auth Middleware', () => { it('should allow authenticated requests', async () => { const request = createMockRequest('/api/protected', { headers: { Cookie: 'session=valid-session' }, }); const context = createMockContext({ auth: { getUser: async () => ({ id: 'user-123' }) }, }); const next = vi.fn().mockResolvedValue(new Response('OK')); const response = await middleware(request, next, context); expect(next).toHaveBeenCalled(); expect(response.status).toBe(200); }); it('should reject unauthenticated requests', async () => { const request = createMockRequest('/api/protected'); const context = createMockContext({ auth: { getUser: async () => null }, }); const next = vi.fn(); const response = await middleware(request, next, context); expect(next).not.toHaveBeenCalled(); expect(response.status).toBe(401); }); }); ``` ## Testing Queue Handlers [Section titled “Testing Queue Handlers”](#testing-queue-handlers) ```typescript // workers/email-queue.test.ts import { describe, it, expect, vi } from 'vitest'; import handler from './email-queue'; import { createMockMessage, createMockEnv } from '@cloudwerk/testing'; describe('Email Queue Handler', () => { it('should process email messages', async () => { const env = createMockEnv(); const message = createMockMessage({ body: { type: 'welcome', to: 'user@example.com' }, }); await handler.queue([message], env, {}); expect(message.ack).toHaveBeenCalled(); expect(env.EMAIL_API.send).toHaveBeenCalledWith({ to: 'user@example.com', template: 'welcome', }); }); it('should retry on failure', async () => { const env = createMockEnv(); env.EMAIL_API.send.mockRejectedValue(new Error('API Error')); const message = createMockMessage({ body: { type: 'welcome', to: 'user@example.com' }, }); await handler.queue([message], env, {}); expect(message.retry).toHaveBeenCalled(); expect(message.ack).not.toHaveBeenCalled(); }); }); ``` ## Testing Durable Objects [Section titled “Testing Durable Objects”](#testing-durable-objects) ```typescript // workers/counter.test.ts import { describe, it, expect } from 'vitest'; import { Counter } from './counter'; import { createMockDurableObjectState } from '@cloudwerk/testing'; describe('Counter Durable Object', () => { it('should increment count', async () => { const state = createMockDurableObjectState(); const counter = new Counter(state, {}); const response = await counter.fetch( new Request('http://counter/increment') ); expect(await response.text()).toBe('1'); }); it('should persist count', async () => { const state = createMockDurableObjectState(); const counter = new Counter(state, {}); await counter.fetch(new Request('http://counter/increment')); await counter.fetch(new Request('http://counter/increment')); await counter.fetch(new Request('http://counter/increment')); const response = await counter.fetch(new Request('http://counter/')); expect(await response.text()).toBe('3'); expect(state.storage.put).toHaveBeenCalledWith('count', 3); }); }); ``` ## Testing Utilities [Section titled “Testing Utilities”](#testing-utilities) ### `createTestContext()` [Section titled “createTestContext()”](#createtestcontext) Create a full test context: ```typescript const ctx = await createTestContext({ // Seed database seed: async (db) => { await db.insertInto('users').values([ { id: '1', name: 'User 1' }, { id: '2', name: 'User 2' }, ]).execute(); }, }); ``` ### `createMockLoaderArgs()` [Section titled “createMockLoaderArgs()”](#createmockloaderargs) Create mock loader arguments: ```typescript const args = createMockLoaderArgs({ params: { id: '123' }, request: new Request('http://localhost/users/123'), context: { env: { API_KEY: 'test-key' }, }, }); ``` ### `createMockRequest()` [Section titled “createMockRequest()”](#createmockrequest) Create mock requests: ```typescript const request = createMockRequest('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Test' }), }); ``` ### `createMockContext()` [Section titled “createMockContext()”](#createmockcontext) Create mock context: ```typescript const context = createMockContext({ env: { DB: mockDb, KV: mockKv }, auth: { getUser: async () => mockUser }, }); ``` ## Running Tests [Section titled “Running Tests”](#running-tests) ```bash # Run all tests pnpm test # Watch mode pnpm test --watch # Coverage pnpm test --coverage # Specific file pnpm test app/api/users/route.test.ts ``` ## Next Steps [Section titled “Next Steps”](#next-steps) * **[CLI Reference](/api/cli/)** - Test command options * **[Configuration](/api/configuration/)** - Test configuration * **[Examples](/examples/blog/)** - See tests in action # Triggers API > Complete API reference for @cloudwerk/trigger - event-driven handlers for scheduled, queue, R2, webhook, email, D1, and tail events. The `@cloudwerk/trigger` package provides a unified API for handling all Cloudflare event types through the `defineTrigger()` factory function. ## Installation [Section titled “Installation”](#installation) ```bash pnpm add @cloudwerk/trigger ``` ## defineTrigger() [Section titled “defineTrigger()”](#definetrigger) Creates a trigger handler for a specific event source. ```typescript import { defineTrigger } from '@cloudwerk/trigger' export default defineTrigger({ source: TriggerSource, handle: (event: TriggerEvent, ctx: TriggerContext) => Awaitable, config?: TriggerConfig, }) ``` ### Parameters [Section titled “Parameters”](#parameters) | Parameter | Type | Description | | --------- | --------------- | --------------------------------------------------- | | `source` | `TriggerSource` | Event source configuration (see source types below) | | `handle` | `Function` | Async handler function receiving event and context | | `config` | `TriggerConfig` | Optional configuration (retries, timeout, etc.) | ### Returns [Section titled “Returns”](#returns) Returns a `TriggerDefinition` object that Cloudwerk registers automatically. ## Trigger Source Types [Section titled “Trigger Source Types”](#trigger-source-types) ### ScheduledSource [Section titled “ScheduledSource”](#scheduledsource) Cron-based scheduled execution. ```typescript interface ScheduledSource { type: 'scheduled' cron: string // Cron expression timezone?: string // IANA timezone (default: 'UTC') } ``` **Example:** ```typescript export default defineTrigger({ source: { type: 'scheduled', cron: '0 9 * * 1-5', // Weekdays at 9 AM timezone: 'America/New_York', }, async handle(event, ctx) { console.log(`Scheduled at: ${event.scheduledTime}`) }, }) ``` **Event Properties:** | Property | Type | Description | | --------------- | -------- | -------------------------------------------- | | `cron` | `string` | The cron expression that triggered the event | | `scheduledTime` | `Date` | Scheduled execution time | *** ### QueueSource [Section titled “QueueSource”](#queuesource) Queue message consumption. ```typescript interface QueueSource { type: 'queue' name: string // Queue name batchSize?: number // Max messages per batch (1-100, default: 10) batchTimeout?: string // Max wait time ('5s', '1m', etc.) retryDelay?: string // Delay between retries (default: '1m') deadLetterQueue?: string // DLQ for failed messages } ``` **Example:** ```typescript export default defineTrigger({ source: { type: 'queue', name: 'notifications', batchSize: 25, deadLetterQueue: 'notifications-dlq', }, async handle(event, ctx) { for (const message of event.messages) { await processMessage(message.body) message.ack() } }, }) ``` **Event Properties:** | Property | Type | Description | | ---------- | ---------------- | ----------------- | | `queue` | `string` | Queue name | | `messages` | `QueueMessage[]` | Array of messages | **QueueMessage:** | Property | Type | Description | | --------------------- | ---------- | --------------------------------- | | `id` | `string` | Unique message ID | | `body` | `unknown` | Message payload | | `timestamp` | `Date` | When message was sent | | `attempts` | `number` | Delivery attempt count | | `ack()` | `Function` | Acknowledge successful processing | | `retry(options?)` | `Function` | Request retry with optional delay | | `deadLetter(reason?)` | `Function` | Send to dead letter queue | *** ### R2Source [Section titled “R2Source”](#r2source) R2 object storage events. ```typescript interface R2Source { type: 'r2' bucket: string // R2 bucket name events: R2EventType[] // Event types to listen for prefix?: string // Key prefix filter suffix?: string // Key suffix filter } type R2EventType = | 'object:create' | 'object:delete' | 'object:create:put' | 'object:create:copy' | 'object:create:multipart' ``` **Example:** ```typescript export default defineTrigger({ source: { type: 'r2', bucket: 'uploads', events: ['object:create'], prefix: 'images/', suffix: '.jpg', }, async handle(event, ctx) { const { key, size, etag } = event.object await generateThumbnail(key) }, }) ``` **Event Properties:** | Property | Type | Description | | -------- | --------------- | ----------------------------------- | | `bucket` | `string` | Bucket name | | `action` | `string` | Event action (‘create’ or ‘delete’) | | `object` | `R2EventObject` | Object metadata | **R2EventObject:** | Property | Type | Description | | ------------ | -------- | -------------------- | | `key` | `string` | Object key | | `size` | `number` | Object size in bytes | | `etag` | `string` | Object ETag | | `uploadedAt` | `Date` | Upload timestamp | *** ### WebhookSource [Section titled “WebhookSource”](#webhooksource) HTTP webhook events with signature verification. ```typescript interface WebhookSource { type: 'webhook' path: string // Webhook endpoint path methods?: HttpMethod[] // Allowed HTTP methods (default: ['POST']) verifier?: WebhookVerifier // Built-in or custom verifier } type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' ``` **Example with Built-in Verifier:** ```typescript import { stripeVerifier } from '@cloudwerk/trigger/verifiers' export default defineTrigger({ source: { type: 'webhook', path: '/webhooks/stripe', verifier: stripeVerifier({ secret: process.env.STRIPE_WEBHOOK_SECRET, }), }, async handle(event, ctx) { const stripeEvent = event.payload if (stripeEvent.type === 'checkout.session.completed') { await fulfillOrder(stripeEvent.data.object) } }, }) ``` **Event Properties:** | Property | Type | Description | | ---------- | --------- | ------------------------------ | | `request` | `Request` | Original HTTP request | | `payload` | `unknown` | Parsed and verified payload | | `headers` | `Headers` | Request headers | | `verified` | `boolean` | Whether signature was verified | *** ### EmailSource [Section titled “EmailSource”](#emailsource) Incoming email events. ```typescript interface EmailSource { type: 'email' address?: string // Filter by recipient address domain?: string // Filter by recipient domain } ``` **Example:** ```typescript export default defineTrigger({ source: { type: 'email', domain: 'support.myapp.com', }, async handle(event, ctx) { const { from, to, subject, text, html } = event await createSupportTicket({ from, subject, body: text || html }) }, }) ``` **Event Properties:** | Property | Type | Description | | ------------- | --------------------- | ----------------------- | | `from` | `string` | Sender email address | | `to` | `string` | Recipient email address | | `subject` | `string` | Email subject | | `text` | `string \| null` | Plain text body | | `html` | `string \| null` | HTML body | | `attachments` | `EmailAttachment[]` | File attachments | | `headers` | `Map` | Email headers | | `raw` | `ReadableStream` | Raw email stream | *** ### D1Source [Section titled “D1Source”](#d1source) Database change events. ```typescript interface D1Source { type: 'd1' database: string // D1 database binding name tables?: string[] // Tables to watch (all if omitted) operations?: D1Operation[] // Operations to watch (all if omitted) } type D1Operation = 'INSERT' | 'UPDATE' | 'DELETE' ``` **Example:** ```typescript export default defineTrigger({ source: { type: 'd1', database: 'DB', tables: ['users', 'orders'], operations: ['INSERT', 'UPDATE'], }, async handle(event, ctx) { const { table, operation, newRow, oldRow } = event await syncToAnalytics(table, operation, newRow) }, }) ``` **Event Properties:** | Property | Type | Description | | ------------ | --------------------------------- | --------------------------------- | | `database` | `string` | Database binding name | | `table` | `string` | Affected table name | | `operation` | `D1Operation` | INSERT, UPDATE, or DELETE | | `newRow` | `Record \| null` | New row data (INSERT/UPDATE) | | `oldRow` | `Record \| null` | Previous row data (UPDATE/DELETE) | | `primaryKey` | `unknown` | Primary key value | *** ### TailSource [Section titled “TailSource”](#tailsource) Log consumption from other Workers. ```typescript interface TailSource { type: 'tail' workers: string[] // Worker names to consume logs from filters?: TailFilter[] // Optional log filters } interface TailFilter { field: 'outcome' | 'status' | 'method' | 'samplingRate' value: string | number operator?: 'equals' | 'contains' | 'startsWith' } ``` **Example:** ```typescript export default defineTrigger({ source: { type: 'tail', workers: ['api-worker', 'auth-worker'], filters: [ { field: 'outcome', value: 'exception' }, ], }, async handle(event, ctx) { for (const log of event.logs) { if (log.outcome === 'exception') { await alertOnCall(log) } } }, }) ``` **Event Properties:** | Property | Type | Description | | -------- | ----------- | -------------------- | | `logs` | `TailLog[]` | Array of log entries | | `worker` | `string` | Source worker name | *** ## TriggerContext [Section titled “TriggerContext”](#triggercontext) The context object passed to all trigger handlers. ```typescript interface TriggerContext { env: Env // Environment bindings traceId: string // Unique trace ID attempt: number // Current retry attempt (1-based) waitUntil(promise: Promise): void // Extend execution lifetime emit(triggerName: string, payload: unknown): Promise // Chain triggers noRetry(): void // Prevent automatic retry } ``` ### Context Methods [Section titled “Context Methods”](#context-methods) #### waitUntil() [Section titled “waitUntil()”](#waituntil) Extend the handler’s lifetime for background work. ```typescript async handle(event, ctx) { await criticalWork() // Non-critical work runs after handler completes ctx.waitUntil(sendAnalytics(event)) ctx.waitUntil(updateMetrics()) } ``` #### emit() [Section titled “emit()”](#emit) Chain to another trigger, preserving trace ID. ```typescript async handle(event, ctx) { await processOrder(event.payload) // Trigger downstream processing await ctx.emit('send-confirmation', { orderId: event.payload.orderId, }) } ``` #### noRetry() [Section titled “noRetry()”](#noretry) Prevent automatic retry on failure. ```typescript async handle(event, ctx) { try { await processEvent(event) } catch (error) { if (isPermanentError(error)) { ctx.noRetry() // Don't retry permanent failures await logPermanentFailure(error) } throw error } } ``` *** ## TriggerConfig [Section titled “TriggerConfig”](#triggerconfig) Optional configuration for trigger behavior. ```typescript interface TriggerConfig { retries?: number // Max retry attempts (default: 3) retryDelay?: string // Delay between retries (default: '1m') timeout?: string // Handler timeout (default: '30s') observability?: { metrics?: boolean // Enable metrics (default: true) tracing?: boolean // Enable tracing (default: true) } } ``` **Example:** ```typescript export default defineTrigger({ source: { type: 'scheduled', cron: '0 * * * *' }, config: { retries: 5, retryDelay: '2m', timeout: '5m', observability: { metrics: true, tracing: true, }, }, async handle(event, ctx) { // Handler with custom retry behavior }, }) ``` *** ## Webhook Verifiers [Section titled “Webhook Verifiers”](#webhook-verifiers) Built-in signature verifiers for popular services. ### stripeVerifier [Section titled “stripeVerifier”](#stripeverifier) ```typescript import { stripeVerifier } from '@cloudwerk/trigger/verifiers' stripeVerifier({ secret: string, // Webhook signing secret tolerance?: number, // Timestamp tolerance in seconds (default: 300) }) ``` ### githubVerifier [Section titled “githubVerifier”](#githubverifier) ```typescript import { githubVerifier } from '@cloudwerk/trigger/verifiers' githubVerifier({ secret: string, // Webhook secret events?: string[], // Allowed event types (optional) }) ``` ### slackVerifier [Section titled “slackVerifier”](#slackverifier) ```typescript import { slackVerifier } from '@cloudwerk/trigger/verifiers' slackVerifier({ signingSecret: string, // Slack signing secret tolerance?: number, // Timestamp tolerance (default: 300) }) ``` ### shopifyVerifier [Section titled “shopifyVerifier”](#shopifyverifier) ```typescript import { shopifyVerifier } from '@cloudwerk/trigger/verifiers' shopifyVerifier({ secret: string, // Webhook secret }) ``` ### linearVerifier [Section titled “linearVerifier”](#linearverifier) ```typescript import { linearVerifier } from '@cloudwerk/trigger/verifiers' linearVerifier({ secret: string, // Webhook signing secret }) ``` ### customHmacVerifier [Section titled “customHmacVerifier”](#customhmacverifier) Create a custom HMAC verifier for any service. ```typescript import { customHmacVerifier } from '@cloudwerk/trigger/verifiers' customHmacVerifier({ secret: string, algorithm: 'sha256' | 'sha1' | 'sha512', header: string, // Header containing signature prefix?: string, // Signature prefix (e.g., 'sha256=') encoding?: 'hex' | 'base64', // Signature encoding (default: 'hex') getPayload?: (request: Request) => Promise, // Custom payload extraction }) ``` **Example:** ```typescript const myVerifier = customHmacVerifier({ secret: process.env.WEBHOOK_SECRET, algorithm: 'sha256', header: 'X-Custom-Signature', prefix: 'sha256=', }) ``` *** ## Testing Utilities [Section titled “Testing Utilities”](#testing-utilities) Mock event factories for testing triggers. ```typescript import { mockScheduledEvent, mockQueueEvent, mockR2Event, mockWebhookEvent, mockEmailEvent, mockD1Event, mockTailEvent, mockTriggerContext, } from '@cloudwerk/trigger/testing' ``` ### mockScheduledEvent [Section titled “mockScheduledEvent”](#mockscheduledevent) ```typescript const event = mockScheduledEvent({ cron: '0 * * * *', scheduledTime: new Date('2024-01-15T10:00:00Z'), }) ``` ### mockQueueEvent [Section titled “mockQueueEvent”](#mockqueueevent) ```typescript const event = mockQueueEvent({ queue: 'notifications', messages: [ { id: '1', body: { userId: 'user_123' }, attempts: 1 }, { id: '2', body: { userId: 'user_456' }, attempts: 1 }, ], }) ``` ### mockTriggerContext [Section titled “mockTriggerContext”](#mocktriggercontext) ```typescript const ctx = mockTriggerContext({ env: mockEnv, traceId: 'test-trace-123', }) ``` ### Testing Example [Section titled “Testing Example”](#testing-example) ```typescript import { describe, it, expect, vi } from 'vitest' import { mockScheduledEvent, mockTriggerContext } from '@cloudwerk/trigger/testing' import trigger from './daily-cleanup' describe('daily-cleanup trigger', () => { it('should clean up expired records', async () => { const event = mockScheduledEvent({ cron: '0 0 * * *' }) const ctx = mockTriggerContext({ env: { DB: mockD1Database() }, }) await trigger.handle(event, ctx) expect(ctx.env.DB.prepare).toHaveBeenCalledWith( expect.stringContaining('DELETE FROM') ) }) }) ``` *** ## Error Handling [Section titled “Error Handling”](#error-handling) ### TriggerError [Section titled “TriggerError”](#triggererror) Base error class for trigger failures. ```typescript import { TriggerError } from '@cloudwerk/trigger' class TriggerError extends Error { readonly code: string readonly retryable: boolean readonly context?: Record } ``` ### WebhookVerificationError [Section titled “WebhookVerificationError”](#webhookverificationerror) Thrown when webhook signature verification fails. ```typescript import { WebhookVerificationError } from '@cloudwerk/trigger' try { await verifySignature(request) } catch (error) { if (error instanceof WebhookVerificationError) { console.error('Invalid signature:', error.message) return new Response('Unauthorized', { status: 401 }) } } ``` ### TriggerTimeoutError [Section titled “TriggerTimeoutError”](#triggertimeouterror) Thrown when handler exceeds configured timeout. ```typescript import { TriggerTimeoutError } from '@cloudwerk/trigger' ``` *** ## CLI Commands [Section titled “CLI Commands”](#cli-commands) ### List Triggers [Section titled “List Triggers”](#list-triggers) ```bash cloudwerk triggers list ``` ### Show Trigger Details [Section titled “Show Trigger Details”](#show-trigger-details) ```bash cloudwerk triggers info ``` ### Test Trigger Locally [Section titled “Test Trigger Locally”](#test-trigger-locally) ```bash cloudwerk triggers test --event '{"key": "value"}' ``` ### Generate Types [Section titled “Generate Types”](#generate-types) ```bash cloudwerk triggers generate-types ``` *** ## Type Definitions [Section titled “Type Definitions”](#type-definitions) ### Full Type Reference [Section titled “Full Type Reference”](#full-type-reference) ```typescript // Trigger definition interface TriggerDefinition { source: TriggerSource handle: TriggerHandler config?: TriggerConfig } // Handler signature type TriggerHandler = ( event: E, ctx: TriggerContext ) => Awaitable // Awaitable helper type Awaitable = T | Promise // Union of all source types type TriggerSource = | ScheduledSource | QueueSource | R2Source | WebhookSource | EmailSource | D1Source | TailSource // Union of all event types type TriggerEvent = | ScheduledEvent | QueueEvent | R2Event | WebhookEvent | EmailEvent | D1Event | TailEvent ``` *** ## Limits [Section titled “Limits”](#limits) | Limit | Value | | -------------------------------- | ------------ | | Maximum cron triggers per Worker | 3 | | Minimum cron interval | 1 minute | | Maximum handler timeout (free) | 30 seconds | | Maximum handler timeout (paid) | 15 minutes | | Maximum queue batch size | 100 messages | | Maximum webhook payload | 100 MB | *** ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Triggers Guide](/guides/triggers/)** - Patterns and best practices * **[Queues Guide](/guides/queues/)** - Queue consumer patterns * **[Services API](/api/services/)** - Service extraction # Examples > Example applications built with Cloudwerk. Learn from complete example applications. ## Available Examples [Section titled “Available Examples”](#available-examples) * [Blog](/examples/blog/) - Full-featured blog with authentication * [Linkly](/examples/linkly/) - URL shortener with analytics * [Gallery](/examples/gallery/) - Image gallery with uploads ## Running Examples [Section titled “Running Examples”](#running-examples) Each example can be cloned and run locally: ```bash # Clone the example npx degit squirrelsoft-dev/cloudwerk/examples/blog my-blog # Install dependencies cd my-blog pnpm install # Run development server pnpm dev ``` ## What’s in Each Example [Section titled “What’s in Each Example”](#whats-in-each-example) All examples demonstrate: * File-based routing with pages and layouts * Data loading with loader functions * Form handling with actions * Authentication flows * Database operations with D1 Check individual example pages for specific features demonstrated. # Blog with D1 Database > Build a full-featured blog with D1 database, Tailwind CSS, and static site generation. In this tutorial, you’ll build a complete blog application with: * **D1 Database** for storing posts * **Tailwind CSS** for styling * **Static Site Generation** for fast page loads * **Dynamic routing** for individual post pages [View Source on GitHub ](https://github.com/squirrelsoft-dev/cloudwerk/tree/main/examples/blog)See the complete source code for this example ## Project Overview [Section titled “Project Overview”](#project-overview) The final project structure: * app/ * layout.tsx * page.tsx * globals.css * about/ * page.tsx * posts/ * \[slug]/ * page.tsx * lib/ * db.ts * markdown.ts * components/ * PostCard.tsx * migrations/ * 0001\_create\_posts.sql * cloudwerk.config.ts * wrangler.toml * package.json ## Step 1: Create the Project [Section titled “Step 1: Create the Project”](#step-1-create-the-project) 1. Create a new Cloudwerk app: ```bash pnpm dlx @cloudwerk/create-app blog --renderer hono-jsx cd blog ``` 2. Install dependencies: ```bash pnpm install ``` ## Step 2: Set Up D1 Database [Section titled “Step 2: Set Up D1 Database”](#step-2-set-up-d1-database) The starter template already includes Tailwind CSS configured in `app/globals.css`. 1. Add a D1 database binding using the Cloudwerk CLI: ```bash npm run bindings add d1 ``` When prompted: * **Binding name**: `DB` * **Database name**: `blog-db` * **Add to wrangler.toml?**: Yes This creates the database and automatically configures `wrangler.toml`. 2. Generate TypeScript types for your bindings: ```bash npm run bindings generate-types ``` This creates type definitions so you can import `DB` from `@cloudwerk/core/bindings`. 3. Create a migration file: ```bash wrangler d1 migrations create blog-db create_posts ``` This creates `migrations/0001_create_posts.sql`. ### Database Schema [Section titled “Database Schema”](#database-schema) Open the migration file and add the posts table with seed data: migrations/0001\_create\_posts.sql ````sql CREATE TABLE posts ( id TEXT PRIMARY KEY, slug TEXT UNIQUE NOT NULL, title TEXT NOT NULL, excerpt TEXT, content TEXT NOT NULL, published_at TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_posts_slug ON posts(slug); CREATE INDEX idx_posts_published ON posts(published_at); -- Seed data INSERT INTO posts (id, slug, title, excerpt, content, published_at) VALUES ('1', 'hello-world', 'Hello World', 'Welcome to my blog built with Cloudwerk.', '# Hello World Welcome to my blog! This is my first post built with **Cloudwerk**. ## What is Cloudwerk? Cloudwerk provides file-based routing that compiles to Hono, with integrated support for: - D1 databases - KV storage - R2 object storage - Authentication - Queues Stay tuned for more posts!', '2025-01-15'), ('2', 'getting-started', 'Getting Started with Cloudwerk', 'Learn how to build your first app.', '# Getting Started with Cloudwerk In this post, we will walk through building your first Cloudwerk application. ## Prerequisites - Node.js 20+ - pnpm (recommended) - A Cloudflare account ## Create Your App ```bash pnpm dlx @cloudwerk/create-app my-blog --renderer hono-jsx cd my-blog pnpm install ```` Visit `http://localhost:3000` to see your app!’, ‘2025-01-20’); ````plaintext Apply the migration locally: ```bash wrangler d1 migrations apply blog-db --local ```` ## Step 3: Create Database Helpers [Section titled “Step 3: Create Database Helpers”](#step-3-create-database-helpers) Create a module to interact with the D1 database: app/lib/db.ts ```typescript import { DB } from '@cloudwerk/core/bindings' export interface Post { id: string slug: string title: string excerpt: string | null content: string published_at: string | null created_at: string } export async function getPosts(): Promise { const result = await DB .prepare( 'SELECT id, slug, title, excerpt, published_at, created_at FROM posts WHERE published_at IS NOT NULL ORDER BY published_at DESC' ) .all() return result.results } export async function getPostBySlug(slug: string): Promise { const result = await DB .prepare('SELECT * FROM posts WHERE slug = ?') .bind(slug) .first() return result } export async function getAllSlugs(): Promise { const result = await DB .prepare('SELECT slug FROM posts WHERE published_at IS NOT NULL') .all<{ slug: string }>() return result.results.map((r: { slug: string }) => r.slug) } ``` Tip Import `DB` directly from `@cloudwerk/core/bindings` to access your D1 database. Run `pnpm bindings` to generate TypeScript types for your bindings. ## Step 4: Create a Markdown Renderer [Section titled “Step 4: Create a Markdown Renderer”](#step-4-create-a-markdown-renderer) Create a simple markdown renderer for blog posts: app/lib/markdown.ts ````typescript type MarkdownNode = { type: string content?: string level?: number lang?: string items?: string[] } function parseMarkdown(source: string): MarkdownNode[] { const lines = source.split('\n') const nodes: MarkdownNode[] = [] let i = 0 while (i < lines.length) { const line = lines[i] // Headers const headerMatch = line.match(/^(#{1,6})\s+(.+)$/) if (headerMatch) { nodes.push({ type: 'heading', level: headerMatch[1].length, content: headerMatch[2], }) i++ continue } // Code blocks if (line.startsWith('```')) { const lang = line.slice(3).trim() const codeLines: string[] = [] i++ while (i < lines.length && !lines[i].startsWith('```')) { codeLines.push(lines[i]) i++ } nodes.push({ type: 'code', lang: lang || undefined, content: codeLines.join('\n'), }) i++ continue } // Unordered lists if (line.match(/^[-*]\s+/)) { const items: string[] = [] while (i < lines.length && lines[i].match(/^[-*]\s+/)) { items.push(lines[i].replace(/^[-*]\s+/, '')) i++ } nodes.push({ type: 'list', items }) continue } // Empty lines if (line.trim() === '') { i++ continue } // Paragraphs const paragraphLines: string[] = [] while (i < lines.length && lines[i].trim() !== '' && !lines[i].match(/^[#\-*`]/)) { paragraphLines.push(lines[i]) i++ } if (paragraphLines.length > 0) { nodes.push({ type: 'paragraph', content: paragraphLines.join(' ') }) } } return nodes } function formatInlineText(text: string): string { return text .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/\*(.+?)\*/g, '$1') .replace(/`(.+?)`/g, '$1') } function escapeHtml(text: string): string { return text .replace(/&/g, '&') .replace(//g, '>') } export function renderMarkdown(source: string): string { const nodes = parseMarkdown(source) const parts: string[] = [] for (const node of nodes) { switch (node.type) { case 'heading': { const Tag = `h${node.level}` const classes: Record = { 1: 'text-3xl font-bold mt-8 mb-4', 2: 'text-2xl font-semibold mt-6 mb-3', 3: 'text-xl font-medium mt-4 mb-2', } parts.push( `<${Tag} class="${classes[node.level!] || 'font-medium mt-4 mb-2'}">${formatInlineText(node.content!)}` ) break } case 'paragraph': parts.push( `

${formatInlineText(node.content!)}

` ) break case 'code': parts.push( `
${escapeHtml(node.content!)}
` ) break case 'list': parts.push('
    ') for (const item of node.items!) { parts.push(`
  • ${formatInlineText(item)}
  • `) } parts.push('
') break } } return parts.join('\n') } ```` ## Step 5: Update the Root Layout [Section titled “Step 5: Update the Root Layout”](#step-5-update-the-root-layout) Update the layout to set the blog title: app/layout.tsx ```tsx import type { LayoutProps } from '@cloudwerk/core' import globals from './globals.css?url' export default function RootLayout({ children }: LayoutProps) { return ( Blog - Cloudwerk {children} ) } ``` Note Use `?url` suffix to import CSS files as URLs. This enables Vite to process the CSS and return a hashed URL for cache busting. ## Step 6: Create the Home Page [Section titled “Step 6: Create the Home Page”](#step-6-create-the-home-page) The home page displays a list of all published posts: app/page.tsx ```tsx import type { PageProps, LoaderArgs } from '@cloudwerk/core' import { getPosts, type Post } from './lib/db' import PostCard from './components/PostCard' export async function loader(_args: LoaderArgs) { const posts = await getPosts() return { posts } } interface HomePageProps extends PageProps { posts: Post[] } export default function HomePage({ posts }: HomePageProps) { return (

My Blog

A personal blog built with Cloudwerk and Cloudflare Workers.

Latest Posts

{posts.length > 0 ? (
{posts.map((post) => ( ))}
) : (

No posts yet.

)}
) } ``` ## Step 7: Create the PostCard Component [Section titled “Step 7: Create the PostCard Component”](#step-7-create-the-postcard-component) Create a reusable component to display post previews: app/components/PostCard.tsx ```tsx import type { Post } from '../lib/db' interface PostCardProps { post: Omit } export default function PostCard({ post }: PostCardProps) { const date = post.published_at ? new Date(post.published_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }) : null return ( ) } ``` ## Step 8: Create the Post Detail Page [Section titled “Step 8: Create the Post Detail Page”](#step-8-create-the-post-detail-page) This page uses **Static Site Generation (SSG)** to pre-render posts at build time: app/posts/\[slug]/page.tsx ```tsx import type { PageProps, LoaderArgs } from '@cloudwerk/core' import { NotFoundError } from '@cloudwerk/core' import { raw } from 'hono/html' import { getPostBySlug, getAllSlugs, type Post } from '../../lib/db' import { renderMarkdown } from '../../lib/markdown' // Enable static site generation export const config = { rendering: 'static', } // Generate static paths for all posts export async function generateStaticParams() { const slugs = await getAllSlugs() return slugs.map((slug) => ({ slug })) } export async function loader({ params }: LoaderArgs) { const post = await getPostBySlug(params.slug) if (!post) { throw new NotFoundError(`Post not found: ${params.slug}`) } const html = renderMarkdown(post.content) return { post, html } } interface PostPageProps extends PageProps { post: Post html: string } export default function PostPage({ post, html }: PostPageProps) { const date = post.published_at ? new Date(post.published_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }) : null return (

{post.title}

{date && }
{raw(html)}
) } ``` Static Site Generation By setting `config.rendering = 'static'` and exporting `generateStaticParams()`, Cloudwerk will pre-render these pages at build time. This results in instant page loads since HTML is served directly from Cloudflare’s edge network. ## Step 9: Create the About Page [Section titled “Step 9: Create the About Page”](#step-9-create-the-about-page) Add a static about page: app/about/page.tsx ```tsx export default function AboutPage() { return (

About

Welcome to my blog! This is a demo application built with{' '} Cloudwerk, a full-stack framework for Cloudflare Workers.

Features

  • File-based routing with dynamic segments
  • Server-side rendering with Hono JSX
  • D1 database integration
  • Static site generation for blog posts
  • Tailwind CSS styling

Technology Stack

  • Cloudwerk framework
  • Cloudflare Workers
  • Cloudflare D1 (SQLite)
  • Hono JSX
  • Tailwind CSS v4
) } ``` ## Step 10: Run the Development Server [Section titled “Step 10: Run the Development Server”](#step-10-run-the-development-server) Start the development server: ```bash pnpm dev ``` Visit `http://localhost:3000` to see your blog! ## Step 11: Build and Deploy [Section titled “Step 11: Build and Deploy”](#step-11-build-and-deploy) 1. Build the application: ```bash pnpm build ``` This will: * Bundle your server code * Process Tailwind CSS * Pre-render static pages (posts) * Output everything to `dist/` 2. Apply migrations to production: ```bash wrangler d1 migrations apply blog-db --remote ``` 3. Deploy to Cloudflare: ```bash pnpm deploy ``` ## Summary [Section titled “Summary”](#summary) You’ve built a complete blog with: * **D1 Database**: Storing posts with full-text content and metadata * **Type-safe bindings**: Using `@cloudwerk/core/bindings` for database access * **Static Site Generation**: Pre-rendering blog posts for instant loads * **Dynamic routing**: Using `[slug]` for post URLs * **Tailwind CSS**: Modern, utility-first styling * **Server-side rendering**: Fast initial page loads ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Database Guide](/guides/database/)** - Learn more about D1 patterns * **[Data Loading](/guides/data-loading/)** - Advanced loader patterns * **[Authentication](/guides/authentication/)** - Add user authentication * **[Deployment](/guides/deployment/)** - Production deployment options # Gallery - Image Uploads > Build an image gallery with Cloudflare Images (hosted) and R2 with IMAGES binding for on-the-fly transformations. In this tutorial, you’ll build an image gallery that demonstrates two approaches to image handling: * **Hosted Images** - Upload to Cloudflare Images with pre-defined variants * **R2 + IMAGES Binding** - Store in R2 with on-the-fly transformations [View Source on GitHub ](https://github.com/squirrelsoft-dev/cloudwerk/tree/main/examples/gallery)See the complete source code for this example ## Project Overview [Section titled “Project Overview”](#project-overview) The final project structure: * app/ * layout.tsx * page.tsx # Home with links to both demos * globals.css * images/ * gallery.ts # Image variants definition * components/ * image-grid.tsx # Thumbnail grid with click handling * image-dialog.tsx # Modal for full-size preview * hosted/ * page.tsx # Hosted Images demo * route.ts # Upload/list/delete API * variants/ * page.tsx # Variant setup helper * route.ts # Variant management API * r2/ * page.tsx # R2 + IMAGES demo * route.ts # Upload/list/delete API * \[id]/ * route.ts # Transform and serve images * cloudwerk.config.ts * wrangler.toml * package.json ## Step 1: Create the Project [Section titled “Step 1: Create the Project”](#step-1-create-the-project) 1. Create a new Cloudwerk app: ```bash pnpm dlx @cloudwerk/create-app gallery --renderer hono-jsx cd gallery ``` 2. Install the images package: ```bash pnpm add @cloudwerk/images ``` ## Step 2: Configure Bindings [Section titled “Step 2: Configure Bindings”](#step-2-configure-bindings) Update `wrangler.toml` with R2 and IMAGES bindings: wrangler.toml ```toml name = "gallery" main = "dist/index.js" compatibility_date = "2024-09-23" compatibility_flags = ["nodejs_compat"] [assets] directory = "./dist/static" binding = "ASSETS" # R2 bucket for storing images [[r2_buckets]] binding = "GALLERY_BUCKET" bucket_name = "gallery-images" # IMAGES binding for on-the-fly transforms [images] binding = "IMAGES" [vars] CF_ACCOUNT_ID = "your-account-id" CF_ACCOUNT_HASH = "your-account-hash" # CF_IMAGES_TOKEN should be set as a secret, not here ``` Caution Set `CF_IMAGES_TOKEN` as a secret using `wrangler secret put CF_IMAGES_TOKEN` rather than in `wrangler.toml`. Generate TypeScript types for your bindings: ```bash npm run bindings generate-types ``` ## Step 3: Define Image Variants [Section titled “Step 3: Define Image Variants”](#step-3-define-image-variants) Create an image definition with variants for thumbnails and display: app/images/gallery.ts ```typescript import { defineImage } from '@cloudwerk/images' export default defineImage({ variants: { thumbnail: { width: 128, height: 128, fit: 'cover' }, display: { width: 1280, height: 720, fit: 'contain' }, }, }) ``` Tip Variants define how images are transformed when served. The `thumbnail` variant creates 128x128 square crops, while `display` fits images within 1280x720 while preserving aspect ratio. ## Step 4: Create Shared Components [Section titled “Step 4: Create Shared Components”](#step-4-create-shared-components) ### Image Grid Component [Section titled “Image Grid Component”](#image-grid-component) Create a client component that displays images in a grid and handles click events: app/components/image-grid.tsx ```tsx 'use client' import { useState } from 'hono/jsx' import ImageDialog from './image-dialog' interface ImageItem { id: string thumbnailUrl: string displayUrl: string filename?: string } interface ImageGridProps { images: ImageItem[] } export default function ImageGrid({ images = [] }: ImageGridProps) { const [selectedImage, setSelectedImage] = useState(null) if (images.length === 0) { return (
No images yet. Upload one above!
) } return ( <>
{images.map((image) => ( ))}
setSelectedImage(null)} /> ) } ``` ### Image Dialog Component [Section titled “Image Dialog Component”](#image-dialog-component) Create a modal component for full-size image preview: app/components/image-dialog.tsx ```tsx 'use client' import { useState, useEffect } from 'hono/jsx' interface ImageDialogProps { src: string alt: string isOpen: boolean onClose: () => void } export default function ImageDialog({ src, alt, isOpen, onClose }: ImageDialogProps) { useEffect(() => { function handleKeyDown(e: KeyboardEvent) { if (e.key === 'Escape') { onClose() } } if (isOpen) { document.addEventListener('keydown', handleKeyDown) document.body.style.overflow = 'hidden' } return () => { document.removeEventListener('keydown', handleKeyDown) document.body.style.overflow = '' } }, [isOpen, onClose]) if (!isOpen) return null return (
{alt} e.stopPropagation()} />
) } ``` ## Step 5: Hosted Images Demo [Section titled “Step 5: Hosted Images Demo”](#step-5-hosted-images-demo) The hosted images demo uploads images to Cloudflare Images and serves them via imagedelivery.net URLs. ### Hosted Images Page [Section titled “Hosted Images Page”](#hosted-images-page) app/hosted/page.tsx ```tsx import type { PageProps, LoaderArgs } from '@cloudwerk/core' import { createImageClient } from '@cloudwerk/images' import ImageGrid from '../components/image-grid' interface ImageItem { id: string thumbnailUrl: string displayUrl: string filename?: string } interface LoaderData { images: ImageItem[] error?: string } function buildImageUrl(accountHash: string, imageId: string, variant: string): string { return `https://imagedelivery.net/${accountHash}/${imageId}/${variant}` } export async function loader({ context }: LoaderArgs): Promise { const env = context.env as Record const accountId = env.CF_ACCOUNT_ID const accountHash = env.CF_ACCOUNT_HASH const token = env.CF_IMAGES_TOKEN if (!accountId || !token || !accountHash) { return { images: [], error: 'Missing CF_ACCOUNT_ID, CF_ACCOUNT_HASH, or CF_IMAGES_TOKEN.', } } const client = createImageClient(accountId, token) try { const result = await client.list({ perPage: 50 }) return { images: result.map((img) => ({ id: img.id, thumbnailUrl: buildImageUrl(accountHash, img.id, 'thumbnail'), displayUrl: buildImageUrl(accountHash, img.id, 'display'), filename: img.filename, })), } } catch (err) { return { images: [], error: err instanceof Error ? err.message : 'Failed to load images', } } } export default function HostedPage(props: PageProps & LoaderData) { const images = Array.isArray(props.images) ? props.images : [] return (

Hosted Images

Upload images to Cloudflare Images with pre-defined variants.

{props.error && (

{props.error}

)}
) } ``` ### Hosted Images API Route [Section titled “Hosted Images API Route”](#hosted-images-api-route) app/hosted/route.ts ```typescript import type { CloudwerkHandlerContext } from '@cloudwerk/core' import { json } from '@cloudwerk/core' import { getBinding } from '@cloudwerk/core/bindings' import { createImageClient } from '@cloudwerk/images' export async function POST(request: Request, _context: CloudwerkHandlerContext) { const accountId = getBinding('CF_ACCOUNT_ID') const token = getBinding('CF_IMAGES_TOKEN') if (!accountId || !token) { return json({ error: 'Missing configuration' }, 500) } const client = createImageClient(accountId, token) const formData = await request.formData() const file = formData.get('image') as File | null if (!file) { return json({ error: 'No image provided' }, 400) } await client.upload(file) return new Response(null, { status: 303, headers: { Location: '/hosted' }, }) } ``` Note Hosted Images requires creating variants in the Cloudflare dashboard. Create `thumbnail` and `display` variants matching your `defineImage` configuration. ## Step 6: R2 + IMAGES Binding Demo [Section titled “Step 6: R2 + IMAGES Binding Demo”](#step-6-r2--images-binding-demo) The R2 demo stores images in R2 and uses the IMAGES binding for on-the-fly transformations. ### R2 Page [Section titled “R2 Page”](#r2-page) app/r2/page.tsx ```tsx import type { PageProps } from '@cloudwerk/core' import { GALLERY_BUCKET } from '@cloudwerk/core/bindings' import ImageGrid from '../components/image-grid' interface LoaderData { images: Array<{ id: string thumbnailUrl: string displayUrl: string filename?: string }> error?: string } export async function loader(): Promise { if (!GALLERY_BUCKET) { return { images: [], error: 'GALLERY_BUCKET binding not configured.', } } const result = await GALLERY_BUCKET.list() const imageExtensions = ['.webp', '.jpg', '.jpeg', '.png', '.gif'] const images = result.objects .filter((obj) => imageExtensions.some((ext) => obj.key.endsWith(ext))) .map((obj) => ({ id: obj.key, thumbnailUrl: `/r2/${encodeURIComponent(obj.key)}?type=thumbnail`, displayUrl: `/r2/${encodeURIComponent(obj.key)}?type=display`, filename: obj.customMetadata?.originalName, })) return { images } } export default function R2Page(props: PageProps & LoaderData) { const images = Array.isArray(props.images) ? props.images : [] return (

R2 + IMAGES Binding

Upload images to R2 and serve with on-the-fly transformations.

How it works:

  1. Upload: Image is converted to WebP and stored in R2
  2. Thumbnail: Resized to 128x128 on-the-fly via IMAGES binding
  3. Display: Resized to 1280x720 on-the-fly via IMAGES binding
) } ``` ### R2 Upload Route [Section titled “R2 Upload Route”](#r2-upload-route) app/r2/route.ts ```typescript import type { CloudwerkHandlerContext } from '@cloudwerk/core' import { json } from '@cloudwerk/core' import { getBinding } from '@cloudwerk/core/bindings' import type { CloudflareImagesBinding } from '@cloudwerk/images' interface R2Bucket { put(key: string, value: ArrayBuffer, options?: { httpMetadata?: { contentType?: string } customMetadata?: Record }): Promise } export async function POST(request: Request, _context: CloudwerkHandlerContext) { const IMAGES = getBinding('IMAGES') const BUCKET = getBinding('GALLERY_BUCKET') const formData = await request.formData() const file = formData.get('image') as File | null if (!file) { return json({ error: 'No image provided' }, 400) } const id = crypto.randomUUID() const fileBuffer = await file.arrayBuffer() let imageData: ArrayBuffer let contentType: string let extension: string // Convert to WebP using IMAGES binding if available if (IMAGES && typeof IMAGES.input === 'function') { try { const transformed = await IMAGES .input(fileBuffer) .output({ format: 'image/webp' }) imageData = await transformed.response().arrayBuffer() contentType = 'image/webp' extension = 'webp' } catch { // Fallback to original format imageData = fileBuffer contentType = file.type || 'image/jpeg' extension = file.name.split('.').pop() || 'jpg' } } else { imageData = fileBuffer contentType = file.type || 'image/jpeg' extension = file.name.split('.').pop() || 'jpg' } const key = `${id}.${extension}` await BUCKET.put(key, imageData, { httpMetadata: { contentType }, customMetadata: { originalName: file.name }, }) return new Response(null, { status: 303, headers: { Location: '/r2' }, }) } ``` ### R2 Transform Route [Section titled “R2 Transform Route”](#r2-transform-route) app/r2/\[id]/route.ts ```typescript import type { CloudwerkHandlerContext } from '@cloudwerk/core' import { getBinding } from '@cloudwerk/core/bindings' import type { CloudflareImagesBinding } from '@cloudwerk/images' interface R2Bucket { get(key: string): Promise<{ body: ReadableStream httpMetadata?: { contentType?: string } } | null> } export async function GET( request: Request, { params }: CloudwerkHandlerContext<{ id: string }> ) { const url = new URL(request.url) const type = url.searchParams.get('type') || 'display' const key = decodeURIComponent(params.id) const BUCKET = getBinding('GALLERY_BUCKET') const object = await BUCKET.get(key) if (!object) { return new Response('Not found', { status: 404 }) } // Transform with IMAGES binding if available try { const IMAGES = getBinding('IMAGES') if (IMAGES && typeof IMAGES.input === 'function') { const transform = type === 'thumbnail' ? { width: 128, height: 128, fit: 'cover' as const } : { width: 1280, height: 720, fit: 'contain' as const } const transformed = await IMAGES .input(object.body) .transform(transform) .output({ format: 'image/webp' }) return transformed.response() } } catch { // Fall through to serve original } // Fallback: serve original image return new Response(object.body, { headers: { 'Content-Type': object.httpMetadata?.contentType || 'image/jpeg', 'Cache-Control': 'public, max-age=3600', }, }) } ``` On-the-fly vs Hosted **Hosted Images** stores multiple variant copies and is best for fixed sizes. **R2 + IMAGES** stores one copy and transforms on request, offering more flexibility but using compute on each request. ## Step 7: Run the Development Server [Section titled “Step 7: Run the Development Server”](#step-7-run-the-development-server) Start the development server: ```bash pnpm dev ``` Visit `http://localhost:8787` to see your gallery! ## Testing the Application [Section titled “Testing the Application”](#testing-the-application) 1. **Test R2 uploads**: Navigate to `/r2` and upload an image 2. **View thumbnails**: See the thumbnail grid populate 3. **Preview images**: Click a thumbnail to open the full-size dialog 4. **Test Hosted Images**: Configure credentials and navigate to `/hosted` 5. **Compare approaches**: Note that R2 transforms on-the-fly while Hosted serves pre-generated variants ## Step 8: Build and Deploy [Section titled “Step 8: Build and Deploy”](#step-8-build-and-deploy) 1. Create the R2 bucket: ```bash wrangler r2 bucket create gallery-images ``` 2. Build the application: ```bash pnpm build ``` 3. Deploy to Cloudflare: ```bash pnpm deploy ``` ## Summary [Section titled “Summary”](#summary) You’ve built an image gallery demonstrating two Cloudflare image approaches: * **Hosted Images**: Upload to Cloudflare’s image CDN with pre-defined variants * **R2 + IMAGES Binding**: Store in R2 with on-the-fly transformations ### Key Differences [Section titled “Key Differences”](#key-differences) | Feature | Hosted Images | R2 + IMAGES | | ----------- | --------------------------------- | -------------------- | | Storage | Cloudflare Images CDN | R2 bucket | | Variants | Pre-defined, created in dashboard | On-the-fly, any size | | Cost | Per-image stored + delivery | R2 storage + compute | | Flexibility | Fixed variants | Dynamic transforms | ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Images Guide](/guides/images/)** - Learn more about image handling * **[R2 Storage](/guides/storage/)** - Deep dive into R2 patterns * **[Deployment](/guides/deployment/)** - Production deployment options # Linkly - Link Shortener > Build a link shortener with D1 database, KV caching, and rate limiting. In this tutorial, you’ll build a complete link shortening service with: * **D1 Database** for persistent link storage * **KV Cache** for fast redirects * **Rate Limiting** middleware to prevent abuse * **Analytics** tracking (click counts) [View Source on GitHub ](https://github.com/squirrelsoft-dev/cloudwerk/tree/main/examples/linkly)See the complete source code for this example [Try the Live Demo ](https://linkly.cloudwerk.dev)See Linkly in action - shorten URLs and view click statistics ## Project Overview [Section titled “Project Overview”](#project-overview) The final project structure: * app/ * layout.tsx * page.tsx * globals.css * \[code]/ * route.ts # GET /:code - redirect handler * stats/ * \[code]/ * page.tsx # Link statistics page * api/ * middleware.ts # Rate limiting * shorten/ * route.ts # POST /api/shorten * lib/ * db.ts # D1 database helpers * cache.ts # KV cache helpers * components/ * shorten-form.tsx # Client component for URL input * migrations/ * 0001\_create\_links.sql * cloudwerk.config.ts * wrangler.toml * package.json ## Step 1: Create the Project [Section titled “Step 1: Create the Project”](#step-1-create-the-project) 1. Create a new Cloudwerk app: ```bash pnpm dlx @cloudwerk/create-app linkly --renderer hono-jsx cd linkly ``` 2. Install dependencies: ```bash pnpm install ``` ## Step 2: Set Up D1 Database [Section titled “Step 2: Set Up D1 Database”](#step-2-set-up-d1-database) 1. Add a D1 database binding: ```bash npm run bindings add d1 ``` When prompted: * **Binding name**: `DB` * **Database name**: `linkly-db` * **Add to wrangler.toml?**: Yes 2. Generate TypeScript types for your bindings: ```bash npm run bindings generate-types ``` This creates type definitions so you can import `DB` from `@cloudwerk/core/bindings`. 3. Create a migration file: ```bash wrangler d1 migrations create linkly-db create_links ``` This creates `migrations/0001_create_links.sql`. ### Database Schema [Section titled “Database Schema”](#database-schema) Open the migration file and add the links table: migrations/0001\_create\_links.sql ```sql CREATE TABLE links ( id TEXT PRIMARY KEY, url TEXT NOT NULL, code TEXT UNIQUE NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP, clicks INTEGER DEFAULT 0 ); CREATE INDEX idx_links_code ON links(code); ``` Apply the migration locally: ```bash wrangler d1 migrations apply linkly-db --local ``` ## Step 3: Set Up KV Cache [Section titled “Step 3: Set Up KV Cache”](#step-3-set-up-kv-cache) 1. Add a KV namespace for caching: ```bash npm run bindings add kv ``` When prompted: * **Binding name**: `LINKLY_CACHE` * **Namespace name**: `linkly-cache` * **Add to wrangler.toml?**: Yes 2. Regenerate TypeScript types: ```bash npm run bindings generate-types ``` This adds `LINKLY_CACHE` to your bindings types. The KV cache will store URLs for fast redirect lookups, avoiding database queries for popular links. ## Step 4: Create Database Helpers [Section titled “Step 4: Create Database Helpers”](#step-4-create-database-helpers) Create a module to interact with the D1 database: app/lib/db.ts ```typescript import { DB } from '@cloudwerk/core/bindings' export interface Link { id: string url: string code: string created_at: string clicks: number } /** * Generate a random 6-character alphanumeric code */ export function generateCode(): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' let code = '' for (let i = 0; i < 6; i++) { code += chars.charAt(Math.floor(Math.random() * chars.length)) } return code } /** * Create a new shortened link */ export async function createLink(url: string, code: string): Promise { const id = crypto.randomUUID() await DB.prepare('INSERT INTO links (id, url, code) VALUES (?, ?, ?)') .bind(id, url, code) .run() return { id, url, code, created_at: new Date().toISOString(), clicks: 0, } } /** * Get a link by its short code */ export async function getLinkByCode(code: string): Promise { const result = await DB.prepare('SELECT * FROM links WHERE code = ?') .bind(code) .first() return result } /** * Increment the click count for a link */ export async function incrementClicks(code: string): Promise { await DB.prepare('UPDATE links SET clicks = clicks + 1 WHERE code = ?') .bind(code) .run() } /** * Check if a code already exists */ export async function codeExists(code: string): Promise { const link = await getLinkByCode(code) return link !== null } /** * Generate a unique code that doesn't exist in the database */ export async function generateUniqueCode(): Promise { let code = generateCode() let attempts = 0 const maxAttempts = 10 while (await codeExists(code) && attempts < maxAttempts) { code = generateCode() attempts++ } if (attempts >= maxAttempts) { throw new Error('Failed to generate unique code') } return code } ``` Tip Import `DB` directly from `@cloudwerk/core/bindings` to access your D1 database. Run `npm run bindings generate-types` to generate TypeScript types for your bindings. ## Step 5: Create Cache Helpers [Section titled “Step 5: Create Cache Helpers”](#step-5-create-cache-helpers) Create a module for KV cache operations: app/lib/cache.ts ```typescript import { LINKLY_CACHE } from '@cloudwerk/core/bindings' /** Cache TTL in seconds (1 hour) */ const CACHE_TTL = 3600 /** * Get a cached URL by its short code */ export async function getCachedUrl(code: string): Promise { return LINKLY_CACHE.get(`url:${code}`) } /** * Cache a URL by its short code */ export async function cacheUrl(code: string, url: string): Promise { await LINKLY_CACHE.put(`url:${code}`, url, { expirationTtl: CACHE_TTL, }) } ``` Tip The cache uses a 1-hour TTL. Popular links will stay cached, while infrequently accessed links will expire and be re-fetched from the database on the next access. ## Step 6: Create Rate Limiting Middleware [Section titled “Step 6: Create Rate Limiting Middleware”](#step-6-create-rate-limiting-middleware) Create middleware to prevent API abuse: app/api/middleware.ts ```typescript import type { Middleware } from '@cloudwerk/core' import { LINKLY_CACHE } from '@cloudwerk/core/bindings' import { createRateLimiter, createFixedWindowStorage, } from '@cloudwerk/core/middleware' // Rate limit: 10 requests per minute per IP const RATE_LIMIT = 10 const RATE_WINDOW = 60 // seconds export const middleware: Middleware = async (request, next) => { // Create rate limiter with KV storage const storage = createFixedWindowStorage(LINKLY_CACHE, 'ratelimit:api:') const rateLimiter = createRateLimiter({ limit: RATE_LIMIT, window: RATE_WINDOW, storage, }) // Check rate limit const { response, result } = await rateLimiter.check(request) // If rate limited, return 429 response if (response) { return response } // Continue to route handler const res = await next() // Add rate limit headers to response const headers = rateLimiter.headers(result) for (const [key, value] of Object.entries(headers)) { res.headers.set(key, String(value)) } return res } ``` Note The middleware uses the same KV namespace as the cache, with a different key prefix (`ratelimit:api:`). This is a common pattern to consolidate KV namespaces. ## Step 7: Create the Shorten API Route [Section titled “Step 7: Create the Shorten API Route”](#step-7-create-the-shorten-api-route) Create the endpoint to shorten URLs: app/api/shorten/route.ts ```typescript import type { CloudwerkHandler } from '@cloudwerk/core' import { json, badRequest } from '@cloudwerk/core' import { createLink, generateUniqueCode } from '../../lib/db' import { cacheUrl } from '../../lib/cache' interface ShortenRequest { url: string } interface ShortenResponse { code: string shortUrl: string url: string } /** * Validate that a string is a valid URL */ function isValidUrl(str: string): boolean { try { const url = new URL(str) return url.protocol === 'http:' || url.protocol === 'https:' } catch { return false } } export const POST: CloudwerkHandler = async (request, _context) => { // Parse request body let body: ShortenRequest try { body = await request.json() } catch { return badRequest('Invalid JSON body') } // Validate URL const { url } = body if (!url) { return badRequest('URL is required') } if (!isValidUrl(url)) { return badRequest('Invalid URL format. Must be a valid HTTP or HTTPS URL.') } // Generate unique short code const code = await generateUniqueCode() // Store in database await createLink(url, code) // Pre-cache in KV for fast redirects await cacheUrl(code, url) // Build response const origin = new URL(request.url).origin const response: ShortenResponse = { code, shortUrl: `${origin}/${code}`, url, } return json(response, 201) } ``` ## Step 8: Create the Redirect Route [Section titled “Step 8: Create the Redirect Route”](#step-8-create-the-redirect-route) Create the redirect handler for short links: app/\[code]/route.ts ```typescript import type { CloudwerkHandler, CloudwerkHandlerContext } from '@cloudwerk/core' import { redirect, notFoundResponse, getContext } from '@cloudwerk/core' import { getLinkByCode, incrementClicks } from '../lib/db' import { getCachedUrl, cacheUrl } from '../lib/cache' interface Params { code: string } export const GET: CloudwerkHandler = async ( _request, { params }: CloudwerkHandlerContext ) => { const { code } = params const { executionCtx } = getContext() // Try cache first (fast path) let url = await getCachedUrl(code) if (url) { // Track click in background executionCtx.waitUntil(incrementClicks(code)) return redirect(url, 302) } // Cache miss - check database const link = await getLinkByCode(code) if (!link) { return notFoundResponse('Link not found') } url = link.url // Cache for next time and track click in background executionCtx.waitUntil( Promise.all([ cacheUrl(code, url), incrementClicks(code), ]) ) return redirect(url, 302) } ``` Background Processing with waitUntil Using `waitUntil` allows the redirect response to be sent immediately while click tracking and caching happen in the background. This improves response latency significantly. ## Step 9: Update the Home Page [Section titled “Step 9: Update the Home Page”](#step-9-update-the-home-page) Replace the starter page with the link shortener UI: app/page.tsx ```tsx import ShortenForm from './components/shorten-form' export default function HomePage() { return (
{/* Logo/Brand */}

Linkly

{/* Tagline */}

Shorten your links, track your clicks

{/* Shorten Form */}
{/* Info */}

Built with Cloudwerk using D1 for storage and KV for caching.

Rate limited to 10 requests per minute.

) } ``` ## Step 10: Create the Shorten Form Component [Section titled “Step 10: Create the Shorten Form Component”](#step-10-create-the-shorten-form-component) Create a client component for the URL input form: app/components/shorten-form.tsx ```tsx 'use client' import { useState } from 'hono/jsx' interface ShortenResult { code: string shortUrl: string url: string } export default function ShortenForm() { const [url, setUrl] = useState('') const [result, setResult] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const [copied, setCopied] = useState(false) async function handleSubmit(e: Event) { e.preventDefault() setError(null) setResult(null) setLoading(true) try { const response = await fetch('/api/shorten', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }), }) const data = await response.json() if (!response.ok) { setError(data.error || 'Failed to shorten URL') return } setResult(data) setUrl('') } catch { setError('Network error. Please try again.') } finally { setLoading(false) } } async function copyToClipboard() { if (!result) return await navigator.clipboard.writeText(result.shortUrl) setCopied(true) setTimeout(() => setCopied(false), 2000) } return (
{/* Form */}
setUrl((e.target as HTMLInputElement).value)} placeholder="https://example.com/long-url" required class="flex-1 px-4 py-3 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all" />
{/* Error */} {error && (
{error}
)} {/* Result */} {result && (

Your shortened link:

{result.shortUrl}

Original: {result.url}

)}
) } ``` ## Step 11: Update the Layout [Section titled “Step 11: Update the Layout”](#step-11-update-the-layout) Update the layout with Linkly branding: app/layout.tsx ```tsx import type { LayoutProps } from '@cloudwerk/core' import globals from './globals.css?url' export default function RootLayout({ children }: LayoutProps) { return ( Linkly - Link Shortener {children} ) } ``` ## Step 12: Create the Stats Page [Section titled “Step 12: Create the Stats Page”](#step-12-create-the-stats-page) Create a page to display link statistics: app/stats/\[code]/page.tsx ```tsx import type { PageProps, LoaderArgs } from '@cloudwerk/core' import { NotFoundError } from '@cloudwerk/core' import { getLinkByCode, type Link } from '../../lib/db' interface StatsLoaderData { link: Link shortUrl: string } export async function loader({ params, request }: LoaderArgs<{ code: string }>): Promise { const link = await getLinkByCode(params.code) if (!link) throw new NotFoundError() const origin = new URL(request.url).origin return { link, shortUrl: `${origin}/${link.code}`, } } function formatDate(dateString: string): string { const date = new Date(dateString) return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', }) } export default function StatsPage({ link, shortUrl }: PageProps & StatsLoaderData) { return (
{/* Header */}

Link Statistics

{/* Stats Card */}
{/* Click Count */}
{link.clicks.toLocaleString()}
{link.clicks === 1 ? 'click' : 'clicks'}
{/* Short URL with copy button */}
{shortUrl}
{/* Original URL */}
{link.url}
{/* Created Date */}
{formatDate(link.created_at)}
{/* Back Link */} ← Create another link
) } ``` The stats page uses a **loader function** to fetch link data server-side before rendering. If the link doesn’t exist, it throws `NotFoundError` which returns a 404 response. Tip The stats page is accessible at `/stats/{code}`. After shortening a URL, click “View Stats” to see the click count, creation date, and both URLs. ## Step 13: Run the Development Server [Section titled “Step 13: Run the Development Server”](#step-13-run-the-development-server) Start the development server: ```bash pnpm dev ``` Visit `http://localhost:8787` to see your link shortener! ## Testing the Application [Section titled “Testing the Application”](#testing-the-application) 1. **Shorten a URL**: Enter a long URL and click “Shorten” 2. **Copy the link**: Click “Copy” to copy the shortened URL 3. **View statistics**: Click “View Stats” to see the stats page 4. **Test redirect**: Open the short link in a new tab 5. **Verify clicks**: Refresh the stats page to see the click count increase 6. **Test rate limiting**: Submit more than 10 requests per minute 7. **Test 404**: Visit `/stats/invalid` to see 404 handling ## Step 14: Build and Deploy [Section titled “Step 14: Build and Deploy”](#step-14-build-and-deploy) 1. Build the application: ```bash pnpm build ``` 2. Apply migrations to production: ```bash wrangler d1 migrations apply linkly-db --remote ``` 3. Deploy to Cloudflare: ```bash pnpm deploy ``` ## Summary [Section titled “Summary”](#summary) You’ve built a complete link shortener with: * **D1 Database**: Persistent storage for links and click counts * **KV Cache**: Fast redirect lookups with automatic TTL * **Rate Limiting**: Protection against API abuse using `@cloudwerk/core/middleware` * **Click Analytics**: Background tracking with `waitUntil` * **Statistics Page**: Server-side rendered stats with loader functions * **Client Components**: Interactive form with real-time feedback ## Architecture Highlights [Section titled “Architecture Highlights”](#architecture-highlights) ### Caching Strategy [Section titled “Caching Strategy”](#caching-strategy) The redirect route implements a read-through cache pattern: 1. Check KV cache first (sub-millisecond latency) 2. On cache miss, query D1 database 3. Cache the result for future requests ### Background Processing [Section titled “Background Processing”](#background-processing) Using `executionCtx.waitUntil()` allows: * Immediate response to the user * Click tracking in the background * Cache population without blocking ### Rate Limiting [Section titled “Rate Limiting”](#rate-limiting) The API middleware protects against abuse: * 10 requests per minute per IP * Uses the same KV namespace with a different prefix * Returns standard rate limit headers ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Database Guide](/guides/database/)** - Learn more about D1 patterns * **[Middleware](/guides/middleware/)** - Advanced middleware patterns * **[Authentication](/guides/authentication/)** - Add user authentication * **[Deployment](/guides/deployment/)** - Production deployment options # Getting Started > Learn the basics of Cloudwerk and get up and running quickly. Start building full-stack applications on Cloudflare Workers with Cloudwerk. ## Quick Links [Section titled “Quick Links”](#quick-links) * [Installation](/getting-started/installation/) - Install Cloudwerk and create your first project * [Quick Start](/getting-started/quick-start/) - Build your first app in minutes * [Project Structure](/getting-started/project-structure/) - Understand the file conventions ## What is Cloudwerk? [Section titled “What is Cloudwerk?”](#what-is-cloudwerk) Cloudwerk is a full-stack framework for Cloudflare Workers. It provides: * **File-based routing** that compiles to Hono * **Server-side rendering** with Hono JSX or React * **Data loading** with loader functions * **Integrated auth** with sessions, OAuth, and passkeys * **D1, KV, R2, Queues** - all Cloudflare primitives, simplified ## Prerequisites [Section titled “Prerequisites”](#prerequisites) Before you begin, ensure you have: * Node.js 20+ * A package manager (pnpm, npm, or yarn) * A Cloudflare account ([sign up free](https://dash.cloudflare.com/sign-up)) # Installation > Set up a new Cloudwerk project from scratch. Get started with Cloudwerk by creating a new project or adding it to an existing Cloudflare Workers project. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) Before you begin, ensure you have: * **Node.js 20+** - Cloudwerk requires Node.js 20 or later * **pnpm, npm, or yarn** - Any package manager works * **Cloudflare account** - [Sign up for free](https://dash.cloudflare.com/sign-up) * **Wrangler CLI** - Cloudflare’s CLI tool (installed automatically) ## Create a New Project [Section titled “Create a New Project”](#create-a-new-project) * npm ```bash npx @cloudwerk/create-app@latest my-app cd my-app npm install npm run dev ``` * pnpm ```bash pnpm dlx @cloudwerk/create-app my-app cd my-app pnpm install pnpm dev ``` The CLI will prompt you to select a UI renderer: ```plaintext ? Select UI renderer: ○ Hono JSX (recommended) - Lightweight (~3kb), Workers-optimized ○ React - Full ecosystem, larger bundle (~45kb) ○ None (API only) - No UI rendering, pure API routes ``` You can also skip the prompt by specifying the renderer directly: ```bash npx @cloudwerk/create-app@latest my-app --renderer hono-jsx ``` ## Manual Installation [Section titled “Manual Installation”](#manual-installation) If you prefer to set up Cloudwerk manually or add it to an existing project: 1. Create a new directory and initialize your project: ```bash mkdir my-app && cd my-app pnpm init ``` 2. Install Cloudwerk packages and Hono: ```bash pnpm add @cloudwerk/core @cloudwerk/cli @cloudwerk/ui hono ``` 3. Install development dependencies: ```bash pnpm add -D wrangler typescript @types/node ``` 4. Create a `cloudwerk.config.ts` file: ```typescript // cloudwerk.config.ts import { defineConfig } from '@cloudwerk/core'; export default defineConfig({ // Your configuration options }); ``` 5. Create a `tsconfig.json` file: ```json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "jsx": "react-jsx", "jsxImportSource": "hono/jsx", "types": ["@cloudflare/workers-types"] }, "include": ["app/**/*", "cloudwerk.config.ts"], "exclude": ["node_modules"] } ``` 6. Create your app directory with a home page: ```bash mkdir -p app ``` ```tsx // app/page.tsx export default function HomePage() { return (

Welcome to Cloudwerk

Your full-stack framework for Cloudflare Workers.

); } ``` 7. Add scripts to `package.json`: ```json { "scripts": { "dev": "cloudwerk dev", "build": "cloudwerk build", "deploy": "cloudwerk deploy" } } ``` ## Project Structure [Section titled “Project Structure”](#project-structure) After installation, your project will look like this: ```plaintext my-app/ ├── app/ │ ├── page.tsx # Home page (/) │ ├── layout.tsx # Root layout │ ├── middleware.ts # Global middleware │ └── api/ │ └── health/ │ └── route.ts # API endpoint (/api/health) ├── public/ # Static assets ├── cloudwerk.config.ts # Configuration ├── wrangler.toml # Cloudflare config ├── tsconfig.json └── package.json ``` ## Verify Installation [Section titled “Verify Installation”](#verify-installation) Start the development server: ```bash pnpm dev ``` Open in your browser. You should see your home page. ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Quick Start](/getting-started/quick-start/)** - Build your first pages and API routes * **[Project Structure](/getting-started/project-structure/)** - Learn the file conventions * **[Routing Guide](/guides/routing/)** - Deep dive into file-based routing # Project Structure > Learn the file conventions and directory structure of a Cloudwerk project. Cloudwerk uses a file-based routing system where your directory structure defines your application’s routes. This guide covers all the file conventions you need to know. ## Overview [Section titled “Overview”](#overview) A typical Cloudwerk project has this structure: * app/ * page.tsx # Home page (/) * layout.tsx # Root layout * middleware.ts # Global middleware * error.tsx # Error boundary * not-found.tsx # 404 page * about/ * page.tsx # About page (/about) * users/ * page.tsx # Users list (/users) * \[id]/ * page.tsx # User profile (/users/:id) * edit/ * page.tsx # Edit user (/users/:id/edit) * api/ * health/ * route.ts # Health endpoint (/api/health) * users/ * route.ts # Users API (/api/users) * \[id]/ * route.ts # User API (/api/users/:id) * auth/ # Authentication config * config.ts # Auth configuration * callbacks.ts # Auth callbacks * rbac.ts # Role definitions * providers/ * github.ts # OAuth provider * credentials.ts # Email/password * queues/ # Queue consumers * email.ts # Email queue * notifications.ts # Notifications queue * services/ # Service definitions * email/ * service.ts # Email service * payments/ * service.ts # Payments service * objects/ # Durable Objects * counter.ts # Counter DO * chat-room.ts # Chat room DO * triggers/ # Event triggers * daily-cleanup.ts # Scheduled trigger * stripe-webhook.ts # Webhook trigger * public/ # Static assets * … * cloudwerk.config.ts # Configuration * wrangler.toml # Cloudflare config * package.json * tsconfig.json ## File Conventions [Section titled “File Conventions”](#file-conventions) ### page.tsx [Section titled “page.tsx”](#pagetsx) The `page.tsx` file defines a publicly accessible page at that route: ```tsx // app/about/page.tsx -> /about export default function AboutPage() { return

About Us

; } ``` Pages can export a `loader()` function for server-side data fetching: ```tsx export async function loader({ params, context }: LoaderArgs) { return { data: await fetchData() }; } export default function Page({ data }: PageProps & { data: Data }) { return
{/* render data */}
; } ``` ### layout.tsx [Section titled “layout.tsx”](#layouttsx) The `layout.tsx` file wraps pages and nested layouts: ```tsx // app/layout.tsx export default function RootLayout({ children }: LayoutProps) { return ( {children} ); } ``` Layouts can also have their own `loader()` functions: ```tsx import { getUser, getSession } from '@cloudwerk/auth' export async function loader() { const user = getUser() const session = getSession() return { user, session } } export default function DashboardLayout({ children, user }: LayoutProps & { user: User | null }) { return (
{children}
); } ``` ### route.ts [Section titled “route.ts”](#routets) The `route.ts` file defines API endpoints that handle HTTP methods: ```typescript // app/api/users/route.ts import { DB } from '@cloudwerk/core/bindings' import { json } from '@cloudwerk/core' export async function GET() { const { results: users } = await DB.prepare('SELECT * FROM users').all() return json(users) } export async function POST(request: Request) { const body = await request.json() const id = crypto.randomUUID() await DB .prepare('INSERT INTO users (id, name, email) VALUES (?, ?, ?)') .bind(id, body.name, body.email) .run() return json({ id, ...body }, { status: 201 }) } ``` Import bindings directly from `@cloudwerk/core/bindings` for clean, ergonomic access. Run `cloudwerk bindings generate-types` to enable TypeScript autocomplete. Supported methods: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS` ### middleware.ts [Section titled “middleware.ts”](#middlewarets) The `middleware.ts` file runs before route handlers: ```typescript // app/middleware.ts import type { Middleware } from '@cloudwerk/core'; export const middleware: Middleware = async (request, next) => { // Run before the route handler console.log(`${request.method} ${request.url}`); const response = await next(); // Run after the route handler return response; }; ``` Middleware is inherited by child routes. Place middleware at any level: * app/ * middleware.ts # Runs for all routes * admin/ * middleware.ts # Runs for /admin/\* routes * page.tsx ### error.tsx [Section titled “error.tsx”](#errortsx) The `error.tsx` file handles errors in that route segment: ```tsx // app/error.tsx import type { ErrorBoundaryProps } from '@cloudwerk/core'; export default function ErrorPage({ error, errorType }: ErrorBoundaryProps) { return (

Something went wrong

{error.message}

{error.digest &&

Error ID: {error.digest}

} Go home
); } ``` ### not-found.tsx [Section titled “not-found.tsx”](#not-foundtsx) The `not-found.tsx` file handles 404 errors: ```tsx // app/not-found.tsx export default function NotFoundPage() { return (

404 - Page Not Found

Go home
); } ``` *** ## Convention Directories [Section titled “Convention Directories”](#convention-directories) Cloudwerk uses convention directories for authentication, queues, services, durable objects, and triggers. These are automatically discovered and registered. ### app/auth/ - Authentication [Section titled “app/auth/ - Authentication”](#appauth---authentication) Authentication configuration files: * app/auth/ * config.ts # Main auth configuration * callbacks.ts # Lifecycle callbacks * pages.ts # Custom page paths * rbac.ts # Role and permission definitions * providers/ * github.ts # OAuth provider * google.ts # OIDC provider * credentials.ts # Email/password * email.ts # Magic link ```typescript // app/auth/config.ts import { defineAuthConfig } from '@cloudwerk/auth/convention' export default defineAuthConfig({ basePath: '/auth', session: { strategy: 'database', maxAge: 30 * 24 * 60 * 60, }, }) // 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!, }) ) ``` Tip See the [Authentication Guide](/guides/authentication/) for complete documentation. ### app/queues/ - Queue Consumers [Section titled “app/queues/ - Queue Consumers”](#appqueues---queue-consumers) Queue consumers are defined as individual files: * app/queues/ * email.ts # email queue * image-processing.ts # imageProcessing queue * notifications.ts # notifications queue File names are converted to camelCase queue names. ```typescript // app/queues/email.ts import { defineQueue } from '@cloudwerk/queue' interface EmailMessage { to: string subject: string body: string } export default defineQueue({ async process(message) { await sendEmail(message.body) message.ack() }, }) ``` Tip See the [Queues Guide](/guides/queues/) for complete documentation. ### app/services/ - Service Definitions [Section titled “app/services/ - Service Definitions”](#appservices---service-definitions) Services are defined in named directories: * app/services/ * email/ * service.ts # Required: service definition * utils.ts # Optional: helper utilities * payments/ * service.ts * analytics/ * service.ts Directory names are converted to camelCase service names (`email/` → `email`, `user-management/` → `userManagement`). ```typescript // app/services/email/service.ts import { defineService } from '@cloudwerk/service' export default defineService({ methods: { async send({ to, subject, body }) { const response = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { 'Authorization': `Bearer ${this.env.RESEND_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ to, subject, html: body }), }) return response.json() }, }, }) ``` Tip See the [Services Guide](/guides/services/) for complete documentation. ### app/objects/ - Durable Objects [Section titled “app/objects/ - Durable Objects”](#appobjects---durable-objects) Durable Objects are defined as individual files: * app/objects/ * counter.ts # Counter DO * chat-room.ts # ChatRoom DO * game-session.ts # GameSession DO File names are converted to PascalCase class names. ```typescript // app/objects/counter.ts import { defineDurableObject } from '@cloudwerk/durable-object' interface CounterState { count: number } export default defineDurableObject({ init: async (ctx) => ({ count: 0 }), methods: { async increment(ctx, amount: number = 1) { ctx.setState({ count: ctx.state.count + amount }) await ctx.storage.put('state', ctx.state) return ctx.state.count }, async getCount(ctx) { return ctx.state.count }, }, }) ``` Tip See the [Durable Objects Guide](/guides/durable-objects/) for complete documentation. ### app/triggers/ - Event Triggers [Section titled “app/triggers/ - Event Triggers”](#apptriggers---event-triggers) Event triggers are defined as individual files: * app/triggers/ * daily-cleanup.ts # Scheduled trigger * stripe-webhook.ts # Webhook trigger * image-uploaded.ts # R2 trigger * support-email.ts # Email trigger ```typescript // app/triggers/daily-cleanup.ts import { defineTrigger } from '@cloudwerk/trigger' export default defineTrigger({ source: { type: 'scheduled', cron: '0 0 * * *', // Daily at midnight }, async handle(event, ctx) { await ctx.env.DB.exec('DELETE FROM sessions WHERE expires_at < datetime("now")') }, }) // app/triggers/stripe-webhook.ts import { defineTrigger } from '@cloudwerk/trigger' import { stripeVerifier } from '@cloudwerk/trigger/verifiers' export default defineTrigger({ source: { type: 'webhook', path: '/webhooks/stripe', verifier: stripeVerifier({ secret: process.env.STRIPE_WEBHOOK_SECRET!, }), }, async handle(event, ctx) { const stripeEvent = event.payload if (stripeEvent.type === 'checkout.session.completed') { await fulfillOrder(stripeEvent.data.object) } }, }) ``` Tip See the [Triggers Guide](/guides/triggers/) for complete documentation. *** ## Dynamic Routes [Section titled “Dynamic Routes”](#dynamic-routes) ### Single Segment [Section titled “Single Segment”](#single-segment) Use brackets for dynamic segments: ```plaintext app/users/[id]/page.tsx -> /users/:id ``` Access params in your loader or component: ```tsx export async function loader({ params }: LoaderArgs) { // params.id is the dynamic value return { user: await getUser(params.id) }; } ``` ### Catch-All Segments [Section titled “Catch-All Segments”](#catch-all-segments) Use `[...slug]` to catch all subsequent segments: ```plaintext app/docs/[...slug]/page.tsx -> /docs/* ``` ```tsx export async function loader({ params }: LoaderArgs) { // params.slug is an array like ['guides', 'routing'] const path = params.slug.join('/'); return { doc: await getDoc(path) }; } ``` ### Optional Catch-All [Section titled “Optional Catch-All”](#optional-catch-all) Use `[[...slug]]` for optional catch-all: ```plaintext app/shop/[[...categories]]/page.tsx -> /shop -> /shop/electronics -> /shop/electronics/phones ``` ## Route Groups [Section titled “Route Groups”](#route-groups) Use `(groupName)` to organize routes without affecting the URL: * app/ * (marketing)/ * page.tsx # / * about/ * page.tsx # /about * (dashboard)/ * layout.tsx # Dashboard layout * dashboard/ * page.tsx # /dashboard * settings/ * page.tsx # /settings ## Colocation [Section titled “Colocation”](#colocation) You can colocate components, utilities, and tests alongside your routes: * app/ * users/ * page.tsx * UserCard.tsx # Component (not a route) * user.utils.ts # Utility (not a route) * page.test.ts # Test (not a route) Only `page.tsx`, `layout.tsx`, `route.ts`, and `middleware.ts` are treated as routes. ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Routing Guide](/guides/routing/)** - Deep dive into routing patterns * **[Data Loading](/guides/data-loading/)** - Server-side data fetching * **[Authentication](/guides/authentication/)** - Add auth to your app * **[Services](/guides/services/)** - Build reusable services * **[Queues](/guides/queues/)** - Background job processing * **[Triggers](/guides/triggers/)** - Event-driven architecture # Quick Start > Build your first Cloudwerk page and API route in minutes. This guide walks you through building your first Cloudwerk application with pages, data loading, and API routes. ## Create Your First Page [Section titled “Create Your First Page”](#create-your-first-page) 1. Create a new page file at `app/page.tsx`: ```tsx // app/page.tsx export default function HomePage() { return (

Welcome to Cloudwerk

Your full-stack framework for Cloudflare Workers.

); } ``` 2. Start the dev server: ```bash pnpm dev ``` 3. Open to see your page. ## Add Data Loading [Section titled “Add Data Loading”](#add-data-loading) Cloudwerk uses `loader()` functions for server-side data fetching: ```tsx // app/page.tsx import type { PageProps } from '@cloudwerk/core' import { db } from '@cloudwerk/core/bindings' export async function loader() { const { results: posts } = await db .prepare('SELECT id, title, excerpt FROM posts ORDER BY created_at DESC LIMIT 10') .all() return { posts } } interface Props extends PageProps { posts: Array<{ id: string; title: string; excerpt: string }> } export default function HomePage({ posts }: Props) { return (

Latest Posts

) } ``` ## Create Dynamic Routes [Section titled “Create Dynamic Routes”](#create-dynamic-routes) Use brackets `[param]` for dynamic route segments: ```tsx // app/posts/[id]/page.tsx import type { PageProps } from '@cloudwerk/core' import { NotFoundError } from '@cloudwerk/core' import { params } from '@cloudwerk/core/context' import { db } from '@cloudwerk/core/bindings' export async function loader() { const post = await db .prepare('SELECT id, title, content FROM posts WHERE id = ?') .bind(params.id) .first() if (!post) { throw new NotFoundError('Post not found') } return { post } } interface Props extends PageProps { post: { id: string; title: string; content: string } } export default function PostPage({ post }: Props) { return (

{post.title}

{post.content}
) } ``` Tip For rendering HTML content, use a sanitization library like DOMPurify to prevent XSS attacks. ## Add a Layout [Section titled “Add a Layout”](#add-a-layout) Layouts wrap pages and persist across navigation: ```tsx // app/layout.tsx import type { LayoutProps } from '@cloudwerk/core'; export default function RootLayout({ children }: LayoutProps) { return ( My Cloudwerk App
{children}

Built with Cloudwerk

); } ``` ## Create an API Route [Section titled “Create an API Route”](#create-an-api-route) API routes handle HTTP requests without rendering UI: ```typescript // app/api/posts/route.ts import { DB } from '@cloudwerk/core/bindings' import { json } from '@cloudwerk/core' export async function GET() { const { results: posts } = await DB .prepare('SELECT * FROM posts ORDER BY created_at DESC') .all() return json(posts) } export async function POST(request: Request) { const body = await request.json() const id = crypto.randomUUID() await DB .prepare('INSERT INTO posts (id, title, content, created_at) VALUES (?, ?, ?, ?)') .bind(id, body.title, body.content, new Date().toISOString()) .run() return json({ id, title: body.title }, { status: 201 }) } ``` Tip Run `cloudwerk bindings generate-types` to enable TypeScript autocomplete for your bindings like `DB`, `KV`, etc. ## Add Middleware [Section titled “Add Middleware”](#add-middleware) Middleware runs before route handlers: ```typescript // app/middleware.ts import type { Middleware } from '@cloudwerk/core'; export const middleware: Middleware = async (request, next) => { const start = Date.now(); // Run the route handler const response = await next(); // Add timing header const duration = Date.now() - start; response.headers.set('X-Response-Time', `${duration}ms`); return response; }; ``` ## Deploy to Cloudflare [Section titled “Deploy to Cloudflare”](#deploy-to-cloudflare) Deploy your application globally: ```bash pnpm deploy ``` Your app is now live on Cloudflare Workers! ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Project Structure](/getting-started/project-structure/)** - Understand file conventions * **[Routing Guide](/guides/routing/)** - Learn advanced routing patterns * **[Data Loading](/guides/data-loading/)** - Master server-side data fetching * **[Database Guide](/guides/database/)** - Connect to Cloudflare D1 # Guides > In-depth guides to help you master Cloudwerk features. Learn how to build real applications with Cloudwerk’s features. ## Core Concepts [Section titled “Core Concepts”](#core-concepts) * [Routing](/guides/routing/) - File-based routing and layouts * [Data Loading](/guides/data-loading/) - Server-side data fetching * [Forms & Actions](/guides/forms-and-actions/) - Handle form submissions ## Data & Storage [Section titled “Data & Storage”](#data--storage) * [Database](/guides/database/) - Work with D1 SQLite * [Queues](/guides/queues/) - Background job processing * [Images](/guides/images/) - Image optimization and delivery ## Infrastructure [Section titled “Infrastructure”](#infrastructure) * [Authentication](/guides/authentication/) - Add auth to your app * [Security](/guides/security/) - CSRF, CSP, and security headers * [Durable Objects](/guides/durable-objects/) - Stateful edge compute * [Services](/guides/services/) - Service bindings and RPC * [Triggers](/guides/triggers/) - Cron and scheduled tasks ## Deployment [Section titled “Deployment”](#deployment) * [Deployment](/guides/deployment/) - Deploy to Cloudflare Workers # Authentication > Implement authentication with providers, sessions, RBAC, and multi-tenancy using @cloudwerk/auth. 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. ## Quick Start [Section titled “Quick Start”](#quick-start) 1. Install the auth package: ```bash pnpm add @cloudwerk/auth ``` 2. Create your auth configuration: ```typescript // 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: ```typescript // 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: ```tsx // 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

Welcome, {user.name}

} ``` ## Convention File Structure [Section titled “Convention File Structure”](#convention-file-structure) Auth configuration is defined using convention files in the `app/auth/` directory: * app/auth/ * config.ts # Main auth configuration * callbacks.ts # Lifecycle callbacks * pages.ts # Custom auth page paths * rbac.ts # Role and permission definitions * providers/ * github.ts # OAuth provider * google.ts # OIDC provider * credentials.ts # Email/password * email.ts # Magic link ## Providers [Section titled “Providers”](#providers) ### OAuth Providers [Section titled “OAuth Providers”](#oauth-providers) Cloudwerk includes pre-built OAuth providers for popular services: * GitHub ```typescript // 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 }) ) ``` * Google ```typescript // app/auth/providers/google.ts import { defineProvider, google } from '@cloudwerk/auth/convention' export default defineProvider( google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, scope: 'openid email profile', }) ) ``` * Discord ```typescript // app/auth/providers/discord.ts import { defineProvider, discord } from '@cloudwerk/auth/convention' export default defineProvider( discord({ clientId: process.env.DISCORD_CLIENT_ID!, clientSecret: process.env.DISCORD_CLIENT_SECRET!, }) ) ``` #### Custom OAuth Provider [Section titled “Custom OAuth Provider”](#custom-oauth-provider) Create a custom OAuth 2.0 provider: ```typescript // 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, } }, }) ) ``` ### Credentials Provider [Section titled “Credentials Provider”](#credentials-provider) Email/password authentication with customizable validation: ```typescript // 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: 'user@example.com', 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, } }, }) ) ``` ### Email / Magic Link Provider [Section titled “Email / Magic Link Provider”](#email--magic-link-provider) Passwordless authentication via email: ```typescript // app/auth/providers/email.ts import { defineProvider, email } from '@cloudwerk/auth' export default defineProvider( email({ from: 'noreply@myapp.com', 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, from: 'noreply@myapp.com', subject: 'Sign in to MyApp', html: `Click here to sign in`, }), }) }, }) ) ``` How Magic Links Work When a user clicks the magic link in their email, Cloudwerk’s auth system automatically: 1. Validates the token from the URL 2. Creates a session for the user 3. Redirects to your configured callback URL No additional route handler is needed—the `/auth/callback/email` route is handled automatically. ### WebAuthn / Passkey Provider [Section titled “WebAuthn / Passkey Provider”](#webauthn--passkey-provider) Modern passwordless authentication with passkeys: ```typescript // 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](/guides/authentication/passkey-setup/). ## Session Management [Section titled “Session Management”](#session-management) ### Session Strategies [Section titled “Session Strategies”](#session-strategies) Cloudwerk supports two session strategies: * Database (KV) Server-side sessions stored in Cloudflare KV: ```typescript // 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`: ```toml [[kv_namespaces]] binding = "AUTH_SESSIONS" id = "your-kv-namespace-id" ``` * JWT (Stateless) Stateless JWT sessions stored in cookies: ```typescript // app/auth/config.ts import { defineAuthConfig } from '@cloudwerk/auth/convention' export default defineAuthConfig({ session: { strategy: 'jwt', maxAge: 30 * 24 * 60 * 60, }, // Required for JWT signing secret: process.env.AUTH_SECRET, }) ``` ### Cookie Configuration [Section titled “Cookie Configuration”](#cookie-configuration) Customize session cookies: ```typescript export default defineAuthConfig({ cookies: { sessionToken: { name: '__Secure-session', options: { secure: true, httpOnly: true, sameSite: 'lax', path: '/', }, }, }, }) ``` ## Route Protection [Section titled “Route Protection”](#route-protection) ### Auth Middleware [Section titled “Auth Middleware”](#auth-middleware) Protect entire route segments with middleware: ```typescript // app/dashboard/middleware.ts import { authMiddleware } from '@cloudwerk/auth/middleware' export const middleware = authMiddleware({ unauthenticatedRedirect: '/login', }) ``` ### Context Helpers [Section titled “Context Helpers”](#context-helpers) Use context helpers in loaders and handlers: ```typescript 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 } } ``` ### API Route Protection [Section titled “API Route Protection”](#api-route-protection) For JSON APIs, return errors instead of redirects: ```typescript // 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 }) } ``` ## Role-Based Access Control (RBAC) [Section titled “Role-Based Access Control (RBAC)”](#role-based-access-control-rbac) ### Defining Roles [Section titled “Defining Roles”](#defining-roles) Create role and permission definitions: ```typescript // 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 }, }) ``` ### Checking Permissions [Section titled “Checking Permissions”](#checking-permissions) ```typescript 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... } ``` ### Role Middleware [Section titled “Role Middleware”](#role-middleware) Protect routes by role: ```typescript // 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'], }) ``` ### Custom Authorization [Section titled “Custom Authorization”](#custom-authorization) For complex authorization logic: ```typescript // 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 [Section titled “Multi-Tenancy”](#multi-tenancy) 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. ### Tenant Resolution [Section titled “Tenant Resolution”](#tenant-resolution) Configure how tenants are identified: ```typescript // 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() } ``` ### Resolution Strategies [Section titled “Resolution Strategies”](#resolution-strategies) | Strategy | Example | Description | | ----------- | ------------------- | -------------------------- | | `subdomain` | `acme.myapp.com` | Tenant from subdomain | | `path` | `/t/acme/dashboard` | Tenant from URL path | | `header` | `X-Tenant-ID: acme` | Tenant from request header | | `cookie` | `tenant=acme` | Tenant from cookie | ## Rate Limiting [Section titled “Rate Limiting”](#rate-limiting) ### Auth-Specific Rate Limiters [Section titled “Auth-Specific Rate Limiters”](#auth-specific-rate-limiters) Protect auth endpoints from brute force attacks: ```typescript // 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() } ``` ### Pre-built Limiters [Section titled “Pre-built Limiters”](#pre-built-limiters) | Limiter | Default | Description | | ------------------------------------ | ------------ | ---------------------------- | | `createLoginRateLimiter` | 5 per 15 min | Login attempts by IP + email | | `createPasswordResetRateLimiter` | 3 per hour | Password reset by IP | | `createEmailVerificationRateLimiter` | 5 per hour | Email verification by IP | ## Client-Side Auth [Section titled “Client-Side Auth”](#client-side-auth) ### Sign In / Sign Out [Section titled “Sign In / Sign Out”](#sign-in--sign-out) ```typescript import { signIn, signOut, getSession } from '@cloudwerk/auth/client' // Sign in with provider await signIn('github') // Sign in with credentials await signIn('credentials', { email: 'user@example.com', 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) } ``` ### Auth Store (for frameworks) [Section titled “Auth Store (for frameworks)”](#auth-store-for-frameworks) ```typescript 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() ``` ### React Component Example [Section titled “React Component Example”](#react-component-example) Here’s a complete example of using auth state in a React component: ```tsx // 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 } return (
Welcome, {session.user.name}
) } ``` ## Auth Callbacks [Section titled “Auth Callbacks”](#auth-callbacks) Customize the auth flow with lifecycle callbacks: ```typescript // 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 }, }) ``` ## Password Utilities [Section titled “Password Utilities”](#password-utilities) ```typescript 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 ``` ## Error Handling [Section titled “Error Handling”](#error-handling) Auth errors can be caught and handled: ```typescript 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 } ``` ## Security Best Practices [Section titled “Security Best Practices”](#security-best-practices) Security Checklist * Always use HTTPS in production * Set `httpOnly` and `secure` flags on session cookies * Use `sameSite: 'lax'` or `'strict'` for cookies * Use `@cloudwerk/security` middleware for CSRF protection (see [Security Guide](/guides/security/)) * Implement rate limiting on auth endpoints * Store password hashes, never plain text * Use strong secrets (32+ characters) * Rotate session tokens after login ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Passkey Setup](/guides/authentication/passkey-setup/)** - Implement passwordless passkey authentication * **[Auth API Reference](/api/auth/)** - Complete API documentation * **[Database Guide](/guides/database/)** - Store user data in D1 * **[Middleware](/api/context/)** - Advanced middleware patterns # Passkey Setup > Complete guide to implementing passwordless passkey authentication in your Cloudwerk application. Passkeys provide a modern, passwordless authentication experience using WebAuthn. Users authenticate with biometrics (Face ID, Touch ID, Windows Hello) or security keys instead of passwords. This guide walks you through setting up passkey authentication in a Cloudwerk application. Why Passkeys? * **More secure** - Resistant to phishing, credential stuffing, and password breaches * **Better UX** - No passwords to remember or type * **Cross-device** - Synced across devices via iCloud Keychain, Google Password Manager, etc. * **Fast** - Authentication typically takes under 2 seconds ## Overview [Section titled “Overview”](#overview) The passkey implementation uses: * **KV** for temporary challenge storage (with automatic TTL expiration) * **D1** for persistent user and credential storage * **Convention-based routing** for automatic endpoint registration ## Project Structure [Section titled “Project Structure”](#project-structure) After setup, your auth-related files will look like this: * app/ * auth/ * config.ts # Auth configuration * callbacks.ts # Auth lifecycle hooks * providers/ * passkey.ts # Passkey provider config * components/ * PasskeyLoginForm.tsx * PasskeySignupForm.tsx * login/ * page.tsx * signup/ * page.tsx * dashboard/ * middleware.ts # Protected route * page.tsx * middleware.ts # Root session middleware * migrations/ * 0001\_auth\_tables.sql # Database schema * wrangler.toml # Cloudflare bindings ## Setup Steps [Section titled “Setup Steps”](#setup-steps) 1. **Install the auth package** ```bash pnpm add @cloudwerk/auth ``` 2. **Create Cloudflare bindings** Add KV and D1 bindings to your `wrangler.toml`: ```toml # KV namespace for sessions and challenges [[kv_namespaces]] binding = "AUTH_SESSIONS" id = "your-kv-namespace-id" # D1 database for users and credentials [[d1_databases]] binding = "DB" database_name = "my-app-db" database_id = "your-database-id" ``` Create these resources if you haven’t already: ```bash # Create KV namespace wrangler kv:namespace create AUTH_SESSIONS # Create D1 database wrangler d1 create my-app-db ``` 3. **Create database migration** Create `migrations/0001_auth_tables.sql`: ```sql -- Users table CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT UNIQUE, email_verified TEXT, name TEXT, image TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); -- WebAuthn credentials table CREATE TABLE IF NOT EXISTS webauthn_credentials ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, public_key TEXT NOT NULL, counter INTEGER NOT NULL DEFAULT 0, aaguid TEXT, transports TEXT, backed_up INTEGER NOT NULL DEFAULT 0, device_type TEXT NOT NULL DEFAULT 'singleDevice', created_at TEXT NOT NULL DEFAULT (datetime('now')), last_used_at TEXT, name TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_credentials_user_id ON webauthn_credentials(user_id); ``` Run the migration: ```bash wrangler d1 migrations apply my-app-db ``` 4. **Configure the passkey provider** Create `app/auth/providers/passkey.ts`: ```typescript import { defineProvider, passkey } from '@cloudwerk/auth/convention' export default defineProvider( passkey({ rpName: 'My App', // Shown in browser prompts rpId: 'localhost', // Your domain (localhost for dev) origin: 'http://localhost:3000', // Full origin URL authenticatorAttachment: 'platform', // Use device biometrics userVerification: 'preferred', // Request biometric when available kvBinding: 'AUTH_SESSIONS', // KV binding name from wrangler.toml d1Binding: 'DB', // D1 binding name from wrangler.toml }) ) ``` 5. **Configure auth settings** Create `app/auth/config.ts`: ```typescript import { defineAuthConfig } from '@cloudwerk/auth/convention' export default defineAuthConfig({ basePath: '/auth', session: { strategy: 'database', maxAge: 30 * 24 * 60 * 60, // 30 days updateAge: 24 * 60 * 60, // Refresh daily }, }) ``` 6. **Add root middleware for session context** Create `app/middleware.ts`: ```typescript import { createCoreAuthMiddleware } from '@cloudwerk/auth/middleware' export const middleware = createCoreAuthMiddleware({ strategy: 'database', kvBinding: 'AUTH_SESSIONS', pages: { signIn: '/login', }, }) ``` 7. **Create the client-side components** See the [Client Components](#client-components) section below. 8. **Create login and signup pages** See the [Pages](#pages) section below. 9. **Protect routes with middleware** Create `app/dashboard/middleware.ts`: ```typescript import { authMiddleware } from '@cloudwerk/auth/middleware' export const middleware = authMiddleware({ unauthenticatedRedirect: '/login', }) ``` ## Provider Configuration [Section titled “Provider Configuration”](#provider-configuration) The passkey provider accepts these options: | Option | Type | Default | Description | | ------------------------- | -------------------------------------------- | -------------------------- | ------------------------------------- | | `rpName` | `string` | **required** | Display name shown in browser prompts | | `rpId` | `string` | `window.location.hostname` | Your domain (no protocol/port) | | `origin` | `string \| string[]` | `window.location.origin` | Allowed origins | | `kvBinding` | `string` | — | KV binding name for challenges | | `d1Binding` | `string` | — | D1 binding name for credentials | | `authenticatorAttachment` | `'platform' \| 'cross-platform'` | — | Authenticator type preference | | `userVerification` | `'required' \| 'preferred' \| 'discouraged'` | `'preferred'` | Biometric requirement | | `residentKey` | `'required' \| 'preferred' \| 'discouraged'` | `'required'` | Discoverable credential requirement | | `timeout` | `number` | `60000` | Operation timeout (ms) | ### Authenticator Attachment [Section titled “Authenticator Attachment”](#authenticator-attachment) * **`platform`** - Built-in authenticators only (Touch ID, Face ID, Windows Hello) * **`cross-platform`** - External authenticators only (USB security keys, phones) * **`undefined`** - Allow both types ### Production Configuration [Section titled “Production Configuration”](#production-configuration) For production, update your passkey provider with your actual domain: ```typescript export default defineProvider( passkey({ rpName: 'My App', rpId: 'myapp.com', // Your production domain origin: 'https://myapp.com', // HTTPS required in production authenticatorAttachment: 'platform', userVerification: 'preferred', kvBinding: 'AUTH_SESSIONS', d1Binding: 'DB', }) ) ``` Domain Configuration The `rpId` must match your domain exactly. Once users register passkeys with a specific `rpId`, they can only use those passkeys on that domain. Changing `rpId` will invalidate all existing passkeys. ## Auto-Registered Routes [Section titled “Auto-Registered Routes”](#auto-registered-routes) The passkey provider automatically registers these API endpoints: | Endpoint | Method | Description | | ------------------------------------ | ------ | ----------------------------------------- | | `/auth/passkey/register/options` | POST | Get WebAuthn options for registration | | `/auth/passkey/register/verify` | POST | Verify registration and create credential | | `/auth/passkey/authenticate/options` | POST | Get WebAuthn options for authentication | | `/auth/passkey/authenticate/verify` | POST | Verify authentication and create session | You don’t need to create route files for these - they’re handled automatically. ## Client Components [Section titled “Client Components”](#client-components) The WebAuthn API requires client-side JavaScript. Here are complete, working components: ### Base64URL Utilities [Section titled “Base64URL Utilities”](#base64url-utilities) Both components need these utilities for WebAuthn API compatibility: ```typescript function base64UrlToBuffer(base64url: string): ArrayBuffer { const padding = '='.repeat((4 - (base64url.length % 4)) % 4) const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/') + padding const binary = atob(base64) const bytes = new Uint8Array(binary.length) for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i) } return bytes.buffer } function bufferToBase64Url(buffer: ArrayBuffer): string { const bytes = new Uint8Array(buffer) let binary = '' for (let i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i]) } const base64 = btoa(binary) return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') } ``` ### Login Form [Section titled “Login Form”](#login-form) ```tsx // app/components/PasskeyLoginForm.tsx 'use client' import { useState } from 'hono/jsx' export default function PasskeyLoginForm() { const [email, setEmail] = useState('') const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const handleSubmit = async (e: Event) => { e.preventDefault() setError(null) setLoading(true) try { // Step 1: Get authentication options from server const optionsRes = await fetch('/auth/passkey/authenticate/options', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email.trim() }), credentials: 'include', }) if (!optionsRes.ok) { const err = await optionsRes.json().catch(() => ({})) throw new Error(err.error || 'Failed to get authentication options') } const options = await optionsRes.json() // Step 2: Call WebAuthn API const credential = await navigator.credentials.get({ publicKey: { ...options, challenge: base64UrlToBuffer(options.challenge), allowCredentials: options.allowCredentials?.map((c) => ({ ...c, id: base64UrlToBuffer(c.id), })), }, }) as PublicKeyCredential | null if (!credential) { throw new Error('Authentication was cancelled') } const response = credential.response as AuthenticatorAssertionResponse // Step 3: Verify with server const verifyRes = await fetch('/auth/passkey/authenticate/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ credential: { id: credential.id, rawId: bufferToBase64Url(credential.rawId), response: { clientDataJSON: bufferToBase64Url(response.clientDataJSON), authenticatorData: bufferToBase64Url(response.authenticatorData), signature: bufferToBase64Url(response.signature), userHandle: response.userHandle ? bufferToBase64Url(response.userHandle) : null, }, type: credential.type, }, }), credentials: 'include', }) if (!verifyRes.ok) { const err = await verifyRes.json().catch(() => ({})) throw new Error(err.error || 'Authentication failed') } // Success - redirect to dashboard window.location.href = '/dashboard' } catch (err) { setLoading(false) if (err instanceof Error) { if (err.name === 'NotAllowedError') { setError('Authentication was cancelled or not allowed') } else { setError(err.message) } } else { setError('An error occurred') } } } return (
{error &&
{error}
} setEmail(e.target.value)} placeholder="you@example.com" />
) } ``` ### Signup Form [Section titled “Signup Form”](#signup-form) ```tsx // app/components/PasskeySignupForm.tsx 'use client' import { useState } from 'hono/jsx' export default function PasskeySignupForm() { const [name, setName] = useState('') const [email, setEmail] = useState('') const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const handleSubmit = async (e: Event) => { e.preventDefault() setError(null) setLoading(true) try { // Step 1: Get registration options from server const optionsRes = await fetch('/auth/passkey/register/options', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email.trim(), name: name.trim() }), credentials: 'include', }) if (!optionsRes.ok) { const err = await optionsRes.json().catch(() => ({})) throw new Error(err.error || 'Failed to get registration options') } const options = await optionsRes.json() // Step 2: Call WebAuthn API to create credential const credential = await navigator.credentials.create({ publicKey: { ...options, challenge: base64UrlToBuffer(options.challenge), user: { ...options.user, id: base64UrlToBuffer(options.user.id), }, excludeCredentials: options.excludeCredentials?.map((c) => ({ ...c, id: base64UrlToBuffer(c.id), })), }, }) as PublicKeyCredential | null if (!credential) { throw new Error('Passkey creation was cancelled') } const response = credential.response as AuthenticatorAttestationResponse // Step 3: Verify with server const verifyRes = await fetch('/auth/passkey/register/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ credential: { id: credential.id, rawId: bufferToBase64Url(credential.rawId), response: { clientDataJSON: bufferToBase64Url(response.clientDataJSON), attestationObject: bufferToBase64Url(response.attestationObject), transports: response.getTransports?.() ?? [], }, type: credential.type, }, userId: options.user.id, }), credentials: 'include', }) if (!verifyRes.ok) { const err = await verifyRes.json().catch(() => ({})) throw new Error(err.error || 'Registration failed') } // Success - redirect to dashboard window.location.href = '/dashboard' } catch (err) { setLoading(false) if (err instanceof Error) { if (err.name === 'NotAllowedError') { setError('Passkey creation was cancelled or not allowed') } else { setError(err.message) } } else { setError('An error occurred') } } } return (
{error &&
{error}
} setName(e.target.value)} placeholder="Jane Smith" /> setEmail(e.target.value)} placeholder="you@example.com" />
) } ``` ## Pages [Section titled “Pages”](#pages) ### Login Page [Section titled “Login Page”](#login-page) ```tsx // app/login/page.tsx import { isAuthenticated, redirect } from '@cloudwerk/auth' import PasskeyLoginForm from '../components/PasskeyLoginForm' export async function loader() { if (isAuthenticated()) { throw redirect('/dashboard') } return {} } export default function LoginPage() { return (

Sign in

Don't have an account? Sign up

) } ``` ### Signup Page [Section titled “Signup Page”](#signup-page) ```tsx // app/signup/page.tsx import { isAuthenticated, redirect } from '@cloudwerk/auth' import PasskeySignupForm from '../components/PasskeySignupForm' export async function loader() { if (isAuthenticated()) { throw redirect('/dashboard') } return {} } export default function SignupPage() { return (

Create account

Already have an account? Sign in

) } ``` ## Auth Callbacks [Section titled “Auth Callbacks”](#auth-callbacks) Customize the authentication flow with callbacks: ```typescript // app/auth/callbacks.ts import { defineAuthCallbacks } from '@cloudwerk/auth/convention' export default defineAuthCallbacks({ async signIn({ user, account }) { // Called on successful authentication // Return false to deny sign-in console.log(`User ${user.id} signed in via ${account.provider}`) return true }, async session({ session, user }) { // Customize session data return { ...session, data: { ...session.data, role: user.data?.role, }, } }, }) ``` ## How It Works [Section titled “How It Works”](#how-it-works) ### Registration Flow [Section titled “Registration Flow”](#registration-flow) 1. User submits email and name 2. Server generates WebAuthn options with a random challenge 3. Challenge is stored in KV with 10-minute TTL 4. Browser prompts user to create passkey (biometric verification) 5. Browser returns attestation with public key 6. Server verifies attestation, consumes challenge 7. Server creates user in D1 and stores credential 8. Server creates session and sets cookie ### Authentication Flow [Section titled “Authentication Flow”](#authentication-flow) 1. User submits email 2. Server looks up user’s credentials in D1 3. Server generates WebAuthn options with allowed credentials 4. Challenge is stored in KV with 10-minute TTL 5. Browser prompts user to authenticate (biometric verification) 6. Browser returns assertion with signature 7. Server verifies signature using stored public key 8. Server validates and increments counter (clone detection) 9. Server creates session and sets cookie ## Security Features [Section titled “Security Features”](#security-features) The passkey implementation includes: * **Single-use challenges** - Challenges are deleted after verification * **Challenge expiration** - 10-minute TTL prevents replay attacks * **Counter validation** - Detects cloned authenticators * **Origin verification** - Prevents phishing attacks * **RP ID validation** - Ensures correct domain * **Signature verification** - Cryptographic proof of possession ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### ”Challenge storage not configured” [Section titled “”Challenge storage not configured””](#challenge-storage-not-configured) Ensure your passkey provider has `kvBinding` set and the binding exists in `wrangler.toml`: ```typescript passkey({ // ... kvBinding: 'AUTH_SESSIONS', // Must match wrangler.toml }) ``` ### “User not found” during authentication [Section titled ““User not found” during authentication”](#user-not-found-during-authentication) The email must match an existing user with a registered passkey. Check that: 1. User exists in the `users` table 2. User has a credential in `webauthn_credentials` table ### ”NotAllowedError” in browser [Section titled “”NotAllowedError” in browser”](#notallowederror-in-browser) This usually means: * User cancelled the passkey prompt * No passkey exists for this site * Browser doesn’t support passkeys * Site is not served over HTTPS (except localhost) ### Passkeys not syncing across devices [Section titled “Passkeys not syncing across devices”](#passkeys-not-syncing-across-devices) Ensure you’re using `authenticatorAttachment: 'platform'` and: * iOS: iCloud Keychain is enabled * Android: Google Password Manager is enabled * macOS: iCloud Keychain or Chrome profile sync ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Authentication Overview](/guides/authentication/)** - Other auth providers (OAuth, credentials) * **[Database Guide](/guides/database/)** - D1 database patterns * **[Middleware](/api/context/)** - Advanced middleware configuration # Data Loading > Learn how to fetch and manage data in Cloudwerk applications. Cloudwerk provides a powerful data loading system using `loader()` functions that run on the server before rendering. ## Loader Basics [Section titled “Loader Basics”](#loader-basics) Export a `loader()` function from any page or layout to fetch data: ```tsx // app/users/page.tsx import type { PageProps } from '@cloudwerk/core' import { db } from '@cloudwerk/core/bindings' export async function loader() { const { results: users } = await db.prepare('SELECT * FROM users').all() return { users } } export default function UsersPage({ users }: PageProps & { users: User[] }) { return (
    {users.map(user => (
  • {user.name}
  • ))}
) } ``` ## Loader Arguments [Section titled “Loader Arguments”](#loader-arguments) The `LoaderArgs` object provides access to: ```typescript interface LoaderArgs { request: Request; // The incoming request params: Record; // URL parameters context: HonoContext; // Hono context for bindings and middleware state } ``` ### Using Request Data [Section titled “Using Request Data”](#using-request-data) ```tsx export async function loader({ request }: LoaderArgs) { // Access query parameters const url = new URL(request.url); const page = parseInt(url.searchParams.get('page') ?? '1'); const limit = parseInt(url.searchParams.get('limit') ?? '10'); // Access headers const authHeader = request.headers.get('Authorization'); return { page, limit }; } ``` ### Using Route Parameters [Section titled “Using Route Parameters”](#using-route-parameters) ```tsx // app/users/[id]/page.tsx import { params } from '@cloudwerk/core/context' import { db } from '@cloudwerk/core/bindings' export async function loader() { const user = await db .prepare('SELECT * FROM users WHERE id = ?') .bind(params.id) .first() return { user } } ``` ### Using Importable Bindings [Section titled “Using Importable Bindings”](#using-importable-bindings) Import bindings directly from `@cloudwerk/core/bindings` for clean, ergonomic access: ```tsx import { db, kv, r2 } from '@cloudwerk/core/bindings' import { getUser } from '@cloudwerk/auth' export async function loader() { // Database (D1) const { results: users } = await db.prepare('SELECT * FROM users').all() // Key-Value store (KV) const cached = await kv.get('cache-key') // Object storage (R2) const file = await r2.get('uploads/image.png') // Get authenticated user const user = getUser() return { users, cached, user } } ``` Tip Run `cloudwerk bindings generate-types` to enable TypeScript autocomplete for your bindings. ### Using Context Parameter [Section titled “Using Context Parameter”](#using-context-parameter) Alternatively, you can access bindings through the `context` parameter. This is useful when you need access to request-specific data or middleware-set values: ```tsx export async function loader({ context }: LoaderArgs) { // Database (D1) via context const db = context.env.DB const { results: users } = await db.prepare('SELECT * FROM users').all() // Key-Value store (KV) via context const kv = context.env.CACHE const cached = await kv.get('cache-key') // Middleware-set values const tenant = context.get('tenant') // Cookies const theme = context.req.cookie('theme') return { users } } ``` ## Error Handling [Section titled “Error Handling”](#error-handling) ### Not Found Errors [Section titled “Not Found Errors”](#not-found-errors) Throw `NotFoundError` to trigger a 404 response: ```tsx import { NotFoundError } from '@cloudwerk/core' import { params } from '@cloudwerk/core/context' import { db } from '@cloudwerk/core/bindings' export async function loader() { const user = await db .prepare('SELECT * FROM users WHERE id = ?') .bind(params.id) .first() if (!user) { throw new NotFoundError('User not found') } return { user } } ``` ### Redirects [Section titled “Redirects”](#redirects) Throw `RedirectError` to redirect: ```tsx import { RedirectError } from '@cloudwerk/core' import { getUser, isAuthenticated } from '@cloudwerk/auth' export async function loader() { if (!isAuthenticated()) { throw new RedirectError('/login') } const user = getUser() return { user } } ``` ### Generic Errors [Section titled “Generic Errors”](#generic-errors) Any thrown error will be caught by the nearest `error.tsx` boundary: ```tsx export async function loader({ context }: LoaderArgs) { const response = await fetch('https://api.example.com/data'); if (!response.ok) { throw new Error('Failed to fetch data'); } return { data: await response.json() }; } ``` ## Layout Loaders [Section titled “Layout Loaders”](#layout-loaders) Layouts can also have loaders. They execute from parent to child: ```tsx // app/layout.tsx import { getUser } from '@cloudwerk/auth' export async function loader() { const user = getUser() return { user } } export default function RootLayout({ children, user }: LayoutProps & { user: User | null }) { return (
{children} ) } ``` ```tsx // app/dashboard/layout.tsx import { RedirectError } from '@cloudwerk/core' import { getUser } from '@cloudwerk/auth' import { db } from '@cloudwerk/core/bindings' export async function loader() { // This runs AFTER the root layout loader const user = getUser() if (!user) { throw new RedirectError('/login') } const { results: notifications } = await db .prepare('SELECT * FROM notifications WHERE user_id = ? AND read = 0') .bind(user.id) .all() return { notifications: notifications } } ``` ## Parallel Data Loading [Section titled “Parallel Data Loading”](#parallel-data-loading) When a route has multiple loaders (layouts + page), they execute in parallel when possible: ```plaintext Root Layout Loader ─┬─> Dashboard Layout Loader ─┬─> Page Loader │ │ └─> Sidebar Slot Loader ─────┘ ``` Tip Structure your loaders to be independent when possible. This enables parallel execution and faster page loads. ## Caching [Section titled “Caching”](#caching) ### Response Caching [Section titled “Response Caching”](#response-caching) Set cache headers in your loader: ```tsx import { db } from '@cloudwerk/core/bindings' export async function loader() { const { results: data } = await db.prepare('SELECT * FROM posts').all() return { data, // Will be merged with response headers headers: { 'Cache-Control': 'public, max-age=3600', }, } } ``` ### KV Caching [Section titled “KV Caching”](#kv-caching) Use Cloudflare KV for server-side caching: ```tsx import { kv, db } from '@cloudwerk/core/bindings' export async function loader() { const cacheKey = 'popular-posts' // Try cache first const cached = await kv.get(cacheKey, 'json') if (cached) { return { posts: cached } } // Fetch fresh data const { results: posts } = await db .prepare('SELECT * FROM posts ORDER BY views DESC LIMIT 10') .all() // Cache for 5 minutes await kv.put(cacheKey, JSON.stringify(posts), { expirationTtl: 300, }) return { posts } } ``` ## TypeScript [Section titled “TypeScript”](#typescript) Define your loader return type for full type safety: ```tsx import type { PageProps } from '@cloudwerk/core' import { NotFoundError } from '@cloudwerk/core' import { params } from '@cloudwerk/core/context' import { db } from '@cloudwerk/core/bindings' interface User { id: string name: string email: string } interface Post { id: string title: string } interface LoaderData { user: User posts: Post[] } export async function loader(): Promise { const user = await db .prepare('SELECT * FROM users WHERE id = ?') .bind(params.id) .first() if (!user) { throw new NotFoundError('User not found') } const { results: posts } = await db .prepare('SELECT * FROM posts WHERE author_id = ?') .bind(user.id) .all() return { user, posts } } export default function UserPage({ user, posts }: PageProps & LoaderData) { return (

{user.name}

{user.email}

Posts

    {posts.map(post =>
  • {post.title}
  • )}
) } ``` ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Forms and Actions](/guides/forms-and-actions/)** - Handle form submissions * **[Database Guide](/guides/database/)** - Work with Cloudflare D1 * **[Authentication](/guides/authentication/)** - Protect your routes # Database (D1) > Work with Cloudflare D1 SQL database in your Cloudwerk application. Cloudwerk provides integration with Cloudflare D1, a serverless SQLite database that runs at the edge. ## Getting Started [Section titled “Getting Started”](#getting-started) ### Create a D1 Database [Section titled “Create a D1 Database”](#create-a-d1-database) 1. Create a new D1 database using the Cloudwerk CLI: ```bash # Create D1 database and auto-configure wrangler.toml cloudwerk bindings add d1 ``` Or with wrangler directly: ```bash wrangler d1 create my-database ``` 2. If using wrangler directly, copy the database ID from the output and add it to `wrangler.toml`: ```toml [[d1_databases]] binding = "DB" database_name = "my-database" database_id = "your-database-id" ``` 3. Your D1 database is now available via `db` from `@cloudwerk/core/bindings`. ### Database Migrations [Section titled “Database Migrations”](#database-migrations) Create a new migration file: ```bash # Create a new migration file wrangler d1 migrations create my-database create_users # This creates: migrations/0001_create_users.sql ``` Write your migration SQL: ```sql -- migrations/0001_create_users.sql CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, name TEXT NOT NULL, password_hash TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX idx_users_email ON users(email); ``` Run migrations: ```bash # Local development wrangler d1 migrations apply my-database --local # Production wrangler d1 migrations apply my-database --remote ``` ## Using D1 in Loaders [Section titled “Using D1 in Loaders”](#using-d1-in-loaders) Import the `db` binding directly from `@cloudwerk/core/bindings`: ```tsx // app/users/page.tsx import type { PageProps } from '@cloudwerk/core' import { db } from '@cloudwerk/core/bindings' export async function loader() { const { results: users } = await db.prepare('SELECT * FROM users').all() return { users } } export default function UsersPage({ users }: PageProps & { users: User[] }) { return (
    {users.map(user => (
  • {user.name}
  • ))}
) } ``` ### Using Context Parameter [Section titled “Using Context Parameter”](#using-context-parameter) Alternatively, access D1 via `context.env.DB` in loader functions: ```tsx import type { PageProps, LoaderArgs } from '@cloudwerk/core' export async function loader({ context }: LoaderArgs) { const db = context.env.DB const { results: users } = await db.prepare('SELECT * FROM users').all() return { users } } ``` ## Using D1 in Route Handlers [Section titled “Using D1 in Route Handlers”](#using-d1-in-route-handlers) Import the `db` binding directly from `@cloudwerk/core/bindings`: ```typescript // app/api/users/route.ts import { db } from '@cloudwerk/core/bindings' import { json } from '@cloudwerk/core' export async function GET() { const { results: users } = await db.prepare('SELECT * FROM users').all() return json(users) } ``` Tip Run `cloudwerk bindings generate-types` to enable TypeScript autocomplete for your D1 database binding. ### Using getContext() [Section titled “Using getContext()”](#using-getcontext) Alternatively, use `getContext()` for full context access: ```typescript import { json, getContext } from '@cloudwerk/core' export async function GET() { const { env } = getContext() const { results: users } = await env.DB.prepare('SELECT * FROM users').all() return json(users) } ``` ## Query Patterns [Section titled “Query Patterns”](#query-patterns) ### Select Queries [Section titled “Select Queries”](#select-queries) ```typescript // Get all rows const { results: users } = await db.prepare('SELECT * FROM users').all(); // Get single row by ID const user = await db .prepare('SELECT * FROM users WHERE id = ?') .bind(userId) .first(); // Select specific columns const { results: emails } = await db .prepare('SELECT id, email FROM users') .all(); // With conditions const { results: activeUsers } = await db .prepare(` SELECT * FROM users WHERE status = ? AND created_at > ? ORDER BY created_at DESC LIMIT 10 `) .bind('active', '2024-01-01') .all(); ``` ### Insert Queries [Section titled “Insert Queries”](#insert-queries) ```typescript // Insert single row const id = crypto.randomUUID(); await db .prepare('INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)') .bind(id, 'user@example.com', 'John Doe', hashedPassword) .run(); // Insert and return the row (use a separate query) await db .prepare('INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)') .bind(id, email, name, passwordHash) .run(); const user = await db .prepare('SELECT id, email, name FROM users WHERE id = ?') .bind(id) .first(); ``` ### Update Queries [Section titled “Update Queries”](#update-queries) ```typescript // Update by ID await db .prepare('UPDATE users SET name = ?, updated_at = ? WHERE id = ?') .bind('Jane Doe', new Date().toISOString(), userId) .run(); // Conditional update await db .prepare(` UPDATE posts SET status = 'published' WHERE author_id = ? AND status = 'draft' `) .bind(userId) .run(); ``` ### Delete Queries [Section titled “Delete Queries”](#delete-queries) ```typescript // Delete by ID await db .prepare('DELETE FROM users WHERE id = ?') .bind(userId) .run(); // Delete with conditions await db .prepare('DELETE FROM sessions WHERE expires_at < ?') .bind(new Date().toISOString()) .run(); ``` ## Relationships [Section titled “Relationships”](#relationships) ### Joining Tables [Section titled “Joining Tables”](#joining-tables) ```typescript // Inner join const { results: postsWithAuthors } = await db .prepare(` SELECT posts.id, posts.title, posts.content, users.name as author_name, users.email as author_email FROM posts INNER JOIN users ON users.id = posts.author_id `) .all(); // Left join const { results: usersWithPosts } = await db .prepare(` SELECT users.id, users.name, posts.title as post_title FROM users LEFT JOIN posts ON posts.author_id = users.id `) .all(); ``` ### Subqueries [Section titled “Subqueries”](#subqueries) ```typescript // Users with post count const { results: usersWithCounts } = await db .prepare(` SELECT users.id, users.name, (SELECT COUNT(*) FROM posts WHERE posts.author_id = users.id) as post_count FROM users `) .all(); ``` ## Transactions [Section titled “Transactions”](#transactions) Use `batch()` for atomic operations: ```typescript const userId = crypto.randomUUID(); const profileId = crypto.randomUUID(); // All statements execute atomically await db.batch([ db.prepare('INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)') .bind(userId, email, name, hash), db.prepare('INSERT INTO profiles (id, user_id, bio, avatar_url) VALUES (?, ?, ?, ?)') .bind(profileId, userId, '', null), ]); // If any statement fails, all changes are rolled back ``` ## Aggregations [Section titled “Aggregations”](#aggregations) ```typescript // Count const countResult = await db .prepare('SELECT COUNT(*) as count FROM users') .first(); const total = countResult?.count ?? 0; // Group by with aggregation const { results: monthlyStats } = await db .prepare(` SELECT strftime('%Y-%m', created_at) as month, COUNT(*) as count FROM posts WHERE author_id = ? GROUP BY month ORDER BY month DESC `) .bind(userId) .all(); ``` ## Best Practices [Section titled “Best Practices”](#best-practices) Performance Tips * Use indexes for frequently queried columns * Use `LIMIT` and pagination for large result sets * Select only needed columns instead of `SELECT *` * Use `batch()` for multiple related operations * Use prepared statements with `.bind()` to prevent SQL injection ### Pagination [Section titled “Pagination”](#pagination) ```typescript const PAGE_SIZE = 20; export async function loader({ request, context }: LoaderArgs) { const url = new URL(request.url); const page = parseInt(url.searchParams.get('page') ?? '1'); const offset = (page - 1) * PAGE_SIZE; const db = context.env.DB; const [usersResult, countResult] = await Promise.all([ db.prepare(` SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ? `) .bind(PAGE_SIZE, offset) .all(), db.prepare('SELECT COUNT(*) as total FROM users').first(), ]); return { users: usersResult.results, pagination: { page, pageSize: PAGE_SIZE, total: countResult?.total ?? 0, totalPages: Math.ceil((countResult?.total ?? 0) / PAGE_SIZE), }, }; } ``` ### Error Handling [Section titled “Error Handling”](#error-handling) ```typescript export async function loader({ params, context }: LoaderArgs) { const db = context.env.DB; try { const user = await db .prepare('SELECT * FROM users WHERE id = ?') .bind(params.id) .first(); if (!user) { throw new NotFoundError('User not found'); } return { user }; } catch (error) { if (error instanceof NotFoundError) { throw error; } console.error('Database error:', error); throw new Error('Failed to load user'); } } ``` ## TypeScript Support [Section titled “TypeScript Support”](#typescript-support) Define types for your database rows: ```typescript // lib/db/types.ts export interface User { id: string; email: string; name: string; password_hash: string; created_at: string; updated_at: string; } export interface Post { id: string; title: string; content: string; author_id: string; status: 'draft' | 'published'; created_at: string; updated_at: string; } ``` Use generics with D1 queries: ```typescript import type { User, Post } from '../lib/db/types'; export async function loader({ params, context }: LoaderArgs) { const db = context.env.DB; const user = await db .prepare('SELECT * FROM users WHERE id = ?') .bind(params.id) .first(); const { results: posts } = await db .prepare('SELECT * FROM posts WHERE author_id = ?') .bind(params.id) .all(); return { user, posts }; } ``` ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Data Loading](/guides/data-loading/)** - Server-side data fetching patterns * **[Forms and Actions](/guides/forms-and-actions/)** - Handle form submissions * **[Authentication](/guides/authentication/)** - Protect your routes # Deployment > Deploy your Cloudwerk application to Cloudflare Workers. Deploy your Cloudwerk application to Cloudflare’s global network with a single command. ## Quick Deploy [Section titled “Quick Deploy”](#quick-deploy) ```bash pnpm deploy ``` This builds your application and deploys it to Cloudflare Workers. ## Configuration [Section titled “Configuration”](#configuration) ### wrangler.toml [Section titled “wrangler.toml”](#wranglertoml) Configure your deployment in `wrangler.toml`: ```toml name = "my-cloudwerk-app" main = ".cloudwerk/worker.js" compatibility_date = "2024-01-01" # Production environment [env.production] name = "my-cloudwerk-app" routes = [ { pattern = "example.com/*", zone_name = "example.com" } ] # Staging environment [env.staging] name = "my-cloudwerk-app-staging" routes = [ { pattern = "staging.example.com/*", zone_name = "example.com" } ] # D1 Database [[d1_databases]] binding = "DB" database_name = "my-database" database_id = "your-database-id" # KV Namespace [[kv_namespaces]] binding = "KV" id = "your-kv-id" # R2 Bucket [[r2_buckets]] binding = "R2" bucket_name = "my-bucket" # Environment variables [vars] ENVIRONMENT = "production" ``` ### Environment Variables [Section titled “Environment Variables”](#environment-variables) Set secrets using Wrangler: ```bash # Set a secret wrangler secret put API_KEY # Set for specific environment wrangler secret put API_KEY --env staging ``` Caution Never commit secrets to your repository. Use `wrangler secret` for sensitive values. ## Deployment Environments [Section titled “Deployment Environments”](#deployment-environments) ### Development [Section titled “Development”](#development) Local development with hot reload: ```bash pnpm dev ``` ### Staging [Section titled “Staging”](#staging) Deploy to staging environment: ```bash pnpm deploy --env staging ``` ### Production [Section titled “Production”](#production) Deploy to production: ```bash pnpm deploy --env production ``` Or simply: ```bash pnpm deploy ``` ## CI/CD Integration [Section titled “CI/CD Integration”](#cicd-integration) ### GitHub Actions [Section titled “GitHub Actions”](#github-actions) ```yaml # .github/workflows/deploy.yml name: Deploy on: push: branches: [main] pull_request: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Setup pnpm uses: pnpm/action-setup@v2 with: version: 8 - name: Install dependencies run: pnpm install --frozen-lockfile - name: Run tests run: pnpm test - name: Build run: pnpm build - name: Deploy to Staging if: github.event_name == 'pull_request' run: pnpm deploy --env staging env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - name: Deploy to Production if: github.ref == 'refs/heads/main' && github.event_name == 'push' run: pnpm deploy --env production env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} ``` ### GitLab CI [Section titled “GitLab CI”](#gitlab-ci) ```yaml # .gitlab-ci.yml stages: - test - deploy test: stage: test image: node:20 script: - corepack enable - pnpm install --frozen-lockfile - pnpm test deploy_staging: stage: deploy image: node:20 script: - corepack enable - pnpm install --frozen-lockfile - pnpm deploy --env staging environment: name: staging url: https://staging.example.com only: - merge_requests deploy_production: stage: deploy image: node:20 script: - corepack enable - pnpm install --frozen-lockfile - pnpm deploy --env production environment: name: production url: https://example.com only: - main ``` ## Database Migrations [Section titled “Database Migrations”](#database-migrations) Run migrations before deployment: 1. Create migration files: ```bash mkdir -p migrations ``` 2. Add migration to CI/CD: ```yaml - name: Run Migrations run: wrangler d1 migrations apply my-database --remote env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} ``` 3. Or run manually: ```bash wrangler d1 migrations apply my-database --remote ``` ## Custom Domains [Section titled “Custom Domains”](#custom-domains) ### Configure Domain [Section titled “Configure Domain”](#configure-domain) 1. Add your domain to Cloudflare DNS 2. Configure routes in `wrangler.toml`: ```toml routes = [ { pattern = "api.example.com/*", zone_name = "example.com" } ] ``` 3. Deploy: ```bash pnpm deploy ``` ### Multiple Domains [Section titled “Multiple Domains”](#multiple-domains) ```toml routes = [ { pattern = "example.com/*", zone_name = "example.com" }, { pattern = "www.example.com/*", zone_name = "example.com" }, { pattern = "api.example.com/*", zone_name = "example.com" }, ] ``` ## Rollbacks [Section titled “Rollbacks”](#rollbacks) ### Using Wrangler [Section titled “Using Wrangler”](#using-wrangler) ```bash # List deployments wrangler deployments list # Rollback to previous version wrangler rollback ``` ### Using Versions [Section titled “Using Versions”](#using-versions) ```bash # List versions wrangler versions list # Deploy specific version wrangler versions deploy VERSION_ID ``` ## Monitoring [Section titled “Monitoring”](#monitoring) ### Logs [Section titled “Logs”](#logs) View real-time logs: ```bash wrangler tail ``` Filter logs: ```bash # Filter by status wrangler tail --status error # Filter by search term wrangler tail --search "user-123" ``` ### Analytics [Section titled “Analytics”](#analytics) View analytics in the Cloudflare dashboard: 1. Go to Workers & Pages 2. Select your worker 3. View the Analytics tab ## Performance Optimization [Section titled “Performance Optimization”](#performance-optimization) ### Bundle Size [Section titled “Bundle Size”](#bundle-size) Keep your bundle small: ```typescript // cloudwerk.config.ts import { defineConfig } from '@cloudwerk/core'; export default defineConfig({ build: { minify: true, treeshake: true, }, }); ``` ### Caching [Section titled “Caching”](#caching) Configure caching headers: ```typescript export async function loader({ context }: LoaderArgs) { const data = await fetchData(); return { data, headers: { 'Cache-Control': 'public, max-age=3600', }, }; } ``` ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Common Issues [Section titled “Common Issues”](#common-issues) **Build fails:** ```bash # Clear cache and rebuild rm -rf .cloudwerk pnpm build ``` **Deployment fails:** ```bash # Check authentication wrangler whoami # Verify configuration wrangler deploy --dry-run ``` **Runtime errors:** ```bash # Check logs wrangler tail --status error ``` Debug Tips * Use `wrangler dev` to test locally before deploying * Check the Cloudflare dashboard for error details * Use `console.log` in production (visible in `wrangler tail`) * Test with `--dry-run` before actual deployment ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Configuration Reference](/api/configuration/)** - All config options * **[CLI Reference](/api/cli/)** - Cloudwerk CLI commands * **[Cloudflare Limits](/reference/cloudflare-limits/)** - Platform limits # Durable Objects > Build stateful edge applications with type-safe Durable Objects using @cloudwerk/durable-object. Cloudwerk provides a convention-based Durable Objects system through `@cloudwerk/durable-object`. Define type-safe, stateful objects with native Cloudflare RPC, storage options (KV and SQLite), WebSocket support, and alarms. ## Quick Start [Section titled “Quick Start”](#quick-start) 1. Create a Durable Object: ```typescript // app/objects/counter.ts import { defineDurableObject } from '@cloudwerk/durable-object' interface CounterState { value: number } export default defineDurableObject({ init: () => ({ value: 0 }), methods: { async increment(amount = 1) { this.state.value += amount return this.state.value }, async getValue() { return this.state.value }, }, }) ``` 2. Use it from your routes: ```typescript // app/api/counter/[id]/route.ts import { durableObjects } from '@cloudwerk/core/bindings' import { json } from '@cloudwerk/core' export async function POST(request: Request, { params }) { const id = durableObjects.Counter.idFromName(params.id) const stub = durableObjects.Counter.get(id) // Direct RPC call - no HTTP overhead const newValue = await stub.increment(5) return json({ value: newValue }) } ``` 3. Durable Objects are automatically discovered and registered during build. ## Convention Structure [Section titled “Convention Structure”](#convention-structure) Durable Objects are defined in the `app/objects/` directory: * app/objects/ * counter.ts # Counter object * chat-room.ts # Chat room with WebSockets * user-session.ts # User session object * rate-limiter.ts # Rate limiter **Naming Convention:** * File names are kebab-case to PascalCase class names * `counter.ts` becomes class `Counter`, binding `COUNTER` * `chat-room.ts` becomes class `ChatRoom`, binding `CHAT_ROOM` ## Defining Objects [Section titled “Defining Objects”](#defining-objects) ### Basic Object with State [Section titled “Basic Object with State”](#basic-object-with-state) ```typescript // app/objects/counter.ts import { defineDurableObject } from '@cloudwerk/durable-object' interface CounterState { value: number lastUpdated: number } export default defineDurableObject({ init(ctx) { return { value: 0, lastUpdated: Date.now(), } }, methods: { async increment(amount = 1) { this.state.value += amount this.state.lastUpdated = Date.now() return this.state.value }, async decrement(amount = 1) { this.state.value = Math.max(0, this.state.value - amount) this.state.lastUpdated = Date.now() return this.state.value }, async getValue() { return this.state.value }, async reset() { this.state.value = 0 this.state.lastUpdated = Date.now() return 0 }, }, }) ``` ### With Schema Validation [Section titled “With Schema Validation”](#with-schema-validation) Use Zod for runtime state validation: ```typescript import { defineDurableObject } from '@cloudwerk/durable-object' import { z } from 'zod' const SessionSchema = z.object({ userId: z.string(), expiresAt: z.number(), data: z.record(z.unknown()), }) type SessionState = z.infer export default defineDurableObject({ schema: SessionSchema, init: () => ({ userId: '', expiresAt: 0, data: {}, }), methods: { async setUser(userId: string, ttl = 3600) { this.state.userId = userId this.state.expiresAt = Date.now() + ttl * 1000 }, async getData() { if (Date.now() > this.state.expiresAt) { throw new Error('Session expired') } return this.state }, }, }) ``` ## RPC Methods [Section titled “RPC Methods”](#rpc-methods) Methods defined in the `methods` object become native Cloudflare RPC methods: ```typescript export default defineDurableObject({ methods: { // Simple method async getItems() { return this.state.items }, // Method with parameters async addItem(productId: string, quantity: number) { const existing = this.state.items.find(i => i.productId === productId) if (existing) { existing.quantity += quantity } else { this.state.items.push({ productId, quantity }) } return this.state.items }, // Method accessing environment async checkout() { const db = this.ctx.env.DB await db.prepare('INSERT INTO orders...').run() this.state.items = [] return { success: true } }, }, }) ``` ### Calling RPC Methods [Section titled “Calling RPC Methods”](#calling-rpc-methods) ```typescript // From route handlers import { durableObjects } from '@cloudwerk/core/bindings' export async function POST(request: Request, { params }) { const id = durableObjects.Cart.idFromName(params.userId) const stub = durableObjects.Cart.get(id) // Direct RPC calls - type-safe and fast await stub.addItem('prod_123', 2) const items = await stub.getItems() const result = await stub.checkout() return json({ items, result }) } ``` ## Storage Options [Section titled “Storage Options”](#storage-options) ### Key-Value Storage [Section titled “Key-Value Storage”](#key-value-storage) Access the built-in KV storage via `this.ctx.storage`: ```typescript export default defineDurableObject({ methods: { async setPreference(key: string, value: unknown) { await this.ctx.storage.put(`pref:${key}`, value) }, async getPreference(key: string) { return this.ctx.storage.get(`pref:${key}`) }, async getAllPreferences() { const prefs = await this.ctx.storage.list({ prefix: 'pref:' }) return Object.fromEntries(prefs) }, async clearPreferences() { const prefs = await this.ctx.storage.list({ prefix: 'pref:' }) for (const key of prefs.keys()) { await this.ctx.storage.delete(key) } }, }, }) ``` ### Storage API [Section titled “Storage API”](#storage-api) ```typescript // Get single value const value = await this.ctx.storage.get('key') // Get multiple values const map = await this.ctx.storage.get(['key1', 'key2']) // List keys with prefix const entries = await this.ctx.storage.list({ prefix: 'user:', limit: 100, reverse: false, }) // Put single value await this.ctx.storage.put('key', value) // Put multiple values await this.ctx.storage.put({ key1: value1, key2: value2, }) // Delete await this.ctx.storage.delete('key') await this.ctx.storage.deleteAll() ``` ### Transactions [Section titled “Transactions”](#transactions) For atomic operations: ```typescript methods: { async transfer(fromKey: string, toKey: string, amount: number) { await this.ctx.storage.transaction(async (txn) => { const from = await txn.get(fromKey) ?? 0 const to = await txn.get(toKey) ?? 0 if (from < amount) { txn.rollback() throw new Error('Insufficient funds') } await txn.put(fromKey, from - amount) await txn.put(toKey, to + amount) }) }, } ``` ### SQLite Storage [Section titled “SQLite Storage”](#sqlite-storage) Enable SQLite for relational data: ```typescript // app/objects/analytics.ts export default defineDurableObject({ sqlite: true, // Enable SQLite init(ctx) { // Create tables on initialization ctx.sql.run(` CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, properties TEXT, timestamp INTEGER NOT NULL ) `) ctx.sql.run(` CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events (timestamp) `) return { initialized: true } }, methods: { async trackEvent(name: string, properties?: Record) { this.ctx.sql.run( 'INSERT INTO events (name, properties, timestamp) VALUES (?, ?, ?)', [name, JSON.stringify(properties), Date.now()] ) }, async getRecentEvents(limit = 100) { const cursor = this.ctx.sql.exec( 'SELECT * FROM events ORDER BY timestamp DESC LIMIT ?', [limit] ) return cursor.toArray() }, async getEventCounts(since: number) { const cursor = this.ctx.sql.exec( 'SELECT name, COUNT(*) as count FROM events WHERE timestamp > ? GROUP BY name', [since] ) return cursor.toArray() }, }, }) ``` ## WebSocket Support [Section titled “WebSocket Support”](#websocket-support) Build real-time applications with WebSocket support: ```typescript // app/objects/chat-room.ts import { defineDurableObject } from '@cloudwerk/durable-object' interface ChatState { messages: Array<{ user: string; text: string; timestamp: number }> } export default defineDurableObject({ init: () => ({ messages: [] }), async fetch(request) { if (request.headers.get('Upgrade') !== 'websocket') { return new Response('Expected WebSocket', { status: 426 }) } const url = new URL(request.url) const username = url.searchParams.get('username') ?? 'Anonymous' // Create WebSocket pair const pair = new WebSocketPair() const [client, server] = Object.values(pair) // Accept with tags for filtering this.ctx.acceptWebSocket(server, [username]) // Send recent messages server.send(JSON.stringify({ type: 'history', messages: this.state.messages.slice(-50), })) // Notify others this.broadcast({ type: 'join', user: username, timestamp: Date.now(), }, server) return new Response(null, { status: 101, webSocket: client }) }, async webSocketMessage(ws, message) { const data = JSON.parse(message as string) const [username] = this.ctx.getTags(ws) if (data.type === 'message') { const msg = { user: username, text: data.text, timestamp: Date.now(), } this.state.messages.push(msg) // Keep last 1000 messages if (this.state.messages.length > 1000) { this.state.messages = this.state.messages.slice(-1000) } this.broadcast({ type: 'message', ...msg }) } }, async webSocketClose(ws, code, reason) { const [username] = this.ctx.getTags(ws) this.broadcast({ type: 'leave', user: username, timestamp: Date.now(), }) }, methods: { async getOnlineUsers() { const sockets = this.ctx.getWebSockets() return sockets.map(ws => this.ctx.getTags(ws)[0]) }, async getRecentMessages(limit = 50) { return this.state.messages.slice(-limit) }, }, }) ``` ### WebSocket Context API [Section titled “WebSocket Context API”](#websocket-context-api) ```typescript // Accept a WebSocket connection this.ctx.acceptWebSocket(ws, ['tag1', 'tag2']) // Get all connected WebSockets const all = this.ctx.getWebSockets() // Get WebSockets by tag const admins = this.ctx.getWebSockets('admin') // Get tags for a WebSocket const tags = this.ctx.getTags(ws) ``` ## Alarms [Section titled “Alarms”](#alarms) Schedule periodic tasks with alarms: ```typescript // app/objects/session.ts export default defineDurableObject({ init(ctx) { // Schedule cleanup alarm ctx.storage.setAlarm(Date.now() + 30 * 60 * 1000) // 30 minutes return { data: {}, lastActivity: Date.now() } }, async alarm() { const idleTime = Date.now() - this.state.lastActivity if (idleTime > 30 * 60 * 1000) { // Inactive for 30 minutes, clean up await this.ctx.storage.deleteAll() } else { // Reschedule alarm await this.ctx.storage.setAlarm(Date.now() + 30 * 60 * 1000) } }, methods: { async touch() { this.state.lastActivity = Date.now() }, async getData() { await this.touch() return this.state.data }, }, }) ``` ### Alarm API [Section titled “Alarm API”](#alarm-api) ```typescript // Set alarm (replaces existing) await this.ctx.storage.setAlarm(Date.now() + 60000) // 1 minute from now await this.ctx.storage.setAlarm(new Date('2024-12-31')) // Get current alarm const alarmTime = await this.ctx.storage.getAlarm() // Cancel alarm await this.ctx.storage.deleteAlarm() ``` ## HTTP Fetch Handler [Section titled “HTTP Fetch Handler”](#http-fetch-handler) Handle HTTP requests in addition to RPC: ```typescript export default defineDurableObject({ init: () => ({ value: 0 }), async fetch(request) { const url = new URL(request.url) if (request.method === 'GET' && url.pathname === '/') { return Response.json({ value: this.state.value, timestamp: Date.now(), }) } if (request.method === 'POST' && url.pathname === '/increment') { const body = await request.json() this.state.value += body.amount ?? 1 return Response.json({ value: this.state.value }) } return new Response('Not Found', { status: 404 }) }, methods: { // RPC methods are preferred over fetch async increment(amount = 1) { this.state.value += amount return this.state.value }, }, }) ``` ### Calling via Fetch [Section titled “Calling via Fetch”](#calling-via-fetch) ```typescript const id = durableObjects.Counter.idFromName('my-counter') const stub = durableObjects.Counter.get(id) // HTTP fetch (less efficient than RPC) const response = await stub.fetch(new Request('http://counter/increment', { method: 'POST', body: JSON.stringify({ amount: 5 }), })) const data = await response.json() ``` ## Accessing Durable Objects [Section titled “Accessing Durable Objects”](#accessing-durable-objects) ### ID Creation [Section titled “ID Creation”](#id-creation) ```typescript import { durableObjects } from '@cloudwerk/core/bindings' // From a name (deterministic) const id = durableObjects.Counter.idFromName('user-123-cart') // From a string ID const id = durableObjects.Counter.idFromString('some-id-string') // Generate unique ID const id = durableObjects.Counter.newUniqueId() // Get stub const stub = durableObjects.Counter.get(id) ``` ### ID Properties [Section titled “ID Properties”](#id-properties) ```typescript const id = durableObjects.Counter.idFromName('my-counter') id.toString() // String representation id.name // 'my-counter' (if created with idFromName) id.equals(otherId) // Compare IDs ``` ## Common Patterns [Section titled “Common Patterns”](#common-patterns) ### Rate Limiter [Section titled “Rate Limiter”](#rate-limiter) ```typescript // app/objects/rate-limiter.ts interface RateLimitState { requests: number[] } export default defineDurableObject({ init: () => ({ requests: [] }), methods: { async check(limit: number, windowMs: number) { const now = Date.now() const windowStart = now - windowMs // Filter to requests in window this.state.requests = this.state.requests.filter(t => t > windowStart) if (this.state.requests.length >= limit) { return { allowed: false, remaining: 0, resetAt: this.state.requests[0] + windowMs, } } this.state.requests.push(now) return { allowed: true, remaining: limit - this.state.requests.length, resetAt: this.state.requests[0] + windowMs, } }, async reset() { this.state.requests = [] }, }, }) ``` ### Collaborative Document [Section titled “Collaborative Document”](#collaborative-document) ```typescript // app/objects/document.ts interface DocumentState { content: string version: number } export default defineDurableObject({ sqlite: true, init(ctx) { ctx.sql.run(` CREATE TABLE IF NOT EXISTS history ( version INTEGER PRIMARY KEY, content TEXT, author TEXT, timestamp INTEGER ) `) return { content: '', version: 0 } }, async fetch(request) { if (request.headers.get('Upgrade') === 'websocket') { return this.handleWebSocket(request) } if (request.method === 'GET') { return Response.json({ content: this.state.content, version: this.state.version, }) } return new Response('Not Found', { status: 404 }) }, async webSocketMessage(ws, message) { const data = JSON.parse(message as string) if (data.type === 'edit') { this.applyEdit(data.operations, data.author) this.broadcastState() } }, methods: { async getContent() { return { content: this.state.content, version: this.state.version, } }, async getHistory(limit = 10) { const cursor = this.ctx.sql.exec( 'SELECT * FROM history ORDER BY version DESC LIMIT ?', [limit] ) return cursor.toArray() }, }, }) ``` ## Best Practices [Section titled “Best Practices”](#best-practices) Durable Object Tips * Use meaningful IDs (`user-123-cart`) instead of random UUIDs when possible * Keep objects focused on a single concern * Use alarms for periodic cleanup and maintenance * Prefer RPC methods over HTTP fetch for better performance * Use transactions for atomic multi-key operations * Enable SQLite for relational data patterns ## Error Handling [Section titled “Error Handling”](#error-handling) ```typescript import { DurableObjectNotFoundError, DurableObjectRPCError, } from '@cloudwerk/durable-object' try { const stub = durableObjects.Counter.get(id) await stub.increment(5) } catch (error) { if (error instanceof DurableObjectRPCError) { console.error('RPC failed:', error.methodName, error.message) } } ``` ## Limits [Section titled “Limits”](#limits) | Limit | Value | | --------------------- | --------------------------- | | Storage per object | 128 KB (KV), 1 GB (SQLite) | | Concurrent requests | Unlimited (single-threaded) | | WebSocket connections | 32,768 per object | | Alarm precision | \~1 second | ## Next Steps [Section titled “Next Steps”](#next-steps) * **[Durable Objects API Reference](/api/durable-objects/)** - Complete API documentation * **[Real-time Chat Example](/examples/realtime-chat/)** - Full WebSocket example * **[Services Guide](/guides/services/)** - Service extraction patterns # Forms and Actions > Handle form submissions and mutations in Cloudwerk. Cloudwerk provides a simple pattern for handling form submissions using actions. Actions are server-side functions that process form data and return responses. ## Basic Form Handling [Section titled “Basic Form Handling”](#basic-form-handling) ### Simple Form [Section titled “Simple Form”](#simple-form) Create a form that submits to an API route: ```tsx // app/contact/page.tsx export default function ContactPage() { return (