Getting the worker running on your machine, and the conventions for adding to it.
Read ../../CONTRIBUTING.md for the PR process and
../../CLAUDE.md for the rules the gates enforce.
npm install
npm run db:migrate # apply D1 migrations locally
npm run dev:hmr # Vite dev server with HMR — the fast iteration loopnpm run dev is the other one: it runs the full react-router build and then
wrangler dev against the real bundled worker. No HMR, but it is
production-shaped, so use it to verify built behaviour before you push.
npm run dev:tunnel is dev plus a Cloudflare Quick Tunnel. You need it to
exercise the collaborative-editing WebSockets: the session cookie is Secure,
and a page served over http://localhost opens ws://, which browsers do not
treat as a secure scheme — so the cookie is withheld and the handshake 401s.
Over the tunnel both presence and Y.Doc sync work. The tunnel URL is a new
origin (log in again there) and it is public while running — stop it when
you are done.
| Command | Purpose |
|---|---|
npm run dev |
Build + run the real bundled worker locally |
npm run dev:hmr |
Vite dev server with HMR |
npm run dev:tunnel |
dev over a public Cloudflare Quick Tunnel (needed for WebSockets) |
npm run build |
react-router build — bundles server/ + app/ into one worker |
npm run deploy |
standalone: build + wrangler deploy |
npm run deploy:saas |
saas: build + deploy with wrangler.saas.jsonc |
npm run type-check |
Full: typegen, then app + api + e2e + tests passes, serially |
npm run type-check:api |
The server/ program only — the fastest loop for API work |
npm run type-check:app |
The app/worker program (brings the api project up to date first) |
npm run lint |
ESLint + the lint:* conformance gates |
npm run test:unit |
API unit tests (vitest.api.config.ts) |
npm run test:web |
Web unit tests (vitest.config.ts) |
npm run test:workers |
Real-workerd tests (vitest.workers.config.ts) |
npm run test:e2e |
Playwright |
npm run db:generate |
Generate a forward migration from schema changes |
npm run db:migrate |
Apply migrations to local D1 |
npm run db:check |
Drift gate — schema and migrations/ must agree |
Do not hand-run what the pre-commit hook already runs. The hook does a tiered
type-check, lint-staged, and every conformance gate declared at the
precommit rung in scripts/lib/gate-registry.mjs — two dozen of them, run in
one node process by scripts/run-gates.mjs, not the three this paragraph used
to name. Commits with no staged .ts/.tsx skip the type-check entirely. Run
the full suite once before pushing, not after every edit.
That registry is the list; do not maintain a copy of it here. npm run lint
(pre-push and CI) runs every gate, at both rungs.
workers/app.ts Single-worker entry: Hono mounts the API in-process and
delegates page routes to React Router SSR
server/ Hono API — business logic, D1, R2
api/ route modules
lib/validations/ Zod schemas (never inline in a handler)
lib/db/schema/ Drizzle schema — the source of truth for migrations
services/ business logic
app/ React Router web UI (SSR on Workers)
routes/ page + resource routes, registered in app/routes.ts
packages/shared-ui/ Design System 0523 components
packages/api-types/ Hono app type re-export for end-to-end type safety
migrations/ D1 SQL, generated by drizzle-kit
messages/ i18n catalogue (paraglide)
tests/ unit / workers / e2e — see develop/testing.md
scripts/ setup, seed, backup, key rotation, and the lint:* gates
- Create the route module in
app/routes/. - Register it in
app/routes.ts— placement matters: the auth layout, the settings layout, and the library layout each wrap their children. - Load data in the route's
loader, calling the in-process API throughcreateApi(context). Client code must neverfetch('/api/...')— that request carries no session and is unauthenticated. - Build the UI from
packages/shared-uiand Design System tokens (bg-ih-primary,text-ih-fg-1). Raw palette utilities faillint:ds.
- Add or extend a route module under
server/api/. - Put the Zod schema in
server/lib/validations/— every endpoint that takes user input validates with Zod. TypeScript generics onc.req.json<T>()are not validation. - Business logic goes in
server/services/, usingScopedDBso the tenant filter cannot be forgotten. - Register the route in
server/index.ts. - Declare route metadata — see
conventions/route-metadata.md. The gate fails the build without it.
Read a capability off the deployment profile; never branch on env.APP_MODE
yourself. The seam is server/lib/deployment-profile.ts and there is a gate
that keeps it the only reader. See
../reference/deployment-modes.md.
architecture.md— single-worker architecture and request flowtesting.md— which suite a spec belongs to, and how to run itdesign-system.md— tokens, components, dark mode../reference/database.md— schema-first migration flow