> ## Documentation Index
> Fetch the complete documentation index at: https://pulian.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# RSC Plugin Platform

> Build independently deployed RSC plugin microservices behind one public Next.js Host.

# RSC Plugin Platform

Use this guide to scaffold, customize, run, secure, and verify independently compiled React Server Component plugins behind one public `HttpNext` listener.

> API and package boundaries: [`@hile/rsc`](/packages/rsc).

This is the canonical implementation guide for independently compiled React Server Component plugins behind one public Next.js listener and origin. Pages, assets, Server Functions, and development SSE use different paths on that same listener. It is intentionally domain-free and detailed enough for an AI agent with no repository history to scaffold, customize, run, and verify the topology.

Also read [RSC packages](/packages/rsc) for package boundaries and API contracts. The supported quick start is scaffold-first: generate the maintained `create-hile` templates and customize them. The snippets below explain and extend those complete files; they are not a request to reimplement lifecycle plumbing from a blank directory.

## Target Topology

```text theme={null}
Browser
  -> one public HttpNext listener/origin
       -> Host Next layout and route
       -> plugin browser assets and CSS
       -> Server Function POST
       -> Host RSC runtime and exact build lease
            -> internal @hile/micro stream -> plugin RSC service
            -> internal @hile/micro request -> plugin Server Function -> @hile/model

Registry
  <- explicitly secured plugin announcement
  -> Host discovers, authorizes, streams to disk, verifies, and activates
```

The plugin has an internal Micro TCP listener because it is a microservice. It has no public HTTP listener. The Host is the only process that owns public HTTP. Registry presence plus an announcement accepted by the selected trust policy causes automatic installation and activation; there is no static plugin list or manual activation call.

## Complete Example

The smallest complete composition has three parts:

```ts theme={null}
// Plugin: verified artifact + scanned models + one internal Micro runtime.
const service = new RscPluginService({
  manifest,
  renderer: createOfficialRscRenderer(artifactRoot),
  serverFunctions: new RscArtifactServerFunctionRuntime(artifactRoot),
})
await service.load(modelsDirectory)
const pluginRuntime = new HileRscPluginRuntime({
  application: pluginApplication,
  service,
  port: pluginMicroPort,
  discovery: { namespace, instanceId, priority: 0, generation: 0, artifactRoot, authentication },
})
await pluginRuntime.start()

// Host service: automatic trusted discovery + catalog-backed internal client.
const locator = createCatalogRscPluginLocator(deployments, (deployment) =>
  createHileRscPluginClient(hostApplication, deployment.namespace))
const discovery = new HileRscDiscoveryHost({
  application: hostApplication,
  artifacts,
  deployments,
  runtime: HILE_RSC_RUNTIME,
  snapshotConcurrency: 16,
  authorize: discoveryAuthorizer,
})
await discovery.start()

// Dynamic Next page: exact active build + Flight decode + request cancellation.
const tree = await new RscHostRuntime({
  locator,
  decoder: { decode: decodePluginFlight },
}).render({
  pluginId,
  request: { buildId: active.buildId, path, searchParams },
  signal: getHttpNextRequestSignal(),
  timeout: 30_000,
  idleTimeout: 10_000,
  window: 8,
})
```

This summary omits startup rollback, middleware mounting, provider wrapping, credentials, and shutdown. The generated template files supply those required pieces and the following sections explain their customization points; do not deploy the summary by itself.

## AI Implementation Rules

Before writing code, preserve these invariants:

1. Use `HileRscPluginRuntime` as the plugin lifecycle composition root.
2. Use `HileRscDiscoveryHost` for automatic Host deployment.
3. Use `RscHostRuntime` plus `decodePluginFlight()` inside a dynamic Next route.
4. Use a module-level `'use server'` file with `defineRscServerFunction()` and call scanned `defineActionModel()` definitions through its explicit API argument.
5. Keep exact React/RSC versions identical across Host, plugin, and build config.
6. Treat `buildId` and artifact directories as immutable. A changed artifact requires a new build ID or development revision.
7. Choose discovery trust explicitly: bind each HMAC `keyId` to a plugin-ID allowlist, or use `trusted-internal` only when every internal Micro peer is trusted.
8. Mount plugin assets, Server Functions, and optional development SSE on the same Host listener/origin.
9. Pass the request abort signal into `RscHostRuntime.render()`.
10. Do not expose server bundles, artifact paths, namespaces, or internal message addresses to the browser.
11. Configure bounded render timeouts/window and Registry snapshot concurrency for production.
12. Keep loading/error renderer functions inside a Host Client Component.
13. Choose remote or Host Suspense ownership explicitly; keep a Host-owned boundary stable across navigation when the product must retain the previous page until the delegated remote boundaries are ready.

## Recommended Project Layout

```text theme={null}
rsc-plugin/
  .env
  .env.prod
  hile-rsc.json
  package.json
  scripts/dev.mjs
  src/
    models/example/increment.model.ts
    plugin/actions.ts
    plugin/interactive.tsx
    plugin/page.tsx
    plugin/plugin.css
    services/plugin.boot.ts

rsc-host/
  .env
  .env.prod
  next.config.ts
  package.json
  src/
    app/layout.tsx
    app/page.tsx
    app/plugins/[pluginId]/[[...path]]/page.tsx
    services/runtime.boot.ts
```

Inside a `create-hile` template source, `_env` and `_env.prod` are generator placeholders. The generated project contains `.env` and `.env.prod`; do not create runtime files literally named `_env`.

The fastest supported start is to scaffold twice and select the indicated template in the interactive prompt:

```bash theme={null}
npx create-hile create rsc-plugin
# select: rsc-plugin
npx create-hile create rsc-host
# select: rsc-host
```

The generated projects already contain the lifecycle composition described below. Rename plugin identity, namespaces, ports, credentials, Host allowlist, and package names together before running them.

## 1. Install Exact Dependencies

Plugin runtime and compiler:

```bash theme={null}
pnpm add @hile/cli @hile/core @hile/micro @hile/model @hile/rsc @hile/rsc-discovery-hile react@19.2.8 react-dom@19.2.8 react-server-dom-webpack@19.2.8
pnpm add -D @hile/rsc-build @hile/rsc-development @types/node @types/react @types/react-dom fix-esm-import-path typescript
```

