Files
mizan/backends/mizan-fastapi
Ryth Azhur e00b3a177e Docs carry the shape of the system, not its status
MIZAN.md's phase-numbered implementation order, PRODUCT_ARCHITECTURE's
"Deferred until Render revenue funds it" and "Shipped in", PSR_VS_EDGE's
current-state section, and the READMEs' passing-test counts were all reporting
where the work stood rather than what the system is. OWED_SURFACE keeps its
subject — surface that is specified but unbuilt — stated as the shape each unit
owes.

The channel sections follow the renamed slots: Params / ClientMessage /
ServerMessage, and Channel as the base class on both backends.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 14:03:29 -04:00
..

mizan-fastapi

FastAPI backend adapter for the Mizan protocol. One decorator on a server function. Typed React client generated. Invalidation automatic.

Scope

mizan-fastapi's surface is RPC dispatch, context bundling, JSON-body invalidation, auth gating, and channels over a multiplexed WebSocket. Forms, Shapes, and SSR sit outside that surface — a FastAPI project reaches for its own native equivalents (Pydantic, ORM-of-choice, FastAPI's SSR ecosystem).

Install

uv add mizan-fastapi

Setup

# main.py
from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError

from mizan_fastapi import (
    MizanError,
    mizan_exception_handler,
    mizan_validation_handler,
    router as mizan_router,
    ws_router,
)


app = FastAPI()
app.include_router(mizan_router, prefix="/api/mizan")
app.include_router(ws_router, prefix="/api/mizan")
app.add_exception_handler(MizanError, mizan_exception_handler)
app.add_exception_handler(RequestValidationError, mizan_validation_handler)

The exception handlers render every error path through the Mizan envelope ({"error": {"code", "message", "details"}}) so the kernel's MizanError parses status + code on the frontend regardless of which failure happened.

Define server functions

from mizan_core.client.function import client
from mizan_core.registry import register
from pydantic import BaseModel


class EchoOutput(BaseModel):
    message: str


@client
def echo(request, text: str) -> EchoOutput:
    return EchoOutput(message=text)


register(echo, "echo")

mizan-fastapi has no auto-discovery (FastAPI doesn't have an app registry to walk). Register every @client-decorated function explicitly. A typical project keeps registrations in main.py (alongside the FastAPI app) or in a dedicated clients.py imported during startup.

@client parameters

@client                              # plain RPC function
@client(context="global")            # singleton context — fetched once, SSR-hydrated
@client(context="user")              # named context — fetched per provider mount
@client(affects="user")              # mutation — invalidates the user context
@client(affects=user_profile)        # mutation — invalidates a specific function
@client(auth=True)                   # requires authentication
@client(auth="staff")                # requires is_staff
@client(auth="superuser")            # requires is_superuser
@client(auth=lambda req: ...)        # custom predicate
@client(rev=2)                       # cache revision (busts on bump)

Forms parameters are accepted by the decorator (they're a mizan-core primitive) and carry no meaning to this adapter.

Channels

A channel is a named fan-out over the WebSocket ws_router serves. Subclass Channel, declare whichever payload models the channel carries, and register it. The three model names are read from the client's side: Params keys the fan-out, ClientMessage travels up, ServerMessage travels down.

from mizan_fastapi import Channel, register_channel
from pydantic import BaseModel


class Chat(Channel):

    class Params(BaseModel):
        room: str

    class ClientMessage(BaseModel):
        text: str

    class ServerMessage(BaseModel):
        user: str
        text: str

    def authorize(self, params: Params) -> bool:
        return True

    def receive(self, params: Params, msg: ClientMessage) -> ServerMessage:
        return self.ServerMessage(user="anon", text=msg.text)


register_channel(Chat, "chat")

Server code pushes to a group from anywhere:

await Chat.push(Chat.ServerMessage(user="system", text="hello"), room="general")

Registered channels contribute to the exported IR, so codegen emits the <Pascal>Params / <Pascal>ClientMessage / <Pascal>ServerMessage types and the matching frontend hook.

Group membership lives in the process that holds the socket, so a push reaches only the subscribers attached to that process. Fan-out that spans processes is a shared broker in front of broadcast.

Auth integration

The executor expects request.state.user to be populated by your FastAPI middleware or dependency tree before dispatch:

from fastapi import Request


@app.middleware("http")
async def attach_user(request: Request, call_next):
    request.state.user = await resolve_user_from_token(request)
    return await call_next(request)

Where resolve_user_from_token returns either a user object with is_authenticated, is_staff, is_superuser attributes, or None for an anonymous request. The executor branches on those for auth=True, auth="staff", auth="superuser" requirements.

Generate the frontend

The codegen is the mizan-generate Rust binary (source at protocol/mizan-codegen/; protocol/mizan-generate/ is a thin npm launcher that dispatches to the platform binary). Point a mizan.toml at your FastAPI app and run the CLI:

# frontend/mizan.toml
output = "src/api"
targets = ["react"]

[source.fastapi]
module = "main"                      # module to import for @client side effects
cwd = "../backend"                   # python cwd for module resolution
command = ["uv", "run", "python"]    # optional — defaults to ["python"]
mizan-generate --config mizan.toml

The codegen drives python -m mizan_fastapi.ir <module> under the hood, parses the emitted KDL IR, then emits Stage 1 (typed callXxx/fetchXxx over the runtime kernel) + Stage 2 (<MizanContext> provider, per-context providers, use{Hook}() hooks) into src/api/.

// app.tsx
import { MizanContext } from "./api"

export default function App({ children }) {
    return <MizanContext baseUrl="/api/mizan">{children}</MizanContext>
}
// any component
import { useEcho, useCurrentUser } from "./api"

const echo = useEcho()
echo.mutate({ text: "hi" }).then(r => console.log(r.message))

const user = useCurrentUser()  // global context — auto-fetched, auto-refreshed on mutation

Running tests

uv sync --extra dev
uv run pytest

Schema export CLI

For codegen consumption (or any tooling that wants the Mizan schema):

python -m mizan_fastapi.ir <module>

Imports the named module (which must register every @client function as import-time side effects), then prints the Mizan KDL IR to stdout.

Architecture

mizan-fastapi is one of two reference backend adapters (the other is backends/mizan-django). Both implement the same Mizan protocol on top of the shared cores/mizan-python core (@client, registry, MWT, HMAC cache keys). The AFI conformance suite at tests/afi/ gates that the two adapters emit equivalent schemas for the same registered functions. See docs/AFI_ARCHITECTURE.md.

A live e2e harness exercises this adapter end-to-end at examples/fastapi-react-site/ (real Chromium → React with generated hooks → FastAPI server, driven by Playwright).