App structure
Everything is a feature, flat. Each decorates itself; `main` gathers them. No `app/` folder, no central wiring.The skeleton
examples/docs/
├── main.py # creates Bretzel(...) + app.include(...)
├── features/ # everything flat
│ ├── shell.py # @layout — the shell (nav, chrome)
│ ├── home.py # @page("/")
│ ├── components.py # @page("/components")
│ ├── errors.py # @error_page(404) / @error_page(500)
│ └── …
└── lib/ # shared helpers (introspect, blocks)
A feature decorates itself
The decorator only MARKS the function. The actual registration happens in `app.include(…)` — the import order has no effect at all.# features/home.py
from bretzel import page, ui
from examples.docs.features.shell import shell
@page("/", layout=shell, title="Home")
def home_page() -> None:
ui.heading("Welcome")
main gathers
`app.include(module, …)` scans the modules for their marks. The `shell` (`@layout`) does not need including — it resolves by reference through `layout=shell`.# main.py
from bretzel import Bretzel
app = Bretzel(secret_key="…", mode="dev")
from examples.docs.features import home, components, errors
app.include(home, components, errors)
Flat, yes — but not necessarily
“Flat” is the default, not a constraint. One folder per domain works, and the framework has nothing to know: `include` scans the top-level callables of the module it is given. So it is enough for the sub-folder's `__init__.py` to RE-EXPORT its pages.myapp/
├── main.py
└── billing/ # one domain, one folder
├── __init__.py # re-exports the pages
├── quotes.py # @page("/quotes")
└── invoices.py # @page("/invoices")
# billing/__init__.py
from myapp.billing.quotes import quotes_page
from myapp.billing.invoices import invoices_page
# main.py
from myapp import billing
app.include(billing) # both routes mount
Re-export the FUNCTIONS, not the modules
The trap is there, and it is silent: if the `__init__.py` re-exports the MODULES (`from . import quotes, invoices`) instead of the functions, `include` finds no mark and the routes return 404 — with no error at startup. Measured. In that case, include the modules themselves: `app.include(billing.quotes, billing.invoices)`.
The 11 markers
A feature does not declare views only. Everything that follows is placed the same way — one decorates a function at module level, and `include` picks it up.| What one writes | What it declares |
|---|---|
| @page("/pricing") | a route that renders a view |
| @layout | a reusable shell (nav, chrome) — referenced by `layout=` |
| @error_page(404) | the rendering of an HTTP status |
| @download("/export.csv") | a route that returns a FILE, not a page |
| @refreshable(deps=[Cart]) | a zone that re-renders when a state mutates |
| @background | work launched after the response, off the critical path |
| @idempotent | an action a double click must not run twice |
| @auth.source | where an identity can come from (an API token, a trusted proxy…) |
| @auth.door(OIDC(...)) | an OAuth/OIDC door, and the decision to accept the profile |
| @middleware | one pass on every request |
| @startup / @shutdown | opening and closing the app's resources |
When the app grows: declaring a contract
Flat and with no contract, nothing stops a feature importing another quietly. `Feature` makes the dependency explicit: what I provide, what I use, what I read. The graph is checked at startup — an unknown dependency, a cycle and a name collision are errors, not run-time surprises.# features/cart/feature.py
from bretzel import Feature
from .state import CartState
from .ui import cart_page
feature = Feature(
name="cart",
kind="page",
provides=[CartState, cart_page], # my public surface
uses=["catalog_data", "money"], # the only ones I may import
)
`kind=` takes one of these 10 values: `data`, `error`, `facade`, `infra`, `job`, `layout`, `logic`, `page`, `shell`, `state`.The graph rendered live on a demo app:App map →
Having the structure judged
`check` reads the code written against the framework. With `--deep`, it mounts the named app and arbitrates its map too — which reading files alone cannot see.py -m bretzel.cli.main check examples
py -m bretzel.cli.main check --deep examples.docs.main:app
These docs run exactly like that: `examples/docs/features/` flat, `shell.py` as the `@layout`, `main.py` doing the `include`. What you are reading IS the pattern. `core/` (when it exists) carries the global-only: shared states, cross-cutting config — no views.