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>
85 lines
3.8 KiB
Markdown
85 lines
3.8 KiB
Markdown
# 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.
|
|
|
|
```python
|
|
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::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 (`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.
|