diff --git a/Dockerfile.test b/Dockerfile.test
index d976219..9c04acc 100644
--- a/Dockerfile.test
+++ b/Dockerfile.test
@@ -7,7 +7,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*
-# Install djarea from local source with channels support
+# Install mizan from local source with channels support
COPY django/ /app/django/
RUN pip install --no-cache-dir /app/django[channels] daphne
diff --git a/MIZAN.md b/MIZAN.md
index e467de0..f3debd4 100644
--- a/MIZAN.md
+++ b/MIZAN.md
@@ -6,7 +6,7 @@ This plan was written by Ryth's Claude.ai session after an extended design conve
reviewing the full codebase, the original @compose discussion from January 2025, and
several rounds of architectural refinement. Treat this as the spec.
-The framework formerly called Djarea is now called **MIZAN**. Package names, imports,
+The framework formerly called mizan is now called **MIZAN**. Package names, imports,
and references should be updated accordingly. The internal codegen engine is called
**Maison** — it lives inside Mizan and does not need its own public surface.
diff --git a/Makefile b/Makefile
index 15e5922..fae896d 100644
--- a/Makefile
+++ b/Makefile
@@ -20,7 +20,7 @@ test-react:
test-integration: docker-up
@echo "Waiting for backend..."
- @timeout 30 sh -c 'until curl -sf http://localhost:8000/api/djarea/session/ > /dev/null 2>&1; do sleep 1; done'
+ @timeout 30 sh -c 'until curl -sf http://localhost:8000/api/mizan/session/ > /dev/null 2>&1; do sleep 1; done'
cd react && npm run test:integration
@$(MAKE) docker-down
@@ -41,6 +41,6 @@ test-all: test test-integration
clean:
docker compose -f docker-compose.test.yml down -v --remove-orphans 2>/dev/null || true
- rm -rf django/src/djarea.egg-info django/dist django/build
+ rm -rf django/src/mizan.egg-info django/dist django/build
rm -rf react/dist react/node_modules
rm -f example/db.sqlite3
diff --git a/README.md b/README.md
index 92670f2..9df30b1 100644
--- a/README.md
+++ b/README.md
@@ -1,94 +1,84 @@
-# DJAREA
+# mizan
-A modern Django + React Framework for perfectionists with deadlines.
+Django + React server functions framework. RPC, not REST.
-Write a Pydantic function, add the @client decorator, use configurable **Shape** types for your models.
-
-Djarea generates the entire React client: all your type interfaces, function call hooks, autoatic JWT, and a simple `` to make it all work.
-
-No API routing, no serializers, no REST/CRUD bullshit.
+You define Python functions. mizan generates typed React hooks. No API routes, no serializers, no endpoint boilerplate.
```python
-@client
-def current_user(request) -> UserShape:
- return UserShape.query(lambda qs: qs.filter(pk=request.user.pk))[0]
+# Django
+@client(context='global')
+def current_user(request) -> UserOutput:
+ return UserOutput(email=request.user.email)
```
-
```tsx
-const user: UserShape = useCurrentUser() // typed, cached, SSR-hydrated
+// React (generated)
+const user = useCurrentUser() // typed, SSR-hydrated, auto-refreshed
```
-The **Function** is the API contract. The **Shape** is the query. The hook is the artifact. That's it.
+## Packages
-Starts with session auth and upgrades to JWT on login. **It just works**.
+| Package | Path | Install |
+|---------|------|---------|
+| `mizan` (Python) | `django/` | `uv add "mizan[channels] @ git+..."` |
+| `@rythazhur/mizan` (TypeScript) | `react/` | `npm install @rythazhur/mizan@git+...` |
-## What Djarea does
-
-A `@client` function in Django becomes a callable hook in React. The function's type signature orchestrates the entire pipeline for you — input validation, output serialization, TypeScript interfaces, and SQL projection.
-
-```python
-class ArticleShape(Shape[Article]):
- id: int | None = None
- title: str
- author: FlatAuthorShape
- tags: list[TagShape] = []
-```
-
-One Djarea **Shape** does three things simultaneously:
-- Defines the Pydantic model for validation and serialization
-- Generates a django-readers spec for a lean, field-scoped SQL query
-- Produces the TypeScript interface on the React side
-
-Shapes are your codebase's **single source of truth** for backend/frontend data transfer.
-
-## Quick start
+## Quick Start
### 1. Django setup
```python
# settings.py
INSTALLED_APPS = [
- "djarea",
+ "mizan",
"myapp",
]
# urls.py
from django.urls import include, path
urlpatterns = [
- path("api/djarea/", include("djarea.urls")),
+ path("api/mizan/", include("mizan.urls")),
]
# asgi.py (for WebSocket support)
-from djarea import wrap_asgi
+from mizan import wrap_asgi
from django.core.asgi import get_asgi_application
application = wrap_asgi(get_asgi_application())
```
-### 2. Define your client functions
+### 2. Define server functions
```python
-# myapp/clients.py
-from djarea.client import client
-from djarea.shapes import Shape
+# 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, text: str) -> EchoOutput:
+def echo(request: HttpRequest, text: str) -> EchoOutput:
return EchoOutput(message=text)
+
+register(echo, "echo")
```
-Functions in `clients.py` are discovered automatically — same convention as `models.py`.
+### 3. Register in apps.py
-### 3. Generate TypeScript
+```python
+class MyAppConfig(AppConfig):
+ name = "myapp"
-To get your generated React client, set this up in your frontend root:
+ def ready(self):
+ import myapp.mizan_clients # noqa: F401
+```
-```javascript
-// django.config.mjs
+### 4. Generate TypeScript
+
+```bash
+# django.config.mjs
export default {
source: {
django: {
@@ -100,23 +90,27 @@ export default {
}
```
-Run this command everytime your client needs updating. You can also throw this it on a file watcher pointed at your backend code:
-
```bash
-npx djarea-generate
+npx mizan-generate
```
-### 4. Use in React
+This produces typed hooks, a typed provider, form hooks with Zod validation, and channel hooks.
+
+### 5. Use in React
```tsx
-import { DjangoContext, useEcho, useCurrentUser, DjangoError } from '@/api'
+// layout.tsx
+import { DjangoContext } from '@/api'
-// layout.tsx — one provider, handles everything
export default function Layout({ children }) {
return {children}
}
+```
+```tsx
// page.tsx
+import { useEcho, useCurrentUser, DjangoError } from '@/api'
+
function MyComponent() {
const user = useCurrentUser()
const echo = useEcho()
@@ -127,80 +121,91 @@ function MyComponent() {
console.log(result.message) // typed
} catch (e) {
if (e instanceof DjangoError) {
- console.log(e.code) // NOT_FOUND, VALIDATION_ERROR, etc.
- e.getFieldErrors('email') // field-level errors
+ console.log(e.code) // NOT_FOUND, VALIDATION_ERROR, etc.
}
}
}
}
```
-## Shapes
+## Features
-Shapes are Djarea's data protocol. A Shape defines exactly which fields to select from the database, validated through Pydantic and projected through django-readers. Different views get different Shapes — same model, different queries.
+| 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 |
-```python
-# Full detail page — joins books with chapters
-class AuthorDetailShape(Shape[Author]):
- id: int | None = None
- name: str
- bio: str
- books: list[BookShape] = []
+## Architecture
-# Dropdown menu — two columns, no joins
-class FlatAuthorShape(Shape[Author]):
- id: int | None = None
- name: str
+```
+React app
+ └─ ← 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
```
-```python
-# Detail page: SELECT id, name, bio + prefetch books
-authors = AuthorDetailShape.query()
+The generated `DjangoContext` is the **only provider** needed. It wraps `mizanProvider` + `ChannelProvider` and handles session init, CSRF, context auto-fetching, and WebSocket connection.
-# Dropdown: SELECT id, name. That's it.
-authors = FlatAuthorShape.query()
+## 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')
+ }
+}
```
-Shapes also support diffing. When the frontend sends state back, the diff system compares incoming data against the current database state and tells you exactly what changed:
-
-```python
-@client
-def update_articles(request, articles: list[ArticleShape]) -> dict:
- for article, diff in ArticleShape.diff_many(articles):
- if diff.is_new:
- create_article(article)
- elif diff.changed:
- update_fields(article, diff.changed)
- for tag in diff.tags.created:
- add_tag(article, tag)
- for tag_id in diff.tags.deleted:
- remove_tag(article, tag_id)
- return {"ok": True}
-```
-
-One query fetches all current state. The diff is per-field and per-nested-relation. Your service code only touches what actually changed.
-
-## The `@client` decorator
-
-The decorator controls transport, caching, auth, and SSR behavior:
-
-| Decorator | React hook | What it does |
-|-----------|-----------|--------------|
-| `@client` | `useEcho()` | HTTP call, returns typed result |
-| `@client(context='global')` | `useCurrentUser()` | Fetched once, cached in context, SSR-hydrated |
-| `@client(context='local')` | `useArticle({ id })` | Cached per unique params |
-| `@client(websocket=True)` | `useSearch()` | Runs over WebSocket instead of HTTP |
-| `@client(auth=True)` | — | Requires authentication |
-| `@client(auth='staff')` | — | Requires staff status |
-| `@client(auth=my_check)` | — | Custom auth callable |
+Error codes: `NOT_FOUND`, `VALIDATION_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`, `BAD_REQUEST`, `INTERNAL_ERROR`, `NOT_IMPLEMENTED`.
## Forms
-Django forms become typed React hooks with client-side Zod validation:
+Django forms get typed React hooks with client-side Zod validation:
```python
-class ContactForm(DjareaFormMixin, forms.Form):
- djarea = DjareaFormMeta(
+# Django
+class ContactForm(mizanFormMixin, forms.Form):
+ mizan = mizanFormMeta(
name="contact",
title="Contact Us",
submit_label="Send",
@@ -216,22 +221,22 @@ class ContactForm(DjareaFormMixin, forms.Form):
```
```tsx
+// React (generated)
const form = useContactForm()
-form.schema // field metadata, title, submit label
+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 } }
```
-Zod schemas are generated from the Django form definition. Validation runs client-side first, server-side second. No duplicated validation logic.
-
## Channels
WebSocket channels with typed messages:
```python
+# Django
class ChatChannel(ReactChannel):
class Params(BaseModel):
room: str
@@ -252,6 +257,7 @@ class ChatChannel(ReactChannel):
```
```tsx
+// React (generated)
const chat = useChatChannel({ room: 'general' })
chat.status // 'connecting' | 'connected' | 'disconnected'
@@ -259,111 +265,32 @@ chat.messages // ChatDjangoMessage[]
chat.send({ text: 'hello' })
```
-## Architecture
-
-```
-React app
- └─ ← generated provider (session, CSRF, WebSocket)
- ├─ useCurrentUser() ← context hook (SSR-hydrated)
- ├─ useEcho() ← function hook
- ├─ useContactForm() ← form hook (Zod + server validation)
- └─ useChatChannel() ← channel hook (WebSocket)
- │
- ├─ HTTP: POST /api/djarea/call/ { fn: "echo", args: { text: "hi" } }
- └─ WS: { action: "rpc", fn: "echo", args: { text: "hi" } }
- │
- Django executor
- ├─ Pydantic input validation
- ├─ Auth check
- ├─ Function execution
- └─ Pydantic output serialization
-```
-
-All transport goes through a single endpoint. The generated `DjangoContext` is the only provider. It handles session init, CSRF, context auto-fetching, and WebSocket connection.
-
-## Code generation
-
-`npx djarea-generate` reads Django schemas at build time (no running server) and produces:
-
-| File | Contents |
-|------|----------|
-| `generated.djarea.ts` | Pydantic model types |
-| `generated.django.tsx` | `DjangoContext` provider + typed hooks |
-| `generated.django.server.ts` | SSR hydration helper |
-| `generated.forms.ts` | Form hooks with Zod schemas |
-| `generated.channels.ts` | Channel message types |
-| `generated.channels.hooks.tsx` | Channel hooks |
-| `index.ts` | Re-exports |
-
-## Error handling
-
-All errors from server functions throw as `DjangoError`:
-
-```tsx
-if (e instanceof DjangoError) {
- e.code // 'NOT_FOUND' | 'VALIDATION_ERROR' | 'UNAUTHORIZED' | ...
- e.message // human-readable
- e.details // field-level validation errors
- e.isAuthError()
- e.isValidationError()
- e.getFieldErrors('email')
-}
-```
-
-## Why RPC instead of REST
-
-REST exposes your database tables as CRUD endpoints and pushes business logic to the frontend. "Submit an application" becomes PATCH one resource, POST another, PUT a third — choreographed by client code.
-
-Djarea keeps business logic on the server. You write functions that do things. The frontend calls them. The server knows what "submit" means. The client doesn't need to.
-
-If you delete the frontend of a REST app, your backend is a database. If you delete the frontend of a Djarea app, your backend still has your entire application logic.
-
-## Packages
-
-| Package | Install |
-|---------|---------|
-| `djarea` (Python) | `pip install djarea` |
-| `@rythazhur/djarea` (TypeScript) | `npm install @rythazhur/djarea` |
-
-For WebSocket support: `pip install "djarea[channels]"`
-
## Testing
```bash
-# Django
-cd django && uv run pytest
+# Django unit tests
+cd django && uv sync --extra dev --extra channels && uv run pytest
-# React
+# React unit tests
cd react && npm test
-# E2E (Playwright, real browser + real backend)
+# E2E integration tests (real browser, real backend)
docker compose -f docker-compose.test.yml up -d
-cd e2e/harness && npx djarea-generate && npx playwright test
+cd e2e/harness && npm install && npx mizan-generate && npx vite --port 5174 &
+npx playwright test
-# Everything
+# All at once
make test-all
```
-## Project structure
+## Project Structure
```
-djarea/
- django/ Python package
- react/ TypeScript package
- example/ Integration test backend
- e2e/ Playwright E2E tests
+mizan/
+ django/ Python package (mizan)
+ react/ TypeScript package (@rythazhur/mizan)
+ example/ Integration test backend (Docker)
+ desktop/ PyWebView desktop test app
+ e2e/ Playwright E2E tests + React harness
Makefile Test orchestration
```
-
-## Disclosure
-
-Djarea was developed with the assistance of IDE AI Assistance and later with Claude Code.
-
-The architecture, design decisions, developer experience standards and technical direction are mine. I've been programming for 16 years and have a lot of opinions!
-
-DX ideas are inspired by the amazing work of these projects and the hardworking folks behind them:
-- Django Ninja
-- Django Readers
-- Django RAPID Architecture
-- React
-- Next.js
\ No newline at end of file
diff --git a/desktop/app.py b/desktop/app.py
index 604f858..0492959 100644
--- a/desktop/app.py
+++ b/desktop/app.py
@@ -1,9 +1,9 @@
#!/usr/bin/env python
"""
-Djarea Desktop — PyWebView + Django local RPC.
+mizan Desktop — PyWebView + Django local RPC.
Starts a local Django ASGI server and opens a native desktop window.
-All communication between the UI and backend uses Djarea server functions.
+All communication between the UI and backend uses mizan server functions.
"""
import os
@@ -63,7 +63,7 @@ def main():
base_url = f"http://{host}:{port}"
- if not wait_for_server(f"{base_url}/api/djarea/session/"):
+ if not wait_for_server(f"{base_url}/api/mizan/session/"):
print("ERROR: Django server failed to start", file=sys.stderr)
sys.exit(1)
@@ -83,7 +83,7 @@ def main():
import webview
window = webview.create_window(
- title="Djarea Desktop",
+ title="mizan Desktop",
url=base_url,
width=1024,
height=768,
diff --git a/desktop/backend/asgi.py b/desktop/backend/asgi.py
index a7a5338..2852595 100644
--- a/desktop/backend/asgi.py
+++ b/desktop/backend/asgi.py
@@ -6,8 +6,8 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
django.setup()
from django.core.asgi import get_asgi_application
-from djarea import wrap_asgi
+from mizan import wrap_asgi
-import backend.djarea_clients # noqa: F401
+import backend.mizan_clients # noqa: F401
application = wrap_asgi(get_asgi_application())
diff --git a/desktop/backend/djarea_clients.py b/desktop/backend/djarea_clients.py
index acb1b2c..80e7993 100644
--- a/desktop/backend/djarea_clients.py
+++ b/desktop/backend/djarea_clients.py
@@ -1,7 +1,7 @@
"""
Desktop RPC server functions.
-Tests Djarea's appropriateness for desktop apps:
+Tests mizan's appropriateness for desktop apps:
- Local file system access
- SQLite CRUD
- System introspection
@@ -20,10 +20,10 @@ from pathlib import Path
from django.http import HttpRequest
from pydantic import BaseModel
-from djarea.client import client
-from djarea.channels import ReactChannel
-from djarea.setup.registry import register
-from djarea.channels import register as register_channel
+from mizan.client import client
+from mizan.channels import ReactChannel
+from mizan.setup.registry import register
+from mizan.channels import register as register_channel
# =============================================================================
@@ -40,12 +40,12 @@ class SystemInfoOutput(BaseModel):
home_dir: str
cwd: str
cpu_count: int
- djarea_version: str
+ mizan_version: str
@client(websocket=True)
def system_info(request: HttpRequest) -> SystemInfoOutput:
- import djarea
+ import mizan
return SystemInfoOutput(
os_name=platform.system(),
@@ -56,7 +56,7 @@ def system_info(request: HttpRequest) -> SystemInfoOutput:
home_dir=str(Path.home()),
cwd=os.getcwd(),
cpu_count=os.cpu_count() or 1,
- djarea_version=getattr(djarea, "__version__", "dev"),
+ mizan_version=getattr(mizan, "__version__", "dev"),
)
@@ -114,16 +114,20 @@ def list_files(request: HttpRequest, directory: str = "~") -> ListFilesOutput:
entries = []
try:
- for entry in sorted(dir_path.iterdir(), key=lambda e: (not e.is_dir(), e.name.lower())):
+ for entry in sorted(
+ dir_path.iterdir(), key=lambda e: (not e.is_dir(), e.name.lower())
+ ):
try:
stat = entry.stat()
- entries.append(FileEntry(
- name=entry.name,
- path=str(entry),
- is_dir=entry.is_dir(),
- size=stat.st_size if not entry.is_dir() else 0,
- modified=datetime.fromtimestamp(stat.st_mtime).isoformat(),
- ))
+ entries.append(
+ FileEntry(
+ name=entry.name,
+ path=str(entry),
+ is_dir=entry.is_dir(),
+ size=stat.st_size if not entry.is_dir() else 0,
+ modified=datetime.fromtimestamp(stat.st_mtime).isoformat(),
+ )
+ )
except (PermissionError, OSError):
continue
except PermissionError:
@@ -268,7 +272,9 @@ register(list_notes, "list_notes")
@client(websocket=True)
-def create_note(request: HttpRequest, title: str, content: str = "", pinned: bool = False) -> NoteOutput:
+def create_note(
+ request: HttpRequest, title: str, content: str = "", pinned: bool = False
+) -> NoteOutput:
from backend.models import Note
note = Note.objects.create(title=title, content=content, pinned=pinned)
@@ -403,7 +409,7 @@ def app_info(request: HttpRequest) -> AppInfoOutput:
from django.conf import settings
return AppInfoOutput(
- app_name="Djarea Desktop",
+ app_name="mizan Desktop",
uptime_seconds=round(time.time() - _start_time, 2),
db_path=str(settings.DATABASES["default"]["NAME"]),
pid=os.getpid(),
diff --git a/desktop/backend/settings.py b/desktop/backend/settings.py
index bd01eca..4a37a6d 100644
--- a/desktop/backend/settings.py
+++ b/desktop/backend/settings.py
@@ -1,5 +1,5 @@
"""
-Django settings for the Djarea desktop integration test app.
+Django settings for the mizan desktop integration test app.
Runs entirely local: SQLite database, in-memory channel layer,
no external services required.
diff --git a/desktop/backend/urls.py b/desktop/backend/urls.py
index ad66c10..38e7718 100644
--- a/desktop/backend/urls.py
+++ b/desktop/backend/urls.py
@@ -27,7 +27,7 @@ def serve_dist(request, path="index.html"):
urlpatterns = [
- path("api/djarea/", include("djarea.urls")),
+ path("api/mizan/", include("mizan.urls")),
re_path(r"^(?Passets/.+)$", serve_dist),
path("favicon.ico", serve_dist, {"path": "favicon.ico"}),
path("", serve_dist),
diff --git a/desktop/frontend/index.html b/desktop/frontend/index.html
index 5b30e6d..66412a7 100644
--- a/desktop/frontend/index.html
+++ b/desktop/frontend/index.html
@@ -3,7 +3,7 @@
- Djarea Desktop
+ mizan Desktop