Francis

A small DSL for Elixir web apps. Nothing more than you need.

HTTP, WebSockets, SSE, and static files — without the ceremony. Two commands to start. Two to deploy. Built on Bandit and Plug.

Get started

~30 sec
mix archive.install hex francis
mix francis.new my_app

Deploy

~2 min
mix francis.release
fly launch

What you can build

6 primitives

HTTP 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.