Host runtime:

```bash theme={null}
pnpm add @hile/cli @hile/core @hile/http-next @hile/micro @hile/rsc @hile/rsc-discovery-hile @hile/rsc-next next@16.3.0 react@19.2.8 react-dom@19.2.8
pnpm add -D @hile/rsc-development @types/node @types/react @types/react-dom fix-esm-import-path typescript
```

The runtime versions above are the repository's validated compatibility tuple. For `@hile/*` packages, use the mutually compatible versions already written by the generated templates; when copying commands outside this monorepo, do not combine an older RSC core with newer build/discovery adapters. Do not use ranges for React, React DOM, `react-server-dom-webpack`, or Next without running the complete compatibility suite.

The Host does not directly install `react-server-dom-webpack`: `@hile/rsc-next` uses the compatible RSC implementation compiled into the pinned Next version. The plugin compiler/runtime pins the standalone package explicitly.

Use these Host scripts from the generated template:

```json theme={null}
{
  "scripts": {
    "build:runtime": "tsc -p tsconfig.runtime.json && fix-esm-import-path --preserve-import-type ./dist",
    "build:next": "next build",
    "build": "pnpm build:runtime && pnpm build:next",
    "dev": "hile start --dev --env-file .env",
    "start": "hile start --env-file .env.prod"
  }
}
```

The templates also provide `tsconfig.json`, `tsconfig.runtime.json`, and `next.config.ts`. Keep runtime boot files in the runtime TypeScript build and Next app files in the Next build. Do not move the Hile boot service into the App Router compiler.

## 2. Configure The Immutable Plugin Build

Create `hile-rsc.json`:

```json theme={null}
{
  "pluginId": "org.example.rsc-plugin",
  "cwd": ".",
  "entry": "src/plugin/page.tsx",
  "routes": [{ "path": "/page", "entry": "default" }],
  "metadata": {
    "displayName": "Example plugin",
    "description": "An independently deployed Hile RSC plugin",
    "navigation": [
      { "id": "page", "label": "Example", "path": "/page", "order": 100 }
    ]
  }
}
```

`pluginId` is the stable logical plugin identity. It may be one lowercase identifier such as `analytics` or a lowercase namespaced identifier such as `org.example.analytics`. The omitted `buildId` is generated for each immutable build, while the omitted `outdir` defaults to `.hile-rsc`; set `RSC_BUILD_ID` when a deployment system must provide the identity. Explicit `buildId` and `outdir` remain supported. Optional `styles` entries are build-scoped CSS files: use an explicit relative path such as `./src/plugin/theme.css`, an absolute path, or a package CSS export such as `@example/ui/theme.css`. The compiler content-hashes, deduplicates, copies, and integrity-declares them once per immutable build. These raw static inputs must be self-contained because relative `url()` dependencies and external `@import` files are not copied or rewritten. `routes` maps plugin-internal paths to exports from the server entry and may use named single-segment parameters such as `/items/[itemId]`. Captured values are supplied through `RscRouteProps.params`; exact routes win over parameterized routes, and ambiguous equal-specificity patterns are rejected during manifest validation. Optional `metadata` travels in the same immutable manifest; each navigation path must reference a declared static route because a parameter pattern is not a concrete destination. The Host URL prefix, authorization, visibility, localization, and final navigation components remain Host policy and are not configured here.

For a shared or generated stylesheet that is not imported by the client graph, add the optional field to the same config:

```json theme={null}
{
  "styles": ["./src/plugin/theme.css", "@example/ui/theme.css"]
}
```

With the example Host catch-all, this route is opened at `/plugins/org.example.rsc-plugin/page`. The later `/plugins/demo.rsc.capabilities` and `/details` URLs belong to the richer private test suite, whose build config declares those routes; they are not routes from this minimal config.

Useful scripts:

```json theme={null}
{
  "scripts": {
    "build:rsc": "hile-rsc build",
    "build:runtime": "tsc -b && fix-esm-import-path --preserve-import-type ./dist",
    "build": "pnpm build:rsc && pnpm build:runtime",
    "verify": "hile-rsc verify",
    "dev:rsc": "hile-rsc-dev --config hile-rsc.json --state .hile-rsc/development.json --namespace org.example.rsc-plugin.dev --outdir .hile-rsc/development",
    "dev:service": "NODE_OPTIONS=--conditions=react-server RSC_DEVELOPMENT_STATE=.hile-rsc/development.json hile start --dev --env-file .env",
    "start": "NODE_OPTIONS=--conditions=react-server hile start --env-file .env.prod"
  }
}
```

The plugin process must use `NODE_OPTIONS=--conditions=react-server` so React resolves its server exports.

## 3. Write Server And Client Components

Server entry `src/plugin/page.tsx`:

```tsx theme={null}
import type { RscRouteProps } from '@hile/rsc/plugin'
import InteractiveBoundary from './interactive'

export default async function PluginPage({ rsc, searchParams }: RscRouteProps) {
  const initialValue = Number(searchParams?.count ?? 0)
  return (
    <section data-rsc-plugin={rsc.pluginId}>
      <h1>Independent RSC plugin</h1>
      <InteractiveBoundary initialValue={initialValue} rsc={rsc} />
    </section>
  )
}
```

The default is already a Server Component. Do not add `'use server'` to mark it as one. `rsc` contains the exact selected `{ pluginId, buildId }`, including development revision suffixes. Pass this identity into interactive boundaries instead of copying the base build ID from config.

Client boundary `src/plugin/interactive.tsx`:

```tsx theme={null}
'use client'

import { useActionState, useState } from 'react'
import { increment } from './actions'
import './plugin.css'

type Identity = { pluginId: string; buildId: string }
type State = { value: number; invoked: boolean }

export default function InteractiveBoundary(props: { initialValue: number; rsc: Identity }) {
  const [local, setLocal] = useState(props.initialValue)
  const [state, formAction, pending] = useActionState(increment, {
    value: props.initialValue,
    invoked: false,
  } satisfies State)

  return (
    <div data-build-id={props.rsc.buildId}>
      <button type="button" onClick={() => setLocal((value) => value + 1)}>
        local {local}
      </button>
      <form action={formAction}>
        <input name="value" type="number" defaultValue={props.initialValue} />
        <button disabled={pending}>run model</button>
      </form>
      <output>{state.invoked ? state.value : 'not invoked'}</output>
    </div>
  )
}
```

