# Generated host: route facades

`facade-emitter.mjs` writes one thin `src/app` route file per route (#203). A facade imports only
from a fixed specifier (a package subpath such as `@nextsparkjs/core/...` or a project alias such as
`@/templates/...`), re-exports what Next.js reads at runtime, and re-declares as literals the segment
config Next.js reads statically:

```ts
// Generated by NextSpark from @/templates/blog/[slug]/page. Do not edit: regenerated on every build.
export { default } from "@/templates/blog/[slug]/page"
export { generateMetadata, generateStaticParams } from "@/templates/blog/[slug]/page"
export const revalidate = 60
```

```js
import { emitFacade, FacadeEmitError } from './facade-emitter.mjs'

const { target, content } = await emitFacade({
  kind: 'page',                           // a row of the table below
  target: 'blog/[slug]/page.tsx',         // relative to src/app
  specifier: '@/templates/blog/[slug]/page',
  file: '/abs/project/templates/blog/[slug]/page.tsx',
  projectRoot: '/abs/project',
  cacheComponents: false,                 // the host's next.config value (checked when given)
})
```

Every problem is a `FacadeEmitError` carrying all diagnostics (`code`, `file`, `line`, `exportName`,
`message`); nothing is dropped silently.

## Export table (Next.js 16.4.2)

Source of truth: `next-route-exports.json`. The tests fail when the installed Next.js version differs,
when Next's `AppSegmentConfigSchemaKeys` or `HTTP_METHODS` change, and when this rendering is stale.

| Kind | File | Default | Re-exported | Literal segment config | Facade directive |
| --- | --- | --- | --- | --- | --- |
| page | `page` | required | `generateMetadata`, `metadata`, `generateViewport`, `viewport`, `generateStaticParams` | `revalidate`, `dynamicParams`, `dynamic`, `fetchCache`, `instant`, `prefetch`, `ensureStatic`, `unstable_dynamicStaleTime`, `preferredRegion`, `runtime`, `maxDuration`, `config` | - |
| layout | `layout` | required | `generateMetadata`, `metadata`, `generateViewport`, `viewport`, `generateStaticParams` | `revalidate`, `dynamicParams`, `dynamic`, `fetchCache`, `instant`, `prefetch`, `ensureStatic`, `preferredRegion`, `runtime`, `maxDuration` | - |
| template | `template` | required | - | - | - |
| loading | `loading` | required | - | - | - |
| error | `error` | required | - | - | 'use client' (source must be client) |
| global-error | `global-error` | required | - | - | 'use client' (source must be client) |
| not-found | `not-found` | required | - | - | - |
| global-not-found | `global-not-found` | required | `metadata`, `generateMetadata` | - | - |
| forbidden | `forbidden` | required | - | - | - |
| unauthorized | `unauthorized` | required | - | - | - |
| default | `default` | required | - | - | - |
| route | `route` | forbidden | `GET`, `HEAD`, `OPTIONS`, `POST`, `PUT`, `DELETE`, `PATCH`, `generateStaticParams` | `revalidate`, `dynamicParams`, `dynamic`, `fetchCache`, `preferredRegion`, `runtime`, `maxDuration`, `config` | - |
| sitemap | `sitemap` | required | `generateSitemaps` | `revalidate`, `dynamic`, `fetchCache`, `preferredRegion`, `runtime`, `maxDuration` | - |
| robots | `robots` | required | - | `revalidate`, `dynamic`, `fetchCache`, `preferredRegion`, `runtime`, `maxDuration` | - |
| manifest | `manifest` | required | - | `revalidate`, `dynamic`, `fetchCache`, `preferredRegion`, `runtime`, `maxDuration` | - |
| icon | `icon`, `icon0`-`icon9` | required | `generateImageMetadata`, `size`, `contentType` | `revalidate`, `dynamic`, `fetchCache`, `preferredRegion`, `runtime`, `maxDuration` | - |
| apple-icon | `apple-icon`, `apple-icon0`-`apple-icon9` | required | `generateImageMetadata`, `size`, `contentType` | `revalidate`, `dynamic`, `fetchCache`, `preferredRegion`, `runtime`, `maxDuration` | - |
| opengraph-image | `opengraph-image`, `opengraph-image0`-`opengraph-image9` | required | `generateImageMetadata`, `size`, `contentType`, `alt` | `revalidate`, `dynamic`, `fetchCache`, `preferredRegion`, `runtime`, `maxDuration` | - |
| twitter-image | `twitter-image`, `twitter-image0`-`twitter-image9` | required | `generateImageMetadata`, `size`, `contentType`, `alt` | `revalidate`, `dynamic`, `fetchCache`, `preferredRegion`, `runtime`, `maxDuration` | - |

Emitted forms: **default** `export { default } from '<source>'`; **re-exported** `export { name } from
'<source>'` (never through `export *`, which Next does not see); **literal** `export const name = <literal>`
copied from the source.

Next detects `generateStaticParams`, `generateSitemaps` and `generateImageMetadata` statically through
`specifier.orig`, so a source that exports one under an alias (`export { listSlugs as generateStaticParams }`,
which Next does *not* detect statically) is forwarded keeping that local name, and the facade reads the same:

```ts
import { generateStaticParams as listSlugs } from "@/templates/x/[slug]/page"
export { listSlugs as generateStaticParams }
```

Metadata image kinds accept Next's one-digit variants (`icon1`, `apple-icon9`, `opengraph-image2`,
`twitter-image3`), mapped to the base kind.

