Metadata-Version: 2.4
Name: statezero
Version: 0.1.0b91
Summary: Connect your Python backend to a modern JavaScript SPA frontend with 90% less complexity.
Author-email: Robert <robert.herring@statezero.dev>
Project-URL: homepage, https://www.statezero.dev
Project-URL: repository, https://github.com/state-zero/statezero
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: Django<5.2.0,>=5.1.0
Requires-Dist: django-money<3.5.0,>=3.4.0
Requires-Dist: djangorestframework<3.16.0,>=3.15.0
Requires-Dist: fakeredis<3.0.0,>=2.27.0
Requires-Dist: fastapi<0.116.0,>=0.115.0
Requires-Dist: hypothesis<7.0.0,>=6.108.0
Requires-Dist: jsonschema<5.0.0,>=4.23.0
Requires-Dist: networkx<4.0.0,>=3.4.0
Requires-Dist: openapi-spec-validator<0.8.0,>=0.7.0
Requires-Dist: orjson<4.0.0,>=3.10.0
Requires-Dist: pusher<4.0.0,>=3.3.0
Requires-Dist: pydantic<3.0.0,>=2.10.0
Requires-Dist: rich<14.0.0,>=13.9.0
Requires-Dist: psycopg2<3.0.0,>=2.9.0
Requires-Dist: django-redis<6.0.0,>=5.4.0
Requires-Dist: django-cors-headers<5.0.0,>=4.7.0
Requires-Dist: django-zen-queries<3.0.0,>=2.1.0
Requires-Dist: cytoolz<2.0.0,>=1.0.0
Requires-Dist: docstring-parser<0.18,>=0.17

<p align="center">
  <img src="logo.svg" alt="StateZero" width="120" />
</p>

# StateZero

**Unified, permissioned data, mutations, and actions, wherever they're needed, in realtime.**

StateZero is a non-invasive adaptor that turns your existing Django backend into a reactive, real-time data layer. Like Convex or Firebase, but on top of your own models, your own database, and your own permission logic.

No rewrites. No vendor lock-in. You keep your existing Django app and StateZero layers on top. A more philosophical manifesto for the importance of unified state in a world of AI agents is [here](UNIFIED_STATE.md).

StateZero is also a promise. Every state affordance needed to build AI-native software, implemented thoughtfully, with one guiding question: *what would Django do?* File uploads, real-time sync, permission introspection, bulk signals, optimistic mutations, query optimization. If you need it, it's already here, and it works the way you'd expect it to.

It's not a separate service. It runs inside your Django process. Mutations go through the ORM, so your existing Django signals (`post_save`, `pre_delete`, etc.) fire exactly as they would from a regular view.

Built on Django REST Framework. Authentication, serialization, and request handling use the same battle-tested DRF infrastructure you already know. TokenAuth, SessionAuth, JWT, whatever you use for DRF works out of the box. StateZero doesn't introduce a new HTTP server or require an ASGI migration. It serves over DRF's standard request/response cycle, so it sits happily alongside your existing DRF views. Adopt it for one model, keep your hand-written views for everything else, and expand at your own pace.

Every model gets typed client classes, generated into both TypeScript and Python packages. The ORM interface is standardised across every data access layer: frontend, backend, agent. One way to query. One way to mutate. One set of types. New developers read one set of docs and are productive across the entire stack. AI agents get a typed, self-describing API they can reason about without custom tooling.

Define your permissions and actions once in Django. They become immediately accessible to your Vue/React SPA and your Python agents, with the same query semantics, the same permission enforcement, and no additional views or serializers to write.

Whether it's a query:

```javascript
Todo.objects.filter({ is_completed: false, priority: "high" }).orderBy("-created_at")
```

A mutation:

```javascript
const todo = await Todo.objects.create({ title: "Buy groceries", priority: "high" });
todo.is_completed = true;
await todo.save();
```

Or an action:

```javascript
await assignTodo({ todo_id: todo.id, user_id: currentUser.id });
```

It looks just like the Django ORM, but it's safe to include in untrusted client code or agent sandboxes. Permissions enforced on every call. Any new client works without additional backend code.

## Built for Humans and LLMs

StateZero is for human developers who want to deliver world-class AI-native products quickly. It's also for LLMs that want to ship best-of-breed software that leverages the power of the Python ecosystem and Django's battle-tested rails, without having to navigate or write reams of glue code — the kind of boilerplate that's a magnet for an LLM's tendency to over-engineer and add complexity.