The custom directive graph recognizes the directive prologue, builds browser and SSR graphs with esbuild, externalizes the shared React runtime, emits imported CSS and lazy chunks, and records integrity in `plugin.json`.

Minimal `src/plugin/plugin.css` for proving style delivery:

```css theme={null}
[data-rsc-plugin] {
  padding: 1rem;
  border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
  border-radius: 0.5rem;
}
```

## 4. Add A Model And A Server Function

Create `src/models/example/increment.model.ts`:

```ts theme={null}
import { defineActionModel } from '@hile/model'

export default defineActionModel(async (input: { value: number }, invocation) => {
  if (invocation.signal.aborted) throw invocation.signal.reason
  if (!Number.isFinite(input.value)) throw new TypeError('value must be finite')
  return { value: input.value + 1 }
})
```

Create `src/plugin/actions.ts`:

```ts theme={null}
'use server'

import { defineRscServerFunction } from '@hile/rsc/plugin'

export const increment = defineRscServerFunction(async (
  api,
  _previous: { value: number; invoked: boolean },
  formData: FormData,
) => {
  const raw = formData.get('value')
  if (typeof raw !== 'string' || raw.trim() === '') throw new TypeError('value is required')
  const value = Number(raw)
  if (!Number.isFinite(value)) throw new TypeError('value must be finite')
  const result = await api.invokeModel('example/increment', { value }) as { value: number }
  return { ...result, invoked: true }
})
```

This follows the React/Next module-level Server Function shape: the client imports an async callable and `useActionState` supplies previous state and form data. `defineRscServerFunction()` keeps that public signature intact while its callback explicitly receives the request API first. Hile compiles a build-scoped reference instead of using Next's application compiler. The request still enters the one public Host, is authorized there, acquires the exact build lease, runs in the plugin microservice, then invokes the Model.

The Client Component does not manually append `buildId` to the Server Function arguments. The compiled Server Function reference and `RscNextClientRuntime` carry the exact plugin/build identity to the Host gateway. Passing `rsc` into the Client Boundary remains useful for display, cache keys, diagnostics, and any lower-level direct Action request; it must match the active deployment and must never be replaced with the base config build ID.

Model rules:

* `RscPluginService.load(modelsDirectory)` scans `*.model.*` through `@hile/loader`.
* Only default-exported `defineActionModel()` definitions are externally callable.
* `defineModel()` remains internal.
* The action ID is the relative path without `.model` and extension.
* Model pipelines and services remain supported because execution uses `@hile/model`.
* Keep authentication/authorization in the Host policy and application layer; validate action input again in the Server Function or Model.

Supported Hile Server Function syntax is deliberately narrower than Next: use a plugin-owned module-level `'use server'` file whose exports are created by `defineRscServerFunction()`. Ordinary unwrapped async exports, inline closure-capturing directives, re-exports, synchronous exports, mixed client/server directives, and dependency-owned directives fail during compilation.

## 5. Start The Plugin Microservice

The complete maintained implementation is `packages/create-hile/templates/rsc-plugin/src/services/plugin.boot.ts`. Its required sequence is:

1. Resolve the production artifact or current development revision.
2. Verify the entire artifact against `HILE_RSC_RUNTIME`.
3. Create one internal `Application` namespace.
4. Construct `RscPluginService` with the renderer and `RscArtifactServerFunctionRuntime`.
5. Call `service.load(modelsDirectory)`.
6. Optionally bind model and artifact development state.
7. Construct and start `HileRscPluginRuntime`.
8. Register `runtime.close()` with the Hile shutdown callback.

Essential composition:

```ts theme={null}
const service = new RscPluginService({
  manifest,
  renderer: createOfficialRscRenderer(artifactRoot),
  serverFunctions: new RscArtifactServerFunctionRuntime(artifactRoot),
})
await service.load(modelsDirectory)

const runtime = new HileRscPluginRuntime({
  application,
  service,
  port: Number(process.env.PLUGIN_MICRO_PORT),
  discovery: {
    namespace,
    instanceId: process.env.RSC_INSTANCE_ID?.trim() || namespace,
    priority: Number(process.env.RSC_DISCOVERY_PRIORITY ?? 0),
    generation: Number(process.env.RSC_DISCOVERY_GENERATION ?? 0),
    artifactRoot,
    // Deployment trust boundary: every peer able to reach this internal Micro mesh is trusted.
    authentication: { mode: 'trusted-internal' },
  },
})
await runtime.start()
shutdown(() => runtime.close())
```

Do not duplicate attach/listen/publish/drain/rollback order in every plugin. The class owns that lifecycle.

Validate required production values before constructing the runtime. In particular, reject a non-integer port outside `1..65535`, an empty namespace, and a missing artifact directory. In HMAC mode, also reject an empty discovery key or secret.

Plugin environment:

```dotenv theme={null}
REGISTRY_HOST=127.0.0.1
REGISTRY_PORT=9876
MICRO_NAMESPACE=org.example.rsc-plugin.dev
PLUGIN_MICRO_PORT=4101
RSC_ARTIFACT_ROOT=.hile-rsc
RSC_INSTANCE_ID=org.example.rsc-plugin.dev
RSC_DISCOVERY_PRIORITY=0
RSC_DISCOVERY_GENERATION=0
```

Every concurrently running plugin instance needs a unique internal namespace and port. Multiple builds may share a `pluginId`; priority and compatibility select the active candidate.

Identity meanings:

* `pluginId`: stable logical UI plugin identity and Host route key;
* `buildId`: immutable artifact identity selected under that plugin ID;
* `MICRO_NAMESPACE` / discovery `namespace`: routable internal service instance that serves the selected artifact; development configures a stable value, while production normally omits it and derives `${pluginId}.${buildId}` so concurrent immutable builds cannot share a Micro route;
* `RSC_INSTANCE_ID`: optional unique publisher identity; development configures a stable value for incremental updates, while production normally reuses the derived build-scoped namespace so rolling immutable deployments cannot share a discovery topic;
* `RSC_DISCOVERY_GENERATION`: non-negative monotonic publication generation; increase it for newer immutable deployments at equal priority, while runtime updates increment it automatically;
* `hile-rsc-dev --namespace`: the internal namespace recorded in development state; it must match the service namespace that will publish that revision.

