Project structure | Koze | Primitives Docs

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 from koze:request work 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.koze is 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.bindings
  • migrations[].new_sqlite_classes
  • workflows[]
  • containers[]
  • pipelines[]
  • assets.directory, assets.binding
  • queues.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.ico
  • src/assets/hero.png/hero.png
  • src/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 in src/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.ts are 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.