With StateZero, LLMs focus on delivering the features their users actually need, built on thoughtful, tastefully implemented abstractions their humans will find easy to grok and reason about.

Consider permissions. In a traditional API, a new feature means a new endpoint, a new serializer, new permission checks. An LLM faithfully builds the 20th this week. Its human is tired, zoned out from reviewing boilerplate, and misses that the LLM misread a permission boundary buried in legacy view code and conflicting documentation. Now sensitive data is leaking to the wrong users. Without StateZero, their human is extremely upset. Maybe they even just got fired.

With StateZero, the LLM just needs to get the permission class right once. As long as its human is happy with that one file, the LLM can build features and display data wherever it needs to, knowing it cannot accidentally expose something it shouldn't. One class, one review, enforced everywhere.

No more catastrophic data leaks from well intentioned, hard working LLMs.

Because everything respects Django ORM conventions, and the generated clients ship with full types and detailed schemas, LLMs can write code confidently. They intuitively understand how it works. It's not like learning a new library. It's Django, on the client. Even down to response shapes and error messages. `getOrCreate` returns `[instance, created]` because that's what Django returns. An LLM doesn't have to check the docs. It already knows.

## How It Works

Clients query using ORM syntax (`.filter()`, `.exclude()`, `.order_by()`).

No SQL crosses the wire. These compile to a secure, statically analysable AST that the server validates against your permission classes before any database query is executed. Every field path, filter condition, and ordering clause is checked.

The server parses the AST into Django ORM operations, optimizes the query, and returns only what the client asked for and is allowed to see.

A regular user and a superuser run the same query and get different rows, different fields, different permissions. Same endpoint, same permission classes.

### Query Semantics

The full Django ORM query language is available from every client. Not a subset. The actual lookup operators, relationship traversals, and field types that Django supports.

**Lookup operators.** Every standard Django modifier works across the wire:

| Category | Lookups |
|----------|---------|
| Comparison | `exact`, `iexact`, `lt`, `gt`, `lte`, `gte`, `in`, `range`, `isnull` |
| String | `contains`, `icontains`, `startswith`, `istartswith`, `endswith`, `iendswith` |
| Date/time parts | `year`, `month`, `day`, `hour`, `minute`, `second`, `week`, `week_day`, `iso_week_day`, `quarter`, `iso_year`, `date`, `time` |

These compose with each other the same way they do in Django: `created_at__year__gte`, `due_date__month__in`, `updated_at__date__lt`.

**Relationship traversal.** Filter, order, and select across any relationship depth. ForeignKey, OneToOne, ManyToMany, and reverse relations all work:

```javascript
// FK traversal
Todo.objects.filter({ category__name__icontains: "work" })

// Reverse relation
Category.objects.filter({ todo_set__priority: "high" })

// Deep nesting
Todo.objects.filter({ project__team__organization__name: "Acme" })

// M2M
Todo.objects.filter({ tags__name__in: ["urgent", "blocked"] })
```

**Date and timezone handling.** Datetime fields support part extraction and comparison. Django's `USE_TZ` and `TIME_ZONE` settings are respected, so timezone-aware queries work correctly:

```javascript
Todo.objects.filter({ created_at__year: 2025, created_at__quarter: 1 })
Todo.objects.filter({ due_date__week_day: 2 })  // Monday
Todo.objects.filter({ updated_at__date__gte: "2025-06-01" })
```

**JSON field filtering.** Filter into arbitrary nested paths on `JSONField`:

```javascript
Todo.objects.filter({ metadata__assignee__name__icontains: "alice" })
Todo.objects.filter({ settings__notifications__enabled: true })
Todo.objects.filter({ config__retry_count__gte: 3 })
```

**Q objects.** Full boolean logic with AND, OR, and NOT. Nest arbitrarily:

```javascript
import { Q } from "@statezero/core";

Todo.objects.filter({
  Q: [Q("OR", { priority: "high" }, { due_date__lt: "2025-12-31" })],
});

// Python client
Todo.objects.filter(
    (Q(priority="high") | Q(due_date__lt="2025-12-31")) & ~Q(status="archived")
).fetch()
```

**F expressions.** Reference field values in updates. Arithmetic, abs, round, floor, ceil, min, max:

```javascript
await Todo.objects.filter({ id: 1 }).update({ view_count: F("view_count + 1") });
await Product.objects.update({ price: F("price * 1.1") });
```

