{"version":"v1.0","client_state":{"DocsNavigation.default":{"fields":{"query":""},"persist":"memory","send_to_server":true},"ColorScheme.default":{"fields":{"mode":"system"},"persist":"local","send_to_server":true}},"endpoints":{"action":"/_bretzel/action","sse":"/_bretzel/sse","refetch":"/_bretzel/refetch"},"csrf":"17075d9ccc068673917465187a90f9ce","page_id":"a4acded1ab7442da924f56df552118c8","address":""}

Server state

Server state is the source of truth: it lives in Python, it is stored server side, it can be shared and protected. One inherits from one of the pre-scoped bases according to the scope wanted.

The four scopes

One does not pass `scope=`: one inherits from the base. The lifetime changes, the API is identical.
BaseScopeLifetime
PageState1 page shownsurvives actions (POST), reset on F5 / nav
SessionStatesession cookieuntil the session expires
UserStateauthenticated accountAuthRequiredError with no auth
AppStatewhole processshared by every request
A simple rule: start with `PageState`; move up to Session / User / App only when sharing demands it.

Mutate → re-render

A handler mutates the state; every `@refreshable(deps=[…])` zone that reads that state is re-rendered automatically. The detail (controlling what re-renders, realtime) is in “Server reactivity”.

0

Click → the handler mutates `SrvCounter().n` → the `deps=[SrvCounter]` zone re-renders. One server round trip.

Declaring a state

An immutable default → directly. A mutable default → `field(default_factory=…)`. `@validator` normalises on write, `@computed` derives automatically.
class Cart(SessionState):
    """A rich example — field / factory / validator / computed together."""

    items: list[dict] = field(default_factory=list)
    coupon: str = field(default='')
    discount: float = field(default=0.0)

    @validator("coupon")
    def _upper(self, value: str) -> str:
        return value.strip().upper()

    @computed
    def total(self) -> float:
        raw = sum(it.get("price", 0.0) for it in self.items)
        return raw * (1 - self.discount)

Business types in the store

A field is not limited to what JSON can write. `date`, `datetime`, `Decimal`, `UUID` and any app enumeration cross the store and come back the RIGHT type — you never read back a string to re-parse. The containers follow: `list[date]`, `dict[str, Decimal]`.
class Facture(SessionState):
    """BUSINESS types filed as they are — not strings to re-parse."""

    emise: datetime.date = field(default_factory=datetime.date.today)
    montant: decimal.Decimal = field(default_factory=lambda: decimal.Decimal("0"))
    reference: uuid.UUID = field(default_factory=uuid.uuid4)
    etat: Etat = field(default=Etat.BROUILLON)
For a type of your own, `register_type(MyType, encode=…, decode=…)` once at startup. It is the same route as the shipped types: there is no special case reserved for the framework.

What concurrency breaks

Two requests writing the SAME field do not see each other. The commit writes only the fields touched, so two gestures on different fields coexist with no effort — but two gestures on the same total lose each other, in silence: the counter advances more slowly than the clicks, and only a second user reveals it.
class Visites(AppState):
    """A total SHARED by the whole process — hence `merge="add"`."""

    vues: int = field(default=0, merge="add")
`merge="add"` declares the field ADDITIVE: the store combines the two increments instead of keeping one. With no waiting, no lock. `bretzel check` has a rule for forgetting it (`undeclared-shared-counter`), because the mistake neither raises nor shows.When the gesture COMPUTES from what it read — filtering a list, removing an item — no merge can repair it. There, one serialises:
def delete(target: str) -> None:
    with Kanban.lock() as store:
        store.tasks = [t for t in store.tasks
                       if t["id"] != target]
The block is a small transaction: the lock taken THEN the state re-read on entry, the fields written THEN the lock released on exit. Two requests on the same key wait for each other — that is the price, and it is paid only there. `async with` inside an `async def`.

State in the URL

A `PageState` is keyed by a render uuid that is NEW at every navigation: that is why a sort or a filter survives neither a page change nor the back button. Declaring the fields the URL is authoritative for makes the view shareable by link, and the browser's arrows go back and forth again.
class Ventes(PageState, addressable=True):
    """State in the ADDRESS — two fields published, one that is not."""

    region: str = field(default="all", url="region")
    mini: int = field(default=0, url="mini")
    #: No ``url=``: so this field cannot be published, even with
    #: ``addressable=True``. It is the guarantee, not a guideline.
    notes: str = field(default="")
`field(url=…)` NAMES, `addressable=True` TURNS ON — and the two are separate because a published field is PUBLIC: browser history, access logs, `Referer` header. A field nothing has named cannot leave by accident; that is how `DatatableState.filters` stays out of the address by construction. To survive a navigation WITHOUT being published, it is the scope you need: `scope="session"`.

The states, at a glance

Scope, fields (type, default, validators) and computed of this page's states — read at render time from the code.
SrvCounterServerStatesession
FieldTypeDefault@validator
nint0—
CartServerStatesession
FieldTypeDefault@validator
itemslistlist() (factory)—
couponstr''_upper
discountfloat0.0—
@computed :total
FactureServerStatesession
FieldTypeDefault@validator
emisedatetoday() (factory)—
montantDecimal<lambda>() (factory)—
referenceUUIDuuid4() (factory)—
etatEtat<Etat.BROUILLON: 'brouillon'>—
VisitesServerStateapp
FieldTypeDefault@validator
vuesint0—
VentesServerStatepage
FieldTypeDefault@validator
regionstr'all'—
miniint0—
notesstr''—
In the URL:region → regionmini → mini
A field missing from this row never goes into the URL.