diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,6 @@
+# Revision history for gemini-router
+
+## 0.1.0.0 -- 2020-07-24
+
+* First version. Released on an unsuspecting world.
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2020, Francesco Gazzetta
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Francesco Gazzetta nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Network/Gemini/Router.hs b/Network/Gemini/Router.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gemini/Router.hs
@@ -0,0 +1,164 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE CPP #-}
+module Network.Gemini.Router (
+-- * The Route monad transformer
+  RouteT
+, Route
+, RouteIO
+-- * Running Routes
+, runRouteT
+, runRouteT'
+-- * Building Routes
+, end
+, dir
+, capture
+, input
+, optionalInput
+, custom
+-- * Getters
+, getRequest
+, getPath
+) where
+
+import Network.Gemini.Server
+
+import Data.Maybe (fromMaybe)
+
+import Data.Functor.Identity (Identity)
+import Control.Applicative (Alternative(..))
+import Control.Monad.Trans.Class (MonadTrans(..))
+import Control.Monad.IO.Class (MonadIO(..))
+
+import Network.URI (uriQuery, pathSegments, unEscapeString)
+
+#if __GLASGOW_HASKELL__ < 808
+import Control.Monad.Fail (MonadFail(..))
+#endif
+
+
+-- The RouteT monad transformer
+-------------------------------
+
+-- | Represents a way of routing requests through different handlers
+newtype RouteT m a = RouteT { runRouteT :: Request -> [String] -> m (Maybe a) }
+
+type Route = RouteT Identity
+type RouteIO = RouteT IO
+
+instance Functor f => Functor (RouteT f) where
+  fmap f r = RouteT $ \req path -> fmap f <$> runRouteT r req path
+
+instance Applicative f => Applicative (RouteT f) where
+  pure x = RouteT $ \_ _ -> pure $ pure x
+  f <*> x = RouteT $ \req path ->
+    fmap (<*>) (runRouteT f req path) <*> runRouteT x req path
+
+instance Monad m => Monad (RouteT m) where
+  rx >>= f = RouteT $ \req path -> do
+    mx <- runRouteT rx req path
+    runRouteT (maybe (RouteT $ \_ _ -> pure Nothing) f mx) req path
+
+instance MonadTrans RouteT where
+  lift = RouteT . const . const . fmap pure
+
+instance MonadIO m => MonadIO (RouteT m) where
+  liftIO = RouteT . const . const . fmap pure . liftIO
+
+-- TODO all other transformers instances
+
+instance Monad m => MonadFail (RouteT m) where
+  --TODO or maybe we shoudl just throw an exception
+  --or is it possible to somehow directly return Response 42 err mempty? it would require early return... like happstack
+  fail _ = empty
+
+-- | 'empty' skips to the next route.
+-- @r1 '<|>' r2@ means go to @r2@ if @r1@ skips
+instance Monad f => Alternative (RouteT f) where
+  empty = RouteT $ \_ _ -> pure Nothing
+  r1 <|> r2 = RouteT $ \req path -> do
+    maybe1 <- runRouteT r1 req path
+    maybe2 <- runRouteT r2 req path
+    pure $ maybe1 <|> maybe2
+
+-- Running routes
+-------------------------------
+
+-- MAYBE swap names with runRouteT
+-- | Given a @run@ function for the inner 'Monad', make a 'Handler'
+runRouteT' :: (m (Maybe Response) -> IO (Maybe Response)) -- ^ Inner @run@
+           -> RouteT m Response
+           -> Handler
+runRouteT' runM r req = fromMaybe notFound <$> runM (runRouteT r req path)
+  where
+    notFound = Response 51 "Not found" mempty
+    path = unEscapeString <$> pathSegments req
+
+-- Building Routes
+-------------------------------
+
+-- | Match on the end of the path
+end :: Applicative f
+    => RouteT f a -- ^ Route to run
+    -> RouteT f a
+end r = RouteT $ \req path -> case path of
+  [] -> runRouteT r req path
+  _ -> pure Nothing
+
+-- | Match on a specific path segment
+dir :: Applicative f
+    => String -- ^ What the segment must match
+    -> RouteT f a -- ^ Route to run on the rest of the path
+    -> RouteT f a
+dir str r = RouteT $ \req path -> case path of
+  frag:rest | frag == str -> runRouteT r req rest
+  _                       -> pure Nothing
+
+-- TODO use a parsing class
+-- | Match on an arbitrary path segment, and capture it
+capture :: Applicative f
+        => (String -> RouteT f a) -- ^ Function that takes the segment and
+                                  -- returns the route to run on the rest of
+                                  -- the path
+        -> RouteT f a
+capture f = RouteT $ \req path -> case path of
+  frag:rest -> runRouteT (f frag) req rest
+  _         -> pure Nothing
+
+-- TODO use a parsing class
+-- | Require a query string, by asking the client (code 10) if necessary
+input :: Applicative f
+      => String -- ^ String to return to the client if there is no query string
+      -> (String -> RouteT f Response) -- ^ Function that takes the query string
+                                       -- and returns the route to run on the
+                                       -- rest of the path
+      -> RouteT f Response
+input q f = RouteT $ \req path -> case uriQuery req of
+  '?':query -> runRouteT (f $ unEscapeString query) req path
+  _         -> pure $ pure $ Response 10 q mempty
+
+-- | Capture, if present, the query string
+optionalInput :: Applicative f
+              => (Maybe String -> RouteT f a) -- ^ Function that takes the
+                                              -- query string (if present) and
+                                              -- returns the route to run on
+                                              -- the rest of the path
+              -> RouteT f a
+optionalInput f = RouteT $ \req path -> case uriQuery req of
+  '?':query -> runRouteT (f $ Just $ unEscapeString query) req path
+  _         -> runRouteT (f Nothing)                       req path
+
+-- | Build custom routes. Takes a function that takes the request and the
+-- remaining path segments and returns the result. A 'Nothing' makes the
+-- request fall through to the next route
+custom :: (Request -> [String] -> m (Maybe a)) -> RouteT m a
+custom = RouteT
+
+-- Getters
+-------------------------------
+
+getRequest :: Applicative m => RouteT m Request
+getRequest = RouteT $ \req _ -> pure $ Just req
+
+getPath :: Applicative m => RouteT m [String]
+getPath = RouteT $ \_ path -> pure $ Just path
+
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,9 @@
+# gemini-router
+
+[![Hackage](https://img.shields.io/hackage/v/gemini-router.svg)](https://hackage.haskell.org/package/gemini-router)
+[![builds.sr.ht status](https://builds.sr.ht/~fgaz/gemini-router.svg)](https://builds.sr.ht/~fgaz/gemini-router?)
+
+**A simple Happstack-style Gemini router**
+
+More info at https://sr.ht/~fgaz/haskell-gemini/
+
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/gemini-router.cabal b/gemini-router.cabal
new file mode 100644
--- /dev/null
+++ b/gemini-router.cabal
@@ -0,0 +1,35 @@
+cabal-version:       2.2
+
+name:                gemini-router
+version:             0.1.0.0
+synopsis:            A simple Happstack-style Gemini router
+description:
+  This package contains a 'Router' monad transformer that works on top of the
+  gemini-server package, with a functional+monadic+alternative interface,
+  similar to Happstack. With it you can define gemini endpoints, capture parts
+  of the request, and pass the requests to specific handlers.
+homepage:            https://sr.ht/~fgaz/haskell-gemini/
+bug-reports:         https://todo.sr.ht/~fgaz/haskell-gemini
+license:             BSD-3-Clause
+license-file:        LICENSE
+author:              Francesco Gazzetta
+maintainer:          Francesco Gazzetta <fgaz@fgaz.me>
+copyright:           © 2020 Francesco Gazzetta and contributors
+category:            Gemini
+extra-source-files:  CHANGELOG.md, README.md
+
+source-repository head
+  type:                git
+  location:            https://git.sr.ht/~fgaz/gemini-router
+
+library
+  exposed-modules:     Network.Gemini.Router
+  -- other-modules:
+  other-extensions:    FlexibleInstances, CPP
+  build-depends:       base ^>=4.13.0.0 || ^>=4.14.0.0
+                     , gemini-server ^>=0.1.0.0
+                     , transformers ^>=0.5.6.2
+                     , network-uri ^>=2.6.3.0 || ^>=2.7.0.0
+  -- hs-source-dirs:
+  default-language:    Haskell2010
+