When `RSC_DEVELOPMENT_STATE` is set, the plugin boot resolves and verifies the matching development record; otherwise it passes `RSC_ARTIFACT_ROOT` and optional `RSC_BUILD_ID` to `resolveVerifiedRscPluginArtifact()`, which selects and verifies once. Production must not set `RSC_DEVELOPMENT_STATE`.

Use `resolveHileRscPluginIdentity()` from `@hile/rsc-discovery-hile` for the production/development identity policy rather than rebuilding namespace and instance rules in each plugin boot.

## 6. Compose The Single Public Host

The complete maintained implementation is `packages/create-hile/templates/rsc-host/src/services/runtime.boot.ts`. The Host must create:

* `InMemoryRscArtifactCatalog` and `InMemoryRscDeploymentCatalog`;
* an internal Host `Application` and catalog-backed plugin locator;
* the remote client resolver and asset URLs;
* `HileRscDiscoveryHost` with an explicit authorizer;
* `RscServerFunctionGateway` with application authentication/authorization;
* asset, Server Function, and optional development middleware;
* exactly one `HttpNext` instance.

Use `resolveRscStyleAssets(artifacts, pluginId, buildId, assetUrls)` when the Host page must expose plugin CSS before rendering the remote boundary. Emit the returned integrity-declared stylesheet links, or equivalent framework preload metadata, in the HTML head before the decoded plugin tree. Use exactly the same `{ pluginId, buildId }` passed to `RscHostRuntime.render()`; an unregistered build fails closed so CSS from one immutable revision cannot be paired with Flight from another.

For a fully trusted internal Micro mesh, use the matching explicit policy and keep the trust assumption next to the code:

```ts theme={null}
// Deployment trust boundary: every peer able to reach this internal Micro mesh is trusted.
authorize: createTrustedInternalRscDiscoveryAuthorizer()
```

This mode needs no discovery secret, key ID, or plugin allowlist. It does not disable
artifact digest verification, generation rollback protection, transfer bounds, or public
request authorization.

The mode is an exact fail-closed choice: a misspelling or a configuration that also contains
`keyId`/`secret` is rejected before the plugin registers operations or publishes to Registry.
For a live HMAC-to-trusted migration, upgrade every Host reader first; upgraded Hosts continue
to accept old signed announcements, but old Hosts cannot read the new unsigned announcement
shape. Plugin replicas can roll after the Host fleet is ready.

When any internal publisher is outside that trust boundary, HMAC configuration must bind publisher identity to plugin ownership:

```ts theme={null}
authorize: createHmacRscDiscoveryAuthorizer((keyId) => {
  if (keyId !== process.env.RSC_DISCOVERY_KEY_ID) return undefined
  if (!process.env.RSC_DISCOVERY_SECRET) return undefined
  const pluginIds = (process.env.RSC_DISCOVERY_PLUGIN_IDS ?? '')
    .split(',').map((value) => value.trim()).filter(Boolean)
  if (pluginIds.length === 0) return undefined
  return {
    secret: process.env.RSC_DISCOVERY_SECRET,
    pluginIds,
    requireGeneration: process.env.RSC_DISCOVERY_REQUIRE_GENERATION === 'true',
  }
})
```

Never return a secret without a non-empty, explicit `pluginIds` allowlist. An arbitrary Registry client can announce a topic; signatures and ownership policy establish trust. Keep `RSC_DISCOVERY_REQUIRE_GENERATION=false` only during a rolling upgrade with legacy publishers, then switch it to `true`; otherwise an intermediary can strip both generation fields and downgrade to the still-valid legacy signature. The discovery manager rejects changed announcements at an accepted or lower generation and tombstones identities retired after the missing threshold. Supply a caller-owned `generationHighWater` map to `HileRscDiscoveryHost` so this state survives Host reconstruction. The map is exclusively owned by one live Host; `close()` the old Host successfully before constructing its replacement with that store.

Treat Registry topic presence as trusted liveness evidence. Generation signatures reject modified and older deployments, but they cannot distinguish a healthy retained topic from an exact replay of the current signed payload. Protect Registry writes/deletes and its transport; use a separate signed freshness service if the Registry itself is in the adversary model.

Set transfer bounds on `HileRscDiscoveryHost` when application quotas differ from the safe defaults:

```ts theme={null}
const discovery = new HileRscDiscoveryHost({
  // application, artifacts, deployments, runtime, authorize, ...
  maxManifestBytes: 1024 * 1024,
  maxFileBytes: 64 * 1024 * 1024,
  maxTotalBytes: 256 * 1024 * 1024,
  maxArtifactFiles: 4096,
  maxPathBytes: 1024,
  maxPathDepth: 32,
  operationTimeoutMs: 30_000,
  snapshotConcurrency: 16,
})
```

Downloaded bytes first enter an isolated OS temporary directory. Verification and deployment staging create the Host-managed immutable copy; temporary download files are removed on success or failure. Retired artifacts are removed according to deployment drain and asset retention lifecycle. The byte limits are per artifact operation, not tenant storage quotas; add disk/quota monitoring at the Host deployment layer.

Mount the Host adapters:

```ts theme={null}
const serverFunctions = createRscServerFunctionMiddleware({ gateway })

mountRscHostAdapters(host, {
  asset: createRscAssetMiddleware({ catalog: artifacts, mountPath: assetMountPath }),
  serverFunction: async (context, next) => {
    context.requestContext = { headers: context.headers }
    return serverFunctions(context, next)
  },
  middleware: developmentEvents
    ? [createRscDevelopmentEventMiddleware({ events: developmentEvents })]
    : [],
})
```

The application-supplied Server Function authorizer must authenticate the user, verify same-origin/CSRF policy, and authorize `{ pluginId, buildId, referenceId }`. Do not trust a client-provided namespace or internal address. The default Server Function route is `/_hile/rsc/server-functions`.

The generated development example composes `createSameOriginCsrfAuthorizer()` and passes request headers through `context.requestContext`. In a real application, construct the gateway with a policy that first authenticates the request context, then checks user access to the plugin/build/reference, and only then applies same-origin/CSRF verification:

