# Composition (/product/building/composition)



## Where a file goes

| Contents                                                                                                                    | Location                                                                                          |
| --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| A pure helper, a driver, a transport, a framework entry point: nothing with a table, a cache tag, an action or UI behind it | `lib/`                                                                                            |
| Anything with the module shape: actions, queries, types, providers, `ui/`                                                   | `core/<module>/`                                                                                  |
| Reusable UI shared across modules                                                                                           | `components/`                                                                                     |
| A cross-module leftover too small to own a module                                                                           | `actions/`, `hooks/`, `providers/`                                                                |
| A gate, a provider or chrome the route tree mounts directly                                                                 | its concern's module: `core/chrome`, `core/chat`, `core/search`, `core/tenant`, `core/navigation` |
| A route, its metadata, its suspense boundary                                                                                | the app's `app/` tree, and nothing else                                                           |

The axis is **shape, not subject matter**. Tenant, settings, branding, references and storage are infrastructure and still live in `core/` because they carry the module shape. There is no infrastructure folder and no logic folder.

An app's `app/` tree holds route files only. A body a page mounts lives in the module that owns its concern, never beside the route; the build checks it.

## Module anatomy

The module root **is** the logic. Actions, queries, types, contexts and providers sit flat, named by suffix.

```
core/settings/
  index.ts                  the barrel, zero logic
  settings-actions.ts       "use server"
  settings-queries.ts       "use cache"
  module-settings-provider.tsx  context, provider and accessor together
  ui/
    branding-content.tsx
    brand-logo-form.tsx
    index.ts
```

* The filename prefix is the module's **noun, singular**: `products/product-actions.ts`. A module spanning several nouns gets one file per noun.
* Only two subfolders, each only when non-empty. `ui/` holds components that render, flat, the filename carrying any scope. `hooks/` is the module's **public** hook surface: a hook goes there only when the barrel exports it and another module consumes it. A hook only its siblings use stays flat beside them.
* Every module has an `index.ts`, and it is a pure re-export. `ui/` is never reachable through the module barrel; it is always its own sub-path. Server-only surface sits behind a `<module>-server.ts` exported as `/server`, so the barrel stays client-safe.

**Vendor adapters** are the one sanctioned deeper nesting. A hub module aggregating interchangeable third-party vendors, payments, shipping, the shop backend, may nest one folder per vendor once that vendor spans several files. Every file inside is vendor-prefixed, `stripe-client.ts` never `client.ts`; the contract, the resolver and the shared write path stay flat at the module root. The vocabulary is contract, selector, adapter, resolver.

## Providers

A module- or route-scoped context is fronted by a named `*Provider` plus a `useX()` accessor. Render the provider, never the raw context. The context, the provider and the accessor live together in one `*-provider.tsx`, under `core/` and `components/` alike.

```tsx
"use client";

const ModuleSettingsContext = createContext<ModuleSettings | null>(null);

function ModuleSettingsProvider({ settings, children }) {
  return (
    <ModuleSettingsContext value={settings}>{children}</ModuleSettingsContext>
  );
}

function useModuleSettings(): ModuleSettings {
  const settings = use(ModuleSettingsContext);
  if (!settings) {
    throw new Error("useModuleSettings must be used inside its provider");
  }
  return settings;
}
```

A provider is a **client component fed its data as a prop**. The async server component that resolves that data and renders the provider is a `*ServerProvider`, and a client component cannot import it. A provider holding several kinds gives each a named, typed accessor taking a plain key, never a dotted path string.

Two rules bear the most weight. A stateful client provider sits in the app's root layout, outside the suspense boundary the tenant gate postpones; placed inside, it silently never hydrates. And there are no `*Providers` bundles: each provider is mounted inline at the level it applies to, so what a layout provides is readable from the layout.

## Infrastructure must not import a domain

The infrastructure set, branding, identity, messages, navigation, references, settings, storage, tenant, may depend on each other, on `components/` and on `lib/`, and never on products, orders, storefront or any other domain module. A script enforces it in the build. A new `core/` folder counts as a domain until it is added to that script's list. When it fails, lift the shared code up, or invert so the domain reaches into the infrastructure module.

## Page composition

The dominant page is **synchronous**, owns the suspense boundary and the metadata, and composes a shared `*Content` from the module's `ui/` so several apps mount the same body.

```tsx
export default function ProductFormPage(props: FormPageProps) {
  return (
    <Suspense fallback={<PageSpinner />}>
      <ProductFormContent {...props} basePath="/products" />
    </Suspense>
  );
}
```

* A page never fetches data. Per-app variation goes through props, not forks.
* The unresolved `params` promise is passed into the content and awaited below the boundary, so the shell stays shared across links.
* Chrome that must paint in the static shell goes outside the boundary; chrome that should reveal with the body goes inside.
* Slots are `ReactNode` props, not render callbacks.
* An admin route needs no layout of its own: the route gate resolves its requirement from the navigation table. A public route names its own gate.

## Gates and chrome

A **gate** wraps children in a precondition. It returns a component on failure and never throws a not-found: under partial prerendering the static shell has already committed, so the throw buys no status code and only aborts validation. The tenant gate returns an unavailable page; the module and role gates return an access page with the reason as its variant. Only the sign-in and onboarding gates redirect.

The signed-in layout is written out, not bundled: the tenant gate, then the current-user provider, the navigation provider, the reference-data providers, the admin shell, and inside it the route gate around the children. The public layout composes its own chrome the same way, with the translation provider **outside** the tenant gate so its messages also cover the unavailable branch. Never nest a second translation provider to narrow a segment; the one relays the request's single merge, and a nested one re-ships the base to save less than the base.

Never read the session above a boundary in a layout every route shares. Each segment gates itself; awaiting auth there costs every route its prerendered shell.

## Await ordering in a server component

Guard, then data, then translations, formatters and lookups last, batched:

```tsx
export async function VisitsContent() {
  await protect();
  const tenant = await requireTenantSlug();
  const visits = await listVisits(tenant);
  const t = await getTranslations("visits.list");
```

Never call a translation or locale reader inside a cache scope. Cache raw data keyed by slug or id, and translate at the render layer.

## Directives

* `"use server"` lives at the top of the `*-actions.ts` it belongs to, never in a `.tsx`. Never re-export a type from such a file; the bundler miscompiles it into a runtime reference error.
* Queries carry `"use cache"`, a tag and a life per exported function.
* `"use client"` lands on providers at the module root and on `ui/` leaves. Every `*Content` under `core/*/ui` is a server component.

## Extract or leave explicit

Hide **mechanism**, the wiring you never want to read. Keep **structure**, the arrangement of parts that is the meaning of a screen, explicit. A shared component earns its keep when it has one reason to change, takes data rather than mode flags, costs nothing to deviate from, and is aimed at readers who rarely open it. Per-entity files are opened often; do not optimise them for not being read. What looks like duplication after the chrome is extracted is usually uniform use of composable parts. Leave it.

## Naming

* `list*` returns a collection, never null. `get*` returns one, or null. The same at every layer.
* `resolve*` turns candidates into one effective value through precedence. `normalize*` guards and shapes one caller-supplied value.
* Paired implementations of one contract name their files and functions symmetrically; the verb is the only difference.
* A `*Content` is the async suspending body under `core/*/ui`, and the compound-part container under `components/*`. A fallback is a `*Spinner` or a `*Skeleton`.

## Before you finish

```bash
bun run konsistent && bun run check:infra-deps && bun run check:app-routes
```

The build runs those and the rest of the guard set ahead of the compile. A new sub-path must be declared in the package's exports by hand.
