diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+
+The MIT License (MIT)
+
+Copyright (c) 2013 Michael Schröder
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
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/Web/Welshy.hs b/Web/Welshy.hs
new file mode 100644
--- /dev/null
+++ b/Web/Welshy.hs
@@ -0,0 +1,159 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Web.Welshy
+    ( welshy, welshyApp
+
+      -- * Middleware & Routing
+    , Welshy, middleware, route, RoutePattern
+    , get, post, put, patch, delete, head, options
+
+      -- * Actions
+    , Action
+    , halt, pass
+    , catchIO
+
+      -- ** Request
+    , request, body
+    , capture, captures
+    , queryParam, maybeQueryParam, queryParams
+    , jsonParam, maybeJsonParam, jsonParams, jsonData
+    , bearerAuth
+
+      -- ** Response
+    , status, header
+    , text, text'
+    , html, html'
+    , json
+    , file, filePart
+    , source
+
+      -- * Parameter Parsing
+    , Param
+    , FromText(..), maybeFromText
+    ) where
+
+import Control.Applicative
+import Control.Exception
+import qualified Control.Exception.Lifted as Lifted
+import Control.Monad
+import Control.Monad.IO.Class
+import Control.Monad.Trans.Writer hiding (pass)
+import Data.Monoid
+import Data.Text (Text)
+import qualified Data.Text as T
+import Network.HTTP.Types
+import Network.Wai
+import Network.Wai.Handler.Warp
+import System.IO
+
+import Prelude hiding (head)
+
+import Web.Welshy.Action
+import Web.Welshy.FromText
+import Web.Welshy.Request
+import Web.Welshy.Response
+
+-----------------------------------------------------------------------
+
+-- Note [Exception handling]
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~
+-- Ideally, all exceptions would be caught by the server and a 500 response
+-- sent to the client. Alas, due to lazyness this is not possible: any
+-- exceptions occuring inside a 'ResponseBuilder' would need to be caught on
+-- a higher level. ('Warp' provides 'settingsOnException', but at that point
+-- the connection has already been dropped and we can't send a response to
+-- the client anymore.)
+--
+-- Effectively, this means that if something like the following happens
+-- the client will simply get an empty reply:
+--
+-- > text $ error "wat"
+--
+-- (The exception will be logged to stderr though.)
+
+-- TODO: clarify this in user-visible documentation.
+
+-----------------------------------------------------------------------
+
+-- | We use this monad to compose WAI 'Middleware', using the 'middleware'
+-- and 'route' functions.
+newtype Welshy a = Welshy (Writer [Middleware] a)
+    deriving (Functor, Applicative, Monad)
+
+-- | Run a Welshy app using the Warp server.
+welshy :: Port -> Welshy () -> IO ()
+welshy p w = do
+    putStr "Aye, Dwi iawn 'n feddw!"
+    putStrLn $ " (port " ++ show p ++ ") (ctrl-c to quit)"
+    run p (welshyApp w)
+
+-- | Turns a Welshy app into a WAI 'Application'.
+welshyApp :: Welshy () -> Application
+welshyApp (Welshy w) = foldr id notFound (catchError : execWriter w)
+    where
+        notFound :: Application
+        notFound _ = return $ ResponseBuilder notFound404 [] mempty
+
+        -- see Note [Exception Handling]
+        catchError :: Middleware
+        catchError app req = Lifted.catch (app req) $ \e -> do
+            liftIO $ hPrint stderr (e :: SomeException)
+            return $ ResponseBuilder status500 [] mempty
+
+-----------------------------------------------------------------------
+
+-- | Insert middleware into the app. Note that unlike in Scotty,
+-- each middleware is run at the point of insertion.
+middleware :: Middleware -> Welshy ()
+middleware = Welshy . tell . pure
+
+get :: RoutePattern -> Action () -> Welshy ()
+get = route GET
+
+post :: RoutePattern -> Action () -> Welshy ()
+post = route POST
+
+put :: RoutePattern -> Action () -> Welshy ()
+put = route PUT
+
+patch :: RoutePattern -> Action () -> Welshy ()
+patch = route PATCH
+
+delete :: RoutePattern -> Action () -> Welshy ()
+delete = route DELETE
+
+head :: RoutePattern -> Action () -> Welshy ()
+head = route HEAD
+
+options :: RoutePattern -> Action () -> Welshy ()
+options = route OPTIONS
+
+-- | Sinatra-style route pattern. Named parameters are prepended with
+-- a colon (e.g. @\"\/users\/:id\"@) and can be accessed with 'capture'.
+type RoutePattern = Text
+
+-- | Define a route for an HTTP method and URL pattern that runs the given
+-- action. Routes are matched in the order they are defined. If no route
+-- matches, a 404 response is returned.
+route :: StdMethod -> RoutePattern -> Action () -> Welshy ()
+route met pat act = middleware $ \nextApp req ->
+    case matchRoute met pat req of
+        Nothing   -> nextApp req
+        Just caps -> execAction act caps nextApp req
+
+matchRoute :: StdMethod -> RoutePattern -> Request -> Maybe [Param]
+matchRoute met pat req =
+    if Right met == parseMethod (requestMethod req)
+        then go (filter (/= T.empty) $ T.split (=='/') pat) (pathInfo req) []
+        else Nothing
+    where
+        go []     []     prs  = Just prs
+        go []     _      _    = Nothing
+        go _      []     _    = Nothing
+        go (p:ps) (r:rs) prs
+            | p == r          = go ps rs prs
+            | T.null p        = Nothing
+            | T.head p == ':' = go ps rs $ (T.tail p, r) : prs
+            | otherwise       = Nothing
diff --git a/Web/Welshy/Action.hs b/Web/Welshy/Action.hs
new file mode 100644
--- /dev/null
+++ b/Web/Welshy/Action.hs
@@ -0,0 +1,149 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module Web.Welshy.Action where
+
+import Control.Applicative
+import Control.Arrow
+import Control.Exception
+import Control.Monad
+import Control.Monad.IO.Class
+import Control.Monad.Trans.Class
+import Control.Monad.Trans.Resource
+import qualified Data.Aeson as A
+import qualified Data.ByteString.Lazy as BL
+import Data.Conduit.Lazy
+import Data.Maybe
+import Data.Monoid
+import Data.Text (Text)
+import Network.HTTP.Types
+import Network.Wai
+
+-----------------------------------------------------------------------
+
+data Result a = Ok a Response | Halt (Action ()) | Pass
+
+newtype Action a = Action { runAction :: Env -> Response -> IO (Result a) }
+
+instance Functor Action where
+    fmap f m = Action $ \r s -> runAction m r s >>= \case
+        Ok a s1 -> return $ Ok (f a) s1
+        Halt m1 -> return $ Halt m1
+        Pass    -> return $ Pass
+
+instance Applicative Action where
+    pure = return
+    (<*>) = ap
+
+-- TODO: this instance should be highlighted in the documentation
+instance Alternative Action where
+    empty = mzero
+    (<|>) = mplus
+
+instance Monad Action where
+    return a = Action $ \_ s -> return $ Ok a s
+    m >>= k  = Action $ \r s -> runAction m r s >>= \case
+        Ok a s1 -> runAction (k a) r s1
+        Halt s1 -> return $ Halt s1
+        Pass    -> return $ Pass
+
+    fail msg = halt $ error msg
+
+instance MonadPlus Action where
+    mzero       = fail "mzero"
+    m `mplus` n = Action $ \r s -> runAction m r s >>= \case
+        Ok a s1 -> return $ Ok a s1
+        Halt __ -> runAction n r s
+        Pass    -> runAction n r s
+
+instance MonadIO Action where
+    liftIO m = Action $ \_ s -> do
+        a <- m
+        return $ Ok a s
+
+-- | Like `catch` but with the exception handler and result in 'Action'.
+catchIO :: Exception e => IO a -> (e -> Action a) -> Action a
+catchIO act h = either h return =<< (liftIO $ try $ act)
+
+-----------------------------------------------------------------------
+
+-- | A route, query or form parameter and its value.
+type Param = (Text, Text)
+
+data Env = Env { _captures    :: [Param]
+               , _queryParams :: [Param]
+               , _body        :: BL.ByteString
+               , _jsonParams  :: Maybe A.Object
+               , _request     :: Request }
+
+mkEnv :: [Param] -> Request -> ResourceT IO Env
+mkEnv _captures _request = do
+    _body <- BL.fromChunks <$> lazyConsume (requestBody _request)
+    let _queryParams = queryText _request ++ formParams _body _request
+        _jsonParams = either (const Nothing) Just (A.eitherDecode _body)
+    return Env {..}
+
+queryText :: Request -> [Param]
+queryText = map (second $ fromMaybe "") . queryToQueryText . queryString
+
+formParams :: BL.ByteString -> Request -> [Param]
+formParams b req =
+    case lookup hContentType (requestHeaders req) of
+        Just "application/x-www-form-urlencoded" ->
+            map (second $ fromMaybe "") $ queryToQueryText $
+            parseQuery $ BL.toStrict $ b
+        _ -> []
+
+
+execAction :: Action () -> [Param] -> Middleware
+execAction act0 caps nextApp req = run act0 =<< mkEnv caps req
+    where
+        run :: Action () -> Env -> ResourceT IO Response
+        run act env = (lift $ runAction act env okRes) >>= \case
+            Ok _ res  -> return res
+            Halt act' -> run act' env
+            Pass      -> nextApp req
+
+        okRes :: Response
+        okRes = ResponseBuilder ok200 [] mempty
+
+-----------------------------------------------------------------------
+
+-- | Stop running the current action and continue with another one.
+-- The other action will live in the same request environment and can access
+-- the same route captures, but it will start with a fresh default response.
+--
+-- This is incredibly useful for error handling. For example:
+--
+-- > patch "/users/:uid" $ do
+-- >     uid <- capture "uid"
+-- >     user <- getUserFromDB uid
+-- >             `catchIO` (\_ -> halt $ status notFound404)
+-- >     ...
+halt :: Action () -> Action a
+halt m = Action $ \_ _ -> return $ Halt m
+
+-- | Stop the current action and continue with the next matching route.
+pass :: Action a
+pass = Action $ \_ _ -> return Pass
+
+-- | Get the raw WAI 'Request'.
+request :: Action Request
+request = Action $ \r s -> return $ Ok (_request r) s
+
+-- | Get all query parameters.
+queryParams :: Action [Param]
+queryParams = Action $ \r s -> return $ Ok (_queryParams r) s
+
+-- | Get all route captures.
+captures :: Action [Param]
+captures = Action $ \r s -> return $ Ok (_captures r) s
+
+-- | Get the request body.
+body :: Action BL.ByteString
+body = Action $ \r s -> return $ Ok (_body r) s
+
+-- | Modify the raw WAI 'Response'.
+modifyResponse :: (Response -> Response) -> Action ()
+modifyResponse f = Action $ \_ s -> return $ Ok () (f s)
diff --git a/Web/Welshy/FromText.hs b/Web/Welshy/FromText.hs
new file mode 100644
--- /dev/null
+++ b/Web/Welshy/FromText.hs
@@ -0,0 +1,50 @@
+module Web.Welshy.FromText where
+
+import Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as TL
+import Data.Int
+import Data.Word
+import Text.Read (readEither)
+
+-- | A type that can be converted from a strict 'Text' value.
+-- Used for parsing route captures, query parameters, header values, etc.
+--
+-- Minimal complete definition: 'fromText'.
+class FromText a where
+    fromText :: Text -> Either String a
+
+    -- | Allows a specialized way of parsing lists of values.
+    -- The default definition uses 'fromText' to parse comma-delimited lists.
+    fromTextList :: Text -> Either String [a]
+    fromTextList = mapM fromText . T.split (== ',')
+
+-- | > maybeFromText = either (const Nothing) Just . fromText
+maybeFromText :: FromText a => Text -> Maybe a
+maybeFromText = either (const Nothing) Just . fromText
+
+instance FromText a => FromText [a] where
+    fromText = fromTextList
+
+instance FromText Char where
+    fromText t = case T.unpack t of
+                       [c] -> Right c
+                       _   -> Left "fromText Char: no parse"
+    fromTextList = Right . T.unpack
+
+instance FromText Text    where fromText = Right
+instance FromText TL.Text where fromText = Right . TL.fromStrict
+instance FromText Int     where fromText = readEither . T.unpack
+instance FromText Int8    where fromText = readEither . T.unpack
+instance FromText Int16   where fromText = readEither . T.unpack
+instance FromText Int32   where fromText = readEither . T.unpack
+instance FromText Int64   where fromText = readEither . T.unpack
+instance FromText Integer where fromText = readEither . T.unpack
+instance FromText Word    where fromText = readEither . T.unpack
+instance FromText Word8   where fromText = readEither . T.unpack
+instance FromText Word16  where fromText = readEither . T.unpack
+instance FromText Word32  where fromText = readEither . T.unpack
+instance FromText Word64  where fromText = readEither . T.unpack
+instance FromText Bool    where fromText = readEither . T.unpack
+instance FromText Double  where fromText = readEither . T.unpack
+instance FromText Float   where fromText = readEither . T.unpack
diff --git a/Web/Welshy/Request.hs b/Web/Welshy/Request.hs
new file mode 100644
--- /dev/null
+++ b/Web/Welshy/Request.hs
@@ -0,0 +1,126 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Web.Welshy.Request where
+
+import Control.Applicative
+import Control.Monad
+import Data.Aeson (FromJSON, fromJSON)
+import qualified Data.Aeson as A
+import qualified Data.ByteString as B
+import qualified Data.HashMap.Strict as HashMap
+import Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+import Network.HTTP.Types
+import Network.Wai
+
+import Web.Welshy.Action
+import Web.Welshy.FromText
+import Web.Welshy.Response
+
+-----------------------------------------------------------------------
+
+-- | Get a parameter captured by the route pattern.
+--
+--     * If the parameter does not exist, fails with an error.
+--
+--     * If the parameter was found but could not be parsed, 'pass' is called.
+--       This means routes are typed to a degree.
+--
+capture :: FromText a => Text -> Action a
+capture k = (lookup k <$> captures) >>= \case
+    Nothing  -> fail ("unknown capture: " ++ T.unpack k)
+    Just raw -> case fromText raw of
+        Left  _ -> pass
+        Right v -> return v
+
+-- TODO: provide way to customize default response
+-- | Get a query parameter.
+--
+--     * If the parameter does not exist or could not be parsed,
+--       the action halts with HTTP status @400 Bad Request@.
+--
+queryParam :: FromText a => Text -> Action a
+queryParam k = (lookup k <$> queryParams) >>= \case
+    Nothing  -> halt $ status badRequest400
+    Just raw -> case fromText raw of
+        Left  _ -> halt $ status badRequest400
+        Right v -> return v
+
+-- | Like 'queryParam', but returns 'Nothing' if the parameter wasn't found.
+--
+--     * If the parameter could not be parsed,
+--       the action halts with HTTP status @400 BadRequest@
+--
+maybeQueryParam :: FromText a => Text -> Action (Maybe a)
+maybeQueryParam k = (lookup k <$> queryParams) >>= \case
+    Nothing  -> return Nothing
+    Just raw -> case fromText raw of
+        Left  _ -> halt $ status badRequest400
+        Right v -> return (Just v)
+
+-- | Get a JSON parameter.
+--
+--     * If the request body is not a JSON dictionary,
+--       or if the parameter does not exist or could not be parsed,
+--       the action halts with HTTP status @400 Bad Request@.
+--
+jsonParam :: FromJSON a => Text -> Action a
+jsonParam k = (HashMap.lookup k <$> jsonParams) >>= \case
+    Nothing  -> halt $ status badRequest400
+    Just raw -> case fromJSON raw of
+        A.Error   _ -> halt $ status badRequest400
+        A.Success v -> return v
+
+
+-- | Like 'jsonParam', but returns 'Nothing' if the parameter wasn't found.
+--
+--     * If the request body is not a JSON dictionary,
+--       the action halts with HTTP status @400 Bad Request@.
+--
+maybeJsonParam :: FromJSON a => Text -> Action (Maybe a)
+maybeJsonParam k = (HashMap.lookup k <$> jsonParams) >>= \case
+    Nothing  -> return Nothing
+    Just raw -> case fromJSON raw of
+        A.Error   _ -> halt $ status badRequest400
+        A.Success v -> return (Just v)
+
+
+-- | Parse the request body as a JSON object.
+--
+--     * If the body could not be parsed,
+--       the action halts with HTTP status @400 Bad Request@.
+--
+jsonData :: FromJSON a => Action a
+jsonData = A.decode <$> body >>= \case
+    Nothing -> halt $ status badRequest400
+    Just v  -> return v
+
+-- | Get all JSON parameters.
+--
+--     * If the request body is not a JSON dictionary,
+--       the action halts with HTTP status @400 Bad Request@.
+--
+jsonParams :: Action A.Object
+jsonParams = Action $ \r s -> do
+    case _jsonParams r of
+        Nothing -> return $ Halt $ status badRequest400
+        Just v  -> return $ Ok v s
+
+-----------------------------------------------------------------------
+
+-- | Get the bearer token from an authorization header using the @Bearer@
+-- authentication scheme (RFC 6750).
+--
+-- If the request does not have a (syntactically) valid authorization
+-- header for the Bearer scheme, the action halts with HTTP status
+-- @401 Unauthorized@.
+bearerAuth :: FromText a => Action a
+bearerAuth = do
+    headers <- requestHeaders <$> request
+    maybe (halt $ status unauthorized401) return $ do
+        credentials <- lookup hAuthorization headers
+        let (scheme, raw) = B.splitAt 7 credentials
+        guard (scheme == "Bearer ")
+        maybeFromText $ T.decodeUtf8 raw
diff --git a/Web/Welshy/Response.hs b/Web/Welshy/Response.hs
new file mode 100644
--- /dev/null
+++ b/Web/Welshy/Response.hs
@@ -0,0 +1,96 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Web.Welshy.Response where
+
+import Blaze.ByteString.Builder
+import Data.Aeson (ToJSON)
+import qualified Data.Aeson as A
+import Data.ByteString (ByteString)
+import Data.Conduit
+import Data.Text (Text)
+import qualified Data.Text.Encoding as T
+import qualified Data.Text.Lazy as TL
+import qualified Data.Text.Lazy.Encoding as TL
+import Network.HTTP.Types
+import Network.Wai
+
+import Web.Welshy.Action
+
+-----------------------------------------------------------------------
+
+-- | Set the HTTP status of the response. The default is 'ok200'.
+status :: Status -> Action ()
+status s = modifyResponse $ \case
+    (ResponseBuilder _ h b)    -> ResponseBuilder s h b
+    (ResponseFile    _ h f fp) -> ResponseFile    s h f fp
+    (ResponseSource  _ h cs)   -> ResponseSource  s h cs
+
+-- | Add or replace one of the response headers.
+header :: HeaderName -> ByteString -> Action ()
+header k v = modifyResponse $ \case
+    (ResponseBuilder s h b)    -> ResponseBuilder s (update h) b
+    (ResponseFile    s h f fp) -> ResponseFile    s (update h) f fp
+    (ResponseSource  s h cs)   -> ResponseSource  s (update h) cs
+    where update h = (k,v) : filter ((/= k) . fst) h
+
+-- | Set the response body to the given lazy 'TL.Text'
+-- and the content-type to @text/plain@.
+text :: TL.Text -> Action ()
+text t = do
+    header hContentType "text/plain"
+    _builder $ fromLazyByteString $ TL.encodeUtf8 t
+
+-- | Like 'text' but with a strict 'Text' value.
+text' :: Text -> Action ()
+text' t = do
+    header hContentType "text/plain"
+    _builder $ fromByteString $ T.encodeUtf8 t
+
+-- | Set the response body to the given lazy 'TL.Text'
+-- and the content-type to @text/html@.
+html :: TL.Text -> Action ()
+html t = do
+    header hContentType "text/html"
+    _builder $ fromLazyByteString $ TL.encodeUtf8 t
+
+-- | Like 'html' but with a strict 'Text' value.
+html' :: Text -> Action ()
+html' t = do
+    header hContentType "text/html"
+    _builder $ fromByteString $ T.encodeUtf8 t
+
+-- | Set the response body to the JSON encoding of the given value
+-- and the content-type to @application/json@.
+json :: ToJSON a => a -> Action ()
+json v = do
+    header hContentType "application/json"
+    _builder $ fromLazyByteString $ A.encode v
+
+-- | Sends the given file as the response.
+file :: FilePath -> Action ()
+file = flip _file Nothing
+
+-- | 'filePart' @f offset byteCount@ sends @byteCount@ bytes of the file @f@,
+-- beginning at @offset@, as the response.
+filePart :: FilePath -> Integer -> Integer -> Action ()
+filePart f offset byteCount = _file f (Just $ FilePart offset byteCount)
+
+_file :: FilePath -> Maybe FilePart -> Action ()
+_file f part = modifyResponse $ \case
+    (ResponseBuilder s h _)   -> ResponseFile s h f part
+    (ResponseFile    s h _ _) -> ResponseFile s h f part
+    (ResponseSource  s h _)   -> ResponseFile s h f part
+
+_builder :: Builder -> Action ()
+_builder b = modifyResponse $ \case
+    (ResponseBuilder s h _)   -> ResponseBuilder s h b
+    (ResponseFile    s h _ _) -> ResponseBuilder s h b
+    (ResponseSource  s h _)   -> ResponseBuilder s h b
+
+-- | Set the response body to the given 'Source'.
+source :: Source (ResourceT IO) (Flush Builder) -> Action ()
+source src = modifyResponse $ \case
+    (ResponseBuilder s h _)   -> ResponseSource s h src
+    (ResponseFile    s h _ _) -> ResponseSource s h src
+    (ResponseSource  s h _)   -> ResponseSource s h src
diff --git a/example.hs b/example.hs
new file mode 100644
--- /dev/null
+++ b/example.hs
@@ -0,0 +1,25 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+import Control.Applicative
+import Control.Monad
+import qualified Data.Text.Lazy as T
+import Network.HTTP.Types
+import Web.Welshy
+
+fibs :: [Int]
+fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
+
+main :: IO ()
+main = welshy 3000 $ do
+    get "/fibs" $ do
+        offset <- queryParam "offset" <|> return 0
+        length <- queryParam "length"
+
+        when (offset < 0 || length < 0)
+             (halt $ status badRequest400)
+
+        when (offset + length > 1000)
+             (halt $ status requestedRangeNotSatisfiable416)
+
+        let result = take length $ drop offset fibs
+        text $ T.pack $ show result
diff --git a/welshy.cabal b/welshy.cabal
new file mode 100644
--- /dev/null
+++ b/welshy.cabal
@@ -0,0 +1,91 @@
+name:                welshy
+version:             0.1.0.0
+synopsis:            Haskell web framework (because Scotty had trouble yodeling)
+description:
+  A Haskell web framework heavily influenced by the excellent Scotty, 
+  which was in turn influenced by Ruby's Sinatra.
+  .
+  Welshy strives to make it easier to do error handling without overly
+  complicating the control flow. An example:
+  .
+  @&#123;-&#35; LANGUAGE OverloadedStrings &#35;-&#125;@
+  .
+  > import Control.Applicative
+  > import Control.Monad
+  > import qualified Data.Text.Lazy as T
+  > import Network.HTTP.Types
+  > import Web.Welshy
+  > 
+  > fibs :: [Int]
+  > fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
+  >
+  > main :: IO ()
+  > main = welshy 3000 $ do
+  >     get "/fibs" $ do
+  >         offset <- queryParam "offset" <|> return 0
+  >         length <- queryParam "length"
+  >         
+  >         when (offset < 0 || length < 0)
+  >              (halt $ status badRequest400)
+  >         
+  >         when (offset + length > 1000)
+  >              (halt $ status requestedRangeNotSatisfiable416)
+  >         
+  >         let result = take length $ drop offset fibs
+  >         text $ T.pack $ show result
+  .
+  Some of the features demonstrated here:
+  .
+  * You can 'halt' the current action at any point and continue
+    with a different one.
+  .
+  * Functions like 'queryParam' and 'jsonParam' have built-in error handling.
+  .
+  * Welshy's 'Action' monad is an instance of 'Alternative'.
+
+
+
+license:             MIT
+license-file:        LICENSE
+author:              Michael Schröder
+maintainer:          mcschroeder@gmail.com
+homepage:            https://github.com/mcschroeder/welshy
+bug-reports:         https://github.com/mcschroeder/welshy/issues
+copyright:           (c) 2013 Michael Schröder
+category:            Web
+build-type:          Simple
+cabal-version:       >=1.8
+
+extra-source-files:
+  example.hs
+
+library
+  exposed-modules:  Web.Welshy
+  other-modules:    Web.Welshy.Action,
+                    Web.Welshy.FromText,
+                    Web.Welshy.Request,
+                    Web.Welshy.Response
+  build-depends:
+      base ==4.6.*
+    , aeson ==0.6.*
+    , blaze-builder ==0.3.*
+    , bytestring ==0.10.*
+    , conduit ==1.0.*
+    , http-types ==0.8.*
+    , lifted-base ==0.2.*
+    , resourcet ==0.4.*
+    , text ==0.11.*
+    , transformers ==0.3.*
+    , unordered-containers ==0.2.*
+    , wai ==1.4.*
+    , warp ==1.3.*  
+  
+  extensions:
+    GeneralizedNewtypeDeriving
+    -- LambdaCase
+    OverloadedStrings
+    RecordWildCards
+
+source-repository head
+  type:     git
+  location: https://github.com/mcschroeder/welshy.git
