Overview
Chirp reads all configuration from one immutable object:AppConfig. You set
fields in code for local development, or load them from environment variables
for production — both produce the same frozen config that the app runs against.
For most apps, start here:
# Set fields in code
from chirp import App, AppConfig
app = App(config=AppConfig(debug=True, secret_key="dev-only"))# Load from the environment
from chirp import App, AppConfig
app = App(config=AppConfig.from_env()) # reads CHIRP_SECRET_KEY, CHIRP_ENV, CHIRP_PORT, ...Pass no config and you get the defaults:
app = App() # same as App(config=AppConfig())
Every field has IDE autocomplete and type checking, so there are no runtime
KeyErrorsurprises. The fields you reach for most often are below; the full
catalog and exact signatures live in the
complete field and signature reference.
Load configuration from the environment
AppConfig.from_env()builds a config from environment variables — the 12-factor
path you want in production. Unset variables fall back to theAppConfig
defaults, and any keyword you pass overrides the result:
app = App(config=AppConfig.from_env(template_dir="pages", worker_mode="async"))
Variables use the CHIRP_ prefix (override with from_env(prefix="MYAPP_")).
The commonly-set ones:
| Variable | Sets |
|---|---|
CHIRP_SECRET_KEY |
secret_key |
CHIRP_ENV |
env (development / staging / production) |
CHIRP_DEBUG |
debug |
CHIRP_HOST, CHIRP_PORT |
host, port |
CHIRP_ALLOWED_HOSTS |
allowed_hosts(comma/space-separated) |
CHIRP_TRUSTED_PROXIES |
trusted_proxies(comma/space-separated) |
CHIRP_FEATURE_<NAME> |
afeature_flags entry (CHIRP_FEATURE_BETA=true) |
Ifpython-dotenv is installed (pip install chirp[config]), from_env()loads
a.envfile from the current directory first. On Railway, it falls back to
the platform'sPORT, binds 0.0.0.0, and — when CHIRP_ALLOWED_HOSTSis
unset — setsallowed_hosts to your RAILWAY_PUBLIC_DOMAINplus
healthcheck.railway.app.
Fields you set by hand
These are the knobs most apps touch directly. Everything else has a default that works out of the box.
| Field | Type | Default | What it does |
|---|---|---|---|
debug |
bool |
False |
Rich error pages, DevTools, template auto-reload |
secret_key |
str |
"" |
Signing key for sessions, CSRF, and signed state |
host |
str |
"127.0.0.1" |
Bind address forapp.run() / chirp run |
port |
int |
8000 |
Bind port forapp.run() / chirp run |
env |
str |
"development" |
Environment label; a non-development value requires asecret_key |
template_dir |
str | Path |
"templates" |
Directory for Kida templates |
static_dir |
str | Path | None |
"static" |
Static-file directory;Nonedisables default static serving |
allowed_hosts |
tuple[str, ...] |
("*",) |
Host allowlist for production host validation |
worker_mode |
str |
"auto" |
Pounce worker execution:"auto", "sync", "async", or "subinterpreter" |
The remaining fields group by concern below — open the one you need.
Debug mode
Whendebug=True:
- Detailed error pages with tracebacks are shown in the browser.
- Templates auto-reload when modified (no server restart).
- Stricter validation warnings are surfaced.
app = App(config=AppConfig(debug=True, secret_key="dev-only"))
Secret key
secret_keysigns sessions, CSRF tokens, and signed state. Use a strong random
value in production, and read it from the environment rather than hard-coding it:
import os
app = App(config=AppConfig(secret_key=os.environ["CHIRP_SECRET_KEY"]))
Reference field groups
Server, reload & templates
| Field | Type | Default | Description |
|---|---|---|---|
reload_include |
tuple[str, ...] |
(".html", ".css", ".md") |
File suffixes watched by browser reload; use()to disable |
reload_dirs |
tuple[str, ...] |
() |
Extra directories watched alongside the working directory |
dev_browser_reload |
bool | None |
None |
Browser refresh injection;None follows debug |
reload_timeout |
float |
30.0 |
Pounce hot-reload drain timeout |
component_dirs |
tuple[str | Path, ...] |
() |
Additional component/template directories |
extra_loaders |
tuple[Any, ...] |
() |
Kida loaders tried before filesystem loaders |
autoescape |
bool |
True |
Enable HTML autoescaping |
trim_blocks |
bool |
True |
Kida whitespace trimming |
lstrip_blocks |
bool |
True |
Kida leading-whitespace trimming |
strict_undefined |
bool |
True |
Raise on missing template variables |
static_context |
Mapping | dict | None |
None |
Compile-time constants, frozen toMappingProxyTypewhen passed as a dict |
static_url |
str |
"/static" |
URL prefix for static files |
static_stream_threshold |
int |
1048576 |
File size (bytes) at/above which static files stream from disk instead of buffering (1 MiB) |
Security & request limits
| Field | Type | Default | Description |
|---|---|---|---|
csp_nonce_enabled |
bool |
False |
Enable nonce-aware CSP helpers |
strict_transport_security |
str | None |
None |
Strict-Transport-Security header value |
max_request_body_size |
int |
16777216 |
General request-body ceiling for every content type (16 MiB); oversize bodies are rejected with 413 before buffering into RAM |
max_upload_size |
int |
16777216 |
Multipart-specific ceiling on totalmultipart/form-data part size (16 MiB); must be <= max_request_body_size |
upload_spool_threshold |
int |
1048576 |
Bytes anUploadFilekeeps in RAM before spilling to a temp file (1 MiB) |
max_upload_parts |
int |
1000 |
Maximum number of multipart parts; rejects multipart bombs |
Production server & observability
| Field | Type | Default | Description |
|---|---|---|---|
workers |
int |
0 |
Pounce worker count;0lets pounce auto-detect |
metrics_enabled |
bool |
False |
Enable Prometheus metrics |
metrics_path |
str |
"/metrics" |
Metrics endpoint path |
health_path |
str |
"/health" |
Auto-mounted liveness probe path (plain 200; K8slivenessProbe). CHIRP_HEALTH_PATH |
ready_path |
str |
"/ready" |
Auto-mounted readiness probe path (runs registered checks + startup gate; 503 until ready; K8sreadinessProbe). CHIRP_READY_PATH |
rate_limit_enabled |
bool |
False |
Enable rate limiting |
rate_limit_requests_per_second |
float |
100.0 |
Rate limit steady-state rate |
rate_limit_burst |
int |
200 |
Rate limit burst size |
rate_limit_max_tracked_ips |
int |
100000 |
Max distinct client IPs the per-IP rate limiter tracks before LRU eviction |
trusted_proxies |
tuple[str, ...] |
() |
Reverse-proxy peer IPs/hostnames whoseX-Forwarded-For is honored. Empty ignores X-Forwarded-For; "*"trusts every peer (spoofing risk). See Running behind a reverse proxy. |
forwarded_for_trusted_hops |
int |
1 |
TrailingX-Forwarded-For hops to trust. Must be >= 1 (construction fails fast otherwise); only honored when trusted_proxiesis non-empty |
request_queue_enabled |
bool |
False |
Enable request queueing/load shedding |
request_queue_max_depth |
int |
1000 |
Maximum queued requests |
sentry_dsn |
str | None |
None |
Sentry DSN |
sentry_environment |
str | None |
None |
Sentry environment name |
sentry_release |
str | None |
None |
Sentry release identifier |
sentry_traces_sample_rate |
float |
0.1 |
Sentry tracing sample rate |
otel_endpoint |
str | None |
None |
OpenTelemetry OTLP endpoint |
otel_service_name |
str |
"chirp-app" |
OpenTelemetry service name |
lifecycle_logging |
bool |
True |
Enable pounce lifecycle logging |
log_format |
str |
"auto" |
Logging format:"auto", "text", or "json" |
log_level |
str |
"info" |
Logging level |
max_connections |
int |
1000 |
Maximum concurrent connections |
backlog |
int |
2048 |
Socket listen backlog |
keep_alive_timeout |
float |
5.0 |
HTTP keep-alive timeout |
request_timeout |
float |
30.0 |
Request timeout |
ssl_certfile |
str | None |
None |
TLS certificate path |
ssl_keyfile |
str | None |
None |
TLS key path |
SSE, Suspense & browser runtime
| Field | Type | Default | Description |
|---|---|---|---|
sse_heartbeat_interval |
float |
15.0 |
Seconds between SSE heartbeat comments |
sse_retry_ms |
int | None |
None |
SSE reconnection interval sent to the client |
sse_close_event |
str | None |
None |
Optional close event name emitted before stream shutdown |
suspense_error_template |
str | None |
None |
Template containing a global Suspense fallback block |
suspense_error_block |
str |
"fallback" |
Block name for the global Suspense fallback |
safe_target |
bool |
True |
Auto-addhx-target="this"to event-driven elements |
sse_lifecycle |
bool |
True |
Inject SSE connection state and lifecycle events |
view_transitions |
bool | str |
False |
View Transitions tier:False/"off", True/"htmx", or "full" |
speculation_rules |
bool | str |
False |
Speculation Rules tier:False/"off", True/"conservative", "moderate", or "eager" |
delegation |
bool |
False |
Inject delegated handlers for swapped copy/compare controls |
alpine |
bool |
False |
Enable Alpine.js script injection |
alpine_version |
str |
"3.15.8" |
Pinned Alpine.js CDN version |
alpine_csp |
bool |
False |
Use the CSP-safe Alpine build |
htmx |
bool |
False |
Enable opt-in htmx core script injection |
htmx_version |
str |
"2.0.4" |
Pinned htmx CDN version |
Cache & environment
| Field | Type | Default | Description |
|---|---|---|---|
cache_backend |
str |
"memory" |
Cache backend name |
cache_default_ttl |
int |
300 |
Default cache TTL in seconds |
cache_middleware_enabled |
bool |
False |
Enable whole-response cache middleware |
redis_url |
str | None |
None |
Redis URL for Redis-backed features |
audit_sink |
str | None |
"log" |
Lifecycle audit sink:"log", "none", or custom |
feature_flags |
tuple[tuple[str, bool], ...] |
() |
Feature flags loaded byfrom_env() |
http_timeout |
float |
30.0 |
Default outbound HTTP timeout for Chirp helpers |
http_retries |
int |
0 |
Default outbound HTTP retry count |
skip_contract_checks |
bool |
False |
Disable debug-mode startup contract checks |
skip_migrations |
bool |
False |
Skip the on-boot migration run (CHIRP_SKIP_MIGRATIONS); pair with a chirp migratedeploy job |
lazy_pages |
bool |
False |
Lazily load filesystem page modules |
debug_fragment_validator |
bool |
True |
Enable debug-only fragment response validation |
Provisional fields
New in 0.8Provisional subsystem fields
| Field | Type | Default | Reason |
|---|---|---|---|
mcp_path |
str |
"/mcp" |
MCP/tool integration is younger than the core hypermedia surface |
islands |
bool |
False |
Islands runtime API is still settling |
islands_version |
str |
"1" |
Version tag for the provisional islands runtime |
islands_contract_strict |
bool |
False |
Contract strictness will stabilize with the islands API |
passkeys |
bool |
False |
Inject thewindow.chirp.passkeys WebAuthn JS bridge (needs chirp[passkeys]for the server verbs) |
passkeys_version |
str |
"1" |
Version tag / cache-bust marker for the provisional passkeys bridge |
websocket_compression |
bool |
True |
Pounce-facing pass-through; Chirp's first-class realtime story is SSE |
websocket_max_message_size |
int |
10485760 |
Pounce-facing pass-through; no Chirp WebSocket return type exists |
i18n_enabled |
bool |
False |
i18n needs published examples and contract coverage before stabilization |
i18n_default_locale |
str |
"en" |
Provisional i18n option |
i18n_supported_locales |
tuple[str, ...] |
("en",) |
Provisional i18n option |
i18n_directory |
str | Path |
"locales" |
Provisional i18n option |
i18n_cookie_name |
str |
"chirp_locale" |
Provisional i18n option |
i18n_url_prefix |
bool |
False |
Provisional i18n option |
Construction-time validation
AppConfig.__post_init__enforces a few invariants when you construct the config,
so misconfigurations fail loud immediately instead of deep in the launch path:
- Secret key — empty
secret_keywithenvnot"development"raisesConfigurationError. - Upload caps — if
max_upload_sizeexceedsmax_request_body_size, Chirp clamps it to the body cap when the upload cap is still at its default, and otherwise raisesConfigurationError. - Forwarded hops —
forwarded_for_trusted_hopsbelow1raisesConfigurationError. To ignoreX-Forwarded-For, leavetrusted_proxiesempty rather than lowering this.
See also
- App lifecycle — how the app freezes config at startup
- Built-in middleware — middleware that reads config
- Production deployment — env config behind a reverse proxy
- API reference — the complete field and signature catalog