```ts theme={null}
import { createSameOriginCsrfAuthorizer, RscServerFunctionGateway } from '@hile/rsc/host'

const sameOriginCsrf = createSameOriginCsrfAuthorizer({
  expectedOrigin: process.env.RSC_HOST_ORIGIN ?? 'http://127.0.0.1:3000',
  readToken: (context) => {
    const value = context.headers?.['x-rsc-csrf-token']
    return Array.isArray(value) ? value[0] : value
  },
  verifyToken: (token) => token === process.env.RSC_CSRF_TOKEN,
})

const gateway = new RscServerFunctionGateway({
  locator,
  authorize: async (request, context) => {
    const session = await authenticate(context)
    if (!session) return false
    if (!await canInvoke(session, request.pluginId, request.buildId, request.referenceId)) return false
    return sameOriginCsrf({
      pluginId: request.pluginId,
      buildId: request.buildId,
      actionId: request.referenceId,
      input: {},
    }, context)
  },
})
```

`authenticate()` and `canInvoke()` are application ports, not `@hile/rsc` APIs. Apply equivalent authentication/authorization to the dynamic page GET before rendering a plugin. The example `RSC_CSRF_TOKEN` is deliberately only a local pairing value: it reaches browser JavaScript and is not a production secret or user/session authorization mechanism.

HMAC Host environment (omit the discovery key fields in `trusted-internal` mode):

```dotenv theme={null}
HTTP_PORT=3000
REGISTRY_HOST=127.0.0.1
REGISTRY_PORT=9876
HOST_MICRO_NAMESPACE=com.hile.rsc.host
HOST_MICRO_PORT=4103
RSC_ASSET_MOUNT=/_hile/rsc/assets
RSC_HOST_ORIGIN=http://127.0.0.1:3000
RSC_CSRF_TOKEN=replace-this-development-token
RSC_DISCOVERY_POLL_MS=500
RSC_DISCOVERY_MISSING_RECONCILIATIONS=3
RSC_DISCOVERY_KEY_ID=local-development
RSC_DISCOVERY_SECRET=replace-this-shared-secret
RSC_DISCOVERY_PLUGIN_IDS=org.example.rsc-plugin
```

HMAC deployments should source discovery secrets from a secret manager. All deployments need real public authentication and normally use a less frequent discovery poll. The example token is not production security.

## 7. Render Through A Dynamic Next Route

Create `src/app/plugins/[pluginId]/[[...path]]/page.tsx`:

```tsx theme={null}
import { randomUUID } from 'node:crypto'
import { createExecutionContext } from '@hile/context'
import { loadService } from '@hile/core'
import { getHttpNextRequestSignal } from '@hile/http-next'
import { RscClientRuntimeProvider } from '@hile/rsc/client'
import { RscHostRuntime } from '@hile/rsc/host/runtime'
import { decodePluginFlight } from '@hile/rsc-next'
import { RscNextClientRuntime } from '@hile/rsc-next/client'
import { notFound } from 'next/navigation'
import runtimeService from '../../../../services/runtime.boot'

export const dynamic = 'force-dynamic'

export default async function PluginPage({ params, searchParams }: {
  params: Promise<{ pluginId: string; path?: string[] }>
  searchParams: Promise<Record<string, string | string[] | undefined>>
}) {
  const [{ pluginId, path = [] }, query] = await Promise.all([params, searchParams])
  const composition = await loadService(runtimeService)
  const active = composition.deployments.getActive(pluginId)
  if (!active) notFound()

  const runtime = new RscHostRuntime({
    locator: composition.locator,
    decoder: { decode: (flight) => decodePluginFlight(flight) },
  })
  const tree = await runtime.render({
    context: createExecutionContext({ requestId: randomUUID() }),
    pluginId,
    request: {
      buildId: active.buildId,
      path: `/${path.join('/')}`,
      searchParams: Object.fromEntries(Object.entries(query).filter(([, value]) => value !== undefined)),
    },
    signal: getHttpNextRequestSignal(),
    timeout: Number(process.env.RSC_RENDER_TIMEOUT_MS ?? 30_000),
    idleTimeout: Number(process.env.RSC_RENDER_IDLE_TIMEOUT_MS ?? 10_000),
    window: Number(process.env.RSC_RENDER_WINDOW ?? 8),
  })

  return (
    <RscNextClientRuntime serverFunctions={{
      headers: { 'x-rsc-csrf-token': process.env.RSC_CSRF_TOKEN ?? '' },
    }}>
      <RscClientRuntimeProvider assetMountPath={composition.assetMountPath}>
        {tree}
      </RscClientRuntimeProvider>
    </RscNextClientRuntime>
  )
}
```

`force-dynamic` prevents a build-time snapshot of a runtime deployment. The active build is resolved first and the same exact ID is sent to the plugin. The lease remains valid until Flight decode finishes or is aborted. Never request a base config ID after development has activated a revision-suffixed ID.

The total timeout bounds the complete internal Flight stream, the idle timeout resets after every valid chunk, and `window` bounds chunks in transit (1 through 64). The Host should reuse one `verificationCache` across requests and attach an `observe` callback for render outcome, duration, and byte metrics; observer failures do not affect rendering.

For product loading and failure UI, wrap `RscNextClientRuntime` and `RscClientRuntimeProvider` in a Host-owned file with `'use client'`, then pass `renderLoading` and `renderError(error, identity, retry)` there. Do not define these function props in this Server Component: functions may not cross the RSC serialization boundary. The default `suspensePolicy="remote"` preserves this per-component fallback behavior, and the default error UI is safe but intentionally minimal.

When navigation must keep the previously revealed route visible and replace it only after all remote Client Boundaries and their precedence styles are ready, move the runtime wrapper above changing route content so it remains mounted across navigations:

```tsx theme={null}
'use client'

import { Suspense, type ReactNode } from 'react'
import { RscClientRuntimeProvider } from '@hile/rsc/client'
import { RscNextClientRuntime } from '@hile/rsc-next/client'

export function HostRscRuntime({
  assetMountPath,
  children,
}: {
  assetMountPath: string
  children: ReactNode
}) {
  return (
    <RscNextClientRuntime>
      <RscClientRuntimeProvider
        assetMountPath={assetMountPath}
        suspensePolicy="host"
      >
        <Suspense fallback={<span>Loading initial route…</span>}>
          {children}
        </Suspense>
      </RscClientRuntimeProvider>
    </RscNextClientRuntime>
  )
}
```

