Project Structure
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”A typical Cloudwerk project has this structure:
Directoryapp/
- page.tsx # Home page (/)
- layout.tsx # Root layout
- middleware.ts # Global middleware
- error.tsx # Error boundary
- not-found.tsx # 404 page
Directoryabout/
- page.tsx # About page (/about)
Directoryusers/
- page.tsx # Users list (/users)
Directory[id]/
- page.tsx # User profile (/users/:id)
Directoryedit/
- page.tsx # Edit user (/users/:id/edit)
Directoryapi/
Directoryhealth/
- route.ts # Health endpoint (/api/health)
Directoryusers/
- route.ts # Users API (/api/users)
Directory[id]/
- route.ts # User API (/api/users/:id)
Directoryauth/ # Authentication config
- config.ts # Auth configuration
- callbacks.ts # Auth callbacks
- rbac.ts # Role definitions
Directoryproviders/
- github.ts # OAuth provider
- credentials.ts # Email/password
Directoryqueues/ # Queue consumers
- email.ts # Email queue
- notifications.ts # Notifications queue
Directoryservices/ # Service definitions
Directoryemail/
- service.ts # Email service
Directorypayments/
- service.ts # Payments service
Directoryobjects/ # Durable Objects
- counter.ts # Counter DO
- chat-room.ts # Chat room DO
Directorytriggers/ # Event triggers
- daily-cleanup.ts # Scheduled trigger
- stripe-webhook.ts # Webhook trigger
Directorypublic/ # Static assets
- …
- cloudwerk.config.ts # Configuration
- wrangler.toml # Cloudflare config
- package.json
- tsconfig.json
File Conventions
Section titled “File Conventions”page.tsx
Section titled “page.tsx”The page.tsx file defines a publicly accessible page at that route:
// app/about/page.tsx -> /aboutexport default function AboutPage() { return <h1>About Us</h1>;}Pages can export a loader() function for server-side data fetching:
export async function loader({ params, context }: LoaderArgs) { return { data: await fetchData() };}
export default function Page({ data }: PageProps & { data: Data }) { return <div>{/* render data */}</div>;}layout.tsx
Section titled “layout.tsx”The layout.tsx file wraps pages and nested layouts:
// app/layout.tsxexport default function RootLayout({ children }: LayoutProps) { return ( <html> <body>{children}</body> </html> );}Layouts can also have their own loader() functions:
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 ( <div> <Sidebar user={user} /> <main>{children}</main> </div> );}route.ts
Section titled “route.ts”The route.ts file defines API endpoints that handle HTTP methods:
// app/api/users/route.tsimport { 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”The middleware.ts file runs before route handlers:
// app/middleware.tsimport 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:
Directoryapp/
- middleware.ts # Runs for all routes
Directoryadmin/
- middleware.ts # Runs for /admin/* routes
- page.tsx
error.tsx
Section titled “error.tsx”The error.tsx file handles errors in that route segment:
// app/error.tsximport type { ErrorBoundaryProps } from '@cloudwerk/core';
export default function ErrorPage({ error, errorType }: ErrorBoundaryProps) { return ( <div> <h1>Something went wrong</h1> <p>{error.message}</p> {error.digest && <p>Error ID: {error.digest}</p>} <a href="/">Go home</a> </div> );}not-found.tsx
Section titled “not-found.tsx”The not-found.tsx file handles 404 errors:
// app/not-found.tsxexport default function NotFoundPage() { return ( <div> <h1>404 - Page Not Found</h1> <a href="/">Go home</a> </div> );}Convention Directories
Section titled “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”Authentication configuration files:
Directoryapp/auth/
- config.ts # Main auth configuration
- callbacks.ts # Lifecycle callbacks
- pages.ts # Custom page paths
- rbac.ts # Role and permission definitions
Directoryproviders/
- github.ts # OAuth provider
- google.ts # OIDC provider
- credentials.ts # Email/password
- email.ts # Magic link
// app/auth/config.tsimport { defineAuthConfig } from '@cloudwerk/auth/convention'
export default defineAuthConfig({ basePath: '/auth', session: { strategy: 'database', maxAge: 30 * 24 * 60 * 60, },})
// app/auth/providers/github.tsimport { defineProvider, github } from '@cloudwerk/auth/convention'
export default defineProvider( github({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }))app/queues/ - Queue Consumers
Section titled “app/queues/ - Queue Consumers”Queue consumers are defined as individual files:
Directoryapp/queues/
- email.ts # email queue
- image-processing.ts # imageProcessing queue
- notifications.ts # notifications queue
File names are converted to camelCase queue names.
// app/queues/email.tsimport { defineQueue } from '@cloudwerk/queue'
interface EmailMessage { to: string subject: string body: string}
export default defineQueue<EmailMessage>({ async process(message) { await sendEmail(message.body) message.ack() },})app/services/ - Service Definitions
Section titled “app/services/ - Service Definitions”Services are defined in named directories:
Directoryapp/services/
Directoryemail/
- service.ts # Required: service definition
- utils.ts # Optional: helper utilities
Directorypayments/
- service.ts
Directoryanalytics/
- service.ts
Directory names are converted to camelCase service names (email/ → email, user-management/ → userManagement).
// app/services/email/service.tsimport { 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() }, },})app/objects/ - Durable Objects
Section titled “app/objects/ - Durable Objects”Durable Objects are defined as individual files:
Directoryapp/objects/
- counter.ts # Counter DO
- chat-room.ts # ChatRoom DO
- game-session.ts # GameSession DO
File names are converted to PascalCase class names.
// app/objects/counter.tsimport { defineDurableObject } from '@cloudwerk/durable-object'
interface CounterState { count: number}
export default defineDurableObject<CounterState>({ 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 }, },})app/triggers/ - Event Triggers
Section titled “app/triggers/ - Event Triggers”Event triggers are defined as individual files:
Directoryapp/triggers/
- daily-cleanup.ts # Scheduled trigger
- stripe-webhook.ts # Webhook trigger
- image-uploaded.ts # R2 trigger
- support-email.ts # Email trigger
// app/triggers/daily-cleanup.tsimport { 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.tsimport { 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) } },})Dynamic Routes
Section titled “Dynamic Routes”Single Segment
Section titled “Single Segment”Use brackets for dynamic segments:
app/users/[id]/page.tsx -> /users/:idAccess params in your loader or component:
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”Use [...slug] to catch all subsequent segments:
app/docs/[...slug]/page.tsx -> /docs/*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”Use [[...slug]] for optional catch-all:
app/shop/[[...categories]]/page.tsx-> /shop-> /shop/electronics-> /shop/electronics/phonesRoute Groups
Section titled “Route Groups”Use (groupName) to organize routes without affecting the URL:
Directoryapp/
Directory(marketing)/
- page.tsx # /
Directoryabout/
- page.tsx # /about
Directory(dashboard)/
- layout.tsx # Dashboard layout
Directorydashboard/
- page.tsx # /dashboard
Directorysettings/
- page.tsx # /settings
Colocation
Section titled “Colocation”You can colocate components, utilities, and tests alongside your routes:
Directoryapp/
Directoryusers/
- 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”- Routing Guide - Deep dive into routing patterns
- Data Loading - Server-side data fetching
- Authentication - Add auth to your app
- Services - Build reusable services
- Queues - Background job processing
- Triggers - Event-driven architecture