Project structure
Every special file in a Koze app and what it's for
Koze is convention-driven. Drop a file in the right place with the right name and the compiler wires it into the Worker automatically — no explicit registration.
Canonical project shape
my-app/
├─ package.json
├─ vite.config.ts plugin install point + plugin options
├─ wrangler.jsonc Cloudflare deployment config (auto-synced)
├─ tsconfig.json
└─ src/
├─ worker.ts one-line re-export of koze:worker
├─ middleware.ts request/route/response/error hooks
├─ app.koze document shell (<!DOCTYPE>, <html>, <head>)
├─ app.css global stylesheet (optional; auto-linked)
├─ assets/ static files served verbatim at the URL root
├─ routes/
│ ├─ layout.koze shared fragment wrapping every page
│ ├─ index.koze /
│ └─ blog/[slug]/index.koze /blog/:slug
├─ server/
│ ├─ *.ts private server-only modules
│ ├─ *.do.ts Durable Object classes
│ ├─ *.workflow.ts Workflow classes
│ ├─ *.queue.ts Queue consumers
│ ├─ *.sandbox.ts Sandbox classes
│ ├─ *.container.ts Container classes
│ ├─ *.agent.ts Agent classes
│ └─ schemas/ ORM schema modules
└─ lib/ shared browser-safe utilities and components (imported via $lib/*)
There is no project-level config file. Build-time options (security headers, directory overrides) are passed to the koze() Vite plugin in vite.config.ts. Request-time concerns (auth, ORM auto-migration, custom steps) live in src/middleware.ts. Anything generated (the .koze/ directory, dist/) is build output. Never commit edits there.
Route files
src/app.koze — document shell
The outer HTML document. Owns <!DOCTYPE html>, <html>, <head>, and <body>. Exactly one <slot></slot> where the layout+page stream renders.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My App</title>
</head>
<body>
<slot></slot>
</body>
</html>
- Optional. When absent, the framework synthesizes a minimal default shell.
- A top
<script>is allowed for per-request shell data (e.g. CSP nonces,<html lang>selection). It follows the same client-first rules as route and layout top scripts, and imports fromkoze:requestwork just like in a route. - The app shell should stay thin — app-wide chrome (navigation, headers, footers) belongs in
layout.koze.
src/app.css — global stylesheet
Optional. When present, Koze registers it as a Vite client entry (so Tailwind, PostCSS, CSS Modules, Lightning CSS, and every other Vite CSS plugin runs on it) and auto-injects <link rel="stylesheet" href={...}> into the compiled shell before </head>. You never write the <link> tag yourself.
/* src/app.css */
@import "tailwindcss";
@plugin "daisyui";
@source "./routes/**/*.koze";
body {
font-family: system-ui, sans-serif;
}
In dev the file is served through Vite's module graph (live plugin transforms on save). In prod it emits a content-hashed .css asset.
See Styling for Tailwind, CSS frameworks, and migration from the legacy css config.
src/routes/layout.koze — shared fragment
A fragment that wraps every page. Not a document shell — no <!DOCTYPE>, <html>, or <body>. Contains a single <slot></slot> where each page renders.
<script>
import { url } from 'koze:request';
const currentPath = url.pathname;
function navClass(href) {
return `nav-link${currentPath === href ? ' active' : ''}`;
}
</script>
<header class="top-bar">
<a href="/" class={navClass('/')}>Home</a>
<a href="/about" class={navClass('/about')}>About</a>
</header>
<main>
<slot></slot>
</main>
- Fragment, not a document. Think "wrapper component".
- Works like any other route file: top
<script>is authored as client-first code, the SSR-needed parts run on the server, and$server/*imports become client RPC stubs in the browser copy. - Nested layouts (
src/routes/admin/layout.koze) wrap pages under that segment only.
src/routes/<path>/index.koze — page
A regular page. Path becomes the URL pattern.
<script>
import { listItems } from '$server/items';
const items = await listItems();
</script>
<h1>Items ({items.length})</h1>
<ul>
for (const item of items) {
<li>{item.title}</li>
}
</ul>
index.kozeis the default file per segment.- Dynamic segments use
[brackets]:[slug]→:slug,[...rest]→ catchall. - See Routing for the full template syntax.
Render chain
When a user hits /blog/hello:
app.render(data,
layout.render(data,
page.render(data)))
All three files use the same <script> + template + <slot> shape. Only their role differs.
Framework entrypoints
vite.config.ts
Installs the Koze Vite plugin. That single plugin owns route discovery, virtual modules, wrangler.jsonc auto-sync, and the dev/build pipeline.
import { defineConfig } from 'vite';
import { koze } from '@kuratchi/koze/vite';
export default defineConfig({
plugins: [koze()],
});
wrangler.jsonc
Cloudflare deployment config. Koze auto-maintains the following fields at build time — do not hand-edit them:
durable_objects.bindingsmigrations[].new_sqlite_classesworkflows[]containers[]pipelines[]assets.directory,assets.bindingqueues.consumers
Everything else (secrets, custom bindings, compatibility_date, routes) is yours.
src/worker.ts
A one-line re-export. Koze generates the real Worker entry as a virtual module.
export { default } from 'koze:worker';
src/middleware.ts
Where ORM auto-migration, auth, security guards, and your own custom steps compose. Each step hooks into the request lifecycle (request, route, response, error).
import { defineMiddleware } from 'koze:middleware';
import { autoMigrate } from '@kuratchi/kunii';
import { kyzenAuthMiddleware } from '@kuratchi/kyzen/middleware';
import { adminSchema } from './server/schemas/admin';
import { authConfig } from './server/auth-config';
export default defineMiddleware({
// Apply pending D1 migrations on cold start.
migrate: autoMigrate({ DB: adminSchema }),
// Sessions, guards, OAuth, rate-limit, turnstile.
auth: kyzenAuthMiddleware(authConfig),
// Your own steps:
// logging: { async request(ctx, next) { ... } },
});
Both autoMigrate and kyzenAuthMiddleware return ordinary MiddlewareSteps — there is no special framework slot for either. The same shape lets you swap in any third-party auth / ORM / observability tool. See Middleware.
Special directories
src/assets/
Static files served verbatim by Wrangler's ASSETS binding. Vite plugins never touch these — use this directory for finished assets only.
src/assets/favicon.ico→/favicon.icosrc/assets/hero.png→/hero.pngsrc/assets/fonts/inter.woff2→/fonts/inter.woff2
Koze keeps Wrangler's assets.directory in sync automatically.
Tip: Processed →
src/app.css. Verbatim →src/assets/. If Tailwind/PostCSS needs to see it, put it in (or import it from)src/app.css. If it's a finished file you want at/<path>, drop it insrc/assets/.
src/content/
Markdown content lives in named folders under src/content/. Each direct child folder becomes a property on the koze:content virtual module.
src/content/docs/getting-started.md → content.docs.render('getting-started')
src/content/docs/settings/api-keys.md → content.docs.render('settings/api-keys')
src/content/changelog/first-release.md → content.changelog.render('first-release')
Use this for app-owned docs, help text, changelogs, and other Markdown-backed content that should live with the project. See Content.
src/server/
Server-only code. Nothing here is shipped to the browser.
- Route files import server functions via
$server/<name>(aliased to this directory). In top<script>blocks, Koze auto-rewrites those imports to RPC stubs on the client side. - Convention-based files are auto-discovered by singular filename suffix. Plural suffixes such as
*.agents.ts,*.workflows.ts, or*.queues.tsare rejected with a fix-it error:
| Suffix | Purpose | Binding style |
|---|---|---|
*.do.ts |
Durable Object handler | One durable_objects.bindings entry |
*.workflow.ts |
Cloudflare Workflow class | workflows[] |
*.queue.ts |
Queue consumer | queues.consumers[] |
*.pipeline.ts |
Cloudflare Pipelines binding | pipelines[] |
*.sandbox.ts |
Sandbox (Durable Object + container) | durable_objects, containers |
*.container.ts |
Container class | containers[] |
*.agent.ts |
Agents SDK class | Manual Wrangler Durable Object binding |
src/lib/
Shared browser-safe utilities and components. Imported via $lib/<name>.
- Safe to use from top
<script>blocks when the module can execute during SSR. - Safe for DOM helpers when the DOM work runs inside a browser-called function.
- Do not import
$server/*into$lib/*modules that are meant to stay browser-safe.
// src/lib/copy-buttons.ts
export function mountCopyButtons() {
/* ... */
}
<script>
import { mountCopyButtons } from '$lib/copy-buttons';
mountCopyButtons();
</script>
src/server/schemas/
ORM schema modules. Imported by src/middleware.ts and passed to autoMigrate({ DB: schema }) to keep the live database in sync with the declared schema. Same modules feed kunii(env.DB, schema) for query-time type safety.
.koze/ (generated)
Build output — compiled routes, Worker module, DO proxies, transformed server modules, public asset mirror. Add it to .gitignore. Never edit.
Virtual modules
Imported as 'koze:*'. The Vite plugin synthesizes them at build time — they are not real files.
| Module | What it provides |
|---|---|
koze:request |
url, pathname, searchParams, params, slug, method for the current request |
koze:environment |
dev (compiled to a literal boolean) |
koze:workflow |
workflowStatus() and typed workflow helpers |
koze:pipeline |
pipelines, pipeline(), and sendPipeline() for discovered Pipeline bindings |
koze:content |
content.<name>.list() and content.<name>.render(id) for Markdown under src/content/<name> |
koze:navigation |
redirect() and related helpers |
koze:worker |
The full Worker module (fetch + queue + DO exports) |
koze:middleware |
Resolves your src/middleware.ts |
koze:app, koze:layout |
Compiled app shell + layout renderers |
You rarely need to import the last two directly — the compiler wires them into every route.