RSC Plugin Platform
Use this guide to scaffold, customize, run, secure, and verify independently compiled React Server Component plugins behind one publicHttpNext listener.
API and package boundaries: @hile/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 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
Complete Example
The smallest complete composition has three parts:AI Implementation Rules
Before writing code, preserve these invariants:- Use
HileRscPluginRuntimeas the plugin lifecycle composition root. - Use
HileRscDiscoveryHostfor automatic Host deployment. - Use
RscHostRuntimeplusdecodePluginFlight()inside a dynamic Next route. - Use a module-level
'use server'file withdefineRscServerFunction()and call scanneddefineActionModel()definitions through its explicit API argument. - Keep exact React/RSC versions identical across Host, plugin, and build config.
- Treat
buildIdand artifact directories as immutable. A changed artifact requires a new build ID or development revision. - Choose discovery trust explicitly: bind each HMAC
keyIdto a plugin-ID allowlist, or usetrusted-internalonly when every internal Micro peer is trusted. - Mount plugin assets, Server Functions, and optional development SSE on the same Host listener/origin.
- Pass the request abort signal into
RscHostRuntime.render(). - Do not expose server bundles, artifact paths, namespaces, or internal message addresses to the browser.
- Configure bounded render timeouts/window and Registry snapshot concurrency for production.
- Keep loading/error renderer functions inside a Host Client Component.
- 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
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:
1. Install Exact Dependencies
Plugin runtime and compiler:@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:
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
Createhile-rsc.json:
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:
/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:
NODE_OPTIONS=--conditions=react-server so React resolves its server exports.
3. Write Server And Client Components
Server entrysrc/plugin/page.tsx:
'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:
plugin.json.
Minimal src/plugin/plugin.css for proving style delivery:
4. Add A Model And A Server Function
Createsrc/models/example/increment.model.ts:
src/plugin/actions.ts:
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
.modeland 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.
'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 ispackages/create-hile/templates/rsc-plugin/src/services/plugin.boot.ts. Its required sequence is:
- Resolve the production artifact or current development revision.
- Verify the entire artifact against
HILE_RSC_RUNTIME. - Create one internal
Applicationnamespace. - Construct
RscPluginServicewith the renderer andRscArtifactServerFunctionRuntime. - Call
service.load(modelsDirectory). - Optionally bind model and artifact development state.
- Construct and start
HileRscPluginRuntime. - Register
runtime.close()with the Hile shutdown callback.
1..65535, an empty namespace, and a missing artifact directory. In HMAC mode, also reject an empty discovery key or secret.
Plugin environment:
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/ discoverynamespace: 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.
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 ispackages/create-hile/templates/rsc-host/src/services/runtime.boot.ts. The Host must create:
InMemoryRscArtifactCatalogandInMemoryRscDeploymentCatalog;- an internal Host
Applicationand catalog-backed plugin locator; - the remote client resolver and asset URLs;
HileRscDiscoveryHostwith an explicit authorizer;RscServerFunctionGatewaywith application authentication/authorization;- asset, Server Function, and optional development middleware;
- exactly one
HttpNextinstance.
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:
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:
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:
{ 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:
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):
7. Render Through A Dynamic Next Route
Createsrc/app/plugins/[pluginId]/[[...path]]/page.tsx:
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:
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:
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.
- place the Host library’s Next SSR registry/collector around
childrenin 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.
antd@6.6.0 and @ant-design/nextjs-registry@1.3.0, then composes:
<html> or replace the Host collector:
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: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:
- reads Registry candidates;
- authenticates publisher ownership;
- selects a compatible candidate;
- streams
plugin.jsonand every declared artifact directly into isolated temporary files with credit/backpressure and hard limits; - verifies paths, sizes, runtime tuple, and SHA-256 integrity;
- atomically installs and activates the deployment;
- keeps old builds available while existing leases drain.
10. Development Mode
Usehile-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:
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
RscDevelopmentReloadrefreshes the page; - this is full-page refresh after safe activation, not cross-plugin React Fast Refresh state preservation.
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: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:
http://127.0.0.1:3200/for discovery and deployment state;http://127.0.0.1:3200/plugins/demo.rsc.capabilities?label=review&count=3for Server/Client/CSS/lazy/action coverage;http://127.0.0.1:3200/plugins/demo.rsc.capabilities/details?source=reviewfor a server-only plugin route;http://127.0.0.1:3200/plugins/demo.rsc.isolation?marker=reviewfor independent plugin isolation.
- 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
useActionStateand 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
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-serverand 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
keyIdto 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
UseattachRscPluginService, 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.