The instance
from bretzel import Bretzel
app = Bretzel(
title="My App",
secret_key="…", # required, ≥ 16 chars
mode="dev", # "dev" | "prod" (prod by default)
)
Bretzel(...)| Parameter | Type | Default | Passed as |
|---|
| title | str | 'Bretzel App' | keyword-only |
| description | str | None | None | keyword-only |
| theme | Theme | None | None | keyword-only |
| secret_key | str | None | None | keyword-only |
| redis_url | str | None | None | keyword-only |
| workers | int | 1 | keyword-only |
| session_max_age_days | int | 30 | keyword-only |
| action_max_age | int | None | None | keyword-only |
| mobile_breakpoint | int | 768 | keyword-only |
| nav_progress | bool | True | keyword-only |
| pwa | Any | None | keyword-only |
| lang | str | 'en' | keyword-only |
| languages | Sequence[str] | () | keyword-only |
| texts | Mapping[str, str] | None | None | keyword-only |
| static_dir | str | None | None | keyword-only |
| favicon | str | bool | None | None | keyword-only |
| cors_origins | list[str] | None | None | keyword-only |
| trusted_hosts | list[str] | None | None | keyword-only |
| security_headers | bool | True | keyword-only |
| csp | bool | Literal['report-only'] | False | keyword-only |
| csp_sources | Mapping[str, Sequence[str]] | None | None | keyword-only |
| api_docs | bool | False | keyword-only |
| mode | Literal['dev', 'prod'] | None | None | keyword-only |
| secure_cookies | bool | None | None | keyword-only |
| expose_errors | bool | None | None | keyword-only |
| debug | bool | None | None | keyword-only |
| css | Literal['auto', 'build', 'browser'] | None | None | keyword-only |
What `mode` decides — and does not decide
`mode` is a PRESET: it sets the defaults of independent settings, which you can override one by one.| Setting | The question asked | dev | prod |
|---|
| expose_errors= | does the detail of exceptions go into the response? | True | False |
| debug= | should the framework be talkative (warnings, readable IDs)? | True | False |
| css= | who compiles the CSS? | browser | build |
| — | cache headers | no-store | immutable |
The two set themselves independently: `mode="prod", debug=True` gives a talkative production that exposes nothing, and `mode="dev", expose_errors=False` lets you see your real error pages locally.The CSS pipeline
`css="build"` compiles a sheet at startup and serves it as a `<link>`: the CSS is there at the first paint. `css="browser"` lets the Tailwind compiler work inside the page — no binary to install, but the CSS arrives after the first paint. `"auto"` (the default) picks `build` in production and `browser` in dev.To develop in production's exact conditions, ask for `css="build"` — it needs the compiler (`pip install 'bretzel[css]'`) and it adds a few seconds to every startup. With no binary, the application says so and falls back to the browser compiler.app = Bretzel(secret_key="…", mode="dev", css="build")
Cookies and HTTPS
The cookies' `Secure` attribute does NOT depend on the mode: it follows the request's protocol. A `Secure` cookie is never sent back over an `http://` origin, so tying it to the mode would cut the session of every application deployed without TLS.# Behind a proxy that terminates TLS: tell uvicorn,
# and it rewrites the protocol the app sees.
app.run(proxy_headers=True)
# Or force it, if the proxy sends no header.
app = Bretzel(secret_key="…", secure_cookies=True)
The security headers
Three headers are set BY DEFAULT, because they can break no app: `nosniff`, `Referrer-Policy`, `X-Frame-Options`. `security_headers=False` exists for the app that sets them itself upstream, behind its proxy.The CSP, for its part, is OPT-IN — and it is a split, not an oversight: Bretzel cannot guess your app's fonts, CDNs and iframes. It computes what it owes itself (the hashes of its inline scripts, the origins of its assets); you add your own. One widens, one never narrows.# 1. The first rung: the browser EVALUATES the policy
# and reports what would have been blocked, without
# blocking anything.
app = Bretzel(secret_key="…", csp="report-only")
# 2. One looks at what comes back, fills the gaps, then
# closes it.
app = Bretzel(secret_key="…", csp=True, csp_sources={
"font-src": ["https://fonts.gstatic.com"],
"frame-src": ["https://www.youtube.com"],
})
The mode and the sources are two separate axes on purpose: a dict meaning “enable AND extend” would make two ways of writing the same thing. And `report-only` exists precisely so one does not discover in production that an origin was missing.