Render `HostRscRuntime` from a persistent Host layout, not from the changing plugin page, and pass the same `assetMountPath` used by the Host asset middleware. In `host` mode each remote lazy import propagates suspension instead of committing a local empty/loading fallback. Next navigation is transition-aware, so an already revealed stable boundary keeps the previous route until all delegated remote boundaries in that coordinated region are ready, then React commits that replacement together. Plugin-owned nested Suspense boundaries keep their own intentional reveal sequence. The Host fallback still defines cold-entry behavior when there is no previously revealed route. Do not pass `renderLoading` with `suspensePolicy="host"`; the provider rejects that contradictory configuration. Keep `renderError` when the product needs per-component retry and failure isolation.

`RscNextClientRuntime` also installs the Host browser navigation adapter. A remote plugin uses
the framework-neutral entry and never imports Next:

```tsx theme={null}
import { RscLink } from '@hile/rsc/client/navigation'

export function PluginPage() {
  return <RscLink href="/plugins/catalog/details">Details</RscLink>
}
```

`RscLink` delegates eligible same-origin clicks to the Host router after hydration and otherwise
remains a normal anchor. Client Components may use `useRscNavigation()` from the same entry for
imperative `push`, `replace`, `refresh`, or `prefetch`; imperative destinations accept only HTTP(S),
and cross-origin navigation remains a full browser navigation. Do not append `_rsc`, send Flight headers,
or depend on `next/link` or `next/navigation` from a plugin; Next owns its private navigation
request when the Host adapter calls the public router.

`@hile/rsc-next` supports exactly Next 16.3.0 with React 19.2.8 and checks the installed package tuple before decoding through its isolated private Next modules. Do not widen the Next peer range without rerunning the full production SSR/hydration suite.

Do not add `cache()`, `unstable_cache`, or route revalidation around tenant/user-specific plugin rendering by default. Cross-request caching is an application policy; it requires an explicit authorization-safe key and invalidation contract.

The `/plugins` prefix is only this Host route's policy. You may choose another catch-all route without changing the RSC core.

The minimal route maps “no active deployment” to 404 and lets the Host error boundary handle unexpected runtime failures. A product Host should explicitly map authorization denial, deployment still downloading, incompatible build, drained/stale build, upstream unavailable, and request cancellation according to its HTTP/UI policy. Do not put those product decisions into `@hile/rsc`.

## 8. Keep The Outer Layout In The Host

The Host owns `<html>`, `<body>`, navigation, authentication shell, global theme, document metadata, and application-level error boundaries. The plugin tree is rendered inside that shell.

Plugin builds may publish bounded presentation metadata through `plugin.json`. Read it from the current active deployment and artifact catalogs with `listActiveRscPlugins(deployments, artifacts)`, then apply Host authorization and visibility policy before passing serializable navigation items to the shell. This is a derived view, not another plugin inventory, and it changes atomically with active `buildId` selection.

Presentation metadata is data, not executable head code. The Host may map trusted fields such as `displayName` or `description` through Next metadata APIs, but arbitrary plugin code still cannot mutate the Host document head.

```ts theme={null}
import { listActiveRscPlugins } from '@hile/rsc/host/plugin-metadata'

const navigation = listActiveRscPlugins(deployments, artifacts)
  .flatMap(({ pluginId, metadata }) => (metadata?.navigation ?? []).map((item) => ({
    key: `${pluginId}:${item.id}`,
    label: item.label,
    href: `/plugins/${encodeURIComponent(pluginId)}${item.path === '/' ? '' : item.path}`,
    order: item.order ?? 0,
  })))
  .sort((left, right) => left.order - right.order || left.key.localeCompare(right.key))
```

```tsx theme={null}
import type { ReactNode } from 'react'
import { RscDevelopmentReload } from '@hile/rsc-development/client'

export default function Layout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <header>Host navigation</header>
        {process.env.NODE_ENV === 'development' ? <RscDevelopmentReload /> : null}
        <main>{children}</main>
      </body>
    </html>
  )
}
```

For Ant Design or another CSS-in-JS library:

* place the Host library's Next SSR registry/collector around `children` in the Host layout;
* place plugin `ConfigProvider`, `App`, theme, and plugin-only providers inside the plugin client boundary;
* bundle plugin-imported static CSS as plugin assets;
* keep React external/shared so Host and plugin do not create incompatible contexts;
* test raw HTML and hydrated behavior. A visually correct client-only render is not sufficient SSR evidence.

The validated Ant Design Host setup installs `antd@6.6.0` and `@ant-design/nextjs-registry@1.3.0`, then composes:

```bash theme={null}
# in rsc-host
pnpm add antd@6.6.0 @ant-design/nextjs-registry@1.3.0

# in rsc-plugin
pnpm add antd@6.6.0
```

```tsx theme={null}
import { AntdRegistry } from '@ant-design/nextjs-registry'
import HostShell from './host-shell'

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <AntdRegistry>
          <HostShell>{children}</HostShell>
        </AntdRegistry>
      </body>
    </html>
  )
}
```

The Host shell owns navigation and application-wide theme state:

```tsx theme={null}
'use client'

import { App, ConfigProvider, Layout } from 'antd'

export default function HostShell({ children }: { children: React.ReactNode }) {
  return (
    <ConfigProvider theme={{ token: { colorPrimary: '#1677ff' } }}>
      <App>
        <Layout>
          <Layout.Header>Host navigation</Layout.Header>
          <Layout.Content>{children}</Layout.Content>
        </Layout>
      </App>
    </ConfigProvider>
  )
}
```

The plugin owns a separate provider inside its remote Client Boundary. This module is compiled as part of the plugin browser/SSR graph and must not wrap `<html>` or replace the Host collector:

