durable-streams-server
durable-streams-server is a Rust implementation of the
Durable Streams protocol,
built with axum and tokio.
It can run as a standalone server or be embedded into an existing axum
application with build_router or build_router_with_ready.
Features
- Durable Streams HTTP API with create, append, read, head, close, and delete operations
- Live reads via long-polling and Server-Sent Events (SSE)
- In-memory, file-backed, and ACID (
redb) storage backends - Explicit transport modes:
http,tls, andmtls - Reverse-proxy trust gating for
X-Forwarded-*and RFC 7239Forwarded - Layered configuration from TOML files and
DS_*environment variables - Structured startup diagnostics with phase-aware failures
- Structured
application/problem+jsonerror responses - Request telemetry via
tracingwith OpenTelemetry/ECS-friendly field names
Running
From the workspace root:
By default the server listens on http://0.0.0.0:4437, exposes health checks
at /healthz and /readyz, and mounts the protocol at /v1/stream.
Use http.stream_base_path or DS_HTTP__STREAM_BASE_PATH to mount the
protocol at another path.
Storage Backends
The default storage mode is in-memory. For persistence, choose a backend via
DS_STORAGE__MODE:
| Mode | Durability | Use case |
|---|---|---|
memory |
None (lost on restart) | Development and testing |
file-fast |
Buffered writes | Lower-latency persistence where recent data loss is acceptable |
file-durable |
Fsynced writes | Durable persistence without external dependencies |
acid |
Crash-resilient (redb) |
Production workloads requiring transactional durability |
Examples:
DS_STORAGE__MODE=file-durable DS_STORAGE__DATA_DIR=./data
DS_STORAGE__MODE=acid DS_STORAGE__DATA_DIR=./data
Configuration
Configuration is loaded in this order, with later sources overriding earlier ones:
- Built-in defaults
- Built-in profile defaults (
default,dev,prod,prod-tls,prod-mtls) config/default.tomlconfig/<profile>.tomlconfig/local.toml--config <path>- Environment variables
Examples:
The effective config schema is nested and operator-facing:
[]
= "0.0.0.0:4437"
[]
= 104857600
= 10485760
[]
= "*"
= "/v1/stream"
[]
= "memory"
= "./data/streams"
[]
= "http" # http | tls | mtls
[]
= ["http1"] # http1 | http2
[]
= 30
= 60
[]
= "1.2"
= "1.3"
= ["http/1.1"]
[]
= false
= "none" # none | x-forwarded | forwarded
= []
[]
= "none" # none | header
= true
[]
= "info"
Environment overrides follow the TOML path with DS_ prefixes and double
underscores. For example:
transport.mode->DS_TRANSPORT__MODEtransport.tls.cert_path->DS_TRANSPORT__TLS__CERT_PATHproxy.identity.header_name->DS_PROXY__IDENTITY__HEADER_NAME
In practice, operators usually only need a small subset:
| Purpose | Variables | When to set |
|---|---|---|
| Bind and logging | DS_SERVER__BIND_ADDRESS, DS_OBSERVABILITY__RUST_LOG, RUST_LOG |
Override the listen address or log verbosity without editing TOML |
| Storage selection | DS_STORAGE__MODE, DS_STORAGE__DATA_DIR |
Pick persistence mode and storage path |
| Direct TLS | DS_TRANSPORT__MODE, DS_TRANSPORT__TLS__CERT_PATH, DS_TRANSPORT__TLS__KEY_PATH |
Required for tls deployments |
| Direct mTLS | DS_TRANSPORT__MODE, DS_TRANSPORT__TLS__CLIENT_CA_PATH |
Required in addition to TLS vars for mtls deployments |
| HTTP/2 and ALPN | DS_TRANSPORT__HTTP__VERSIONS, DS_TRANSPORT__TLS__ALPN_PROTOCOLS |
Only when you need to override the profile defaults |
| Reverse proxy trust | DS_PROXY__ENABLED, DS_PROXY__FORWARDED_HEADERS, DS_PROXY__TRUSTED_PROXIES |
When the backend should trust a proxy’s forwarded origin metadata |
| Proxy identity handoff | DS_PROXY__IDENTITY__MODE, DS_PROXY__IDENTITY__HEADER_NAME, DS_PROXY__IDENTITY__REQUIRE_TLS |
Only for trusted proxy identity handoff in mtls mode |
Less common overrides, such as stream limits or acid-specific shard tuning,
follow the same DS_<SECTION>__<FIELD> pattern and generally only need to be
set when deviating from the profile examples.
Deployment Styles
The server supports three deployment styles today.
1. Dev HTTP
Use this for local development, CI, or when TLS is terminated elsewhere and the inner network is intentionally trusted.
- Profile:
dev - Transport: direct
http - HTTP versions:
http1 - Storage: defaults to in-memory unless overridden
Example:
Or with persistence:
DS_STORAGE__MODE=file-durable \
DS_STORAGE__DATA_DIR=./data \
2. Direct TLS
Use this when the server terminates TLS itself and clients connect directly.
- Profile:
prod-tls - Transport:
tls - TLS versions:
1.2minimum,1.3supported - ALPN:
http/1.1andh2 - Storage: durable file-backed by default
Example config:
[]
= "file-durable"
= "/var/lib/durable-streams"
[]
= 536870912
= 268435456
[]
= "tls"
[]
= ["http1", "http2"]
[]
= "/etc/durable-streams/tls/server.crt"
= "/etc/durable-streams/tls/server.key"
= "1.2"
= "1.3"
= ["http/1.1", "h2"]
Run it:
3. Direct mTLS
Use this when clients must authenticate with certificates and the server is the TLS termination point.
- Profile:
prod-mtls - Transport:
mtls - TLS versions:
1.2minimum,1.3supported - ALPN:
http/1.1andh2 - Client authentication: required via
transport.tls.client_ca_path
Example config:
[]
= "file-durable"
= "/var/lib/durable-streams"
[]
= 536870912
= 268435456
[]
= "mtls"
[]
= ["http1", "http2"]
[]
= "/etc/durable-streams/tls/server.crt"
= "/etc/durable-streams/tls/server.key"
= "/etc/durable-streams/tls/client-ca.crt"
= "1.2"
= "1.3"
= ["http/1.1", "h2"]
Run it:
Reverse Proxy and TLS Topologies
The server intentionally keeps auth out of the application. If you need edge authn/authz, terminate it at a proxy or gateway and forward only the minimum origin metadata the server is configured to trust.
Edge TLS termination, backend HTTP
This is the common pattern for local ingress or a trusted internal mesh:
- Edge terminates external TLS
- Backend link to durable-streams-server is plain HTTP
transport.mode = "http"proxy.enabled = trueproxy.forwarded_headers = "x-forwarded"or"forwarded"proxy.trusted_proxiescontains only the proxy source IPs/CIDRs
Example:
[]
= "http"
[]
= true
= "x-forwarded"
= ["10.0.0.0/24"]
Edge TLS termination, backend mTLS to the server
Use this when the proxy itself must authenticate to the server:
- Edge or service proxy terminates external TLS
- Proxy establishes mTLS to durable-streams-server
transport.mode = "mtls"proxy.enabled = trueproxy.trusted_proxiescontains only proxy addressesproxy.identity.mode = "header"is only allowed in this mode
Example:
[]
= "mtls"
[]
= ["http1", "http2"]
[]
= "/etc/durable-streams/tls/server.crt"
= "/etc/durable-streams/tls/server.key"
= "/etc/durable-streams/tls/proxy-ca.crt"
= ["http/1.1", "h2"]
[]
= true
= "forwarded"
= ["10.0.10.15/32", "10.0.10.16/32"]
[]
= "header"
= "x-client-identity"
= true
Envoy example
Illustrative deployment shape:
client -> Envoy (TLS or mTLS) -> durable-streams-server (HTTP or mTLS)
If Envoy terminates external TLS and forwards to backend HTTP:
- trust only Envoy addresses in
proxy.trusted_proxies - configure Envoy to emit either
X-Forwarded-*orForwarded - keep
proxy.forwarded_headersaligned with what Envoy emits
If Envoy forwards to backend mTLS:
- use
transport.mode = "mtls" - issue Envoy a client cert signed by
transport.tls.client_ca_path - if Envoy injects an identity header, keep
proxy.identity.mode = "header"and scopetrusted_proxiesnarrowly
HTTP Version Policy
Current protocol support:
httpmode:http1onlytlsmode:http1orhttp1 + http2mtlsmode:http1orhttp1 + http2http3: rejected at config validation time because it is not implemented
ALPN must match the configured HTTP versions:
http1requireshttp/1.1http2requiresh2
If you enable http2, keep the ALPN list consistent:
[]
= ["http1", "http2"]
[]
= ["http/1.1", "h2"]
Migration From Legacy TLS Config
Legacy compatibility still exists for the old TLS/log fields, but new deployments should author the explicit transport model directly.
Field mapping:
server.port->server.bind_address = "0.0.0.0:<port>"[tls].cert_path->[transport.tls].cert_path[tls].key_path->[transport.tls].key_path[log].rust_log->[observability].rust_log
Typical migration steps:
- Replace
server.portwithserver.bind_address. - Move legacy TLS paths under
[transport.tls]. - Add
transport.mode = "tls"ortransport.mode = "mtls". - Add
transport.http.versionsand matchingtransport.tls.alpn_protocols. - If a proxy is in front, move any old implicit trust assumptions into
proxy.enabled,proxy.forwarded_headers, andproxy.trusted_proxies. - If proxy identity handoff is needed, move to
transport.mode = "mtls"and configure[proxy.identity].
Minimal before:
[]
= 4437
[]
= "/etc/ds/server.crt"
= "/etc/ds/server.key"
[]
= "info"
Minimal after:
[]
= "0.0.0.0:4437"
[]
= "tls"
[]
= ["http1", "http2"]
[]
= "/etc/ds/server.crt"
= "/etc/ds/server.key"
= ["http/1.1", "h2"]
[]
= "info"
Startup Failure Troubleshooting
Startup failures are phase-aware. The process reports the failing phase in the
error message, for example [check_tls_files] or [bind_listener].
[load_config]
Likely causes:
- TOML syntax errors
--configpoints at a missing file- invalid
DS_*override values
Check:
config/default.toml,config/<profile>.toml,config/local.toml- override file path passed with
--config - environment values such as
DS_TRANSPORT__MODE,DS_TRANSPORT__TLS__MIN_VERSION
[validate_config]
Likely causes:
transport.modeand TLS fields do not agreetransport.http.versionsandtransport.tls.alpn_protocolsdo not matchproxy.enabled = truewithouttrusted_proxiesproxy.identity.mode = "header"withouttransport.mode = "mtls"- invalid
server.bind_address, base path, or CIDR values
Check:
- transport mode and all required TLS paths
- ALPN vs HTTP version consistency
- proxy trust and identity sections
[check_tls_files]
Likely causes:
- cert, key, or client CA path missing
- path points at a directory, not a file
- file exists but the process cannot read it
Check:
Make sure the configured path names exist and are readable by the service user.
[build_tls_context]
Likely causes:
- PEM file contents are malformed
- certificate chain is empty
- private key is missing or does not match the certificate
- client CA bundle is invalid
Check:
- cert/key pair correctness
- PEM encoding
- CA bundle contents for mTLS deployments
[bind_listener]
Likely causes:
- address already in use
- insufficient privileges for the port
- invalid bind target at runtime
Check:
If binding to a privileged port, ensure the process manager and user privileges match your deployment model.
[start_server]
Likely causes:
- runtime listener or service failure after startup
- storage backend initialisation failure
Check:
- storage directory permissions and available space
- acid/file backend startup logs
- any preceding error chain in the process logs
Library Use
For embedding, the main entry points are:
ConfigandConfigLoadOptionsfor configuration loadingbuild_routerandbuild_router_with_readyfor mounting the HTTP APIInMemoryStorage,FileStorage, andAcidStoragefor backend selection
Example Config Files
The crate ships profile-oriented example files:
config/default.tomlconfig/dev.tomlconfig/prod.tomlconfig/prod-tls.tomlconfig/prod-mtls.toml
Use them as layered building blocks rather than copying the entire merged config by hand.
Verification