Get started
~30 secmix archive.install hex francis
mix francis.new my_app
Deploy
~2 minmix francis.release
fly launch
What you can build
6 primitivesHTTP Actions
get "/hello", fn _ -> %{hello: :world} end
get "/:name", fn %{params: params} -> params["name"] end
post "/", fn conn -> conn.body_params end
Returns strings as HTML, maps as JSON. URL params, body, and the raw conn always available.
WebSockets
ws "/chat", fn
:join, socket -> {:reply, "welcome"}
{:received, msg}, _ -> {:reply, msg}
end
Pattern match on join, close, and incoming messages. Heartbeat and frame size limits included.
Server-Sent Events
sse "/events", fn
:join, socket -> {:reply, socket.id}
{:received, m}, _ -> {:reply, m}
end
Push from any process via send/2. Named events, retry, and keepalive managed for you.
Static Files
use Francis, static: [ from: "priv/static", at: "/" ]
Serve from disk with gzip. Run mix francis.digest for content-hashed filenames and a cache manifest.
Security
plug Francis.Plug.SecureHeaders
plug Francis.Plug.CSP, directives: %{script_src: ["'self'"]}
safe_html conn, user_content # auto-escapes <>&"'
Secure headers and CSP with safe defaults. Open redirects blocked in response helpers by design.
Plug Ecosystem
plug Plug.BasicAuth, username: "user", password: "pass"
plug Plug.Logger
plug MyApp.RateLimiter
Any standard Plug middleware works. No lock-in, no adapters needed.
Honest about scope
Francis is a thin DSL on top of Plug and Bandit. If you need ORMs, generators, live views, or a full asset pipeline, reach for Phoenix. If you want to ship a small HTTP service in an afternoon, you are in the right place.