```tsx theme={null}
'use client'

import { App, Button, Card, ConfigProvider, Space } from 'antd'
import type { ThemeConfig } from 'antd'
import type { RscRouteIdentity } from '@hile/rsc/plugin'

const pluginTheme: ThemeConfig = {
  token: { colorPrimary: '#722ed1', borderRadius: 10 },
}

export default function PluginPanel({ rsc }: { rsc: RscRouteIdentity }) {
  return (
    <ConfigProvider theme={pluginTheme}>
      <App>
        <Card title="Independent plugin">
          <Space>
            <Button type="primary">Hydrated by {rsc.buildId}</Button>
          </Space>
        </Card>
      </App>
    </ConfigProvider>
  )
}
```

For a larger plugin, move the `ThemeConfig` into a plugin-owned module while keeping the provider at this boundary. Re-run raw-HTML, hydration, and compatibility tests whenever Ant Design or its Next registry version changes.

Repository maintainers can compare the larger executable examples in `packages/test-rsc-plugin-capabilities-v2` and `packages/test-rsc-host`. Published-doc readers do not need those private demo paths to implement the provider structure above.

## 9. Production Build And Startup

Start Registry in its own process:

```bash theme={null}
cd rsc-host && pnpm exec hile registry --host 127.0.0.1 --port 9876 --pretty
```

Readiness is the `registry started on port 9876` log plus a successful plugin/Host Registry connection; merely having a process with that PID is not readiness. In production, supervise Registry separately and bind it to the intended private interface.

Then build both generated projects. These commands assume the two projects are siblings, not members of a pnpm workspace:

```bash theme={null}
(cd rsc-plugin && pnpm build && pnpm verify)
(cd rsc-host && pnpm build)
```

Keep Registry running. Start the foreground services in separate terminals:

```bash theme={null}
# terminal 2
cd rsc-plugin && pnpm start

# terminal 3
cd rsc-host && pnpm start
```

Ordering between plugin and Host startup is flexible after Registry exists: discovery reconciles later arrivals. Readiness means the plugin internal listener is ready and its accepted announcement is visible; a process merely existing is not sufficient.

On first reconciliation the Host:

1. reads Registry candidates;
2. authenticates publisher ownership;
3. selects a compatible candidate;
4. streams `plugin.json` and every declared artifact directly into isolated temporary files with credit/backpressure and hard limits;
5. verifies paths, sizes, runtime tuple, and SHA-256 integrity;
6. atomically installs and activates the deployment;
7. keeps old builds available while existing leases drain.

## 10. Development Mode

Use `hile-rsc-dev` plus the plugin service. For deterministic cold startup, run `pnpm dev:rsc`, wait until `.hile-rsc/development.json` contains a revision for the configured namespace, then run `pnpm dev:service` in a second terminal. The current template `scripts/dev.mjs` is a convenience process owner that starts both children and propagates exit/signals; if a cold machine exposes a first-state race, use the deterministic two-terminal order or replace the supervisor with an explicit state-readiness gate.

The development compiler consumes the same optional `styles` configuration as production and includes those files in every immutable revision. A relative style under the plugin `cwd` triggers the normal source watcher; after changing an absolute or package-export style outside `cwd`, invoke a rebuild or reload the config explicitly.

Development binding belongs in the generated plugin boot: `bindRscModelDevelopment()` watches the models directory, while `bindRscPluginDevelopmentState()` activates a verified revision and calls the runtime-supplied publisher only after activation. The Host `onEnabled` observer publishes `RscDevelopmentEvents`, its middleware serves SSE, and the Host root layout renders `RscDevelopmentReload` only in development.

Complete cold-start order for sibling generated projects:

```bash theme={null}
# terminal 1
cd rsc-host && pnpm exec hile registry --host 127.0.0.1 --port 9876 --pretty

# terminal 2: wait for .hile-rsc/development.json to contain the configured namespace
cd rsc-plugin && pnpm dev:rsc

# terminal 3
cd rsc-plugin && pnpm dev:service

# terminal 4
cd rsc-host && pnpm dev
```

After the first successful cold start, `cd rsc-plugin && pnpm dev` may be used as the two-child convenience command. Keep the Registry and Host terminals running.

Development behavior:

* server, browser, and SSR esbuild contexts persist across rebuilds;
* server-only edits reuse browser/SSR outputs when client inputs and Server Function graph are unchanged;
* Client Boundary, transitive client input, CSS, exported boundary, or Server Function graph changes rebuild affected client artifacts;
* every successful revision is immutable and receives an exact revision build ID;
* a failed build keeps the previous deployment serving and the watcher alive;
* model changes atomically reload models without compiling RSC assets;
* after the plugin activates and republishes, the Host downloads/verifies/enables it, then emits SSE and `RscDevelopmentReload` refreshes the page;
* this is full-page refresh after safe activation, not cross-plugin React Fast Refresh state preservation.

Use at least two retained revisions. Size `maxRevisions` for the maximum build-download-activation overlap, and keep `maxSessions` bounded so stale sessions cannot grow indefinitely.

## 11. Upgrade, Removal, And Failure Semantics

* A new build is downloaded and verified before activation.
* New renders select the new build; in-flight renders keep their captured old lease.
* The old runtime/artifact retires only after drain and retention policy allow it.
* A failed candidate does not replace the working build.
* A missing announcement is tolerated for `missingReconciliations`; after that grace, the deployment retires automatically.
* Stopping or uninstalling a plugin therefore disables it without a manual Host activation API.
* `RscPluginService.activate()` is an internal lifecycle primitive used by verified runtime/development orchestration. “No manual activation” means application users and Host routes do not call it as an install workflow.
* Never mutate bytes under an already published build ID. It breaks cache and deployment identity.
* At equal priority, publish newer immutable builds with a higher generation. HMAC mode signs that generation; announcements without generation remain valid legacy generation zero.

## 12. Verification

First verify the generated pair itself:

```bash theme={null}
# artifact and compile checks
(cd rsc-plugin && pnpm build && pnpm verify)
(cd rsc-host && pnpm build)

# with Registry and both production services running
curl --fail http://127.0.0.1:3000/plugins/org.example.rsc-plugin/page
```

The response must contain the plugin's server-rendered heading. In a browser, open the same URL, increment local client state, submit the Server Function form added in sections 3–4, and confirm the Model result appears without a page error. Inspect Network to confirm plugin JS/CSS and the Server Function POST use `127.0.0.1:3000`; no browser request may target the plugin Micro port. Stop the plugin and wait past the configured missing-announcement grace to verify automatic removal, then restart it to verify automatic reinstallation. Edit a Server Component in the four-terminal development topology and verify a new immutable build ID appears only after a successful rebuild; introduce and repair a syntax error to verify last-good behavior.

