# Client/server boundary

Keep server capabilities out of browser and native module graphs.

## Import a leaf [#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](/docs/adr/0002-no-barrels-on-server-capable-modules).

```ts
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 [#enforcement]

1. Nested Oxlint configs reject value imports of server modules from web, mobile and UI. Type-only imports are allowed.
2. Server package export maps enumerate supported subpaths. Add a specific entry deliberately; do not widen it to `./*`.
3. The web build scans emitted client assets for server-only markers with `assert-client-bundle-clean.mjs`.

```sh
pnpm exec oxlint
pnpm --filter web build
```

The 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 [#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](/docs/adr/0012-web-features-mirror-the-backend-shape)). 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`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/apps/web/src/platform/auth/session-query.ts);
- the docs page loader, [`features/docs/docs.page-loader.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/apps/web/src/features/docs/docs.page-loader.ts).

The oRPC client, [`platform/rpc/orpc.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/apps/web/src/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`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/scripts/web-feature-boundaries.test.mjs), rule W3 in [web conventions](/docs/development/web-conventions)) refuses any other importer.

## Extending a contract [#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.


## Key files [#key-files]

- [`packages/config/oxlint-client-boundary.json`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/packages/config/oxlint-client-boundary.json)
- [`apps/web/src/.oxlintrc.json`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/apps/web/src/.oxlintrc.json)
- [`scripts/web-feature-boundaries.test.mjs`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/scripts/web-feature-boundaries.test.mjs)
- [`apps/web/scripts/assert-client-bundle-clean.mjs`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/apps/web/scripts/assert-client-bundle-clean.mjs)
- [`packages/api/src/client.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/packages/api/src/client.ts)
- [`packages/db/src/schema/todo.enums.ts`](https://github.com/CarlosZiegler/template-cloudflare/blob/main/packages/db/src/schema/todo.enums.ts)