The emitter resolves `next/package.json` once (project first, then core) and accepts only `~16.4.2`: the table's
`next` is the verified baseline, and any stable 16.4.x patch at or above it is accepted, while other minors/majors,
lower patches and prereleases are refused (`NS_HOST_UNSUPPORTED_NEXT_VERSION`). It loads Next's segment config
schema from that same package root.
Every emitted facade is also validated by `static-imports.mjs` against an allowed grammar: a `'use client'`
prologue, static imports, `export { … } from '<string>'`, `export { … }` of imported names, and
`export const name = <literal Next reads>`. Nothing else - no expression, call, member access, function, class or
`export *` - so no runtime selection of a module can be written in a facade at all. Generated registries use the
`registry` grammar: static imports plus `export const X = <object/array literal>` whose leaves are literals or imported
identifiers (no computed keys, spreads, methods, getters or calls).

Exports Next.js accepts but the host deliberately does not forward are listed under `notForwarded` in the table
(`unstable_paramMatching` and `unstable_generateParamMatching` in a page or layout, unstable APIs in Next.js 16.4): a
source that has one gets `NS_HOST_UNSUPPORTED_EXPORT` saying that NextSpark's generated host does not forward it and that
Next.js accepts it.

Segment config and `cacheComponents` (verified by probe builds with `next build --webpack` and
`--turbopack`, Next.js 16.3.5; `ensureStatic` by the probe build below, Next.js 16.4.2):