For a generated external project, record the browser's successful Server Function request and use “Copy as cURL” to repeat it with a wrong Origin, missing/incorrect CSRF token, stale build ID, unknown reference ID, oversized body, and an aborted connection. Every altered request must fail without invoking the Model. For upgrade coverage, build a new immutable `buildId`, start it at higher priority, verify new requests select it while an already-started slow request finishes on the old build, then remove the old announcement. For isolation coverage, generate a second plugin with a distinct `pluginId`, namespace, and Micro port, and verify neither plugin's assets or Server Function references resolve under the other identity.

The following exhaustive architecture acceptance suite is available only to maintainers working in the Hile monorepo; it is not a command for scaffolded sibling projects:

```bash theme={null}
pnpm --filter test-rsc-demo-suite test:contracts
pnpm --filter test-rsc-demo-suite test:e2e
pnpm --filter test-rsc-demo-suite test:e2e:dev
```

Monorepo-only interactive reference:

```bash theme={null}
pnpm --filter test-rsc-demo-suite dev
```

Then open:

* `http://127.0.0.1:3200/` for discovery and deployment state;
* `http://127.0.0.1:3200/plugins/demo.rsc.capabilities?label=review&count=3` for Server/Client/CSS/lazy/action coverage;
* `http://127.0.0.1:3200/plugins/demo.rsc.capabilities/details?source=review` for a server-only plugin route;
* `http://127.0.0.1:3200/plugins/demo.rsc.isolation?marker=review` for independent plugin isolation.

An implementation is not complete until tests prove:

* only the Host accepts public HTTP;
* raw HTML contains plugin SSR output;
* hydration, hooks, context, effects, CSS, library components, and lazy chunks work without console errors;
* a module-level Server Function works through `useActionState` and executes the expected scanned Model in the plugin process;
* wrong origin/token, unknown reference, stale build, invalid input, excessive body, and aborted requests fail safely;
* server/SSR artifacts cannot be fetched from the public asset route;
* discovery rejects values outside the selected trust mode; HMAC coverage includes wrong signatures and publisher/plugin ownership mismatches;
* artifact transfer is streamed to disk with bounded size/count/path constraints;
* upgrade preserves in-flight old-build requests and failed rebuilds preserve the last good revision.
* stream window/timeout/idle-timeout, discovery generation ordering, bounded snapshot concurrency, and Host loading/error retry policies are covered.

## Troubleshooting

| Symptom                                     | Likely cause                                                              | Check/fix                                                                                                          |
| ------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Plugin never appears                        | Registry, namespace, selected trust policy, or internal listener mismatch | Confirm Registry address, unique Micro port, readiness log, and matching trusted-internal or HMAC policy           |
| `RSC plugin build mismatch`                 | Host requested a base/stale ID after a revision activated                 | Always resolve `deployments.getActive(pluginId)` and pass that exact ID; never hardcode `buildId` in the Host page |
| Browser asset 403                           | Host origin/asset middleware mismatch or stale build URL                  | Use the same `assetMountPath` for middleware and `RscClientRuntimeProvider`; verify active build and origin        |
| Server Function 403                         | Same-origin, CSRF, authentication, or permission policy rejected it       | Pair `RscNextClientRuntime` headers with Host authorization; do not bypass policy                                  |
| Unknown Model action                        | Wrong relative ID, missing default export, or `defineModel()` used        | Verify `service.load()`, filename, `defineActionModel()`, and path-derived ID                                      |
| Client import fails to compile              | Unsupported directive shape or dependency uses its own React/directives   | Use module-level directives in plugin-owned code and keep React external                                           |
| CSS flashes or is absent                    | Asset mount mismatch or CSS-in-JS collector is at the wrong owner         | Verify emitted style manifest; put Host collector in layout and plugin providers inside client boundary            |
| Edit requires full rebuild                  | Production CLI used instead of persistent development compiler            | Run `hile-rsc-dev` and bind the emitted state to the plugin service                                                |
| Previous revision disappears during refresh | Retention lower than deployment overlap                                   | Increase `maxRevisions`; minimum is two                                                                            |
| Host can render but shutdown hangs          | In-flight Flight/Server Function did not receive cancellation             | Pass `getHttpNextRequestSignal()` and retain transport abort propagation                                           |
| Remote component stays on loading/error UI  | Asset fetch/import failed or retry reused a rejected lazy component       | Configure Host `renderError`, inspect plugin/build/reference identity, and call its supplied `retry` callback      |

## Completion Checklist For AI Agents

* [ ] I used the current templates or explained every deviation.
* [ ] Plugin and Host runtime pins match the compatibility tuple supported by their installed RSC packages.
* [ ] Plugin process uses `--conditions=react-server` and creates no HTTP server.
* [ ] Models load before `HileRscPluginRuntime.start()`.
* [ ] New UI behavior uses module-level `'use server'` → `defineRscServerFunction()` → explicit API → `defineActionModel()`.
* [ ] Host and plugin select the same explicit discovery trust mode; HMAC mode binds `keyId` to explicit plugin IDs.
* [ ] Host mounts assets, Server Functions, and development SSE on its one HTTP server.
* [ ] Dynamic Next route resolves and passes the exact active build ID and abort signal.
* [ ] Render timeout, idle timeout, stream window, observer, and shared verification cache are configured.
* [ ] Discovery generation and snapshot concurrency are configured; HMAC mode signs generations.
* [ ] Loading/error functions live in a Host Client Component and retry is tested.
* [ ] Outer layout remains Host-owned; plugin providers/styles remain plugin-owned.
* [ ] Development keeps the last good immutable revision and reloads after verified activation.
* [ ] Contract, production E2E, and development E2E checks pass.

## Lower-Level Extension Points

Use `attachRscPluginService`, `registerHileRscPluginDiscovery`, `RscDiscoveryManager`, custom locators, or custom artifact catalogs only when building an adapter or persistence implementation. They are not the default application integration path. Keep transport, trust, storage, lifecycle, Next decoding, and business behavior in separate modules.
