Mehari is a cross-platform library for building Gemini servers. It fully implements the Gemini protocol specification. It offers a simple and clean interface to create complete Gemini web apps. It takes heavy inspiration from Dream, a tidy, feature-complete Web framework.

Consult the Tutorial and examples.


Mehari provides several packages:

Implementation choice

IO implementations of Mehari are roughly equivalent in terms of features. However, some differences exist, e.g. Mehari_eio supports concurrent connections and should be preferred for a high performance server.


In these tutorials, Mehari_eio will be used so as not to complicate the snippets with a monadic interface but they can easily be adapted into a Mehari_mirage version.

Respond to request

The first form of abstraction is the Mehari.NET.handler which is essentially an asynchronous function from Mehari.request to Mehari.response. This is the simplest possible handler that responds to all requests in the same way:

(fun _ -> Mehari.response_text "Hello from Mehari")

As handlers take a Mehari.request in parameter we can operate on them. Here we retrieve the URL of the client's request and return it:

(fun req -> Mehari.uri req |> Uri.to_string |> Mehari.response_text)

See request section to consult all client request related functions.


Mehari provides its own representation of status codes as described in the gemini specification. Take a look at the signature of Mehari.response:

val response : 'a status -> 'a -> response

Furthermore, the status input and success are defined as follows:

val input : string status
val success : body -> mime status

The inhabitant type of Mehari.status carries the information of what is necessary for the creation of an associated response. In the case of an input response, a string is needed:

let input_resp =
  Mehari.(response input) "Enter a message"

In the same way, a Mehari.body and a Mehari.mime are required to make a successful response:

let successful_resp =
  let body = Mehari.string "A successful response" in
  Mehari.(response (success body) (gemini ()))

See status for the complete list of status. Other functions described in response section are mostly convenient functions built on top of Mehari.response.


Successful responses are accompanied by a Mehari.body. They are many methods to create a body, for example from a string:

let body = Mehari.string "A response body"

Or from a Gemtext object:

let body =
  Mehari.(gemtext Gemtext.[
    heading `H1 "Thought on Gemtext markup";
    list_item "Date: February 2021";
    list_item "Tags: gemini, reviews";
    heading `H2 "Introduction";
    quote "The format permits richer typographic possibilities than the plain text of Gopher, but remains extremely easy to parse.";
    text "Here is an example of a Python parser that demonstrates the truth of that statement:";
    preformat "..." ~alt:"python"

Which is rendered as the following Gemtext document:

# Thought on Gemtext markup
* Date: February 2021
* Tags: gemini, reviews

## Introduction

> The format permits richer typographic possibilities than the plain text of Gopher, but remains extremely easy to parse.
Here is an example of a Python parser that demonstrates the truth of that statement:
Data stream response

Mehari offers ways to keep client connections open forever and stream data in real time. Be sure to read this quick warning about this approach: note-on-data-stream-response.


Mehari.mime describes how the response body must be interpreted by the client. You can build your own mime with Mehari.make_mime and specify the data encoding with the parameter charset:

let mp3 = Mehari.make_mime "audio/mp3"

Some common MIME type are predefined. See mime section.

The text/gemini MIME type allows an additional parameter lang to specify the languages used in the document according to the Gemini specification:

let french_ascii_gemini = Mehari.gemini ~charset:"ascii" ~lang:["fr"] ()

Mehari.from_filename enables MIME type infering from a filename.

Mehari also provides an experimental Conan integration via Mehari.from_content to infer MIME type from a string.


Obviously, the path of a route corresponds to the "path" component of the url requested by the client. Currently, two types of route exist: "raw" route which are interpreted literally and route supplied as a Perl style regex. Note that routes are "raw" by default:

Mehari_eio.route "/var/gemini" (fun _ -> ...)

In the following snippet, Mehari.param retrieves the first group of the regex starting from index 1. It is possible to have as many groups as desired in the route path.

Mehari_eio.route ~regex:true "/articles/([a-z][A-Z])+" (fun req ->
  Mehari.param req 1
  |> Printf.sprintf "Get article %S"
  |> Mehari.response_text)

It is the purpose of Mehari.NET.router to group routes together to produce a bigger handler:

Mehari_eio.router [
  Mehari_eio.route "/" index_handler;
  Mehari_eio.route "/gemlog" gemlog_handler

In the same way Mehari.NET.scope groups several routes in one under the given prefix:

Mehari_eio.scope "/blog" [
  Mehari_eio.route "/articles" articles_handler;
  Mehari_eio.route "/gemlog" gemlog_handler

Advanced routing


Mehari.NET.middleware allows to run code before and after the execution of another handler and produces a “bigger” Mehari.NET.handler.

This example of an incrementable counter shows how to use them:

let counter = ref 0

let incr_count handler req =
  incr counter;
  handler req

let router =
  Mehari_eio.router [
      Mehari_eio.route "/" (fun _ ->
        Printf.sprintf "Value %i" !counter |> Mehari.response_text);
      Mehari_eio.route "/incr" ~mw:incr_count (fun _ ->
          Mehari.response Mehari.redirect_temp "/");

Rate limit

This road is limited to 3 accesses every 5 minutes:

let limit = Mehari_eio.make_rate_limit 5 `Minute

let limited_route = Mehari_eio.route "/stats" ~rate_limit:limit (fun _ -> ...)

They are described in depth in section rate_limit.

In the same way as shown in section Routing, these features can be mutualized at the scale of several routes using Mehari.NET.scope.


By default, the server runs on port 1965 on IP localhost (usually

This command generate certificates (cert.pem and key.pem), set server common name to localhost and it should work:

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365 -nodes

Here is what you have to do to start the server:

Using Mehari_lwt_unix:

let ( >>= ) = Lwt.Infix.( >>= )

let main () =
  X509_lwt.private_of_pems ~cert:"cert.pem" ~priv_key:"key.pem" >>= fun cert ->
  |> Mehari_io.run_lwt router ~certchains:[ cert ]

let () = (main ())

Using Mehari_eio:

let main ~net ~cwd =
  let certchains =
        X509_eio.private_of_pems ~cert:(cwd / "cert.pem")
          ~priv_key:(cwd / "key.pem");
  in net ~certchains router

let () = @@ fun env -> (module Mirage_crypto_rng.Fortuna) env @@ fun () ->
  main ~net:env#net ~cwd:env#cwd

Virtual hosting

Mehari supports virtual hosting using "server name indication" (SNI). Mehari.NET.virtual_hosts takes a list composed of a couple which represent a domain and his associated handler and produces a "biggest" Mehari.NET.handler.