```python
Todo.objects.filter(id=1).update(view_count=F("view_count") + 1)
Todo.objects.filter(id=1).update(score=F.max(F("score"), F("high_score")))
```

**Aggregations.** `count`, `sum`, `avg`, `min`, `max`. Run on filtered querysets:

```python
Todo.objects.filter(is_completed=True).count()
Todo.objects.filter(status="active").avg("score")
Todo.objects.filter(status="active").sum("amount")
Todo.objects.min("price")
Todo.objects.max("price")
```

**Ordering.** Multi-field, ascending/descending, across relationships:

```javascript
Todo.objects.filter({ is_completed: false }).orderBy("-priority", "due_date", "category__name")
```

Every field path in every operation (filters, ordering, F expressions, aggregations) is validated against the user's permissions before the query executes.

### Query Optimization

Clients specify `depth` and/or `fields`. The server automatically picks `select_related` for FK/O2O, `prefetch_related` with scoped inner querysets for M2M/reverse relations, and `.only()` to limit columns.

The data requirement is defined by the client at the point it's used, not in a separate serializer or view. The server knows exactly what's needed.

Queries are provably optimal with respect to what the client will display. N+1 queries are eliminated. Only the columns and relationships the UI actually renders are fetched. No optimization code required.

```javascript
const todos = Todo.objects
  .filter({ is_completed: false })
  .orderBy("-priority")
  .fetch({ depth: 1, fields: ["title", "priority", "category__name"] });
```

Produces `select_related("category")` and `.only("id", "title", "priority", "category_id")`. One query, minimal data transfer.

### Query Caching & Request Coalescing

Query results are cached per user permission boundary. Two users who see different rows or different fields always get separate cache entries, automatically. No invalidation logic required. New transaction, new cache namespace. No stale data.

When a real-time event fires and multiple clients request the same data simultaneously, only the first request hits the database. The rest wait for that result. Thundering herd solved.

### Denormalized Data Store

Too many projects reach for nested serializers to handle cross-model data, embedding full related objects inside each response. It's the path of least resistance, but it means duplicated data on the wire, no shared cache, and no way to keep related objects in sync across different queries.

StateZero uses a denormalized store instead. Responses come back as a flat `included` map keyed by model type and primary key, with top-level results referencing related objects by PK.

The client resolves relationships automatically from this shared cache. `todo.category.name` traverses the local store, not a nested blob. When a related object updates, every query referencing it sees the change immediately without re-fetching.

```json
{
  "data": [1, 2, 3],
  "included": {
    "app.Todo": { "1": { "title": "Buy groceries", "category": 5 }, ... },
    "app.Category": { "5": { "name": "Personal" }, ... }
  }
}
```

This is the same architecture as Ember Data, JSON:API, and Apollo's normalized cache, but built to work with Django's model graph and permission system.

Building it yourself means implementing PK-indexed storage, relationship resolution, cache invalidation on mutations, and merging partial updates from real-time events. StateZero handles all of it.

### Real-Time Sync

Two speed tiers, both automatic.

**Local mutations** reflect in under 60ms. The JS client maintains a frontend replica of your querysets using query emulation built on sift.js. It handles the full Django query syntax: JSON field filtering, Q objects, M2M fields, lookups like `__icontains` and `__gte`. When you create, update, or delete, the local store updates immediately and every active queryset re-evaluates against the new state.

**Remote changes** from other users, agents, or backend processes arrive in sub-300ms via Pusher. The client merges them into the local store and re-evaluates all active querysets. An agent creates a record and the user's UI updates in the same beat.

Under the hood, StateZero coalesces events within transaction boundaries. A save that touches three models emits one batched update, not three.

Responses include related model data that gets cached in the client's local store, so traversing relationships after a fetch doesn't trigger additional requests.

This is a significant amount of infrastructure. Building it yourself means implementing optimistic state management, a query emulation engine, event coalescing, a client-side cache with relationship resolution, and rollback semantics. StateZero ships it all.

```vue
<script setup>
import { useQueryset } from "@statezero/core/vue";

// Re-renders automatically, local mutations in <60ms, remote changes in <300ms
const todos = useQueryset(() => Todo.objects.filter({ is_completed: false }));
</script>

<template>
  <div v-for="todo in todos.fetch({ limit: 10 })" :key="todo.id">
    {{ todo.title }}
  </div>
</template>
```

### Optimistic Mutations