- invalid with `cacheComponents: true` (build error "Route segment config "X" is not compatible with
  `nextConfig.cacheComponents`"): `dynamic`, `dynamicParams`, `fetchCache`, `revalidate`, **`runtime`**
  (not listed in Next's docs, but even `runtime = 'nodejs'` fails);
- require `cacheComponents: true`: `instant`, `prefetch`, `ensureStatic` (a Server Component module only);
- `unstable_dynamicStaleTime` is page-only (a layout fails the build).

`ensureStatic` (new in Next.js 16.4) was probe-built with `next build` (Turbopack) on 16.4.2, once as a hand-written
`app/page.tsx` / `app/layout.tsx` and once as the facade this emitter writes for the same source:

| Case | Next.js 16.4.2 (hand-written) | Host facade |
| --- | --- | --- |
| page, `'navigation'` / `'shell'`, `cacheComponents: true` | builds | `export const ensureStatic = ...` forwarded as a literal; builds |
| layout, `'navigation'`, `cacheComponents: true` | builds | forwarded; builds |
| page or layout, `cacheComponents: false` | build error: Route "/page" cannot use `export const ensureStatic = ...` without enabling `cacheComponents` | refused before the build, `NS_HOST_CACHE_COMPONENTS_CONFLICT` |
| page, value `'sometimes'` | build error: Invalid ensureStatic value, must be "auto", "shell", "prefetch", "navigation" or false | refused, `NS_HOST_INVALID_SEGMENT_CONFIG_VALUE` |
| page with a dynamic part behind `<Suspense>`, no `ensureStatic` | builds (partial prerender) | builds |
| the same page with `'navigation'` | build error: "encountered uncached or runtime data on a route that must be fully static" | the same build error: the facade keeps the enforcement |

Literal values are accepted exactly as `next/dist/build/analysis/extract-const-value.js` reads them
(no negative numbers, no parentheses, no identifiers) and then validated with Next's own
`parseAppSegmentConfig`.

## Diagnostics

| Code | When |
| --- | --- |
| `NS_HOST_NON_LITERAL_SEGMENT_CONFIG` | segment config exported as anything but `export const x = <literal>` |
| `NS_HOST_INVALID_SEGMENT_CONFIG_VALUE` | a literal Next's schema rejects |
| `NS_HOST_CACHE_COMPONENTS_CONFLICT` | a key the host's `cacheComponents` setting makes Next reject |
| `NS_HOST_CLIENT_SERVER_ONLY_EXPORT` | metadata family, `generateStaticParams` or segment config from a `'use client'` module; a route/metadata route source marked `'use client'` |
| `NS_HOST_CLIENT_REQUIRED` | `error` / `global-error` source without `'use client'` |
| `NS_HOST_ROUTE_DEFAULT_EXPORT` | a Route Handler source with a default export |
| `NS_HOST_ROUTE_NO_METHODS` | a Route Handler source without HTTP methods |
| `NS_HOST_MISSING_DEFAULT` | a component or metadata file source without a default export |
| `NS_HOST_UNSUPPORTED_EXPORT` | any export the table does not list for the kind |
| `NS_HOST_UNCLASSIFIABLE_EXPORT` | `export *`, destructuring exports, `enum`, `namespace`, `export =` |
| `NS_HOST_SERVER_ACTIONS_MODULE` | a `'use server'` module used as a route file |
| `NS_HOST_PARSE_ERROR` | the source does not parse |
| `NS_HOST_INVALID_RATE_LIMIT` | a project or plugin API route exports `rateLimit` as anything but the literal `false` (`rate-limit.mjs`) |
| `NS_HOST_UNSUPPORTED_NEXT_VERSION` | the project resolves a Next.js outside `~<table next>` (other minor/major, lower patch, prerelease), or it lacks the segment config schema |
| `NS_HOST_VARIABLE_LOOKUP` | an emitted facade leaves the allowed facade grammar (internal guard, `static-imports.mjs`) |
| `NS_HOST_TARGET_MISMATCH` / `NS_HOST_INVALID_SPECIFIER` / `NS_HOST_UNKNOWN_KIND` | bad emitter input |

## `nextspark prepare`: the whole generated host

The emitter writes one file; `prepare.mjs` generates the complete `src/app` and the registries of a project and
publishes them safely. The CLI runs `prepare-cli.mjs` from the installed core (`prepare`, `prepare --check`,
`build`, `dev`).

| Module | Role |
| --- | --- |
| `core-routes.mjs` | `loadCoreRouteManifest()`: the one loader of core's route manifest, resolved through the core package's `exports` (`@nextsparkjs/core/routes/manifest.json` -> `dist/routes/manifest.json` in the npm package; in a source checkout `src/routes/manifest.json` wins, since dist is its build output); `[{ kind, target, specifier, protected? }]`. Route modules resolve the same way (`./routes/*`). `routes/variants.json` (`{ "cacheComponents": [...] }`) replaces entries by target when the host's `cacheComponents` is on (read from a literal in `next.config.*`; unknown keeps the manifest entries). No manifest: the core cannot generate the host and prepare fails saying so. Protected targets: `protected: true` or core's `PROTECTED_PATHS` (any level: a whole-file override cannot keep part of a route protected). |
| `mode.mjs` | `resolveHostMode()`: `host` when the core ships the manifest and `src/app` is absent, holds only empty directories and placeholders (`.gitkeep`, `README.md`, OS files - never touched), or is described by a valid generation record (parses, schema version, NextSpark paths only); `legacy-app` when `src/app` holds files and there is no valid record (a committed app tree): `prepare`, `build` and `dev` fail with `LEGACY_APP_MESSAGE`, pointing to `nextspark migrate`, which detects the tree with this same decision and converts it; `no-manifest` otherwise. Read-only. |
| `plan.mjs` | `planHost()`: core < plugin (`plugins/<name>/templates/**`, `plugins/<name>/api/**` at `api/plugins/<name>/**`) < project (`templates/**`, `api/**` at `api/**`). A higher layer replaces a lower one at the same directory + file stem (+ cache-mode suffix); two files of one layer, two plugins, anything over a protected core route, and a page with a Route Handler in one directory are diagnostics naming both sources. Sorted by code unit, so deterministic. |
| `render.mjs` | One facade per planned route through `emitFacade` (every cache mode the host builds is checked; a problem all modes share is reported once). Facade cache for the watcher. |
| `generation.mjs` | Ownership, validation, publication, lock and check (below). |
| `prepare.mjs` | `prepareHost()` / `checkHost()` / `watchHost()` for any host config; `predictHost(config)` resolves to `{ ok, routes, notices, diagnostics, checks }` without writing or throwing on a diagnostic (dry runs such as `migrate --dry-run`): it runs the plan, the emission, the grammar gate, the ownership preflight of `src/app` and the contracts plan with its preflight, read-only, and says which ran in `checks` (`passed` / `failed` / `skipped` / `none`); the registry build is never run there (it writes a staging directory), so `checks.registries` is `skipped`; `projectHostConfig()` for a real project (the registry build runs into a staging directory: `NEXTSPARK_REGISTRIES_OUT`, `NEXTSPARK_GENERATED_HOST=1`, and its wall-clock timestamps are normalized so the same sources give the same bytes). |

Publication (`generation.mjs`):

- everything is planned, emitted, staged and validated (facade grammar for `src/app`, registry grammar where the producer
  declares it, paths only under `src/app` and `.nextspark/registries`) before the first write; a failure leaves the previous
  generation untouched;
- changed files are written to a temporary file beside them and renamed; unchanged files are not touched;
- `.nextspark/generation.json` records schema version, mode, core/cli/next versions, the hash of every input and every
  emitted file. It is written ahead (previous + new files, `pending: true`) and then final, so an interrupted run never leaves
  a file no record owns;
- ownership is proven against the disk: a `src/app` file is NextSpark's only when the previous record lists it **and** its
  bytes still have the recorded hash (for an interrupted publication, also the hash it had before). A listed file edited
  since, or a leftover record next to a tree that replaced the generated one, is foreign: never overwritten or deleted;
- only files of the previous record are deleted; nothing is written, replaced or deleted through a symlink (a symlink in or
  on the way to `src/app` or the registries stops the run; safe-fs refuses it too);
- `src/app` files that are not provably NextSpark's stop the run before it writes, naming them; an unowned file
  byte-identical to its generated content is adopted; placeholders (`.gitkeep`, `README.md`, OS files) are ignored;
- a file already in place of a generated registry that the previous record does not own is taken over only when a
  NextSpark registry build wrote it (byte-identical, or stamped `Generated at: <ISO time>` / `// Generated by NextSpark` in
  its first 30 lines, which every legacy registry carries); anything else stops the run (`NS_HOST_FOREIGN_REGISTRY`) and is
  left untouched;
- one writer at a time: `.nextspark/generation.lock`, only ever created with O_EXCL. A stale lock (its process gone on
  this machine; from another machine, older than 10 minutes; unreadable for more than 30 seconds) is never deleted and
  re-created: the one process that creates `generation.lock.takeover-<inode-mtime>` with O_EXCL re-checks the lock is
  still that instance and renames its own claim over it atomically. A live holder on this machine is never taken over.

`prepare --check` writes nothing and targets the production host (what `prepare` and `build` write): a `nextspark dev`
generation is reported as such; `--check --dev` checks it instead. `src/app` is re-rendered in memory and compared byte for
byte (missing / stale / foreign); a publication that did not finish (`pending`) is not fresh;
registries are compared with their recorded hashes and inputs with their recorded hashes (the registry build is not
side-effect free, so it is not re-run); no `src/app` and no record: "run nextspark prepare".

`nextspark dev`: the first generation must succeed before Next starts; `watchHost` then regenerates on changes under
`templates/`, `api/`, `plugins/`, `entities/`, `config/`, `blocks/`, `messages/`, `nextspark.config.ts`, core's route manifest
and packaged plugin manifests. A failed regeneration keeps the last valid host published **and served** (routes keep answering 200), prints the
diagnostic, and shows it in the browser without pretending the change was applied. What stays is `src/app`: the route
files, the names they forward and the segment config copied as literals. Edits inside modules the existing facades import
still reach Next directly (a facade re-exports the module, it does not copy it), so the message says exactly that:

- a dev generation adds three fixed files under `src/app/_nextspark/` (a private folder, never routes):
  `generation-diagnostic.ts` (`export const generationDiagnostic = ""`), `generation-status-reporter.tsx` (a `'use client'`
  component that, while the diagnostic is non-empty, shows it in a fixed dev panel and calls `console.error` once, which Next's
  dev indicator lists as an issue; the panel is what stays reliable, since Next clears its issue list on every Fast Refresh
  rebuild) and `generation-status.tsx`
  (`withGenerationStatus(Layout)`, the root layout with the reporter rendered before its children);
- the dev root layout facade is `export default withGenerationStatus(<the layout>)` (`dev-facade` grammar; `<the layout>` is the
  root layout's default import, or its composition with a project layout, see below);
- on failure the watcher rewrites only `generation-diagnostic.ts`; Next's HMR re-renders and the reporter shows the
  diagnostic. The next successful regeneration empties it and the panel goes away, with no reload.
  **Not while a source does not parse** (`NS_HOST_PARSE_ERROR`, an entity config that does not parse, or any TypeScript or
  JavaScript source changed in this or an earlier batch that still does not parse; `nextspark.config.*`, `next.config.*`, tests and
  specs aside: the app does not import them, so the panel is where their errors show): Next already shows
  that error, and rewriting a module of the graph while one of them does not parse makes Turbopack drop the fix that
  follows (it kept serving the code from before the error). Nothing is written until the sources parse again.
  The regression guard is the unit test with its negative control (`host-robustness.test.mjs`); `scripts/performance/dev-recovery-e2e.mjs`
  checks recovery and the panel over HTTP (`--project <dir>` on an installed project is where the Turbopack failure reproduced; the
  fixture target does not reproduce it).

A production preparation never emits these files or the composition: zero bytes and zero code in production builds.

## Composition, per-entity routes, API namespaces (stage 4)

A facade forwards one module. Six things are not that, and the host writes them as **composed facades**
(`composed-facade` grammar, `static-imports.mjs`): the facade grammar plus a fixed `.css` import and at most one
`export default wrapper(...)` / any number of `export const NAME = wrapper(...)`, where the callee is one of an explicit
allowlist of core composition wrappers (`CORE_COMPOSITION_WRAPPERS`: each imported from the exact `@nextsparkjs/core/routes/_internal/...`
module that exports it) and each argument is an imported name or a nested allowed call. The one literal is the array of child entity
names that is `createEntityDetailRoute`'s second argument. A runtime resolver (a template lookup by key, a registry accessor,
a wrapper name imported from anywhere else) is never a callee, however it is spelled; there is no member access, no other call, no
spread and no expression. A host with another core (the conformance fixture) passes its own wrappers (`compositionWrappers`).

- **The root layout** always imports the project stylesheet (`import "@/styles/globals.css"`, when `styles/globals.css`
  exists). A project `templates/layout.tsx` is composed, never a replacement:
  `export default withRootLayout(NextSparkTemplate)`, core's providers and protections around the project's layout. The
  project's `metadata` is forwarded (the root layout is protected at the render level, which allows it); without one, core's.
- **Group layouts** `(auth)`, `(public)`, `superadmin`, `devtools`: a project override is composed with core's
  `with<Group>Messages` wrapper, so it cannot drop the group's message provider; **core's protection is always the outer layer**:
  the role-guarded groups (`superadmin`, `devtools`) are composed with `withSuperadminGuard` / `withDevtoolsGuard`, which put the
  messages, the dashboard providers and the role guard around the project's layout (and `with...Messages` alone is not callable from a
  facade). A core layout declares the wrapper as `compose: { wrapper, specifier }` in the core route manifest
  (`routes-manifest.mjs`, `COMPOSED_ROUTES`); a route protected at `protected_all` is never composed. An override without
  `metadata` keeps core's, forwarded from the wrapper's module when it exports it (the `(auth)` and `(public)` modules
  `_internal/{auth,public}-layout` and their `.cc` variants do), else from core's layout. Those modules import no component:
  every client component a route's server graph reaches ships to the browser even if never rendered, so forwarding from
  core's layout would put its default navbar on every page of an override (core's default layouts live in
  `_internal/default-{auth,public}-layout`).
