Livery
One handler set. Every version of HTTP.
Website · Docs · What is Livery · Design
Livery is a web framework for Erlang/OTP. It serves the same router and middleware over HTTP/1.1, HTTP/2, and HTTP/3 from one runtime. WebSocket, WebTransport, Server-Sent Events, OpenAPI, MCP, A2A, and OpenTelemetry-style observability come as modules in the same package. It follows the ideas of Rust's Axum, Tower and Hyper.
Install
%% rebar.config
{deps, [livery]}.
Hello, world
A handler takes a request value and returns a response value. Compile a router, start a service, and you are serving:
Router = livery_router:compile([
{<<"GET">>, <<"/">>, fun(_Req) -> livery_resp:text(200, <<"hello">>) end}
]),
{ok, _Pid} = livery:start_service(#{http => #{port => 8080}, router => Router}).
$ curl localhost:8080/
hello
Run it now: rebar3 shell, paste the two expressions, then curl.
Route, with path params and JSON
show_user(Req) ->
Id = livery_req:binding(<<"id">>, Req),
livery_resp:json(200, json:encode(#{id => Id, name => <<"Ada">>})).
Router = livery_router:compile([
{<<"GET">>, <<"/users/:id">>, fun show_user/1},
{<<"POST">>, <<"/users">>, {users, create}} %% {Module, Function}
]).
Stack middleware (Tower/Axum style)
A middleware is a function call(Req, Next, State): it gets the request
value and Next, the rest of the pipeline, and returns a response
value. Attach it to the whole service or to one route.
livery:start_service(#{
http => #{port => 8080},
router => Router,
middleware => [
{livery_request_id, undefined},
{livery_access_log, #{}},
{livery_body_limit, #{max => 1048576}}
]
}).
One service, three protocols
The same router and middleware over H1, H2, and H3, with an Alt-Svc
header that tells clients H3 is available:
livery:start_service(#{
http => #{port => 80},
https => #{port => 443, cert => Cert, key => Key},
http3 => #{port => 443, cert => Cert, key => Key},
router => Router,
alt_svc => advertise
}).
Add alpn => [h2, http1] to the https map and that one TLS port
serves HTTP/2 and HTTP/1.1, chosen per connection:
https => #{port => 443, cert => Cert, key => Key, alpn => [h2, http1]}
Need only one protocol? livery:start_listener(livery_h1, #{port => 8080, router => Router}).
Call other services
The client uses the same middleware model for the requests you send: wrap a request in timeouts, retries, a circuit breaker, or load balancing.
Client = livery_client:new(#{
base_url => <<"https://api.example.com">>,
stack => [livery_client:timeout(5000), livery_client:retry(#{max => 3})]
}),
{ok, Resp} = livery_client:get(Client, <<"/health">>),
200 = livery_client:status(Resp).
Stream a response
events(_Req) ->
livery_resp:sse(200, fun(Emit) ->
Emit(#{event => <<"tick">>, data => <<"1">>}),
Emit(#{event => <<"tick">>, data => <<"2">>})
end).
Chunked bodies, NDJSON, file responses with byte ranges, WebSocket (over H1, H2 and H3) and WebTransport over H2/H3 work the same way.
Early-response semantics
Sometimes a handler answers before it has read the request body, like
rejecting an oversized upload with a 413. On HTTP/1.1 the connection
would normally close right after the response, and with a large upload
still in flight the close can reach the client before it reads your
413, so the client sees a reset instead.
Livery handles this for you. When you return a full response before the whole body has arrived, it reads and throws away the rest of the request body before closing, so the client gets the response:
reject(_Req) ->
livery_resp:json(413, [], <<"{\"error\":\"too_big\"}">>).
You limit how much it reads, and for how long, on each listener (by default there is no byte limit and a 30 s deadline):
{ok, _} = livery:start_listener(livery_h1, #{
port => 8080,
early_response_drain => {16#400000, 5000}, %% 4 MiB / 5 s
handler => fun reject/1
}).
Override it for a single response, or turn it off with none:
livery_resp:json(413, [], Body, #{early_response_drain => {16#400000, 5000}}).
The per-response override works only on full responses. Streaming responses (SSE, NDJSON, chunked, files) use the listener's limit.
Features
- One handler, three protocols: write a handler once and serve it over H1, H2, and H3, with the same routing and middleware, and Alt-Svc to move clients up to H3.
- Tower-style middleware:
call(Req, Next, State)pipelines over plain values, for the whole service or for one route, in both directions (requests coming in, and requests going out throughlivery_client). - Streaming: chunked, SSE, NDJSON, WebSocket and WebTransport over H2/H3, and file responses with byte ranges.
- OpenAPI: generate a 3.2 document from routes, serve Redoc or Swagger UI, and validate request bodies against a subset of JSON Schema.
- MCP: serve the Model Context Protocol Streamable HTTP transport on the main listener.
- A2A: mount an Agent-to-Agent agent (card, JSON-RPC and REST bindings, SSE streams) on the main listener.
- Observability & auth: OpenTelemetry-style traces and metrics, logs tagged with the trace, JWT/JWKS/OIDC, signed sessions, token introspection.
Ecosystem
Companion libraries built on Livery, each in its own repo:
- livery_grpc - gRPC server and client on top of Livery's HTTP/2 support: all four call types, deadlines, gRPC-Web, server reflection, and the standard health service.
- livery_s3 - S3-compatible object storage client on the Livery HTTP client: AWS SigV4 signing, multipart uploads, and presigned URLs, for AWS S3, Garage, MinIO, Ceph, and Wasabi.
- livery_stripe - Stripe API client on the Livery HTTP client: customers, subscriptions, Checkout, the Billing Portal, and webhook verification.
Documentation
Full guides, tutorials, and the generated API reference live at
https://benoitc.github.io/livery/. The same content is in the repo
under docs/: start with the
Quickstart, then the
tutorials and
how-to guides. To contribute, read
CONTRIBUTING.md; to report a vulnerability, see
SECURITY.md.
Sponsors
Livery is developed and maintained by Enki Multimedia. If your company relies on it, reach out for sponsored support, or sponsor its maintenance via GitHub Sponsors.
License
Apache-2.0.