```javascript
// Optimistic, local store updates in <60ms, server syncs in background
const todo = Todo.objects.create({ title: "Buy groceries" });
todo.title = "Buy organic groceries";
todo.save(); // UI already reflects this. Rolls back if server rejects.

// Confirmed, await server response when you need guarantees
const todo = await Todo.objects.create({ title: "Important meeting" });
await todo.save();
```

## Quick Start

### 1. Install

```bash
pip install statezero
```

### 2. Django Settings

```python
# settings.py
from corsheaders.defaults import default_headers

INSTALLED_APPS = [
    ...,
    "rest_framework",
    "rest_framework.authtoken",
    "corsheaders",
    "statezero.adaptors.django",
    "your_app",
]

MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",
    ...,
    "statezero.adaptors.django.middleware.OperationIDMiddleware",
]

CORS_ALLOWED_ORIGINS = ["http://localhost:5173"]

CORS_ALLOW_HEADERS = list(default_headers) + [
    "x-operation-id",
    "x-canonical-id",
    "x-statezero-sync-token",
    "x-statezero-extra-fields",
]

STATEZERO_SYNC_TOKEN = "your-secret-token"

STATEZERO_PUSHER = {
    "APP_ID": "...",
    "KEY": "...",
    "SECRET": "...",
    "CLUSTER": "eu",
}
```

### 3. Register Models

```python
# todos/crud.py
from statezero.adaptors.django.config import registry
from statezero.core.config import ModelConfig
from statezero.adaptors.django.permissions import IsAuthenticatedPermission
from .models import Todo, Category

registry.register(
    Todo,
    ModelConfig(model=Todo, permissions=[IsAuthenticatedPermission]),
)

registry.register(
    Category,
    ModelConfig(model=Category, permissions=[IsAuthenticatedPermission]),
)
```

### 4. Add URLs

```python
# urls.py
urlpatterns = [
    path("statezero/", include("statezero.adaptors.django.urls")),
]
```

### 5. Frontend Setup

```bash
npm install @statezero/core
```

Create `statezero.config.js` in your project root:

```javascript
const BASE_URL = "http://127.0.0.1:8000/statezero";

export default {
  backendConfigs: {
    default: {
      API_URL: BASE_URL,
      SYNC_TOKEN: "your-secret-token",
      GENERATED_TYPES_DIR: "./src/models",
      GENERATED_ACTIONS_DIR: "./src/actions",
      BACKEND_TZ: "UTC",
      fileUploadMode: "server",
      getAuthHeaders: () => ({
        Authorization: `Token ${localStorage.getItem("auth_token")}`,
      }),
      events: {
        type: "pusher",
        pusher: {
          clientOptions: {
            appKey: "your-pusher-key",
            cluster: "eu",
            forceTLS: true,
            authEndpoint: `${BASE_URL}/events/auth/`,
          },
        },
      },
    },
  },
};
```

### 6. Sync Models & Actions

With the Django server running:

```bash
npx statezero sync
```

This fetches schemas from your backend (authenticated via `SYNC_TOKEN`) and generates typed model classes and action functions into `src/models/` and `src/actions/`.

### 7. Query From Any Client

**JavaScript:**
```javascript
const todos = Todo.objects.filter({ priority: "high" }).orderBy("-created_at");
```

**Python:**
```python
todos = Todo.objects.filter(priority="high").order_by("-created_at").fetch()
```

Same endpoint. Same permissions. Same data shape. Add a new client tomorrow and it works identically.

## Permissions

Model permissions and action permissions are separate systems. Model permissions govern data access (queries, mutations). Action permissions govern server-side functions.

### Model Permissions

Defined once per model. They govern every query and mutation across all clients. No per-view auth logic, no client-specific rules.

Six independent controls:

| Method | Controls | Result |
|--------|----------|--------|
| `filter_queryset` | Which rows are visible | Users see only their own data |
| `exclude_from_queryset` | Which rows are hidden | Archived records hidden from everyone |
| `allowed_actions` | Which CRUD operations are permitted | Regular users can't delete |
| `allowed_object_actions` | Per-instance checks | Only owners can edit |
| `visible_fields` | Which fields appear in responses | Salary hidden from non-managers |
| `editable_fields` / `create_fields` | Which fields accept writes | Users set `title`, not `status` |