- **Role-gated areas** `/superadmin`, `/devtools`: every page, layout, template and default served under the area's URL (core's,
  a plugin's or the project's, in any route group) is composed with `withSuperadminAccess` / `withDevtoolsAccess`
  (`routes/_internal/area-access`; `area-access.cc` in a Cache Components host, the same check inside the segment's own Suspense
  boundary), which checks the session's role on the server before the segment renders, and its
  `generateMetadata` with `withSuperadminMetadata` / `withDevtoolsMetadata` (metadata resolves apart too); a Route Handler's
  methods but `OPTIONS` with `withSuperadminRouteAccess` / `withDevtoolsRouteAccess` (401 / 403 JSON; `OPTIONS` is forwarded
  unchecked, so it must not return data). An intercepting route
  counts by the URL it intercepts (`(.)`, `(..)`, `(...)` resolved). A metadata file there (`icon`, `opengraph-image`, `sitemap`, ...)
  cannot be guarded: it stays a plain facade and is a warning notice (`NS_HOST_AREA_FILE_UNGUARDED`). The area's own
  layout is left out: its message wrapper checks. A layout cannot protect its pages on its own (Next renders every segment of a
  route separately), so the check does not depend on the project's proxy. The area layout declares it as `access: { wrapper, metadata, handler, specifier }`
  in the core route manifest (`ACCESS_ROUTES`); a Cache Components variant keeps the access of the entry it replaces.
