Chirp 0.8.0
The verified SQL→render data layer (Shapes), server-owned reactive values (signals), a security-by-default hardening wave, unified form bind+validate, FileResponse, and HTTP 103 Early Hints.
Focus: give hypermedia apps a verified data layer and a single-connection reactive primitive, and make the secure posture the default rather than an opt-in.
This release lands two large new surfaces — the Shapes SQL→render data contract and the signal() reactive primitive — alongside a security-by-default wave that tightens the framework's out-of-the-box trust boundaries (default CSP, SSE origin, upload and static-file limits) and a broad expansion of the app.check() startup contracts. It requires bengal-pounce>=0.8.0. A handful of stable defaults changed; see Migration Notes before upgrading.
Highlights
Shapes — the verified SQL→render data contract
Chirp's answer to the ORM, without becoming one. Each hypermedia block declares the exact dataShape it needs as a frozen, slotted dataclass with its SQL co-located on the decorator; the shape runs behind the Database facade producing frozen instances; and the new shapecheckcontract proves at startup that the block reads only fields the query provides.
The full set ships in this release:@shape + Shape (fetch/fetch_one/stream), computed= members, nested(...) batched child shapes (a bounded 1 + child-levels query count, independent of child row count, fail-loud when un-expressible), per-@shape/@composite tenant scope= injection, page-level @composite aggregation behind the repository seam, the shapecheck registry-drift / under-fetch / over-fetch checks, and the chirp shapes-codegen CLI for adopting Shapes view-by-view with an --auditdrift report.
The honest marquee is the field-level + registry-drift startup contract — the thing no ORM, SQL tool, or GraphQL layer can offer, because only Chirp owns both the query and the template. Field-level verification is conservative single-object analysis with documented escape hatches; the Shapes guide explains exactly when the check stays silent and how to tune severity.
signal() — server-owned reactive values over one SSE connection
Declare a live value with@app.signal(name, ...), a computed one with @app.derived(name, on=(...)), push updates with app.emit(name, value), and bind it anywhere in a template with {{ signal('name') }} / {{ signal_block('name') }} under a single {{ signal_connect() }}wrapper.
A named value fans out to every binding — declare once, bind many — over one shared/_chirp/live stream, so an SSE-heavy page holds one connection instead of N (a topbar balance and a modal update together from one app.emit). app.check() validates bindings against producers (signal_dead_binding → ERROR, signal_orphan → INFO), and a derived must be a pure function of its inputs so it stays correct across workers. The single-node primitive ships now; a pluggable multi-worker SignalBus backplane is designed in the live-SSE-topics RFC (§12). Run reactive apps with workers=1 / worker_mode="async"until the backplane lands.
Secure by default
The secure posture is now what you get out of the box:
- Secure-by-default scaffolds. Every
chirp newvariant — including--minimaland--shell— wires theSession→CSRF→SecurityHeadersstack, readsCHIRP_SECRET_KEY, and passes the contract in production unchanged. security_stackcontract.app.check()flags any app with a mutating route (POST/PUT/PATCH/DELETE, or a filesystem page shipping_actions.py) that is missing the stack — ERROR in production, WARNING in staging, silent in development. This category is now the canonical owner of the "mutating route" definition.- Default CSP drops
'unsafe-inline'fromscript-src, with full per-request nonce coverage across every framework inline script (Suspense, SSE, Alpine, view-transitions, islands, speculation rules). A standardalpine=Trueapp no longer needsalpine_csp=True, anduse_chirp_ui()owns its own working CSP. - DoS hardening. Static files stream from disk past a threshold instead of buffering into RAM; uploads spool to disk with
413size limits, a multipart-part cap, and path-traversal-safesave(); SSE responses default to same-origin instead of a hardcoded wildcard CORS header.
Forms: bind and validate in one pass
form_or_errors() now runs chirp.validation rules attached to dataclass fields via Annotated — e.g. title: Annotated[str, required, max_length(100)] — in the same pass as binding. No separate validate() call, no Form/ModelForm class. Binding and rule errors merge per field, context["form"] re-populates submitted values, and Optional/union nesting is unwrapped to find the rules.
FileResponse, Early Hints, and opt-in htmx
FileResponseserves files through a dedicated bytes-capable sender with conditional-GET (ETag/Last-Modified/304),Range/206, andIf-Rangesupport — and conditional-GET is restored forHTMLInject/AlpineInject-rewritten static HTML.- HTTP 103 Early Hints via the
Link/preload header convention: set a preload-classLinkheader and Chirp emits a preliminary103frame so the browser preloads while a slow-first-byte page renders. AppConfig(htmx=True)opt-in htmx injection, symmetric withalpine— Chirp injects the htmx core script (with the live CSP nonce) and dedups ondata-chirp="htmx".
Streaming runs off the event loop
BothStream and Suspense renders previously drove kida's synchronous renderer inline on the event loop, so one heavy render stalled every other in-flight request. Both now render on a worker thread (preserving shell-first progressive flush and per-block OOB), closing that head-of-line DoS class. Suspensealso gained true per-block failure isolation — one failing deferred source no longer sweeps its siblings into an error state.
Expanded startup contracts
Newapp.check() categories: shapecheck, security_stack, chirpui_csp, csp_nonce, nojs_floor, i18n_missing_key, suspense_defer, macro_css, htmx_provisioned, static_streaming, trusted_proxies, plus the deploy_debug/deploy_metrics/deploy_sentry rules surfaced through the new chirp check --deployproduction-posture preflight, and five documented static accessibility checks.
Migration Notes
- Default CSP no longer includes
'unsafe-inline'inscript-src. Framework inline scripts now require a per-request nonce. Enable one withCSPNonceMiddlewareorAppConfig(csp_nonce_enabled=True)—use_chirp_ui()and the scaffolds wire this for you.app.check()fails loud (ERROR in production) when an inline script would be un-nonced. Apps that intentionally keep un-nonced inline scripts must re-add'unsafe-inline'to their ownscript-src. - SSE responses default to same-origin.
EventStreamno longer emitsAccess-Control-Allow-Origin: *. Cross-origin SSE must opt in withEventStream(gen(), allow_origin="https://app.example.com"). - Request-body limits split.
AppConfig.max_upload_sizeis now the multipart-total ceiling only; a newAppConfig.max_request_body_size(default 16 MB) caps the general request envelope. Both default to 16 MB, so default configs are unchanged. If you relied onmax_upload_sizeto cap JSON/text bodies, setmax_request_body_sizeto the same value. The long-dead, never-enforcedAppConfig.max_content_lengthis removed in favor ofmax_request_body_size. - SQLite
pool_sizeis now load-bearing. File-backed SQLite opens a bounded WAL connection pool sized byDatabaseConfig.pool_size(previously ignored), with concurrent reads and write-only serialization. In-memory databases are unchanged. Confirm no app setpool_sizeexpecting it to be a no-op.
Dependencies
bengal-pounce>=0.8.0(raised from0.7.0)kida-templates>=0.9.0chirp-ui>=0.9.0(optionaluiextra)