This journey answers a practical question: how do Chirp's individual hypermedia primitives compose into a real application?
It uses five maintained examples instead of inventing another showcase. Each stage isolates one new pressure; Lucky Cat is the integrated capstone. The examples remain separate applications so you can read and test each concern without carrying tutorial-only code into the next one.
Fresh checkout
From a clean clone:
git clone https://github.com/lbliii/chirp.git
cd chirp
uv sync --group dev --extra auth --extra passkeys --extra sessions --extra ui
uv run python -m examples.inventory --check
The inventory check proves that every application below still exists, declares its dependencies and capabilities, has the documented README status, and owns an executable test entrypoint.
Five-minute compiler proof
Run this focused slice before taking the longer journey:
uv run pytest \
examples/standalone/todo/test_app.py::TestTodoOperations::test_plain_add_redirects_after_persisting \
examples/standalone/todo/test_app.py::TestTodoOperations::test_empty_text_returns_422 \
examples/chirpui/kanban_shell/test_app.py::TestBoard::test_index_boosted_fragment_keeps_page_content_contract \
examples/chirpui/kanban_shell/test_app.py::TestSSE::test_sse_includes_oob_swaps \
-q --tb=short
PYTHONPATH=src:. uv run chirp check examples.chirpui.kanban_shell.app:app
uv run pytest \
tests/test_app/test_hypermedia_program.py::test_program_compiles_stable_route_template_block_target_graph \
tests/test_transition_trace.py::test_same_route_has_distinct_normal_boosted_and_targeted_observations \
-q --tb=short
PYTHONPATH=src:. uv run chirp freeze examples.standalone.freeze_site.app:app /tmp/chirp-frozen
That bounded loop proves each layer rather than asking you to trust a diagram:
- Todo executes SQLite-backed mutation and
ValidationErrorpaths through one template. - Kanban exercises boosted outlet selection and post-load SSE/OOB updates.
chirp checkvalidates the assembled route/template/target contract.- The compiler and transition tests show that the internal immutable program and runtime observations share stable transition identities.
chirp freezeexports only the deliberately static-compatible example; the SQL, mutation, session, and SSE applications remain live ASGI programs.
For the visual half of the loop, start Lucky Cat withCHIRP_DEBUG=1, press
Ctrl+Shift+D, and compare its compiled transition IDs across normal,
boosted, targeted, mutation, Suspense, and SSE requests. The longer sections
below explain each piece and the three deliberate failure drills.
The path
| Stage | Executable reference | New application pressure | Proof |
|---|---|---|---|
| 1 | standalone/todo |
SQLite migrations and typed queries; one template negotiates full-page and named-block responses; CSRF, validation, htmx fragments, and a plain-POST redirect | uv run pytest examples/standalone/todo -q |
| 2 | chirpui/kanban_shell |
Filesystem pages, a persistent shell, boosted navigation, OOB mutations, auth, and SSE | uv run pytest examples/chirpui/kanban_shell -q |
| 3 | standalone/dashboard_live |
SQLite-backed Suspense followed by post-loadEventStreamfragments |
uv run pytest examples/standalone/dashboard_live -q |
| 4 | chirpui/lucky_cat |
Secure dual-mode mutations, shell OOB state, signals, targeted fragments, Suspense, SSE, DevTools, browser tests, and deploy posture | uv run pytest examples/chirpui/lucky_cat -q |
| 5 | standalone/freeze_site |
Optional static projection for routes whose output does not require live state | uv run pytest examples/standalone/freeze_site -q |
Run the focused journey suite at any point:
uv run pytest \
examples/standalone/todo \
examples/chirpui/kanban_shell \
examples/standalone/dashboard_live \
examples/chirpui/lucky_cat \
examples/standalone/freeze_site \
-q --tb=short
1. Persist one negotiated page
Start withexamples/standalone/todo/app.py and its single index.html.
Page("index.html", "todo_list", ...)is the contract: a browser gets the
document and htmx gets the named list block. The same handler does not branch on
headers.
The mutation path adds the other half of progressive enhancement:
ValidationErrorreturns the list block with status 422 for inline htmx errors.FormAction("/", Fragment(...))returns the updated list to htmx and a 303 redirect to a plain browser.csrf_field()and the standard formmethod/actionkeep the POST valid without relying on JavaScript-generated security state.
The tests exercise both request modes directly:
uv run pytest \
examples/standalone/todo/test_app.py::TestTodoFullPage::test_index_full_page \
examples/standalone/todo/test_app.py::TestTodoFullPage::test_index_fragment \
examples/standalone/todo/test_app.py::TestTodoOperations::test_plain_add_redirects_after_persisting \
-q
2. Add a shell without adding an API layer
Kanban Shell keeps server-rendered HTML as the transport. Read these paths in order:
pages/page.pyreturnsPagewith both the narrow block and shell outlet.app.pyreturnsValidationError,Fragment, andOOBfrom mutations.test_app.pyproves full, targeted, boosted, mutation, and SSE modes.
The boosted test is important: it verifies that#mainreceives an outlet
fragment, never a complete HTML document.
uv run pytest \
examples/chirpui/kanban_shell/test_app.py::TestBoard::test_index_full_page \
examples/chirpui/kanban_shell/test_app.py::TestBoard::test_index_fragment \
examples/chirpui/kanban_shell/test_app.py::TestBoard::test_index_boosted_fragment_keeps_page_content_contract \
examples/chirpui/kanban_shell/test_app.py::TestAddTask::test_add_returns_oob \
-q
3. Separate first paint from post-load updates
Dashboard Live uses the same SQLite database for two different time horizons:
Suspenserenders the initial shell and streams resolved OOB blocks during the first response.EventStreamsends new rendered fragments after the page has loaded.
That distinction is architectural, not stylistic. UseStreamfor progressive
first-byte HTML,Suspense for shell plus deferred blocks, and EventStream
for post-load SSE.
4. Inspect the integrated application
Lucky Cat composes the preceding patterns and adds secure sessions, auth, signals, browser evidence, and deployment files. Run it with debug tooling:
CHIRP_DEBUG=1 PYTHONPATH=src:. uv run python examples/chirpui/lucky_cat/app.py
Open the application, press Ctrl+Shift+D, and inspect
window.ChirpHtmxDebug. Compare a normal navigation, a boosted navigation, a
narrow market-chart swap, a trade mutation, the portfolio Suspense response,
and an SSE update. The response shape changes; the template/block contract does
not.
Then run static and behavioral evidence together:
PYTHONPATH=src:. uv run chirp check examples.chirpui.kanban_shell.app:app
uv run pytest examples/chirpui/lucky_cat/test_app.py -q
The browser smoke remains required for DOM behavior that static analysis cannot prove. Install its opt-in dependencies before running it locally:
uv sync --group dev --group browser --extra auth --extra passkeys --extra sessions --extra ui
uv run playwright install chromium
uv run pytest examples/chirpui/lucky_cat/test_browser_smoke.py -q
Runtime routes versus static projection
Static export is an optional projection, not Chirp's deployment model.
| Surface | Posture | Why |
|---|---|---|
TodoGET / |
Static-compatible only with an explicit, stable database snapshot | The HTML can render once, but its usefulness depends on later mutations. |
Todo/todos* |
Runtime-required | Writes, CSRF, validation, and redirects require the live app. |
DashboardGET / |
Runtime-required | Initial HTML awaits live database queries through Suspense. |
Dashboard/events |
Runtime-required | It is a post-load SSE stream. |
| Kanban and Lucky Cat mutations, auth, signals, Suspense, and SSE | Runtime-required | They depend on request/session state or ongoing server work. |
| Freeze Site content routes | Static-compatible | Content is deterministic and the example declares freeze inputs explicitly. |
Use Freeze Site to learn the eligible path:
PYTHONPATH=src:. uv run chirp freeze examples.standalone.freeze_site.app:app /tmp/chirp-frozen
Do not freeze a mutation or SSE route and describe the output as equivalent to the live application.
Three bounded contract drills
These are temporary edits to maintained examples, not a second contract-lab example. Restore the named file after each drill.
Full document in a boosted target
Inexamples/chirpui/kanban_shell/pages/page.py, temporarily import Template
and replace the finalPage(...)with
Template("page.html", board=board, columns=columns, all_tasks=get_tasks(), active_filters=active_filters).
Then run:
uv run pytest \
examples/chirpui/kanban_shell/test_app.py::TestBoard::test_index_boosted_fragment_keeps_page_content_contract \
-q
The route-smoke failure names the boosted intent, maintarget, and observed
full-document shape. RestorePage: it carries the shell outlet and named block
negotiation thatTemplatedoes not.
Missing OOB block
In_stats_fragment() inside examples/chirpui/kanban_shell/app.py, change
header_stats_oob to missing_stats_oob, then run:
uv run pytest \
examples/chirpui/kanban_shell/test_app.py::TestAddTask::test_add_returns_oob \
-q
The request fails loudly instead of emitting an empty OOB wrapper. Restore the declared block name (or add that named block to the same template); do not mark the region optional to hide a typo.
Mutating form without CSRF state
Remove{{ csrf_field() }}from
examples/standalone/todo/templates/index.html, then run:
PYTHONPATH=src:. uv run chirp check examples.standalone.todo.app:app
The csrf_form finding identifies index.htmland tells you to restore
csrf_field(), add the configured hidden input, or explicitly exempt the route.
Restore the field; a meta tag used only by JavaScript is not a no-JavaScript
form fallback.
git restore examples/chirpui/kanban_shell/pages/page.py \
examples/chirpui/kanban_shell/app.py \
examples/standalone/todo/templates/index.html
Downstream proof
The in-repository path is necessary but not sufficient. Furatena exercises the
same contracts through a substantially larger registry-driven application. The
pinned wheel-based compatibility canary in
#556 checks out revision
da584bf9fe19ec1376fdc0b23c7fb1b657b026b8, installs the Furatena lockfile,
force-installs the built Chirp wheel, and runs the framework-facing integration
slice. Its advisory release result is the downstream evidence paired with this
journey; #500 owns the canary's
release policy and update cadence.