Skip to get started

Francis

v0.4.0

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


The whole app

lib/my_app.ex
defmodule MyApp do
  use Francis

  get "/", fn _ -> "Hello from Francis" end
  get "/users/:id", fn conn -> %{id: conn.params["id"]} end
  post "/echo", fn conn -> conn.body_params end

  ws "/ws", fn {:received, msg}, _socket -> {:reply, msg} end

  unmatched fn _ -> "not found" end
end

mix francis.new my_app writes this file and the project around it. iex -S mix francis.server runs it.


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

Maps become JSON, strings are sent as-is — use html/2 or text/2 to set the type. 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, incoming frames, and messages from other processes. Heartbeat and frame size limits included.

Server-Sent Events

sse "/events", fn
  :join, socket -> {:reply, %{event: "hello", data: socket.id}}
  {:received, update}, _ -> {:reply, update}
end

# from any process holding socket.transport
send(transport, %{event: "price", data: 42})

Server to client only: 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 — this page is served with both. 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.