Import a leaf
A server-capable module must not re-export a neighbor's convenience constant. A client that imports one enum from a Drizzle table module can pull the entire table graph into its bundle. That happened here and led to ADR-0002.
import { PRIORITIES } from "@template-cloudflare/db/schema/todo.enums";
import type { AppRouterClient } from "@template-cloudflare/api/client";Domain enums live in dependency-free *.enums.ts modules. Public Zod contracts live in api/src/features/*/*.contract.ts, importing only safe contracts where necessary. A LEAF MODULE header means no imports may be added to that module. The API client entry exports types only.
Enforcement
- Nested Oxlint configs reject value imports of server modules from web, mobile and UI. Type-only imports are allowed.
- Server package export maps enumerate supported subpaths. Add a specific entry deliberately; do not widen it to
./*. - The web build scans emitted client assets for server-only markers with
assert-client-bundle-clean.mjs.
pnpm exec oxlint
pnpm --filter web buildThe Drizzle schema aggregate is the documented exception: the relations builder needs all tables, and clients cannot reach it. The existing web oRPC adapter also uses a framework-stripped server branch for SSR. Do not copy that exception into an ordinary component.
Server-only modules in the web app
Inside apps/web/src, a module that only the server may load ends in .server.ts or .server.tsx (ADR-0012). TanStack Start's import protection denies **/*.server.* files to the client environment by default. In vite build, such an import that is still in the client graph after the compiler strips .server() branches fails the build; in vite dev it becomes a mock that logs an error when used. The same protection denies **/*.client.* on the server, so no web file uses .client. in its name.
Client code reaches a .server module only through two isomorphic bridges, which use it inside createIsomorphicFn().server() so the compiler strips the branch from the client build:
- the session query,
platform/auth/session-query.ts; - the docs page loader,
features/docs/docs.page-loader.ts.
The oRPC client, platform/rpc/orpc.ts, is the exception named above: it imports no .server file, but value-imports the backend router for in-process SSR inside its own .server() branch, and the web lint config carves it out of the boundary by path.
The web has no createServerFn. Server routes (routes/api/**, routes/[.]well-known/**, and handler-only resource routes such as docs[.]md.ts or sitemap[.]xml.tsx) and server.ts import .server modules directly; the boundary test (scripts/web-feature-boundaries.test.mjs, rule W3 in web conventions) refuses any other importer.
Extending a contract
Put the shared value in a leaf first. Make the table and schema import the leaf. Add its explicit package export, then import that address from consumers. Do not rely on tree-shaking to hide server side effects.
Documentation articles render on the server so examples of server-only names stay out of executable client assets too. The browser receives trusted, repository-authored HTML and small navigation metadata.