diff --git a/OWED_SURFACE.md b/OWED_SURFACE.md
index 6595c33..9991590 100644
--- a/OWED_SURFACE.md
+++ b/OWED_SURFACE.md
@@ -115,16 +115,18 @@ Cross-function graph checks (fail at IR-build time, before any client is emitted
## Unit: mizan-rust-ssr (`cores/mizan-rust-ssr`)
-**Charter.** The embedded-V8 SSR engine and the anti-RSC guard. It owns rendering a build-time JS bundle to HTML in-process via `deno_core`, and the structural guarantee that the SSR surface never imports an RSC/Flight runtime. It does not own the Django template backend (that is `mizan-django/ssr`).
+**Charter.** The embedded-V8 SSR engine, its PyO3 binding, and the anti-RSC guard. It owns rendering a build-time JS bundle to HTML in-process via `deno_core`, exposing that engine to the Python side across an in-process FFI boundary, and the structural guarantee that the SSR surface never imports an RSC/Flight runtime. It does not own the Django template backend (that is `mizan-django/ssr`) or the bundling that produces the JS bundle (that is `mizan-generate`).
**Claims substantiated here.**
- SSR is hand-rolled; no frontend adapter imports an SSR runtime or meta-framework (Next/Nuxt/SvelteKit/RSC/Flight) — the CVE-2025-55182 pre-auth-RCE deserialization class (HOLOMORPHICS/Mizan project note; MEMORY: mizan-ssr-no-framework-runtimes; enforced by `cores/mizan-rust-ssr/tests/no_rsc.rs`).
- SSR renders synchronously from props, injected as validated data (the AFI provides the typed one-way version).
+- The Python side binds the engine through PyO3 — in-process FFI, no subprocess, no JSON-RPC framing; behavior lives in the binary, languages bind to it (docs/SSR_ARCHITECTURE.md § The engine; § AFI boundary; ROADMAP.md § Core Consolidation — SSR in the binary).
**Owed behavioral mechanisms.**
- the engine composes a real `deno_web` web-platform layer (TextEncoder/Decoder, MessagePort, timers) rather than a partial shim, evals the trusted bundle once, and renders per request — observable: the fixture bundle renders `Hello, World!`; a missing global would fail loudly at render, not silently pass (the doc's "partial polyfill is silent-failure-shaped" concern is discharged by using deno_web's real impls).
- props cross as a `v8::json::parse`d value passed as a function argument, never spliced into evaluated source — observable: the injection test feeds a prop string crafted to break out of a string-built call; it renders as inert text and does not set a global, so code injection is structurally absent.
- the no-RSC guard scans authored SSR source and dependencies for the forbidden token set (`react-server-dom`, `renderToReadableStream`, `renderToPipeableStream`, `createFromReadableStream`/`Fetch`, `use server`, `next/`, `nuxt`, `@sveltejs/kit`) and fails on presence — observable: adding any RSC/Flight/meta-framework import to the scanned fixtures turns `no_rsc.rs` red; absence alone is not the guarantee — re-entry is loud.
+- OWED (unbuilt): a PyO3 binding exposes `SsrEngine` to the Python side as an in-process extension — construct-from-bundle and `render(props_json) -> HTML` cross the FFI boundary with no process spawn, honoring V8's one-isolate-per-engine / non-`Send` constraint so the caller holds one engine per (worker thread, bundle) — observable when built: the Django backend imports the engine and calls `render(props)` in-process, and a prop still crosses as a parsed value (the injection guarantee survives the FFI hop). Today `cores/mizan-rust-ssr/src/lib.rs` exposes `SsrEngine` only as a Rust API with Rust-native `#[tokio::test]` coverage and carries no `#[pyclass]`/`#[pymodule]`, so the PyO3 surface the AFI-boundary table names is unsubstantiated.
---
@@ -238,18 +240,22 @@ Return-type branching + origin cache:
## Unit: mizan-django SSR (`backends/mizan-django/src/mizan/ssr`)
-**Charter.** The SSR product's Django half: a template backend that renders `.tsx`/`.jsx` component files through a persistent Bun subprocess, wrapping output with a hydration payload. It owns the Django-template-engine integration and the Bun subprocess lifecycle; it does not own the JS render (that is the Bun worker).
+**Charter.** The SSR product's Django half: a Django template backend that renders `.tsx`/`.jsx` component files by resolving each to its built bundle and driving the PyO3-bound `SsrEngine` in-process, wrapping output with a hydration payload. It owns the Django-template-engine integration, component-to-bundle resolution, prop gathering, and the per-(worker-thread, bundle) engine lifecycle; it does not own the V8 render (that is `cores/mizan-rust-ssr`'s `SsrEngine`) or the bundling that produces the render bundle (that is `mizan-generate`).
**Claims substantiated here.**
- SSR is a Django template backend replacing the rendering engine; the template name IS a `.tsx`/`.jsx` file path; context dict becomes props; output wrapped in `
` + `window.__MIZAN_SSR_DATA__` hydration (docs/SSR_ARCHITECTURE.md).
-- SSR bridge: Django template backend → persistent Bun subprocess via JSON-RPC; worker resolves by file path (`import(file)` + `renderToString`); auto-restarts on crash, thread-safe, correlates by message id (ROADMAP.md § Done; docs/SSR_ARCHITECTURE.md § Implementation surface).
+- The render engine is embedded-V8 inside the Mizan Rust binary, bound via PyO3 — in-process FFI, no external JS runtime serving requests, no subprocess, no JSON-RPC framing (docs/SSR_ARCHITECTURE.md § The engine; § AFI boundary — Backend adapter row).
+- The component path resolves against `DIRS` to its built bundle in `OPTIONS['bundles']` (produced by mizan-generate's SSR bundling step); the backend gathers props and wraps output for hydration (docs/SSR_ARCHITECTURE.md § AFI boundary — Backend adapter row; the `TEMPLATES` config block).
+- One engine per (worker thread, bundle) — V8's Locker constraint makes an engine non-`Send`, so the Django side never shares one across threads (docs/SSR_ARCHITECTURE.md § The engine).
- SSR is orthogonal to RPC and composable; first paint carries data (INVARIANTS.md § SSR).
**Owed behavioral mechanisms.**
-- `MizanTemplates` implements Django's template-backend interface: `get_template(name)` resolves `name` as a file path under `DIRS` and returns a `MizanTemplate` wrapping the absolute path; `render` strips `request`/`csrf_token` and passes the remaining context as props — observable: `render(request, 'components/Hello.tsx', ctx)` renders that file's component with `ctx` as props; `from_string` raises (it renders files, not strings); a missing file raises `TemplateDoesNotExist`.
+- `MizanTemplates` implements Django's template-backend interface: `get_template(name)` resolves `name` as a `.tsx`/`.jsx` file path under `DIRS` and returns a `MizanTemplate` for the resolved component; `render` strips `request`/`csrf_token` and passes the remaining context as props — observable: `render(request, 'components/Hello.tsx', ctx)` renders that component with `ctx` as props; `from_string` raises (it renders files, not strings); a missing file raises `TemplateDoesNotExist`.
+- component-to-bundle resolution: the backend resolves each component file to its self-contained render bundle in `OPTIONS['bundles']` (the `mizan-generate`-produced bundle assigning `globalThis.renderApp`) and hands the bundle to the engine, rather than reading component source directly — observable: `render` of `components/Hello.tsx` loads that component's built bundle from the configured `bundles` directory; a component with no built bundle raises rather than rendering stale or empty HTML.
- rendered output is wrapped for client hydration — observable: output contains `
…
` plus ``, so first paint carries the props the client hydrates from.
-- `SSRBridge` holds one persistent `bun run
` subprocess, correlates requests by message id over newline-delimited JSON-RPC, serializes stdin writes, is thread-safe under concurrent renders, waits for a ready signal on start, and auto-restarts on crash — observable: five concurrent renders return five correct results with no interleaving; killing the subprocess mid-life and rendering again transparently restarts it; a render exceeding the timeout raises `TimeoutError` rather than hanging.
-- OWED (partial, docs/PSR_VS_EDGE.md § Current state): the render-on-mutation orchestration (mutation → trigger local render → store HTML), driven by the manifest's `render_strategy`, wiring the bridge to the PSR path — observable when built: a public-context mutation triggers a local re-render and stores HTML; today the bridge renders on request and the manifest records the strategy, but the mutation→render→store wiring is absent, so PSR-on-mutation is unsubstantiated.
+- engine lifecycle: the backend constructs one `SsrEngine` per (worker thread, bundle) pair and reuses it across requests, never sharing a single engine across threads (the engine is non-`Send`) — observable: concurrent renders on different worker threads each use their own thread-local engine and return correct results with no interleaving; the engine is built once per bundle, not per render.
+- OWED (unbuilt — the PyO3 cutover): the Django backend binds the engine through PyO3 and renders in-process — no `bun run` subprocess, no newline-delimited JSON-RPC, no `SSRBridge`, no ready-signal / auto-restart machinery, and `OPTIONS` carries `bundles` (a directory of built bundles) rather than `worker` (a JS entry file) — observable when built: a render spawns no external JS runtime process and calls the PyO3-bound `SsrEngine.render(props)` directly; `ssr/bridge.py` and the `workers/mizan-ssr` worker are retired. Today this is entirely unsubstantiated: `ssr/bridge.py` spawns `bun run ` and correlates requests over JSON-RPC, `ssr/backend.py` wires `OPTIONS['worker']` to that bridge and renders via `SSRBridge.render`, and the docstrings still describe a "persistent Bun subprocess" — the subprocess architecture the docs (§ The engine, § AFI boundary) no longer describe.
+- OWED (partial, docs/PSR_VS_EDGE.md § Current state): the render-on-mutation orchestration (mutation → trigger local render → store HTML), driven by the manifest's `render_strategy`, wiring the engine to the PSR path — observable when built: a public-context mutation triggers a local re-render and stores HTML; today the engine renders on request and the manifest records the strategy, but the mutation→render→store wiring is absent, so PSR-on-mutation is unsubstantiated.
---
@@ -300,7 +306,7 @@ Return-type branching + origin cache:
- the suite exercises the real HTTP stack (Django test client / LiveServer) — not just RequestFactory — for dispatch, three-tier invalidation, merge, view-path branching, and cache HIT/MISS/scoped-purge — observable: `HTTPIntegrationTests` and `CacheIntegrationTests` assert the JSON body and `X-Mizan-Invalidate` header agree, that a scoped mutation preserves other users' cached entries, and that a second identical fetch is a HIT.
- the auth suite covers every axis (JWT valid/invalid/expired, MWT, session, staff/superuser/callable/PermissionError) and asserts the body never runs on failure — observable: an invalid token returns 401 without session fall-through; an anonymous call to an auth-required function returns before the body; a callable's `PermissionError` message surfaces verbatim.
- the Edge-compatibility suite asserts the properties a CDN cares about (deterministic byte-identical bodies, sorted JSON keys, URL-encoded delimiter-safe headers, `no-store` on errors/mutations, header↔body invalidation agreement, auth-differentiated responses for the same URL) — observable: these tests go red if any of those properties regress, so "Edge caching is possible" is checkable without a CDN.
-- the SSR suite verifies the bridge and template backend end-to-end when Bun is present (ping, render, missing-component error, crash recovery, concurrent renders, hydration wrapper) and skips gracefully otherwise — observable: a killed worker transparently restarts on the next render; five concurrent renders return five correct results.
+- the SSR suite verifies the engine-based render path — template-backend resolution, bundle-driven in-process render, the hydration wrapper, and concurrent renders — and asserts that no external JS runtime process is spawned during the suite (the PyO3-bound `SsrEngine` renders in-process) — observable: a render produces the `` + `__MIZAN_SSR_DATA__` wrapper from the resolved bundle, concurrent renders across worker threads each use their own engine and return correct results, and the suite spawns no `bun`/`node` subprocess. The Bun ping / crash-recovery / auto-restart assertions are retired with the subprocess bridge they exercised; today `test_ssr.py` still drives `SSRBridge` (Bun subprocess, JSON-RPC, killed-worker restart), so this engine-path verification is owed alongside the mizan-django SSR PyO3 cutover.
- the benchmark suite measures HTTP-vs-executor overhead and throughput with correctness assertions on each path — observable: every benchmark also asserts the function's numeric output, so a green benchmark run is also a correctness run.
---
@@ -401,12 +407,14 @@ Return-type branching + origin cache:
**Claims substantiated here.**
- Every frontend adapter is a thin idiomatic wrapper over one shared kernel; the kernel owns `ContextState
= {data,status,error}`, `registerContext`, `mizanCall`/`mizanFetch`, server-driven merge/invalidate, `initSession`, and a pluggable `MizanTransport` (HTTP default, Tauri/webview swap via `configure`) (INVARIANTS.md § Client Kernel; docs/AFI_ARCHITECTURE.md § Kernel model).
+- Shared parameters elevate to required provider props; non-shared params elevate to optional props with per-function override (INVARIANTS.md § Named Contexts; MIZAN.md §2 param elevation / `specify` resolution order).
- Codegen targets the adapter surface, never the raw kernel; React devs get hooks, Vue composables, Svelte stores, same kernel underneath (docs/AFI_ARCHITECTURE.md § Kernel model).
- Vue and Svelte ship as v1 alongside React (docs/AFI_ARCHITECTURE.md § Launch surface).
- Cross-language HMAC pin: `deriveCacheKey` in `mizan-ts` matches the Python key byte-for-byte (docs/CACHE_KEYING.md; README.md § Adapters — TypeScript is the protocol-reference adapter).
**Owed behavioral mechanisms.**
- `@mizan/base` owns the single reconciled view: `ContextState`, the context registry, `mizanCall`/`mizanFetch`, server-driven `merge`/`invalidate`, `initSession`, over a `MizanTransport` interface — observable: the same behaviors the `mizan-rust` port pins (stable-key cache identity, merge-splice, scoped-vs-broad refetch, retry, dual-envelope error parse) hold in TS; the Rust port exists precisely to mirror this file.
+- the generated provider elevates a context's shared params to required props and its non-shared params to optional props, and resolves each member's effective params by overlaying per-function overrides onto the provider props at fetch time (INVARIANTS.md § Named Contexts; MIZAN.md §2 resolution order) — observable: a two-function `user` context where both take `user_id` and only one takes `page` generates a provider with required `user_id` and optional `page`; a per-function override supplies a different `page` for that one member while the shared `user_id` still covers both, and a member still missing a required param at fetch time is a runtime error, not a silent undefined.
- `deriveCacheKey` (mizan-ts) reproduces the Python HMAC key byte-for-byte — observable: the pinned vectors in `cores/mizan-python/tests/test_keys.py::test_cross_language_pin` (`ctx:user:605a1ca5…`, `ctx:user:30fc08eb…`) are asserted against the TS output; any normalization drift (bool/None stringification, key ordering) breaks the pin, which the doc marks a security vulnerability.
- adapters subscribe to the kernel and render in their own idiom without keeping a parallel copy of the truth — observable: a React hook and a Vue composable over the same context read the same kernel entry; mutating in one path updates both because the truth lives once in the kernel.
- OWED (unbuilt, ISSUES.md § Open / ROADMAP.md § Next): `frontends/mizan-vue` and `frontends/mizan-svelte` are runtime kernel-adapter packages — the codegen emits their clients (byte-parity-tested) but no runtime package or live-backend example exists — observable when built: a Vue composable / Svelte store subscribes to `@mizan/base` and refreshes on invalidation against a live backend; today only React has full integration verification, so the "Vue and Svelte ship as v1" claim is unsubstantiated at the runtime layer.
@@ -417,23 +425,23 @@ Return-type branching + origin cache:
## Unit: mizan-codegen (`protocol/mizan-codegen`)
-**Charter.** The single Rust codegen binary that reads KDL IR and emits typed clients for every target (stage1, react, vue, svelte, channels, python, rust), plus the source-fetching that spawns each backend's IR-export command and the Pydantic-pre-step. It owns the IR→client transform; it does not emit IR (backends do).
+**Charter.** The single Rust codegen binary that reads KDL IR and emits typed clients for every target (stage1, react, vue, svelte, channels, python, rust), plus the source-fetching that spawns each backend's IR-export command and the Pydantic-pre-step, plus the SSR bundling step that compiles each SSR entry component into a self-contained render bundle. It owns the IR→client transform and the SSR bundle production; it does not emit IR (backends do) or run the render engine (that is `cores/mizan-rust-ssr`).
**Claims substantiated here.**
- Codegen reads KDL directly — no OpenAPI envelope, no `openapi-typescript`, no per-backend converter; the former JS two-stage codegen is deleted (docs/AFI_ARCHITECTURE.md § Forward-direction primitives).
- Every frontend client is generated from the IR; each target is byte-parity-tested (INVARIANTS.md § Canonical IR & Codegen; ROADMAP.md § Rust codegen).
- The codegen drives the backend's IR-export command as a subprocess and parses the KDL it writes (docs/AFI_ARCHITECTURE.md; backends/*/README.md § Generate the frontend).
+- Stage 1 (typed `callXxx`/`fetchXxx`) + Stage 2 (`` provider, per-context providers, `use{Hook}()`) emission (backends/*/README.md § Generate the frontend).
- Pydantic + Rust DX: a decoru pre-step authors Rust types from Pydantic before the cargo IR bin runs; a generic `[source.script]` source spawns any command emitting KDL (backends/mizan-tauri/README.md § Pydantic; config.rs).
+- mizan-generate's SSR bundling step compiles each SSR entry component together with `react-dom/server.browser` into a self-contained bundle assigning `globalThis.renderApp`, written to the `bundles` directory — the only place node/bun run in the SSR path (docs/SSR_ARCHITECTURE.md § The engine; § AFI boundary — Build step row).
**Owed behavioral mechanisms.**
- `fetch.rs` spawns the configured source's export command (FastAPI `-m mizan_fastapi.ir`, Django `manage.py export_mizan_ir`, Rust `cargo run --bin`, or a generic script) and parses stdout as KDL — no OpenAPI/converter anywhere in the path — observable: a codegen run against a live FastAPI backend consumes only the KDL the CLI writes; the Rust source runs the cargo bin and the optional decoru pre-step first.
- the KDL parser reconstructs the full typed IR (types with struct/list/enum/alias shapes, functions with input/output/nullable/context/affects/merge/form, contexts with param elevation, channels) — observable: `ir_deserialization.rs` reads the AFI fixture back into typed structs and asserts the function set, per-function fields, param elevation, and named-type presence.
- each target emits deterministically and is byte-parity-tested against a committed baseline — observable: `stage1_parity.rs`, `react_parity.rs`, `rust_parity.rs`, `python_parity.rs`, `vue_svelte_parity.rs`, and `channels_smoke.rs` diff emitter output against baselines and fail on any byte drift; two different runs produce identical output.
-- the emitters produce genuinely different, correct artifacts per target — not one shape behind distinct labels — observable: the react target emits `` + per-context providers + `use{Hook}()` reading React context; vue emits composables; svelte emits stores; python emits a Pydantic-typed facade over the PyO3 kernel; rust emits a full crate depending on `mizan-rust` — each byte-checked against its own baseline, and stage1 is auto-included whenever a framework target is requested.
+- the emitters produce genuinely different, correct artifacts per target — not one shape behind distinct labels — observable: the react target emits `` + per-context providers + `use{Hook}()` reading React context (Stage 2), Stage 1 emits the framework-agnostic typed `callXxx`/`fetchXxx`; vue emits composables; svelte emits stores; python emits a Pydantic-typed facade over the PyO3 kernel; rust emits a full crate depending on `mizan-rust` — each byte-checked against its own baseline, and stage1 is auto-included whenever a framework target is requested.
- the codegen tree-shakes and canonicalizes types to match the backend emitters, and hoists inline enums into named Rust/TS types — observable: an unreferenced type is not emitted; an inline `field { enum … }` becomes a top-level Rust enum the struct field references; the channels target emits zero files when the IR carries no channels.
-
----
-
+- OWED (unbuilt): an SSR bundling step compiles each SSR entry component together with `react-dom/server.browser` into a self-contained bundle that assigns `globalThis.renderApp`, emitted into the configured `bundles` directory — this is the only place node/bun run in the SSR path — observable when built: a `mizan-generate` run produces one render bundle per SSR entry component, each evaluable standalone by the embedded-V8 engine (assigning `renderApp` at eval time and rendering from a JSON-parsed props argument). Today the codegen binary emits typed clients (stage1/react/vue/svelte/channels/python/rust) but carries no SSR bundling target in `src/emit/` and no bundling path in `fetch.rs`/`config.rs`, so the bundling step the docs place with `mizan-generate` is unsubstantiated.
## Unit: AFI conformance (`tests/afi`)
**Charter.** The cross-adapter conformance gate: one fixture registered identically in Django, FastAPI, and a Rust app, asserting all three emit byte-identical KDL. It is the executable form of "the IR is the only contract"; it authors no production mechanism.
diff --git a/docs/AFI_ARCHITECTURE.md b/docs/AFI_ARCHITECTURE.md
index d6d1f34..3a80919 100644
--- a/docs/AFI_ARCHITECTURE.md
+++ b/docs/AFI_ARCHITECTURE.md
@@ -28,20 +28,21 @@ cores/ shared language-level primitives
mizan-python/ @client decorator, registry, MWT, HMAC cache keys
mizan-rust/ Rust core — IR build (build_ir()), registry
mizan-rust-macros/ #[derive(Mizan)] / #[mizan::client] proc-macros
+ mizan-rust-ssr/ embedded-V8 SSR engine (deno_core + deno_web); evals the
+ build-time bundle, renders per request; no_rsc guard
protocol/ protocol-level tooling
mizan-codegen/ codegen — Rust binary (crate `mizan-codegen`); reads KDL IR,
emits typed clients. Targets: stage1, react, vue, svelte,
channels, python, rust. Askama templates under templates/.
mizan-generate/ thin npm-package launcher (bin/launcher.mjs) dispatching to
the compiled mizan-codegen binary per platform
-workers/ runtime workers / bridges
- mizan-ssr/ Bun subprocess used by the Django template backend
```
## Two orthogonal products
- **RPC** — typed client generation via codegen
-- **SSR** — server rendering via the Bun bridge
+- **SSR** — server rendering via the embedded-V8 engine in the Rust
+ binary (see `docs/SSR_ARCHITECTURE.md`)
Independent and composable. Either ships standalone; together they
compose.
@@ -68,9 +69,9 @@ contexts, types, and invalidation graph. Every codegen target consumes
KDL. KDL is the contract; everything else (REST envelopes, OpenAPI
documents, framework idioms) is sediment around it.
-The IR must be validated against multiple adapters before it is
-considered stable. Single-adapter validation hides assumptions —
-divergence between adapters is what the IR exists to prevent.
+The IR is validated against multiple adapters — single-adapter
+validation hides assumptions, and divergence between adapters is what
+the IR exists to prevent.
Forward-direction primitives:
@@ -85,17 +86,16 @@ Forward-direction primitives:
- `protocol/mizan-codegen/src/fetch.rs` spawns the configured source
command and parses the KDL it writes.
- Codegen reads KDL directly — no OpenAPI envelope, no
- `openapi-typescript`, no per-backend converter divergence. The
- former JavaScript/Node two-stage codegen (`openapi-typescript` plus
- `.mjs` adapters) has been deleted; codegen is now the single Rust
- binary.
+ `openapi-typescript`, no per-backend converter divergence; codegen
+ is a single Rust binary.
- Edge manifest, MWT claims, and other protocol artifacts derive from
the same registry/IR.
-## Launch surface
+## Authoring surface
-Python (Django) + React. Vue and Svelte ship as v1 alongside React.
-TypeScript backend (`mizan-ts`) proves the protocol is portable.
+Python (Django) + React is the reference stack. Vue and Svelte are
+co-equal codegen targets over the same kernel. The TypeScript backend
+(`mizan-ts`) proves the protocol is portable.
## Why the AFI shape
diff --git a/docs/SSR_ARCHITECTURE.md b/docs/SSR_ARCHITECTURE.md
index 36b453f..ecd6d1c 100644
--- a/docs/SSR_ARCHITECTURE.md
+++ b/docs/SSR_ARCHITECTURE.md
@@ -1,10 +1,9 @@
# SSR Architecture
-*Decided 2026-04-07.*
-
-Mizan's SSR adapter is a **Django template backend**. It plugs into
-Django's existing `TEMPLATES` setting, replacing the template
-rendering engine.
+Mizan's SSR adapter is a **Django template backend** rendering through an
+**embedded V8 engine inside the Mizan Rust binary** (`cores/mizan-rust-ssr`).
+No external JS runtime serves requests — node and bun are build-time tools
+only, and no frontend adapter imports an SSR runtime or meta-framework.
```python
TEMPLATES = [
@@ -12,53 +11,74 @@ TEMPLATES = [
'BACKEND': 'mizan.ssr.MizanTemplates',
'DIRS': [BASE_DIR / 'frontend'],
'OPTIONS': {
- 'worker': 'path/to/mizan-ssr/src/worker.tsx',
+ 'bundles': BASE_DIR / 'frontend' / '.mizan' / 'ssr',
'timeout': 5,
},
}
]
```
-Then `render(request, 'components/Hello.tsx', context)` calls the Bun
-subprocess bridge instead of rendering a Django/Jinja2 template.
-**The template name IS a `.tsx`/`.jsx` file path**, resolved against
-`DIRS`; `get_template` returns a `MizanTemplate` wrapping the absolute
-file path. The context dict becomes the component's props (`request`
-and `csrf_token` stripped). Rendered output is wrapped in
-`…
` plus a
-`` hydration payload.
+`render(request, 'components/Hello.tsx', context)` renders a React component
+instead of a Django/Jinja2 template. **The template name IS a `.tsx`/`.jsx`
+file path**, resolved against `DIRS`; `get_template` returns a
+`MizanTemplate` wrapping the resolved component. The context dict becomes
+the component's props (`request` and `csrf_token` stripped). Rendered output
+is wrapped in `…
` plus a
+`` hydration
+payload the Mizan kernel hydrates from — server-validated data crossing
+one way, as props.
+
+## The engine
+
+`SsrEngine` (`cores/mizan-rust-ssr`) embeds a `deno_core` V8 runtime
+composed with `deno_web`, so the web-platform globals react-dom touches
+(`TextEncoder`/`TextDecoder`, `MessagePort`, timers) are real
+implementations, not shims — a partial polyfill is silent-failure-shaped;
+it passes until a render path hits the gap.
+
+- **The bundle is built, the engine evals it once.** `mizan-generate`'s SSR
+ bundling step compiles each SSR entry component together with
+ `react-dom/server.browser` into a self-contained bundle that assigns
+ `globalThis.renderApp`. The engine evals the trusted bundle at
+ construction and calls `renderApp(props)` per request.
+- **Props never enter evaluated source.** Per-render data crosses as a
+ `v8::json::parse`d value passed as a function argument, so a prop string
+ has no source to break out of — code injection is structurally absent,
+ not filtered.
+- **One isolate per engine, one engine per worker thread.** V8's Locker
+ constraint means an engine is not `Send`; the Django side holds one
+ engine per (worker thread, bundle) pair.
## AFI boundary
| Side | Responsibility |
|---|---|
-| Backend adapter (`SSRBridge`) | Manages the Bun subprocess lifecycle; gathers props |
-| Bun worker (`worker.tsx`) | `import()`s the file path, `renderToString(createElement(Component, props))` |
-| stdin/stdout JSON-RPC | Newline-delimited; `{id, method:"render", params:{file, props}}` → `{id, html}` / `{id, error}`; `ping` → `{id, pong:true}` |
+| Backend adapter (`mizan/ssr`) | Django template-backend interface; resolves the component path to its built bundle; gathers props; wraps output for hydration |
+| Rust binary (`SsrEngine`, via PyO3) | Holds the evaled bundle; parses props to a V8 value; `renderApp(props)` → HTML string |
+| Build step (`mizan-generate`) | Bundles component + `react-dom/server.browser` into the `bundles` directory; the only place node/bun run |
+
+The Python side binds the engine through PyO3 — in-process FFI, no
+subprocess, no JSON-RPC framing. This is the Core-Consolidation shape:
+behavior lives in the binary; languages bind to it.
+
+## No framework runtimes
+
+No frontend adapter imports an SSR runtime or meta-framework — Next, Nuxt,
+SvelteKit, React Server Components / Flight. Those add the
+client-payload-deserialization step behind the pre-auth RCE class
+(CVE-2025-55182); the AFI already provides the typed, Pydantic-validated,
+one-way version they would otherwise import unsafely.
+
+The guarantee is enforced, not assumed: `cores/mizan-rust-ssr/tests/no_rsc.rs`
+scans the authored SSR source and its dependencies for the forbidden token
+set (`react-server-dom`, `renderToReadableStream`, `renderToPipeableStream`,
+`createFromReadableStream`/`Fetch`, `use server`, `next/`, `nuxt`,
+`@sveltejs/kit`) and fails on presence — re-entry is loud.
## Why template backend
-- Django's template system is swappable by design (batteries
- included, but replaceable).
-- Django developers already use `render(request, template, context)`
- — no new API to learn.
+- Django's template system is swappable by design (batteries included, but
+ replaceable).
+- Django developers already use `render(request, template, context)` — no
+ new API to learn.
- URL routing, views, middleware, auth — all unchanged.
-
-> A `templatetags/` package exists for a future `{% mizan_render %}`
-> convenience tag (base.html shell with Mizan components inside), but
-> it is currently empty — no tag is implemented yet.
-
-## Implementation surface
-
-The SSR backend (`mizan/ssr/backend.py`) implements Django's template
-backend interface:
-
-- `MizanTemplates(BaseEngine)` — requires `OPTIONS['worker']` (path to
- `worker.tsx`); `get_template(name)` resolves a file under `DIRS`
-- `MizanTemplate` with `.render(context, request)` → calls the bridge
-- `SSRBridge` (`bridge.py`) — spawns `bun run `, holds the
- persistent subprocess, correlates requests by message id, thread-safe,
- auto-restarts on crash, waits for the worker's ready signal
-
-Everything Django expects from a template backend, but the actual
-rendering routes to Bun.