- **One route per entity** (`entity-routes.mjs`). For every entity of the project and of enabled plugins, the host writes
  `dashboard/(main)/<entity>/{layout,error,loading,page,create/page,[id]/page,[id]/edit/page}` and, for builder entities with
  `access.basePath` and `access.public: true`, the public item route (`(public)<basePath>/[...slug]`, `[slug]` at `/`) and archive
  route. A builder entity with a base path and `access.public` false or unset gets neither: `NS_HOST_ENTITY_NOT_PUBLIC` (a notice,
  printed as `Warning:`) names it and the setting, a page or Route Handler of the project or of a plugin
  at its item URLs (`<basePath>/<dynamic segment>`: any route group, cache mode or param name, `[x]`, `[...x]` or `[[...x]]`) is not used
  and is reported as `NS_HOST_ENTITY_TEMPLATE_NOT_USED` (`Warning:`, naming the file), and the routes answer notFound() for it at request time too; a public
  entity with an archive page and no basePath gets `(public)/<entity>/[[...rest]]`. Each is a facade over the entity's config and
  one factory of its own core module (`routes/_internal/entity-*-route`, `public-*-route`): a route's module graph holds one
  entity config and only the client components its page renders, and nothing looks an entity or a template up by a key.
  A project template for an entity page (`templates/dashboard/(main)/<entity>/{page,create/page,[id]/page,[id]/edit/page}.tsx`; a public
  item's `templates/(public)<basePath>/[slug]/page.tsx`) is composed into that route as its `Template` argument, and runs only after
  core's checks (the entity is enabled and shown in the dashboard): the generated factory is never discarded. The client code these
  routes carry does not depend on how many entities the project has: the create/edit views and the shared client modules read the
  dashboard's own registry (hydrated from server props), never the generated client registry that imports every entity's config. Core has no `[entity]/**` or `(public)/[...slug]` route: `metadata`, `error` and `loading` of the per-entity routes come from `routes/_internal/entity-{list,detail}-metadata`, `entity-error` and `entity-loading`.
  What decides the routes is read from the config's source as literals (`enabled`, `ui.dashboard.showInMenu`,
  `access.basePath`, `access.allowNestedSlugs`, `access.public`, `builder.enabled`, `ui.public.hasArchivePage`; the config
  object must be an object literal): a flag that cannot be read keeps the dashboard route (core's checks answer notFound()),
  and one that decides a public route is a diagnostic (`access.public` too, for a builder entity with a base path). An entity's `slug` must equal its directory name.
  **The template replaces** (`NS_HOST_ENTITY_ROUTES_REPLACED`, a notice, not an error): a project page or Route Handler under
  `templates/`, in a directory other than `dashboard/(main)/<entity>/` (another route group, `templates/dashboard/(staff)/<entity>/...`),
  that **collides** with the entity's generated dashboard routes takes their URL over, as its static segment took core's `[entity]`
  route in 0.x. It collides when it serves a URL a generated page serves (`/dashboard/<entity>`, `/create`, `/[id]`, `/[id]/edit`,
  whatever the dynamic segment's name) or names their dynamic segment differently (`[cohortId]` where they have `[id]`): Next refuses
  both. A template that only adds a URL (`(staff)/<entity>/export`, `(staff)/<entity>/[id]/members`) does not collide: the generated
  routes stay and both are served. On a collision the whole generated dashboard subtree of that entity is dropped (layout, error,
  loading, list, create, `[id]`, `[id]/edit`, and the metadata they forward), so the entity is not served from two route groups
  under two layouts. The entity's permission layout (`createEntityLayoutRoute`) is a security boundary and stays: it is written at the
  top directory of every project tree serving the URL (`dashboard/(staff)/<entity>/layout.tsx`, protected, so a project layout at that
  exact file is refused). Templates inside `dashboard/(main)/<entity>/` keep the composition above. `prepare` prints the replacement
  as `Info:`; `planHost` returns it in `notices`.
  An optional catch-all also serves its parent's URL (`(staff)/<entity>/[[...slug]]` collides with `/dashboard/<entity>`). An intercepting
  route (`(.)[id]`, `@modal/(.)[id]`) is a segment of its own, never a collision: nothing is replaced, and it renders only in its
  own layout tree, not under the generated `[id]` route's layout.
- **Billing webhooks** (`webhooks.mjs`): `nextspark.config.ts` `billing.webhookExtensions: { stripe: './lib/...', polar: './lib/...' }`
  replaces core's webhook route with `export const POST = createStripeWebhookRoute(stripeWebhookExtensions)`, the module
  imported statically (it must export `stripeWebhookExtensions` / `polarWebhookExtensions`).
- **API rate limit** (`rate-limit.mjs`, #226): every method of a Route Handler under `src/app/api/` that a project or plugin
  provides is composed with core's per-address limit, as the 0.x theme and plugin dispatchers did:
  `export const GET = withReadRateLimit(NextSparkGET)` (`GET`, `HEAD`), `withWriteRateLimit` (`POST`, `PUT`, `PATCH`, `DELETE`),
  `OPTIONS` forwarded (`routes/_internal/route-rate-limit`: the limit only, no CORS or origin check). A method declared in the
  source as core's `withRateLimitTier(...)` (named or namespace import from `@nextsparkjs/core/lib/api/rate-limit` or
  `@nextsparkjs/core/lib/api`; the call itself, a wrapper chain around it, or one local constant) is forwarded as is, anything
  else is wrapped (`withRateLimit` limits API-key requests only and does not count); `export const rateLimit = false` forwards the whole
  route and is itself never forwarded (any other value: `NS_HOST_INVALID_RATE_LIMIT`). The host config's `rateLimit`
  (`coreRouteRateLimit()`) turns it on; `projectHostConfig()` sets it, the conformance fixture does not. Under a role-gated area
  the rate limit is the outer call, around the area check. The opted-out routes are listed in one Info notice
  (`NS_HOST_RATE_LIMIT_OPT_OUT`, `renderHost().notices`).

**API namespaces** are enforced by the plan (`plan.mjs`): core owns `/api/v1/**`, the project's `api/` is served at `/api/<path>`,
each plugin's `api/` at `/api/plugins/<plugin>/**`. Stops generation (`NS_HOST_API_NAMESPACE`): a project `api/` route in `/api/v1/**` or
`/api/plugins/**`; a project `templates/api/v1/...` route that replaces no route core has, or `templates/api/plugins/<plugin>/...` route that
replaces no route that plugin serves (a project may override an existing route there, never create one); a plugin route (from `api/` or
`templates/`) outside `/api/plugins/<its-name>/**`. There are no dispatchers: `/api/v1/theme/**` and `/api/v1/plugin/**` do not exist, and the
route-handlers registry imports no handler (API route metadata only).

**Per-entity API override.** One exception to "a project never creates a route under `/api/v1`": `templates/api/v1/<entity>/<rest>` is allowed
when `<entity>` is an entity of the project or of an enabled plugin (`planEntityRoutes(...).entities`, passed to `planHost` as `entityNames`) and
`<rest>` mirrors a route core serves under `api/v1/[entity]` (`route.ts`, `[id]/route.ts`, `[id]/child/[childType]/route.ts`,
`[id]/child/[childType]/[childId]/route.ts`, the same segment names): the planner checks the slot of `api/v1/[entity]/<rest>`, so it follows
whatever core ships. The generated route is the static `/api/v1/<entity>/<rest>`, which Next resolves before `[entity]`; other URLs of the
entity stay on the generic handler. `planHost` returns an Info notice per entity (`NS_HOST_ENTITY_API_OVERRIDDEN`, with the URLs and the
files; `prepare` prints it): the override **replaces** the generic handler for those URLs, so authentication, permissions, rate limits and the
entity's hooks are the project's responsibility there (business rules belong in entity hooks, which every path keeps). `prepare` prints it as `Warning:`. Core's own API namespaces (every first segment core serves under `api/v1/`: `users`, `teams`, `auth`, `billing`, `cron`, `api-keys`, `devtools`, `media`, `media-tags`, `blocks`, `patterns`, `post-categories`, `team-invitations`) are never an entity's: the override is refused there with a hint naming the collision, and an entity named like one is `NS_HOST_ENTITY_CORE_API_NAMESPACE` (checked in `planEntityRoutes`, before any route is planned). A route core serves, replaced at its exact path (`templates/api/v1/users/route.ts`), is allowed as before and now also reports `NS_HOST_CORE_API_REPLACED` (`Warning:`), so no core API is replaced silently. Any other file under
`templates/api/v1/` that replaces no core route, an unknown entity or a shape core does not serve stays `NS_HOST_API_NAMESPACE`.

The plan also refuses what Next.js would fail on later, with the routes named: two pages for one URL in different route
groups (`NS_HOST_URL_CONFLICT`) and dynamic segments it cannot tell apart at one level (`[slug]` and `[entity]`, `[...a]`
and `[[...a]]`: `NS_HOST_DYNAMIC_SEGMENT_CONFLICT`).

**Cache Components** variants (`routes/variants.json`, used when the project's next.config says `cacheComponents: true`, like the PPR
root layout) are of three kinds: the `page.cc.tsx` files of the routes whose segment config Next.js rejects with it (login, signup and both
docs pages: `dynamic`, `dynamicParams`); the `layout.cc.tsx` files of the five layouts that load messages (`(auth)`, `(public)`, `devtools`,
`superadmin`, `dashboard`), because `await getMessages()` reads the request and Next.js refuses that outside Suspense while prerendering
(`_internal/group-layouts.cc`: `(auth)`, `(public)` and `devtools` provide the build-time `STATIC_MESSAGES` of the default locale and put
their pages behind Suspense; `dashboard` and `superadmin` load request messages inside a Suspense boundary and declare their area
request-time; an override composes through `_internal/{auth,public,superadmin,devtools}-layout.cc`, one module per group, so the
guards, dashboard providers and sidebars of the protected groups never reach a public or auth route); and the root layout (`layout.ppr`, whose `main` puts the page behind Suspense as the net for a page that reads `params`,
cookies or headers below no other boundary). The public item routes omit `revalidate` in that mode; a host that builds in Cache Components
mode only imports `_internal/public-item-route.cc`, whose reads of the published item and of the patterns it references are `'use cache'`
functions (`cacheLife` of an hour for an item, tags `entity:<slug>` and `public-item:<slug>:<item slug>`).

**Instant navigation (dev server).** On a Cache Components host `next dev` runs Next.js's instant-navigation check on every page it
serves: it renders the route as a navigation into each segment and logs `Could not validate that a segment in your UI has instant
navigation` (a segment the server render never reached), `... uncached data during prerendering or a navigation` or `... runtime data
(`cookies()`, `headers()`, `params`, `searchParams`) accessed outside of <Suspense>`, and the dev badge shows an issue. The pages work; the check is
about what a navigation would wait for. What it found in the starter, and the fix, are in core, so no project file changes:
- The dashboard's client auth gate (`AuthenticatedDashboardLayout`) rendered a skeleton instead of `children` while the session loaded, so the
  route's own segments were never rendered on the server ("dropped"). It renders the page while the session loads and keeps the signed-in side effects for
  a known user.
- `dashboard/(main)/layout.cc` (one more `layout.cc.tsx` variant, for a layout whose permission check reads the request) renders the shell at once and runs the permission check (session and database) in a
  Suspense boundary around the page; the ISR layout still checks before rendering anything, so a denied request keeps its real redirect.
- The pages that await `params` (the public item routes `public-item-route.cc`, the dashboard detail and edit routes `entity-detail-route.cc` /
  `entity-edit-route.cc`, written for a host with `cacheComponents` on and nothing else) sit behind a boundary of their own
  (`_internal/suspended-route`); the `/signup` variant and core's default home (`(public)/page.cc`) render the page that redirects behind one too, and the public archive and the permission-denied
  page read `searchParams` behind one. The status a visitor gets does not change: the layouts already streamed these pages after their shell.
- A project without `public/favicon.ico` made the browser's own `GET /favicon.ico` hit the public `[slug]` page (an HTML "not found" with a 200);
  new projects ship a favicon, and a request for a page that does not exist is now answered inside a boundary the check accepts.
- `/superadmin` and `/devtools` (#213): their client guards hid the page from the server render until the session loaded, and each segment awaited
  its role check outside Suspense. In a Cache Components host the area layouts (`superadmin/layout.cc`, `devtools/layout.cc` and the composed
  override through `withSuperadminGuard` / `withDevtoolsGuard`) pass `serverChecked` to their guard, which they render only after the role
  check, and every segment under an area is composed from `_internal/area-access.cc`: the same check, inside the segment's own Suspense boundary,
  the segment rendered only once it has passed. The ISR host keeps `_internal/area-access` and its guards' loading state.
Opting a segment out is `export const instant = false` in its source (a literal, as the facade rules require); nothing sets it today.

Tests: `node --test packages/core/scripts/build/registry/host/__tests__/*.test.mjs`.