```python
from statezero.core.interfaces import AbstractPermission
from statezero.core.types import ActionType

class ProjectPermission(AbstractPermission):
    def filter_queryset(self, request, queryset):
        if request.user.is_superuser:
            return queryset
        return queryset.filter(team__members=request.user)

    def exclude_from_queryset(self, request, queryset):
        return queryset.exclude(status="deleted")

    def allowed_actions(self, request, model):
        if request.user.is_superuser:
            return {ActionType.CREATE, ActionType.READ, ActionType.UPDATE, ActionType.DELETE}
        return {ActionType.CREATE, ActionType.READ, ActionType.UPDATE}

    def allowed_object_actions(self, request, obj, model):
        if obj.owner == request.user:
            return {ActionType.READ, ActionType.UPDATE, ActionType.DELETE}
        return {ActionType.READ}

    def visible_fields(self, request, model):
        if request.user.is_superuser:
            return "__all__"
        return {"id", "name", "description", "status", "created_at"}

    def editable_fields(self, request, model):
        return {"name", "description", "status"}

    def create_fields(self, request, model):
        return {"name", "description"}
```

A frontend developer and an AI agent with the same auth token get identical boundaries. One code path to audit.

When multiple model permissions are registered, `filter_queryset` combines with OR (additive, a row is visible if it passes any permission's filter). `exclude_from_queryset` combines with AND (restrictive, a row is excluded if any permission excludes it).

**Built-in model permissions:**

- `AllowAllPermission`, full CRUD, all fields
- `IsAuthenticatedPermission`, authenticated users only
- `IsStaffPermission`, staff users only

### Action Permissions

Actions use a separate permission interface (`AbstractActionPermission`) with two hooks, one before validation and one after:

```python
from statezero.core.interfaces import AbstractActionPermission

class CanManageProjects(AbstractActionPermission):
    def has_permission(self, request, action_name):
        """Before validation. Check the user can call this action at all."""
        return request.user.is_authenticated

    def has_action_permission(self, request, action_name, validated_data):
        """After validation. Check against the actual input data."""
        project = Project.objects.get(id=validated_data["project_id"])
        return project.team.members.filter(id=request.user.id).exists()
```

Compose action permissions with `AnyOf` (OR) and `AllOf` (AND):

```python
from statezero.core.permissions import AnyOf, AllOf

@action(permissions=[AnyOf(IsAdminPermission(), CanManageProjects())])
def archive_project(request, project_id: int):
    ...
```

## Python Client

Generate a standalone Python client from your models. Give an AI agent the client and a scoped auth token. It gets safe, bounded access to data, mutations, and actions. Nothing more.

```bash
python manage.py generate_client --output ./sz
```

Self-contained package. Typed model classes, action functions, httpx-based runtime.

```python
from sz import configure, Q, F, FileObject
from sz.models import Todo

configure(url="https://api.example.com", token="agent-auth-token")

# CRUD
todos = Todo.objects.filter(is_completed=False).order_by("-priority").fetch(limit=20)
todo = Todo.objects.get(id=1)
todo = Todo.objects.create(title="Buy groceries", priority="medium")
todo.update(title="Buy organic groceries")
todo.delete()

# Bulk
Todo.objects.bulk_create([
    {"title": "Task 1", "priority": "high"},
    {"title": "Task 2", "priority": "low"},
])

# Q objects
urgent = Todo.objects.filter(
    Q(priority="high") | Q(due_date__lt="2025-01-01")
).fetch()

# F expressions
Todo.objects.filter(id=1).update(view_count=F("view_count") + 1)

# Aggregations
total = Todo.objects.count()
avg_score = Todo.objects.filter(is_completed=True).avg("score")

# get_or_create
todo, created = Todo.objects.get_or_create(
    title="Daily standup", defaults={"priority": "medium"},
)

# Relationships
todo = Todo.objects.get(id=1, depth=1)
print(todo.category.name)

# Field permissions introspection
perms = Todo.get_field_permissions()

# Validation without saving
Todo.validate_data({"title": "", "priority": "invalid"})

# File uploads
Todo.objects.create(title="Report", attachment=FileObject("/path/to/report.pdf"))

# Actions
from sz.actions import send_email
result = send_email(to="user@example.com", subject="Hello", body="World")
```

## JavaScript Client

```bash
npm i @statezero/core
npx statezero sync   # generates TypeScript models
```

```javascript
// Django-style lookups
const todos = Todo.objects.filter({
  title__icontains: "meeting",
  created_by__email__endswith: "@company.com",
  due_date__lt: "2025-12-31",
});

// Q objects
import { Q } from "@statezero/core";
const results = Todo.objects.filter({
  Q: [Q("OR", { priority: "high" }, { due_date__lt: "tomorrow" })],
});

// F expressions
import { F } from "@statezero/core";
await Product.objects.update({ view_count: F("view_count + 1") });

// Aggregations
const count = await Todo.objects.count();

// get_or_create
const [todo, created] = await Todo.objects.getOrCreate(
  { title: "Daily standup" },
  { defaults: { priority: "medium" } }
);
```

## Actions

Server-side functions callable from any client. Permissions and validation declared once. Schemas auto-generated into TypeScript and Python clients. Agents and frontends call the same logic with the same checks.

Type-hint your function and StateZero infers the input serializer automatically. `str`, `int`, `bool`, `datetime`, `Optional`, `List[Model]`, enums, and `Literal` all work. Docstring parameter descriptions become `help_text` on the generated schema.

```python
from typing import List, Optional
from statezero.core.actions import action
from myapp.models import Project, User, Tag

@action(permissions=[IsAdminPermission()])
def transfer_ownership(request, project: Project, new_owner: User, tags: List[Tag], notify: bool = True):
    """Transfer project ownership to another user.

    Args:
        project: The project to transfer.
        new_owner: The user who will become the new owner.
        tags: Tags to apply to the transferred project.
        notify: Whether to send a notification email.
    """
    project.owner = new_owner
    project.tags.set(tags)
    project.save()
    if notify:
        send_notification(new_owner, project)
    return {"status": "transferred"}
```

For more control, provide an explicit DRF serializer:

```python
from rest_framework import serializers

class TransferInput(serializers.Serializer):
    project_id = serializers.IntegerField()
    new_owner_id = serializers.IntegerField()
    notify = serializers.BooleanField(default=True)

@action(serializer=TransferInput, permissions=[IsAdminPermission()])
def transfer_ownership(request, project_id, new_owner_id, notify):
    ...
```

### Display Metadata & Auto-Rendering

Actions and models can declare rich display metadata: field grouping, custom component hints, help text, and layout trees. The schema is served to the frontend, where `LayoutRenderer` auto-renders the form.

```python
from statezero.core.classes import DisplayMetadata, FieldGroup, FieldDisplayConfig

@action(
    permissions=[CanSendNotifications],
    display=DisplayMetadata(
        display_title="Send Notification",
        display_description="Send notifications to multiple recipients",
        field_groups=[
            FieldGroup(
                display_title="Message Content",
                field_names=["message", "priority"]
            ),
            FieldGroup(
                display_title="Recipients",
                field_names=["recipients"]
            ),
        ],
        field_display_configs=[
            FieldDisplayConfig(field_name="message", display_component="TextArea"),
            FieldDisplayConfig(field_name="priority", display_component="RadioGroup"),
            FieldDisplayConfig(
                field_name="recipients",
                display_component="EmailListInput",
                display_help_text="Add one or more email addresses",
            ),
        ],
    )
)
def send_notification(message: str, recipients: List[str], priority: str = "low", *, request=None):
    ...
```

The same metadata works on model registration:

```python
registry.register(
    Product,
    ModelConfig(
        model=Product,
        permissions=[IsAuthenticatedPermission],
        display=DisplayMetadata(
            display_title="Product Management",
            field_groups=[
                FieldGroup(display_title="Basic Info", field_names=["name", "description", "category"]),
                FieldGroup(display_title="Pricing", field_names=["price", "in_stock"]),
            ],
            field_display_configs=[
                FieldDisplayConfig(field_name="description", display_component="RichTextEditor"),
                FieldDisplayConfig(field_name="category", display_component="CategorySelector", filter_queryset={"name__icontains": ""}),
                FieldDisplayConfig(field_name="price", display_component="CurrencyInput"),
            ],
        ),
    ),
)
```

The frontend receives this as part of the schema response. `LayoutRenderer` is a headless Vue component that walks the layout tree and delegates to your component implementations. You provide a `Control`, `Display`, `Group`, `Alert`, `Label`, `Divider`, and `Tabs` component, and it handles nesting, conditionals, error placement, and form data binding:

```vue
<LayoutRenderer
  :layout="schema.display.layout"
  :schema="schema"
  :components="{ Control: MyAutoField, Group: MySection, ... }"
  :form-data="formData"
  :errors="errors"
  @update:formData="formData = $event"
/>
```

The `Control` component receives the field schema (type, format, choices, related model) and the `display_component` hint. It picks the right input widget: a datepicker for dates, a model selector for FKs, a rich text editor when you specify `"RichTextEditor"`.

No manual form building. Add a field to your model or action and it appears in the UI.

For more complex layouts, use the full layout tree with `VerticalLayout`, `HorizontalLayout`, `Group`, `Tabs`, `Conditional`, `Alert`, `Label`, and `Divider` elements:

```python
from statezero.core.classes import (
    DisplayMetadata, VerticalLayout, HorizontalLayout, Group, Control,
    Conditional, Alert, Tabs, Tab, Label, Divider,
)

@action(
    display=DisplayMetadata(
        layout=VerticalLayout(elements=[
            Alert(severity="info", text="This action cannot be undone."),
            HorizontalLayout(elements=[
                Control(field_name="project", display_component="ProjectSelector"),
                Control(field_name="new_owner"),
            ]),
            Conditional(
                when="formData.notify === true",
                layout=Group(label="Notification Settings", elements=[
                    Control(field_name="notify_message", display_component="TextArea"),
                ]),
            ),
        ])
    )
)
def transfer_ownership(request, project: Project, new_owner: User, notify: bool = True, notify_message: str = ""):
    ...
```

## Model Registration

```python
from statezero.core.config import ModelConfig
from statezero.core.classes import AdditionalField

registry.register(
    MyModel,
    ModelConfig(
        model=MyModel,
        permissions=[MyPermission],
        fields={"id", "name", "status"},              # Expose specific fields (default: "__all__")
        additional_fields=[                             # Computed/virtual fields
            AdditionalField(name="full_name", field=models.CharField()),
        ],
        filterable_fields={"name", "status"},           # Restrict filterable fields
        searchable_fields={"name", "description"},      # Fields for search
        ordering_fields={"name", "created_at"},         # Fields for order_by
        pre_hooks=[set_tenant],                         # Before deserialization
        post_hooks=[audit_log],                         # After deserialization
        force_prefetch=["category"],                    # Always prefetch
    ),
)
```

## Search

Searches across `searchable_fields`:

```python
results = Todo.objects.search("meeting").fetch()
```

For production, use PostgreSQL full-text search with ranked results:

```python
from statezero.adaptors.django.search_providers.postgres_search import PostgresSearchProvider
config.search_provider = PostgresSearchProvider()
```

## Built-In Plumbing

Building agentic software means building a lot of plumbing: file uploads, user context, validation, permission introspection. StateZero ships these out of the box, following Django conventions.

### Current User

Every agent and frontend needs to know who it's acting as. `GET /statezero/me/` returns the authenticated user as a serialized model, with the same field-level permissions applied.

```python
# Python agent
from sz import configure
configure(url="https://api.example.com", token="agent-token")

# GET /statezero/me/, returns the user this token belongs to
```

### File Uploads

Files are a first-class citizen. The Python client handles upload transparently. Pass a `FileObject` and it uploads before creating the record. Two modes:

**Direct upload.** File goes to your server, saved using your existing django-storages configuration:

```python
from sz import FileObject
Todo.objects.create(title="Report", attachment=FileObject("/path/to/file.pdf"))
```

**S3 multipart.** Client gets presigned URLs, uploads directly to S3, no file touches your server. Handles chunking for large files automatically:

```python
from sz import configure, FileObject
configure(url="https://api.example.com", token="...", upload_mode="s3")

# 100MB file uploads in chunks directly to S3
Todo.objects.create(title="Dataset", attachment=FileObject("/path/to/large_file.csv"))
```

Works from bytes and file-like objects too:

```python
Todo.objects.create(title="Generated", attachment=FileObject(b"content", name="output.txt"))
```

### Validation

Validate data without saving. Useful for form validation and agent pre-checks:

```python
# Returns errors without creating anything
Todo.validate_data({"title": "", "priority": "invalid"})
```

### Field Permission Introspection

Agents and frontends can ask what they're allowed to do before attempting it:

```python
perms = Todo.get_field_permissions()
# {"visible": ["id", "title", ...], "editable": ["title", ...], "creatable": ["title", ...]}
```

Frontends use this to render forms, only showing fields the user can edit. Agents use it to know what data they can provide.

### Hooks

Pre-hooks run before validation. Post-hooks run after. Use them to inject server-side data (tenant, created_by, generated IDs) without exposing those fields to clients.

```python
def set_created_by(data, request=None):
    """Pre-hook: inject the current user before validation."""
    if request and hasattr(request, "user"):
        data["created_by"] = request.user.pk
    return data

def generate_order_number(data, request=None):
    """Post-hook: generate a unique order number after validation."""
    if not data.get("order_number"):
        data["order_number"] = f"ORD-{uuid.uuid4().hex[:8].upper()}"
    return data

registry.register(
    Order,
    ModelConfig(
        model=Order,
        permissions=[IsAuthenticatedPermission],
        pre_hooks=[set_created_by],
        post_hooks=[generate_order_number],
    ),
)
```

Pre-hooks can add any DB field, even ones not in the user's `editable_fields`. User input is filtered to allowed fields first, then hooks run. In `DEBUG` mode, hooks are validated for correctness at startup.

### Signals

Django's built-in signals (`post_save`, `pre_delete`, etc.) fire normally because StateZero mutations go through the ORM. But Django doesn't emit signals for bulk operations. StateZero fills that gap for bulk operations performed through StateZero:

```python
from django.dispatch import receiver
from statezero.adaptors.django.signals import post_bulk_create, post_bulk_update, post_bulk_delete

@receiver(post_bulk_create, sender=Todo)
def handle_bulk_create(sender, instances, **kwargs):
    for todo in instances:
        schedule_notification(todo)

@receiver(post_bulk_delete, sender=Todo)
def handle_bulk_delete(sender, pks, **kwargs):
    AuditLog.objects.bulk_create([
        AuditLog(action="delete", object_id=pk) for pk in pks
    ])
```

You can also trigger these signals manually from your own code:

```python
from statezero.adaptors.django.signals import notify_bulk_created, notify_bulk_deleted

objs = Todo.objects.bulk_create([...])
notify_bulk_created(objs)
```

### Schema Discovery

`GET /statezero/models/` lists all registered models. `GET /statezero/<model>/get-schema/` returns the full schema with field types, relationships, and constraints. `GET /statezero/actions-schema/` returns all action schemas. Schema checksums enable efficient client-side caching.

## Django Settings

```python
INSTALLED_APPS = ["statezero.adaptors.django", ...]

# Real-time events
STATEZERO_PUSHER = {
    "app_id": "...", "key": "...", "secret": "...", "cluster": "...",
}

# Schema sync token for production
STATEZERO_SYNC_TOKEN = "your-secret-token"

# Pagination
STATEZERO_DEFAULT_LIMIT = 100

# Unknown write fields: "ignore" (default) or "error"
STATEZERO_EXTRA_FIELDS = "ignore"

# View-level access gate
STATEZERO_VIEW_ACCESS_CLASS = "rest_framework.permissions.IsAuthenticated"
```

## Testing

### Frontend Tests

```python
# tests/settings.py
STATEZERO_TEST_MODE = True
MIDDLEWARE = [..., "statezero.adaptors.django.testing.TestSeedingMiddleware", ...]
STATEZERO_TEST_STARTUP_HOOK = "myapp.test_utils.create_test_user"
```

```bash
python manage.py statezero_testserver --addrport 8000
```

```javascript
import { setupTestStateZero, seedRemote, resetRemote } from "@statezero/core/testing";

const testHeaders = setupTestStateZero({ apiUrl: "http://localhost:8000/statezero", ... });
await resetRemote(testHeaders, () => Todo.remote.delete());
await seedRemote(testHeaders, () => Todo.remote.create({ title: "Seeded" }));
```

### Python Client Tests

```python
from statezero.client.testing import DjangoTestTransport
configure(transport=DjangoTestTransport(user=test_user))
```

## Extensions

- **django-money**, MoneyField serialization
- **django-simple-history**, historical record events
- **django-pydantic-field**, Pydantic model fields
- **Custom field serializers**, register via `CUSTOM_FIELD_SERIALIZERS`

## Production & Contributing

StateZero is actively maintained and used in production, powering a real-time AI-native property orchestration platform. Comprehensive documentation is available at [statezero.dev](https://statezero.dev).

New features and pull requests are welcome, as long as they fit within the philosophy of unified state with the batteries needed for AI-native apps. Workflows, conversations, and event systems are out of scope. These are handled by a separate internal library.

## Links

- [Documentation](https://statezero.dev)
- [GitHub](https://github.com/state-zero/statezero)
