Restore documentation layer — match current substrate

ROADMAP: done items moved out of "Next" (codegen rewrite, SSR bridge,
edge manifest, X-Mizan-Invalidate, return-type branching, affects_params,
kernel extraction, two-stage codegen, mizan-ts). Real "Next" in:
framework-adapter wrapper layer (MizanContext + useMizan + DjangoError
on top of the kernel) for React/Vue/Svelte; A1–A4 from ISSUES.md.

CLAUDE: 4-package layout replaced with the actual 7-package layered
architecture (backend protocol adapters + frontend kernel + framework
adapters + SSR worker). "STALE codegen" section rewritten to describe
what's emitted vs. the wrapper layer that isn't yet.

docs/ now tracked (6 files). AFI_ARCHITECTURE rewritten — replaced
the speculative `mizan-ast`/`mizan-csr`/`mizan-rpc`/`mizan-schema`
package names with the real layout, dropped KDL-schema language for
the actual schema-export format. The other 5 docs/ files were already
current and are tracked as-is.

ARCHITECTURE-REWORK.md deleted — same expert review is re-tracked in
the fresher ISSUES.md, two parallel trackers was sediment.

README.md deleted — drift was beyond surgical fixes (`mizan_clients.py`
convention, `<DjangoContext>` provider, removed `@compose` and
`context='local'`, wrong codegen output filenames, 3-package structure
vs. 7). Rewrite waits for the wrapper-layer codegen to land so
user-facing examples reflect reality.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 20:10:12 -04:00
parent 5c1c583164
commit 6eca514777
10 changed files with 433 additions and 781 deletions

View File

