Files
mizan/docs/SSR_ARCHITECTURE.md
Ryth Azhur e4091dfbe8 SSR docs declare the embedded-V8 architecture; surface re-chartered per appeal
docs/SSR_ARCHITECTURE.md now specifies the decided shape: Django template
backend rendering through the PyO3-bound SsrEngine (cores/mizan-rust-ssr,
deno_core + deno_web), bundles built by mizan-generate, props crossing as
parsed values, no external JS runtime serving requests, no_rsc guard.
docs/AFI_ARCHITECTURE.md follows (mizan-rust-ssr in the cores layout, Bun
worker delisted). OWED_SURFACE.md regenerated via appeal-surface: the
Bun→PyO3 cutover, the PyO3 binding surface, and the mizan-generate SSR
bundling step are now chartered owed mechanisms instead of an orphaned
engine crate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 19:43:16 -04:00

3.8 KiB

SSR Architecture

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.

TEMPLATES = [
    {
        'BACKEND': 'mizan.ssr.MizanTemplates',
        'DIRS': [BASE_DIR / 'frontend'],
        'OPTIONS': {
            'bundles': BASE_DIR / 'frontend' / '.mizan' / 'ssr',
            'timeout': 5,
        },
    }
]

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 <div id="mizan-root">…</div> plus a <script>window.__MIZAN_SSR_DATA__={sorted-json}</script> 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::parsed 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 (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.
  • URL routing, views, middleware, auth — all unchanged.