@@ -1,336 +0,0 @@
# Architecture Rework: Cache Keying & Invalidation
**Date:** 2026-04-06
**Source:** 8 independent expert reviews (Cloudflare, Enterprise Backend, Django, SaaS Founder, Next.js, React Query, Framework Authoring, Serverless Architecture)
---
## Status Key
- [x] Fixed
- [ ] **BUG** — broken in shipped code
- [ ] **DESIGN** — must resolve before implementing cache layer
- [ ] **SPEC** — needs specification before building
- [ ] **OPS** — operational gap for production readiness
- [ ] **DX** — developer experience issue
- [ ] **BUSINESS** — product/pricing concern
---
## Bugs in Shipped Code
### [x] BUG: `Vary: Authorization, Cookie` does nothing on Cloudflare
**Files:** `executor.py:787`, `dispatch.ts:77`, `edge-compat.test.ts:75-79`
Cloudflare ignores all Vary values except `Accept-Encoding` and `Accept` (images only). This header creates a false sense of security — someone reading the code assumes different Authorization headers produce different cache entries. They do not. The edge-compat tests assert the presence of this non-functional header, reinforcing the illusion.
**Origin:** Claude hallucination in a prior session. Not a design decision.
**Fix:** Remove the header from both Python and TypeScript. Remove test assertions. Add a code comment explaining why Vary is not used and pointing to the HMAC cache key strategy (when implemented).
### [x] BUG: `fn` vs `function` wire protocol key mismatch
**Files:** `executor.py:619`, `runtime/index.ts:128`
The Django executor reads `body.get("fn")`. The TypeScript runtime sends `{ function: functionName }`. These don't match. Would break on first real use of the new TS runtime against the Django backend.
**Fix:** Align on one key name. Whichever is chosen, document it as stable wire format.
### [x] BUG: `max-age=0` defeats the PSR caching model
**File:** `executor.py:786`
`Cache-Control: public, max-age=0, stale-while-revalidate=300` means the origin gets hit on every request for background revalidation. This conflicts with PSR's purge-based freshness model, where content should be cached until explicitly invalidated.
**Fix:** For PSR-eligible contexts, emit `Cache-Control: public, s-maxage=31536000`. The CDN caches forever; purge is the only freshness mechanism. Reserve `max-age=0, stale-while-revalidate` for contexts that opt out of PSR or use time-based revalidation.
---
## Critical Design Flaws
### [ ] DESIGN: HMAC concatenation without delimiter
**Severity:** Security vulnerability — cache key collisions across different logical entries
`HMAC(secret, context + user_id + params)` without structured separation means `"user" + "12" + "3"` collides with `"user1" + "2" + "3"`.
**Fix:** Use null-byte delimiters: `HMAC(secret, context + "\x00" + user_id + "\x00" + canonical_sorted_params)`. Or HMAC over a JSON-canonical form. Document the canonical form as part of the AFI protocol spec.
### [ ] DESIGN: Full context flush on deploy = thundering herd
**Severity:** Operational — self-inflicted DDoS on every deploy that changes a decorator
Every deploy that changes any `@client` decorator nukes all cached content for affected contexts. Teams deploying 3-5x/day means the Edge cache is cold 3-5x/day. 100K concurrent users + 10 contexts = 1M origin requests in seconds post-deploy.
**Preferred fix:** Versioned cache keys. Include a manifest content hash in the cache key. Old and new entries coexist during transition. No purge, no thundering herd. 2x cache storage during transition (negligible). Old entries expire naturally via TTL or LRU eviction.
**Alternative fix:** Granular per-context diffing. Only flush contexts whose function signatures, params, or auth requirements actually changed. The manifest already contains per-context param lists to support this.
### [ ] DESIGN: Purge token in customer Workers exposes shared cache
**Severity:** Security — one compromised customer can purge all customers' cache
Every customer Edge Worker deployment carries a Cloudflare API token with `Zone:Cache Purge` permission for `render.mizan.cloud`.
**Fix:** Build a purge proxy Worker on the Mizan zone. Validates purge requests (HMAC signature + customer-scoped URL pattern matching) before forwarding to the Cloudflare purge API. No customer Worker ever holds a direct zone API token.
### [ ] DESIGN: Permission key race condition
**Severity:** Data correctness — stale content served for duration of JWT lifetime
User permission changes (e.g., tier upgrade) don't take effect until JWT expires because: (1) cache key uses only `user_id`, not tier, and (2) permission key comparison uses the JWT-derived value, which is stale until refresh.
**Options:**
- (a) Make permission-relevant attributes part of the cache key (increases cardinality).
- (b) Accept the JWT-lifetime staleness window, document as known constraint.
- (c) Add short-TTL revalidation for permission-sensitive contexts.
**Decision needed before implementation.**
### [ ] DESIGN: No `waitUntil()` in purge/warm flow
**Severity:** Latency — client blocks on cache management operations
If a mutation invalidates N URLs, the Edge Worker must complete all purge API calls before responding. Each call is 50-200ms.
**Fix:** Return mutation response immediately. Fire all purge and warming fetches inside `waitUntil()`. Same Worker invocation, no extra billing, client doesn't block.
---
## Missing Specifications
### [ ] SPEC: Secret rotation protocol
No rotation mechanism, no dual-secret acceptance window, no compromise recovery procedure. Rotating the single secret invalidates every HMAC globally.
**Need:** Key derivation hierarchy (master secret -> per-context derived keys). Rotation at context level. Dual-secret acceptance window during rotation. Document compromise recovery procedure.
### [ ] SPEC: GDPR right-to-erasure for cached content
HMAC keys make targeted per-user cache purge difficult. Must reconstruct every possible HMAC for every context x param combination for a given user.
**Need:** `purge_by_user(user_id)` operation that iterates manifest contexts to reconstruct all HMACs. Tractable if context count is bounded. Audit trail for compliance proof.
### [ ] SPEC: Cache adapter conformance requirements
Every Mizan backend adapter (Python, TypeScript, and future: PHP, C#, Go, etc.) must
implement the origin-side cache protocol. This is NOT a binary ABI or pluggable backend
interface. It is a set of operations each adapter implements in its own language, backed
by Redis. Conformance is verified by a shared test suite (same model as the existing
edge-compat tests that prove Python and TypeScript produce identical protocol output).
**Storage:** Redis. Not pluggable. Not in-memory-only. Redis handles persistence,
cross-worker sharing, and crash recovery. The adapter is a thin protocol layer over
Redis commands.
**Required operations:**
```
cache_get(context: string, params: dict, user_id: string | null, rev: int) -> CachedResponse | null
```
Derives HMAC key from inputs using JSON-canonical form, fetches from Redis.
```
cache_put(context: string, params: dict, user_id: string | null, rev: int, response: CachedResponse) -> void
```
Derives HMAC key, stores response in Redis. Also maintains a reverse index
(context + params -> HMAC keys) so `cache_purge` can find entries to delete.
```
cache_purge(context: string, params: dict | null) -> int
```
Looks up the reverse index for matching entries, deletes them from Redis.
Returns number of entries purged. When `params` is null, purges entire context.
```
cache_purge_user(user_id: string) -> int
```
Iterates all contexts in the manifest, reconstructs HMAC keys for the given
user_id across all param combinations in the reverse index, deletes them.
Required for GDPR right-to-erasure.
**HMAC key derivation (must be identical across all adapters):**
```
key = HMAC-SHA256(secret, JSON.stringify({
"c": context,
"p": sorted_params,
"r": rev,
"u": user_id // omitted for public content
}, sort_keys=True))
```
**MWT validation (must be identical across all adapters):**
Validate the `X-Mizan-Token` header as a standard JWT (HMAC-SHA256). Extract `sub`
(user_id) for cache key derivation, check `exp` for token freshness.
**Conformance test suite:**
Each adapter must pass a shared set of protocol conformance tests verifying:
- Identical HMAC output for identical inputs (cross-language determinism)
- Identical MWT validation behavior
- Correct purge semantics (scoped and broad)
- Correct reverse index maintenance
- Correct `cache_purge_user` behavior
### [ ] SPEC: Client-side cache lifecycle
Runtime is ~95 lines. No `staleTime`, `isFetching`/`isLoading` distinction, garbage collection, retry logic, optimistic updates, `refetchOnWindowFocus`.
**Minimum viable:**
- Loading/fetching state distinction (don't throw on missing data)
- Error return shape: `{ data, isLoading, isFetching, error }`
- `refetchOnWindowFocus` as default
- Mutation lifecycle with rollback support for optimistic updates
- Garbage collection for unmounted context data (configurable delay)
### [x] SPEC: Per-context cache policy
`cache=` on `@client` accepts three forms:
- **Omitted (default):** Invalidation-based. Emits `s-maxage=31536000`. Cache forever,
purge on mutation. Use when your backend is the source of truth.
- **`cache=60` (integer seconds):** TTL-based. Emits `s-maxage=60`. Accept bounded
staleness. Use for unobservable mutations — when your backend mirrors external data
(third-party APIs, aggregations, upstream services) and cannot know when it changes.
- **`cache=False`:** Never cache. Emits `Cache-Control: no-store`. Use for
non-deterministic functions (`random()`, `datetime.now()`).
This is the escape hatch for data the backend doesn't own the mutation scope for.
Positioned in docs as: "Are you the source of truth, or a mirror? Source of truth →
use `affects=`. Mirror → use `cache=N`."
The `cache=int` value flows into the edge manifest per-context, so the Edge Worker
and CDN respect it without special handling (`s-maxage` is standard CDN behavior).
### [ ] SPEC: Extension points for cache/invalidation lifecycle
Zero hooks for third-party code. No pre-invalidation hook, no custom cache key function, no invalidation transport plugin.
**Minimum viable:**
- `CacheBackend` protocol (third parties implement custom backends)
- `on_invalidate(context, params)` event hook (monitoring/debugging)
- Document these as public API from day one
### [x] SPEC: Manifest versioning
The manifest has no version field. When the schema evolves, Edge Workers can't distinguish v1 from v2 format.
**Fix:** Add `"version": 1` to manifest root before anyone deploys it. Edge Workers check version and fail fast on unknown versions.
### [x] SPEC: Wire format convention
Python emits `snake_case` params (`user_id`). TypeScript conventionally uses `camelCase` (`userId`). The `USER_SCOPED_PARAMS` set in `manifest.ts` contains both conventions. Invalidation headers from Python won't match TypeScript keys expecting `camelCase`.
**Fix:** Document `snake_case` as the wire format convention. TypeScript adapters convert at the boundary.
---
## Operational Gaps
### [ ] OPS: No cache observability
No hit/miss metrics, no cache key debugging, no invalidation audit trail, no manifest version tracking.
**Need:** `X-Mizan-Cache-Status` response header (HIT/MISS/BYPASS/STALE/PURGED/DYNAMIC). Structured logging in Edge Worker. Console-level invalidation event log for devtools.
### [ ] OPS: Purge rate limits at scale
Cloudflare zone purge API: 500 req/10s (free/pro), 2500/10s (Enterprise). Bulk operations can exceed this.
**Need:** Batch purge requests (up to 30 URLs per API call). Document rate limits. Design Cache Tags upgrade path for Enterprise.
### [ ] OPS: Purge-then-warm race condition
Warming fetch arriving at a PoP before purge propagates gets a cache HIT on stale data.
**Fix:** Use `Cache-Control: no-cache` or `cf: { cacheTtl: 0 }` on warming requests to force revalidation.
### [ ] OPS: PSR warming only warms one colo
Warming fetch from a Worker runs in a single datacenter. Only warms that colo's cache (+ upper-tier if Tiered Cache active). Does not warm all 300+ PoPs.
**Document:** PSR warming reduces origin load by warming the shield tier. First request from each edge PoP is still a cache miss to the shield. Not zero-latency for all users.
---
## Django Integration Concerns
### [ ] DX: `@client` breaks decorator stacking
`@client` returns a class (`FunctionWrapper`), not a callable. `@login_required`, `@csrf_exempt`, `@cache_page` cannot compose with it.
**Options:**
- (a) Make `@client` return a wraps-compatible callable that also carries metadata (Django Ninja approach).
- (b) Document incompatibility prominently. Provide Mizan-native equivalents. State that `@client` replaces `@login_required` (via `auth=`), `@cache_page` (via context caching), etc.
### [ ] DX: `JWTUser` too thin for complex auth checks
Works for `is_staff`/`is_superuser`. Fails for allauth relations, DRF permissions, `request.user.groups.all()`, user model relations.
**Need:** Document limitation. Provide `get_full_user()` helper that does DB lookup when needed. Or optionally expand JWT claims.
### [ ] DX: Transaction safety of invalidation
Invalidation in response body is optimistic — fires before `ATOMIC_REQUESTS` commits. If transaction rolls back, invalidation was already sent.
**Need:** Document as known behavior. Recommend `transaction.on_commit()` for critical paths. When building `mizan-cache`, consider two-phase: mark for invalidation during request, execute purge on commit.
### [ ] DX: Admin/ORM writes invisible to invalidation
Only `@client(affects=...)` functions trigger invalidation. Django admin saves, management commands, direct ORM writes are invisible.
**Need:** Document clearly. Provide manual purge API: `purge_context('products', params={'product_id': 42})`.
### [ ] DX: Cache adapter integration for Django
The Python cache adapter is a thin protocol layer over Redis (not a Django cache backend).
Django developers call `mizan.cache.get(context, params, user_id, rev)` directly.
Provide a `mizan.cache.clear()` for test fixture teardown. Document that this is
separate from Django's `CACHES` framework — Mizan owns its own cache protocol.
---
## Business/Product Concerns
### [ ] BUSINESS: Free tier + Cloudflare free = 80% of paid product
Existing `Cache-Control` headers on context fetches are CDN-ready. A developer puts Cloudflare free tier in front and gets stale-while-revalidate at 300+ PoPs for $0. The 20% gap (user-scoped HMAC keying, PSR, render Workers) doesn't exist in code yet.
### [ ] BUSINESS: $20/seat wrong pricing model
"Seat" is undefined for a framework. Usage-based ($0.50/100K requests with generous free tier) or flat-per-project ($29/month) converts better for infrastructure products.
### [ ] BUSINESS: Ship framework first, cloud second
The framework has working code. The cloud product has zero. Risk: building both depletes runway before either has adoption. Recommended: get 500 devs using `@client` + `affects=` on their VPS first, then build the Edge product for the gap they actually hit.
---
## Validated Design Decisions (No Changes Needed)
These were confirmed sound by multiple reviewers:
- **Declarative invalidation graph** (`affects=` + auto-scoping) — unanimously praised as genuinely novel
- **Two-zone `fetch()` pattern** — correct architecture for global CDN caching from Workers
- **Cross-language protocol** — Python/TS with identical manifests, proven by parallel test suites
- **Manifest-driven URL resolution** — eliminates need for cache inventory state (no KV/DOs needed)
- **Typed `ReactContext` for `affects` targeting** — prevents the string-fragility concern (string form is escape hatch only)
- **Replacing React Query** — correct decision given context bundling + transport transparency goals
- **Cost model** — ~$5/month Cloudflare at 10K DAU, ~$20/month at 10x. Origin infra is the real cost.
- **Origin-side Redis cache as L2** — viable fallback behind CDN, same protocol as Edge
---
## Unique Expert Insights
**Cloudflare Expert:**
- Add `cf.cacheTtl` and `cf.cacheEverything` to all `fetch()` subrequests — don't rely solely on response headers
- Consider Cache Tags (`Cache-Tag` response header) from day one for Enterprise upgrade path
- Consider Durable Objects for per-user cache coordination as alternative to HMAC-in-URL
**Enterprise Architect:**
- Key derivation hierarchy: master secret derives per-context keys. Compromise of one context doesn't affect others.
- `X-Mizan-Cache-Version` header on every response for self-healing on version mismatch
**Serverless Expert:**
- Use `renderToReadableStream` (streaming SSR) in Render Worker, not `renderToString`. Memory and CPU budget are tight (128MB / 50ms).
- Cache manifest in `globalThis` in Edge Worker — do not read from KV per-request
- AWS portability: CloudFront invalidation pricing is 10-100x more expensive. Design TTL-based alternative.
**Next.js Expert:**
- PSR doesn't address cold-start pages (initial population before any mutation) or render fan-out (10K parameterized variants re-rendering on one mutation)
- No streaming/Suspense/progressive delivery — entire context response blocks on slowest function
**React Query Expert:**
- Wire existing WebSocket push infrastructure to emit invalidation events for named contexts
- Generated hooks should return `{ data, isLoading, isFetching, error }`, not throw on missing data
**Django Architect:**
- DRF `TokenAuthentication` collision: both use `Authorization: Bearer`, Mizan's JWT decode rejects DRF tokens with a 401
- `mizan-cache` as Django cache backend, not separate system
**Framework Authoring:**
- Define `CacheBackend` protocol before implementing — the abstraction is cheaper to get right before users exist
- Add `"version": 1` to manifest root now — adding it later is harder
- `@client` is approaching parameter overload — if `cache` becomes extensible, use `CachePolicy` object pattern, not more kwargs
**SaaS Founder:**
- The debugging UX for HMAC cache is a black box — invest in an invalidation graph debugging UI as a paid feature
- The `affects=` auto-refetch is the "wow" moment — optimize time-to-that-moment in onboarding

View File

@@ -10,13 +10,23 @@ Django + React ships first. The protocol is language-agnostic (proven by mizan-t
## Package Layout
```
packages/
mizan-django/ Python backend adapter (323+ tests)
mizan-react/ React frontend adapter (33 tests)
mizan-ts/ TypeScript backend adapter (22+ tests, proves AFI)
mizan-ssr/ Bun SSR worker subprocess
```
Two layers per side. Per-framework adapters wrap a single shared kernel; codegen targets the adapter.
**Backend protocol adapters** — implement the wire protocol on a server stack:
- `mizan-django/` — Django adapter
- `mizan-ts/` — TypeScript adapter (proves the protocol is language-agnostic)
**Frontend kernel + framework adapters** — kernel is the imperative client primitive set; each framework adapter wraps the kernel in its own idiomatic constructs:
- `mizan-runtime/` — framework-agnostic kernel; owns data, status, error; adapters subscribe
- `mizan-react/` — React contexts + hooks over the kernel
- `mizan-vue/` — Vue composables over the kernel
- `mizan-svelte/` — Svelte stores/runes over the kernel
**SSR worker:**
- `mizan-ssr/` — Bun subprocess used by the Django template backend
---
@@ -432,8 +442,24 @@ urlpatterns = [
---
## What the Codegen Currently Produces (STALE)
## Codegen Current State
The codegen in `mizan-django/generate/` still produces the **old pattern** that wraps `MizanProvider`. This needs rewriting to use the runtime (`mizanFetch`/`mizanCall`/`registerContext`) directly. See the `mizan-react/src/runtime/index.ts` for the target API.
The codegen is two-stage and per-framework. Stage 1 (in `mizan-django/generate/`) emits the framework-agnostic protocol layer (`callXxx` for mutations, `fetchXxx` for context bundles, types). Stage 2 emits per-framework hooks/composables/stores that subscribe to the `mizan-runtime` kernel.
The SSR pipeline works independently of the codegen — it renders whatever components are registered in the Bun worker. The codegen gap only affects the client-side hooks and context providers.
**What's in place:**
- Function hooks (`useEcho`, `useUserProfile`, etc.) in the React adapter, subscribing to kernel state via `useSyncExternalStore`
- Context hooks for named contexts and `global`
- Channel hooks for WebSocket transport
- Vue and Svelte equivalents (Stage 2 templates compile, but no live-backend example exercises them — see `ISSUES.md` A4)
**What's not yet emitted (the wrapper layer):**
- `<MizanContext>` provider component for React (calls `configure()` and mounts the kernel into the component tree)
- `useMizan()` hook for accessing the kernel from React
- Framework-named error class (e.g. `DjangoError`) wrapping `MizanError` from the kernel
- Vue and Svelte equivalents
The legacy `MizanProvider` in `mizan-react/src/context.tsx` (~750 lines) and the `forms.ts` consumer (~1163 lines) still depend on the pre-kernel API. They're tracked as `ISSUES.md` A1 and A3. Removing them is gated on the wrapper layer above being emitted.
The SSR pipeline is independent of the codegen — it renders whatever components are registered in the Bun worker.

297
README.md
View File

@@ -1,297 +0,0 @@
# mizan
Django + React server functions framework. RPC, not REST.
You define Python functions. mizan generates typed React hooks. No API routes, no serializers, no endpoint boilerplate.
```python
# Django
@client(context='global')
def current_user(request) -> UserOutput:
return UserOutput(email=request.user.email)
```
```tsx
// React (generated)
const user = useCurrentUser() // typed, SSR-hydrated, auto-refreshed
```
## Packages
| Package | Path | Install |
|---------|------|---------|
| `mizan` (Python) | `django/` | `uv add "mizan[channels] @ git+..."` |
| `@rythazhur/mizan` (TypeScript) | `react/` | `npm install @rythazhur/mizan@git+...` |
## Quick Start
### 1. Django setup
```python
# settings.py
INSTALLED_APPS = [
"mizan",
"myapp",
]
# urls.py
from django.urls import include, path
urlpatterns = [
path("api/mizan/", include("mizan.urls")),
]
# asgi.py (for WebSocket support)
from mizan import wrap_asgi
from django.core.asgi import get_asgi_application
application = wrap_asgi(get_asgi_application())
```
### 2. Define server functions
```python
# myapp/mizan_clients.py
from django.http import HttpRequest
from mizan.client import client
from mizan.setup.registry import register
from pydantic import BaseModel
class EchoOutput(BaseModel):
message: str
@client
def echo(request: HttpRequest, text: str) -> EchoOutput:
return EchoOutput(message=text)
register(echo, "echo")
```
### 3. Register in apps.py
```python
class MyAppConfig(AppConfig):
name = "myapp"
def ready(self):
import myapp.mizan_clients # noqa: F401
```
### 4. Generate TypeScript
```bash
# django.config.mjs
export default {
source: {
django: {
managePath: '../backend/manage.py',
command: ['uv', 'run', 'python'],
},
},
output: 'src/api/generated.ts',
}
```
```bash
npx mizan-generate
```
This produces typed hooks, a typed provider, form hooks with Zod validation, and channel hooks.
### 5. Use in React
```tsx
// layout.tsx
import { DjangoContext } from '@/api'
export default function Layout({ children }) {
return <DjangoContext>{children}</DjangoContext>
}
```
```tsx
// page.tsx
import { useEcho, useCurrentUser, DjangoError } from '@/api'
function MyComponent() {
const user = useCurrentUser()
const echo = useEcho()
const handleClick = async () => {
try {
const result = await echo({ text: 'hello' })
console.log(result.message) // typed
} catch (e) {
if (e instanceof DjangoError) {
console.log(e.code) // NOT_FOUND, VALIDATION_ERROR, etc.
}
}
}
}
```
## Features
| Backend | Frontend (generated) | Transport |
|---------|---------------------|-----------|
| `@client` | `useXxx()` | HTTP |
| `@client(context='global')` | `useXxx()` + SSR hydration | HTTP |
| `@client(context='local')` | `useXxx()` with params | HTTP |
| `@client(websocket=True)` | `useXxx()` | WebSocket RPC |
| `@client(auth=True\|'staff'\|callable)` | Auth errors as `DjangoError` | HTTP |
| `mizanFormMixin` | `useXxxForm()` + Zod validation | HTTP |
| `ReactChannel` | `useXxxChannel()` | WebSocket |
| `@compose(...)` | Combined providers | varies |
## Architecture
```
React app
└─ <DjangoContext> ← generated provider (includes ChannelProvider)
├─ useCurrentUser() ← generated context hook (SSR-hydrated)
├─ useEcho() ← generated function hook
├─ useContactForm() ← generated form hook (Zod + server validation)
└─ useChatChannel() ← generated channel hook (WebSocket)
├─ HTTP: POST /api/mizan/call/ { fn: "echo", args: { text: "hi" } }
└─ WS: { action: "rpc", fn: "echo", args: { text: "hi" } }
Django executor
├─ Pydantic input validation
├─ Auth check (session, JWT, or custom)
├─ Function execution
└─ Pydantic output serialization
```
The generated `DjangoContext` is the **only provider** needed. It wraps `mizanProvider` + `ChannelProvider` and handles session init, CSRF, context auto-fetching, and WebSocket connection.
## Code Generation
`npx mizan-generate` reads Django schemas (no running server needed) and produces:
| File | Contents |
|------|----------|
| `generated.mizan.ts` | Pydantic model types (via openapi-typescript) |
| `generated.django.tsx` | `DjangoContext` provider + all typed hooks |
| `generated.django.server.ts` | SSR hydration helper (`getDjangoHydration`) |
| `generated.forms.ts` | Form hooks with Zod schemas (`useContactForm`, etc.) |
| `generated.channels.ts` | Channel message types |
| `generated.channels.hooks.tsx` | Channel hooks (`useChatChannel`, etc.) |
| `index.ts` | Consolidated re-exports |
## Error Handling
All errors from server functions are thrown as `DjangoError`:
```tsx
try {
await echo({ text: 'hello' })
} catch (e) {
if (e instanceof DjangoError) {
e.code // 'NOT_FOUND' | 'VALIDATION_ERROR' | 'UNAUTHORIZED' | 'FORBIDDEN' | ...
e.message // Human-readable message
e.details // Field-level validation errors, etc.
e.isAuthError()
e.isValidationError()
e.getFieldErrors('email')
}
}
```
Error codes: `NOT_FOUND`, `VALIDATION_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`, `BAD_REQUEST`, `INTERNAL_ERROR`, `NOT_IMPLEMENTED`.
## Forms
Django forms get typed React hooks with client-side Zod validation:
```python
# Django
class ContactForm(mizanFormMixin, forms.Form):
mizan = mizanFormMeta(
name="contact",
title="Contact Us",
submit_label="Send",
live_validation=True,
)
name = forms.CharField(max_length=100)
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea)
def on_submit_success(self, request):
send_email(self.cleaned_data)
return {"sent": True}
```
```tsx
// React (generated)
const form = useContactForm()
form.schema // { fields: { name: {...}, email: {...} }, title, submit_label }
form.data // { name: '', email: '', message: '' }
form.set('email', v) // typed setter
form.errors // field-level errors (Zod + server)
form.submit() // → { success: true, data: { sent: true } }
```
## Channels
WebSocket channels with typed messages:
```python
# Django
class ChatChannel(ReactChannel):
class Params(BaseModel):
room: str
class ReactMessage(BaseModel):
text: str
class DjangoMessage(BaseModel):
text: str
user: str
def authorize(self, params):
return self.user.is_authenticated
def group(self, params):
return f"chat_{params.room}"
def receive(self, params, msg):
return self.DjangoMessage(text=msg.text, user=self.user.email)
```
```tsx
// React (generated)
const chat = useChatChannel({ room: 'general' })
chat.status // 'connecting' | 'connected' | 'disconnected'
chat.messages // ChatDjangoMessage[]
chat.send({ text: 'hello' })
```
## Testing
```bash
# Django unit tests
cd packages/mizan-django && uv sync --extra dev --extra channels && uv run pytest
# React unit tests
cd packages/mizan-react && npm test
# E2E integration tests (real browser, real backend)
docker compose -f examples/django-react-site/docker-compose.test.yml up -d
cd examples/django-react-site/harness && npm install && npx mizan-generate && npx vite --port 5174 &
npx playwright test
# All at once
make test-all
```
## Project Structure
```
mizan/
packages/
mizan-runtime/ Client state engine (~150 lines, framework-agnostic)
mizan-django/ Django server adapter (decorators, dispatch, contexts, SSR)
mizan-react/ React adapter (thin wrapper around runtime)
examples/
django-react-site/ E2E tests + Django backend
django-react-desktop-app/ PyWebView desktop app
```

View File

@@ -1,88 +1,50 @@
# Mizan Roadmap
## v1 — Django + React
## v1 — Django + Multi-Framework (React, Vue, Svelte)
### Done
- **@client decorator** — `context=`, `affects=`, `auth=`, `websocket=`
- **ReactContext class** — type-safe context/affects references with linting
- **Named contexts** — functions sharing a context name are grouped into one provider and one fetch
- **`@client` decorator** — `context=`, `affects=`, `auth=`, `websocket=`, `private=`, `route=`, `methods=`, `rev=`, `cache=`
- **`ReactContext` class** — type-safe context/affects references with linting
- **Named contexts** — functions sharing a context name grouped into one provider and one fetch
- **Context bundling endpoint** — `GET /api/mizan/ctx/<name>/` returns all functions in one response
- **Server-driven invalidation (JSON body)** — mutation responses carry `{"result": ..., "invalidate": [...]}`
- **Scoped invalidation** — runtime supports `invalidate: [{context: "user", params: {user_id: 5}}]`
- **Param elevation** — shared params become required provider props, non-shared become optional
- **Schema export** — `x-mizan-functions` + `x-mizan-contexts` for codegen
- **`X-Mizan-Invalidate` header** — second invalidation transport for view-path responses (redirects, HTML)
- **Return-type branching** — data return → RPC path; `HttpResponse` return → view path
- **Scoped invalidation** — `affects_params` lambda; runtime supports `{context, params}` form
- **Auth guards** — `auth=True`, `auth='staff'`, `auth='superuser'`, `auth=callable`
- **JWT + session auth** — auto-detected, CSRF handled
- **MWT** — Mizan Web Token for Edge cache keying (separate secret from JWT/cache)
- **Shapes** — Pydantic + django-readers for typed query projections
- **WebSocket channels** — real-time bidirectional communication
- **Codegen** — generates typed React providers, hooks, mutations from schema
- **Protocol-managed caching** — `no-store` on all origin responses, deterministic JSON on context GETs
- **HMAC cache keying** — origin-side cache with cross-language HMAC conformance (Python + TypeScript pin)
- **Edge manifest** — `python manage.py export_edge_manifest`; both RPC and view-path functions
- **SSR bridge** — Django template backend → persistent Bun subprocess via JSON-RPC
- **`mizan-runtime` kernel** — framework-agnostic imperative client primitives (data/status/error owned by kernel)
- **Two-stage codegen** — Stage 1 emits framework-agnostic protocol layer; Stage 2 emits per-framework hooks (React, Vue, Svelte)
- **`mizan-ts`** — TypeScript backend adapter; proves the protocol is language-agnostic
### Next: X-Mizan-Invalidate Header
---
Second invalidation transport. For view responses (redirects, HTML), invalidation goes in an HTTP header instead of the JSON body. Both transports are first-class AFI spec.
### Next (in progress)
- Header format: `X-Mizan-Invalidate: user;user_id=5, notifications`
- Comma-separated contexts, semicolon-separated params per context
- Decorator auto-adds header to any HttpResponse with `affects=`
- Edge reads this header to purge cached pages
- Runtime also reads it on XHR/fetch responses (htmx path)
- **React adapter wrapper layer** — codegen emits `MizanContext` provider, `useMizan` hook, `DjangoError` class on top of the `mizan-runtime` kernel. Equivalent wrapper layers for Vue and Svelte adapters. The harness in `examples/django-react-site` is blocked on this.
- **Legacy `MizanProvider` removal (A1)** — `mizan-react/src/context.tsx` (~750 lines) replaced by codegen-emitted wrappers. Blocks v1 `mizan-react` publishing.
- **Forms migration to kernel (A3)** — `mizan-react/src/forms.ts` (~1163 lines) currently consumes legacy `MizanProvider`. Rewrite to use `mizanCall` from the kernel. Blocks A1.
- **Allauth extraction (A2)** — `legacy/allauth/` becomes `mizan-django-allauth` package consuming Mizan's public API.
- **Vue/Svelte e2e validation (A4)** — example apps exercising a live backend end-to-end, like `examples/django-react-site` does for React.
- **Test coverage gaps** — T1T12 in `ISSUES.md` (kernel state machine, view-path purge, SSR thread safety, retry logic, cross-language HMAC pin, etc.)
### Next: Return-Type Branching
---
`@client` serves both RPC developers (React/SPA) and view developers (htmx/templates). Return type determines behavior:
### Quality
- **Data return** (dict, Shape, BaseModel) → RPC path. Generates typed hooks. Invalidation in JSON body.
- **HttpResponse return** (render, redirect) → View path. No codegen. Invalidation in `X-Mizan-Invalidate` header.
Same decorator. Same `affects=`. Same invalidation graph. Two paths.
### Next: affects_params
Scoped invalidation with a lambda that extracts which params were affected:
```python
@client(affects='user', affects_params=lambda req: {'user_id': req.user.pk})
def update_name(request, name: str) -> dict:
...
```
Produces `invalidate: [{context: "user", params: {user_id: 5}}]` in JSON body or `X-Mizan-Invalidate: user;user_id=5` in header.
### Next: Edge Manifest
`mizan-generate --manifest` compiles the decorator registry + Django URL conf into static JSON for Edge:
```json
{
"contexts": {
"user": {
"endpoints": ["/api/mizan/ctx/user/"],
"views": ["/profile/:user_id/"],
"params": ["user_id"]
}
}
}
```
Edge reads the manifest at deploy time. When it receives `X-Mizan-Invalidate: user;user_id=5`, it resolves URL patterns with params and purges `/profile/5/` and `/api/mizan/ctx/user/?user_id=5`.
Generated alongside React code. Covers both RPC and view-path functions.
### Next: Codegen Rewrite
Generated code uses the runtime directly (`mizanFetch`, `mizanCall`, `registerContext`) instead of the legacy `MizanProvider` pattern. Mutations have zero invalidation knowledge — the runtime reads the server response.
### Next: SSR Bridge
Django renders React components server-side via a persistent Bun subprocess.
- Bun worker: stdin/stdout JSON-RPC, `renderToString`, component registry
- Django bridge: subprocess management, IPC, request synthesis
- Template tag: `{% mizan_render "ProfilePage" user_profile=profile %}`
- Hydration: `window.__MIZAN_SSR_DATA__` consumed by generated providers
- Generated contexts check SSR data before first fetch
- **H5** — Mutation hooks expose no loading/error state
- **H7** — Redis SCAN blocks request path at scale
- **H8** — Svelte codegen uses Svelte 4 stores; should use Svelte 5 runes
- **H9** — Svelte `destroy()` not auto-called (memory leak)
- **H12** — Forms `triggerValidation` captures stale data
- Medium issues (M1M18) per developer judgment
---
@@ -92,7 +54,7 @@ Django renders React components server-side via a persistent Bun subprocess.
Cloudflare Workers for automatic edge caching.
- Reads the Edge manifest to configure cache rules
- Reads the edge manifest to configure cache rules
- Context GETs cached at edge, keyed by context name + params
- Reads `X-Mizan-Invalidate` header from mutation responses to purge caches
- Reads JSON `invalidate` key from RPC responses for the same purpose
@@ -118,74 +80,13 @@ One-command deployment for Django + React apps.
---
## Protocol Spec (AFI)
## Reference
The protocol is the product. Two invalidation transports. Every endpoint CDN-ready.
Wire protocol shapes (context fetch, mutation call, invalidation transports) are documented in `CLAUDE.md`. Architectural details for specific subsystems live in `docs/`:
### Context fetch
```
GET /api/mizan/ctx/<name>/?param=value
200 OK
Cache-Control: no-store
{
"function_a": { ... },
"function_b": [ ... ]
}
```
### Mutation call (RPC path — JSON body transport)
```
POST /api/mizan/call/
Cache-Control: no-store
{
"result": { ... },
"invalidate": ["context_name"]
}
```
### Mutation call (View path — header transport)
```
POST /profile/update/
302 Found
Location: /profile/5/
Cache-Control: no-store
X-Mizan-Invalidate: user;user_id=5, notifications
```
### Scoped invalidation (JSON)
```json
{
"result": { ... },
"invalidate": [
"notifications",
{ "context": "user", "params": { "user_id": 5 } }
]
}
```
### Scoped invalidation (Header)
```
X-Mizan-Invalidate: user;user_id=5, notifications
```
### Edge manifest
```json
{
"contexts": {
"user": {
"endpoints": ["/api/mizan/ctx/user/"],
"views": ["/profile/:user_id/"],
"params": ["user_id"]
}
}
}
```
- `docs/AFI_ARCHITECTURE.md` — package architecture, kernel model, adapter strategy
- `docs/CACHE_KEYING.md` — HMAC cache key derivation
- `docs/MWT_SPEC.md` — Mizan Web Token format
- `docs/SSR_ARCHITECTURE.md` — Django template backend, Bun bridge
- `docs/PSR_VS_EDGE.md` — protocol-level rendering vs. paid Edge layer
- `docs/PRODUCT_ARCHITECTURE.md` — product surface and pricing tiers

77
docs/AFI_ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,77 @@
# AFI Architecture
Mizan is an **Application Framework Interface (AFI)** — the
server-client unification layer.
## Package layout
Two layers per side. Independent packages, single shared protocol.
**Backend protocol adapters** — implement the wire protocol on a
server stack:
| Package | Role |
|---|---|
| `mizan-django` | Django adapter |
| `mizan-ts` | TypeScript adapter (proves the protocol is language-agnostic) |
**Frontend kernel + framework adapters** — the kernel is the
imperative client primitive set; each framework adapter wraps the
kernel in its own idiomatic constructs:
| Package | Role |
|---|---|
| `mizan-runtime` | Framework-agnostic client kernel — owns data, status, error; adapters subscribe |
| `mizan-react` | React contexts + hooks over the kernel |
| `mizan-vue` | Vue composables over the kernel |
| `mizan-svelte` | Svelte stores/runes over the kernel |
**SSR worker:**
| Package | Role |
|---|---|
| `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
Independent and composable. Either ships standalone; together they
compose.
## Kernel model
The client kernel (`mizan-runtime`) is the one hard thing. Per-
framework adapters are thin idiomatic wrappers around it. Codegen
emits typed bindings against the framework adapter's surface, not
against the raw kernel — so a React developer gets `useEcho()` and
`<MizanContext>`, a Vue developer gets `useEcho()` composables, a
Svelte developer gets readable stores. Same kernel underneath.
## Schema is load-bearing
The backend exports a JSON schema describing every `@client`-decorated
function and context (`x-mizan-functions`, `x-mizan-contexts`). The
schema IS the contract: codegen reads it, the edge manifest derives
from it, MWT auth gates against it.
## Launch surface
Python (Django) + React. Vue and Svelte ship as v1 alongside React.
TypeScript backend (`mizan-ts`) proves the protocol is portable.
## Why the AFI shape
Quadratic ecosystem growth (N server adapters × M client adapters)
collapses to linear (one adapter per stack) when both sides
communicate through a shared protocol.
## Invariants
- All cross-package communication goes through the protocol. No
direct cross-package dependencies.
- New adapters land as new packages, not as modifications to existing
ones.
- Framework adapters wrap the kernel in framework idioms — they
don't bypass it. Codegen targets the adapter, not the raw kernel.

73
docs/CACHE_KEYING.md Normal file
View File

@@ -0,0 +1,73 @@
# Cache Keying
*Discovered 2026-04-06.*
## The gap
Mizan specified invalidation but never specified cache keying.
Without correct cache keying, Edge caching is a **security
vulnerability** — it serves User A's content to User B.
## Why Vary doesn't work
All major CDNs ignore `Vary` for personalized content. No
standardized replacement exists.
## Resolution: HMAC cache key (JSON-canonical form)
```
HMAC-SHA256(secret, JSON.stringify({
"c": context,
"p": sorted_params,
"r": rev,
"u": user_id // omitted for public content
}, sort_keys=True))
```
### Key derivation rules
- **Public content** — URL path + query params (standard CDN).
- **User-scoped content** — HMAC key derivation above.
- **`@client(auth=...)`** determines whether content is user-scoped.
- **`rev` parameter** on `@client` for deploy-time logic
invalidation. Bumped by the developer when function logic changes.
## Identity layer
MWT (Mizan Web Token) — see [MWT_SPEC.md](MWT_SPEC.md). JWT with
Mizan claims on `X-Mizan-Token` header. Replaces the old
`JWTUser` + permission key metadata approach.
## Cache architecture
*Decided 2026-04-06.*
**Not a compiled binary ABI. Not a pluggable Python protocol.**
Each backend adapter (Python, TypeScript, future PHP/C#/Go)
implements the cache protocol in its own language, backed by Redis.
**Conformance verified by a shared test suite.**
### Required operations
- `cache_get`
- `cache_put`
- `cache_purge`
- `cache_purge_user`
### Storage
Redis only. Handles persistence, cross-worker sharing, crash
recovery.
## Deploy invalidation
No full context flush. The `rev` parameter on `@client` is part of
the HMAC key. When the developer bumps `rev`, old cache entries
become **unreachable orphans**. No purge needed; no thundering herd.
## Invariant
All cache-related code must implement *identical* HMAC key
derivation. Cross-language conformance tests enforce this. Any
divergence is a security vulnerability.

59
docs/MWT_SPEC.md Normal file
View File

@@ -0,0 +1,59 @@
# MWT — Mizan Web Token
*Decided 2026-04-06.*
MWT is a standard JWT (RFC 7519, HMAC-SHA256) with Mizan-specific
claims, traveling on the `X-Mizan-Token` header. It is the
protocol's identity layer for cache keying and permission staleness
detection.
## Claims
| Claim | Purpose |
|---|---|
| `sub` | User ID — goes into HMAC cache key derivation |
| `pkey` | Deterministic hash of user's permission state at issuance |
| `exp` | Configurable short TTL — controls permission staleness window (Django setting) |
| `iat` | Issued at |
| `kid` | Key ID — for secret rotation |
| `aud` | Audience binding — prevents cross-tenant replay |
## Key decisions
- **Standard JWT envelope, not proprietary.** Uses standard libraries
for signing and validation.
- **`X-Mizan-Token` header, not `Authorization: Bearer`.** Avoids
collision with DRF, allauth, and existing JWT systems. Cloudflare
WAF/Access do not inspect custom headers.
- **Replaces `JWTUser` + `_try_jwt_auth` entirely.** Old approach is
deleted.
- **App handles authentication** (session, social, etc.). Mizan
issues MWT *from* the authenticated identity.
- **Edge Worker** validates MWT, extracts `sub` for HMAC cache key,
checks `exp`.
- **`pkey` computation must be deterministic:**
`sorted(user.get_all_permissions())` then hash.
- **Client-side: proactive refresh before expiry.** Check TTL before
dispatch, not reactively after a 401.
- **Header-based, not cookie-based.** A cookie would force
`Vary: Cookie`, destroying PSR cache.
## HMAC canonical form
JSON with sorted keys:
```
HMAC(secret, JSON.stringify({"c": context, "p": sorted_params, "u": user_id}))
```
## What this solves
- DRF token collision
- `JWTUser`-too-thin problem
- Permission staleness race condition
- Single validation path across Python and TypeScript Edge
## Usage rule
All cache-layer auth code uses MWT, not Django session or raw JWT.
The `@client(auth=...)` parameter gates on MWT validity.

View File

@@ -0,0 +1,61 @@
# Product Architecture
*Revised April 2026.*
## Launch product: Mizan Render
**$20/seat/month.**
Protocol-aware Edge caching + PSR delivery via Cloudflare + render
Workers + TS backend hosting via Workers for Platforms.
Developer's stack = their backend + database. Cloudflare handles
read traffic, rendering, and caching.
## Deferred: Mizan Deploy
Django hosting requires IaaS compliance: gVisor, KMS, NIS2,
multi-state privacy. ~$58K legal costs.
**Deferred until Render revenue funds it.**
TS "Deploy" exists via Workers for Platforms at no additional
compliance cost.
## Free framework: mizan-cache (origin-side cache)
Python package implementing the **full cache protocol locally**
same HMAC key derivation, metadata schema, and purge semantics as
Edge.
Three backends:
- In-memory dict (default)
- Redis
- SQLite
### Dual purpose
1. Makes the free framework genuinely powerful (PSR + typed hooks +
invalidation + caching with zero cost).
2. Provides a unit-testable surface for all cache mechanics without
Cloudflare.
## Spec additions
- `@client(cache=False)` — uncacheable; emits `Cache-Control: no-store`.
- Cache ABI: `get(key)`, `put(key, response, metadata)`,
`purge(context, params)`.
## Launch compliance (Render only)
Entirely Cloudflare Workers + management API (Django/Postgres):
- GDPR DPA + privacy policy + subprocessor list — ~$5001K legal
- DMCA — $6
- No NIS2, no gVisor, no KMS
## Invariant
All architecture decisions target the Render-only launch posture.
Don't build Deploy infrastructure prematurely.

38
docs/PSR_VS_EDGE.md Normal file
View File

@@ -0,0 +1,38 @@
# PSR vs Edge Delivery
Two distinct layers that prior conversations have conflated. They are
independent.
## PSR — Preemptive Static Rendering
**Protocol feature.** Render HTML on mutation, not on request.
Mechanism: `@client` fires mutation → backend adapter triggers local
render runtime → HTML stored locally.
Works on a $5 VPS with local Bun. **No Edge required.** PSR is part
of the protocol; it's available to every Mizan deployment regardless
of hosting.
## Edge Delivery — Mizan Render (Paid Product)
Pre-rendered HTML cached globally on Cloudflare CDN.
Uses `fetch()` to a render Worker on a separate domain
(`render.mizan.cloud`) instead of `cache.put()`, because Workers
Cache API is per-datacenter only. `fetch()` across zones goes through
the global CDN cache path with Tiered Cache.
This layer is the paid Mizan Render product.
## Caching modes
- **Public content** — preemptive (render on mutation)
- **User-scoped content** — reactive only (purge on mutation, render
on next request)
## Invariant
PSR logic must not couple to Cloudflare-specific APIs. PSR must work
without any cloud infrastructure. Edge delivery extends PSR; it does
not replace it.

50
docs/SSR_ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,50 @@
# 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.
```python
TEMPLATES = [
{
'BACKEND': 'mizan.ssr.MizanTemplates',
...
}
]
```
Then `render(request, 'ProfilePage', context)` calls the Bun
subprocess bridge instead of rendering a Django/Jinja2 template.
**The component name IS the template name.**
## AFI boundary
| Side | Responsibility |
|---|---|
| Backend adapter | Implements `mizan.ssr()` — executes context functions, gathers data |
| Frontend adapter | Implements `renderToHTML()` — takes component + props, produces HTML |
| Bun subprocess | Hosts the frontend adapter |
| stdin/stdout JSON-RPC | Transport between the two |
## 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.
- The template tag `{% mizan_render %}` is a convenience for
developers who *also* use Django templates (e.g., a base.html shell
with Mizan components inside).
## Implementation surface
The SSR bridge module implements Django's template backend interface:
- `BaseEngine` subclass
- `Template` class with `.render(context, request)`
Everything Django expects from a template backend, but the actual
rendering routes to Bun.