diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Ian Duncan (c) 2017
+
+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 Ian Duncan 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/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
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/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,24 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import Control.Concurrent
+import Network.HTTP.Types.Status
+import Network.Wai
+import qualified Network.Wai.Middleware.Brotli as B
+import Network.Wai.Handler.Warp
+import Network.Wai.Application.Static
+import Network.Wai.Middleware.Gzip
+
+main :: IO ()
+main = do
+  let settings =
+        B.defaultSettings
+        {B.brotliFilesBehavior = B.BrotliPreCompressed (B.BrotliCacheFolder "."), B.brotliMinimumSize = 5}
+      -- app = staticApp $ defaultFileServerSettings "."
+      app = \req respond -> respond $ responseStream ok200 [("Content-Type", "text/wow")] $ \send flush -> do
+        send "Hello"
+        flush
+        threadDelay 10000000
+        send "World"
+
+  runEnv 3000 (gzip def {- TODO replace gzip with zopfli -} $ B.brotli settings {- $ sdch sdchSettings -} $ app)
diff --git a/src/Network/Wai/Middleware/Brotli.hs b/src/Network/Wai/Middleware/Brotli.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Wai/Middleware/Brotli.hs
@@ -0,0 +1,400 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+---------------------------------------------------------
+-- |
+-- Module        : Network.Wai.Middleware.Brotli
+-- Copyright     : Ian Duncan
+-- License       : BSD3
+--
+-- Maintainer    : Ian Duncan <ian@iankduncan.com>
+-- Stability     : Unstable
+-- Portability   : portable
+--
+-- Automatic brotli compression of responses.
+--
+-- If you are using this middleware with wai-extra's @gzip@ middleware,
+-- it is important that @brotli@ wraps your application before gzip does
+-- or your responses will be compressed by both, which is not beneficial.
+--
+-- Correct:
+--
+-- > gzip def . brotli defaultSettings
+--
+-- Incorrect:
+--
+-- > brotli defaultSettings . gzip def
+---------------------------------------------------------
+module Network.Wai.Middleware.Brotli
+  ( brotli
+  , defaultSettings
+  , BrotliSettings(..)
+  , defaultShouldCompress
+  , BrotliFiles(..)
+  ) where
+
+import Codec.Compression.Brotli
+import Control.Exception
+import Control.Monad
+import Data.Binary.Builder (toLazyByteString, fromLazyByteString, fromByteString, Builder)
+import qualified Data.ByteString.Char8 as B
+import qualified Data.ByteString.Lazy as L
+import Data.IORef
+import Data.Maybe
+import Data.Monoid
+import Network.HTTP.Types.Header
+import Network.HTTP.Types.Status
+import Network.Wai
+import Network.Wai.Internal
+import System.Directory
+import System.FilePath
+import System.IO
+import System.Posix
+
+data BrotliFiles
+  = BrotliIgnore -- ^ Do not compress file responses
+  | BrotliCompress
+    -- ^ Compress files on the fly. Note that this may counteract zero-copy
+    -- response optimizations on some platforms.
+  | BrotliCacheFolder FilePath
+    -- ^ Compress files, caching them in the specified directory. Note that
+    -- changes to the original files will not invalidate existing cached files,
+    -- so it is important to clear the cache directory appropriately if the
+    -- original file has changed
+  | BrotliPreCompressed BrotliFiles
+    -- ^ Look for the original file, only with the ".br" extension appended to it. Will fall back
+    -- to the provided file setting if the file doesn't exist.
+  deriving (Read, Eq, Show)
+
+data BrotliSettings = BrotliSettings
+  { brotliCompressionSettings :: Request -> Response -> CompressionSettings
+  , brotliMinimumSize :: Int
+  , brotliFilesBehavior :: BrotliFiles
+  , brotliShouldCompress :: BrotliSettings -> Request -> Response -> Bool
+  , brotliMimePrefixes :: [B.ByteString]
+  , brotliMimeSuffixes :: [B.ByteString]
+  }
+
+-- | It is recommended that you combine your custom logic around
+-- deciding to compress with this function, such as:
+--
+-- > \settings req resp -> defaultShouldCompress settings req resp && customPredicate
+defaultShouldCompress :: BrotliSettings -> Request -> Response -> Bool
+defaultShouldCompress settings req resp =
+  let (mclb, hs) = pluckHeader hContentLength (responseHeaders resp)
+      (mctb, hs') = pluckHeader hContentType hs
+  in case mctb of
+       Nothing -> False
+       Just ctb ->
+         case mclb of
+           Nothing -> checkMime settings ctb
+           Just clb ->
+             (fromMaybe True $ do
+                (x, rest) <- B.readInt clb
+                return $
+                  if x >= brotliMinimumSize settings
+                    then True
+                    else False) &&
+             checkMime settings ctb
+
+checkMime :: BrotliSettings -> B.ByteString -> Bool
+checkMime settings ctb =
+  any (\b -> b `B.isPrefixOf` ctb) (brotliMimePrefixes settings) ||
+  any (\b -> b `B.isSuffixOf` ctb) (brotliMimeSuffixes settings)
+
+-- A sane set of starting defaults. Tuned to use faster / better compression
+-- than GZip defaults for non-file responses. Compresses most common text-based formats,
+-- skips compression on responses with known Content-Lengths that already fit
+-- within one TCP packet.
+--
+-- Note that this configuration does *not* compress file responses. Customizing
+-- @brotliFilesBehavior@ will use brotli's maximum compression quality by default, which is
+-- quite slow (albeit achieves very good compression ratios). It is highly recommended that
+-- you have an appropriate caching strategy to avoid compression of files on each request.
+defaultSettings :: BrotliSettings
+defaultSettings =
+  BrotliSettings
+    (\_ resp ->
+       case resp of
+         ResponseFile _ _ _ _ ->
+           defaultCompressionSettings {compressionQuality = 11}
+         _ -> defaultCompressionSettings {compressionQuality = 4})
+    860
+    BrotliIgnore
+    defaultShouldCompress
+    [ "text/"
+    , "application/json"
+    , "application/javascript"
+    , "application/x-javascript"
+    , "application/ecmascript"
+    , "application/xml"
+    , "application/x-font-ttf"
+    , "image/x-icon"
+    , "image/vnd.microsoft.icon"
+    , "application/vnd.ms-fontobject"
+    , "application/x-font-opentype"
+    , "application/x-font-truetype"
+    , "font/eot"
+    , "font/otf"
+    , "font/ttf"
+    , "font/opentype"
+    ]
+    ["+json", "+xml"]
+
+
+pluckHeader :: HeaderName -> [Header] -> (Maybe B.ByteString, [Header])
+pluckHeader hName = foldr go (Nothing, [])
+  where
+    go h@(hKey, hVal) (mh, hs) = if hKey == hName
+      then (Just hVal, hs)
+      else (mh, h:hs)
+
+splitCommas :: B.ByteString -> [B.ByteString]
+splitCommas = B.split ',' . B.filter (/= ' ')
+
+-- SDCH + brotli may be better together than separate, but gzip and brotli together aren't useful
+canUseReqEncoding :: [B.ByteString] -> (Bool, [B.ByteString])
+canUseReqEncoding = foldr go (False, [])
+  where
+    go :: B.ByteString -> (Bool, [B.ByteString]) -> (Bool, [B.ByteString])
+    go bs (seenBr, encs) = if bs == "br"
+      then (True, encs)
+      else if seenBr && bs == "gzip"
+        then (seenBr, encs)
+        else (seenBr, bs:encs)
+
+decodeRequestBody :: Request -> IO Request
+decodeRequestBody req = do
+  let (encoding, hs) = pluckHeader "Content-Encoding" $ requestHeaders req
+  case encoding of
+    Nothing -> return req
+    Just enc ->
+      let encs = splitCommas enc
+          getBs = requestBody req
+      in case encs of
+           ("br":encs) -> do
+             bodyDecompressor <- decomp getBs
+             return $
+               req
+               { requestHeaders =
+                   ("Content-Encoding", B.intercalate ", " encs) : hs
+               , requestBody = bodyDecompressor
+               }
+           _ -> return req
+  where
+    decomp :: IO B.ByteString -> IO (IO B.ByteString)
+    decomp popper = do
+      dc <- decompressor
+      ref <- newIORef dc
+      return $ step ref
+      where
+        step ref = do
+          c <- readIORef ref
+          case c of
+            Done -> return ""
+            Consume f -> do
+              bs <- popper
+              r <- f bs
+              writeIORef ref r
+              step ref
+            Produce bs act -> do
+              r <- act
+              writeIORef ref r
+              return bs
+            Error -> error "Brotli stream decoding error in request body"
+
+-- | Use brotli to decompress request bodies & compress response bodies.
+--
+-- Analyzes the Accept-Encoding and Content-Type headers to determine if
+-- brotli is supported.
+brotli :: BrotliSettings -> Middleware
+brotli settings app req sendResponse = do
+  decodedReq <-
+    decodeRequestBody $
+    (reqWithoutBrotliAcceptEnc
+     { requestHeaders =
+         (hAcceptEncoding, B.intercalate ", " remainingReqEncs) :
+         requestHeaders reqWithoutBrotliAcceptEnc
+     })
+  app decodedReq $ \res ->
+    if isBrotliResp
+      then wrapResponse settings req res sendResponse
+      else sendResponse res
+  where
+    (mEncs, reqWithoutBrotliAcceptEnc) =
+      case pluckHeader hAcceptEncoding $ requestHeaders req of
+        (ms, hs) -> (ms, req {requestHeaders = hs})
+    (isBrotliResp, remainingReqEncs) =
+      case mEncs of
+        Nothing -> (False, [])
+        Just ebs -> canUseReqEncoding $ splitCommas ebs
+
+fixHeaders :: [Header] -> [Header]
+fixHeaders = filter (\(k, _) -> k /= hContentLength)
+
+wrapResponse :: BrotliSettings -> Request -> Response -> (Response -> IO a) -> IO a
+wrapResponse settings req res sendResponse =
+  let shouldCompress = (brotliShouldCompress settings) settings req
+  in if shouldCompress res
+       then case res of
+              ResponseFile status hs fp bp ->
+                compressedFileResponse
+                  settings
+                  addBrotliToContentEnc
+                  req
+                  status
+                  hs
+                  fp
+                  bp
+                  sendResponse
+
+              ResponseBuilder status hs b -> do
+                let compressedResp =
+                      compressWith
+                        (brotliCompressionSettings settings req res)
+                        (toLazyByteString b)
+                sendResponse $
+                  responseLBS status (addBrotliToContentEnc hs) compressedResp
+
+              ResponseStream status hs f ->
+                sendResponse $
+                ResponseStream status (addBrotliToContentEnc hs) $ \send flush -> do
+                  c <- compressor (brotliCompressionSettings settings req res)
+                  ref <- newIORef c
+                  f (compressSend send flush ref) (compressFlush send flush ref)
+                  c' <- readIORef ref
+                  case c' of
+                    Consume consumer ->
+                      consumer (Chunk B.empty) >>= finishStreaming send
+                    Produce _ _ -> error "Shouldn't be producing right now"
+                    Error -> error "Shouldn't be in an error state right now"
+                    Done -> return ()
+
+              ResponseRaw act fallback -> sendResponse $ responseRaw act fallback
+
+       else sendResponse res
+  where
+    addBrotliToContentEnc hs =
+      let (respEncs, hsWithoutBrotliEnc) = pluckHeader hContentEncoding hs
+      in fixHeaders $
+         ((hContentEncoding, maybe "br" (<> ", br") respEncs) :
+          hsWithoutBrotliEnc)
+    finishStreaming send c =
+      case c of
+        Consume _ -> error "Shouldn't be consuming right now"
+        Produce b next ->
+          send (fromByteString b) >> next >>= finishStreaming send
+        Error -> error "Encountered error while finishing stream"
+        Done -> return ()
+
+compressSend :: (Builder -> IO ()) -> IO () -> IORef (BrotliStream Chunk) -> Builder -> IO ()
+compressSend innerSend innerFlush st bs = do
+  let steps = L.toChunks $ toLazyByteString bs
+
+  action <- readIORef st
+  action' <- foldM step action steps
+  writeIORef st action'
+
+  where
+    step c bs = case c of
+      Produce b n -> do
+        innerSend $ fromByteString b
+        r <- n
+        step r bs
+      Consume consumer -> consumer $ Chunk bs
+      Error -> error "Streaming send of response body failed in brotli compression phase"
+      Done -> error "Should not be done compressing while still sending data. Did you send an empty bytestring?"
+
+performFlush :: (Builder -> IO ()) -> IO () -> BrotliStream Chunk -> IO (BrotliStream Chunk)
+performFlush innerSend innerFlush c = case c of
+  Consume consumer -> consumer Flush >>= performFlush'
+  _ -> error "Shouldn't be flushing value when not in consumption state"
+  where
+    performFlush' c = case c of
+      Produce b n -> do
+        innerSend $ fromByteString b
+        r <- n
+        performFlush' r
+      _ -> innerFlush >> pure c
+
+compressFlush :: (Builder -> IO ()) -> IO () -> IORef (BrotliStream Chunk) -> IO ()
+compressFlush innerSend innerFlush ref = do
+  action <- readIORef ref
+  r <- performFlush innerSend innerFlush action
+  writeIORef ref r
+
+compressedFileResponse ::
+     BrotliSettings
+  -> (ResponseHeaders -> ResponseHeaders)
+  -> Request
+  -> Status
+  -> ResponseHeaders
+  -> FilePath
+  -> Maybe FilePart
+  -> (Response -> IO a)
+  -> IO a
+compressedFileResponse settings addBrotliHeaders req status hs p mfp sendResponse =
+  case mfp of
+    Nothing ->
+      case brotliFilesBehavior settings of
+        BrotliIgnore -> sendResponse $ responseFile status hs p Nothing
+        BrotliCompress -> do
+          size <- fileSize <$> getFileStatus p
+          if fromIntegral size >= brotliMinimumSize settings
+            then do
+              let (status', hs', f) =
+                    responseToStream $ responseFile status hs p Nothing
+              f $ \body ->
+                wrapResponse
+                  settings
+                  req
+                  (responseStream status' hs' body)
+                  sendResponse
+            else sendResponse $ responseFile status hs p Nothing
+        BrotliCacheFolder cachePath
+      -- TODO check file size here maybe
+         -> do
+          createDirectoryIfMissing True cachePath
+          let normalized = normalizeOriginalFile p <.> "br"
+              adjustedFile = (cachePath </> normalizeOriginalFile p <.> "br")
+          exists <- doesFileExist adjustedFile
+          when (not exists) $ do
+            (tmpPath, h) <- openBinaryTempFile cachePath ("XXXXX" ++ normalized)
+            (flip onException) (removeFile tmpPath) $ do
+              original <- L.readFile p
+              L.hPut h $ compressWith (brotliCompressionSettings settings req (responseFile status hs p mfp)) original 
+              hClose h
+              rename tmpPath adjustedFile
+          sendResponse $
+            responseFile status (addBrotliHeaders hs) adjustedFile Nothing
+        BrotliPreCompressed fallback -> do
+          let modifiedPath = p <.> "br"
+          exists <- doesFileExist modifiedPath
+          if exists
+            then sendResponse $
+                 responseFile status (addBrotliHeaders hs) modifiedPath Nothing
+            else compressedFileResponse
+                   (settings {brotliFilesBehavior = fallback})
+                   addBrotliHeaders
+                   req
+                   status
+                   hs
+                   p
+                   mfp
+                   sendResponse
+    _ -> do
+      let (status', hs', f) = responseToStream $ responseFile status hs p mfp
+      f $ \body -> do
+        wrapResponse settings req (responseStream status' hs' body) sendResponse
+
+normalizeOriginalFile :: FilePath -> FilePath
+normalizeOriginalFile file = map safe file
+  where
+    safe c
+        | 'A' <= c && c <= 'Z' = c
+        | 'a' <= c && c <= 'z' = c
+        | '0' <= c && c <= '9' = c
+    safe '-' = '-'
+    safe '_' = '_'
+    safe '.' = '.'
+    safe _ = '_'
+
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,207 @@
+{-# LANGUAGE OverloadedStrings #-}
+import Network.HTTP.Types
+import Codec.Compression.Brotli (decompress, compress)
+import Control.Monad.Trans
+import Control.Concurrent.MVar
+import qualified Data.ByteString.Char8 as B
+import qualified Data.ByteString.Lazy as L
+import qualified Data.ByteString.Builder as Builder
+import Data.IORef
+import Data.Monoid
+import Network.Wai
+import Network.Wai.Middleware.Brotli
+import Network.Wai.Internal
+import Network.Wai.Test
+import Test.Tasty
+import Test.Tasty.HUnit
+import Test.Tasty.Hspec
+import System.IO
+
+main :: IO ()
+main = do
+  putStrLn ""
+
+  bod <- L.readFile "/usr/share/dict/words" -- "Hello World 000000000000000000000000000000"
+  let app req respond = respond $ responseLBS status200 [("Content-Type", "text/whatever")] bod
+
+  specs <- testSpecs $ parallel $ do
+
+    it "Encodes text" $ do
+      -- app :: Application
+      putStr "Encoding text: "
+      var <- newEmptyMVar
+      (brotli defaultSettings app)
+        (defaultRequest {requestHeaders = [("Accept-Encoding", "br")]})
+        (\resp -> do
+          putMVar var resp
+          return ResponseReceived)
+      resp <- takeMVar var
+      case resp of
+        ResponseBuilder stats hs b -> do
+          assert (decompress (Builder.toLazyByteString b) == bod)
+        _ -> error "Not what we expected"
+
+    it "Does not encode if brotli not in Accept-Encoding header" $ do
+      var <- newEmptyMVar
+      (brotli defaultSettings app)
+        (defaultRequest {requestHeaders = [("Accept-Encoding", "gzip"), ("Content-Type", "text/whatever")]})
+        (\resp -> do
+          putMVar var resp
+          return ResponseReceived)
+      resp <- takeMVar var
+      case resp of
+        ResponseBuilder stats hs b -> do
+          assert (Builder.toLazyByteString b == bod)
+        _ -> error "Not what we expected"
+
+    it "Decode request body if it's encoded" $ do
+      var <- newEmptyMVar
+      let reqWithBody =
+            defaultRequest
+            { requestMethod = "POST"
+            , requestHeaders = [("Content-Encoding", "br")]
+            , requestBody =
+                return $ L.toStrict $ compress ("Hello World" :: L.ByteString)
+            }
+      (brotli defaultSettings $ \req resp -> do
+        reqBod <- requestBody req
+        resp $ responseLBS status200 [] (L.fromStrict reqBod))
+        reqWithBody
+        (\resp -> do
+          putMVar var resp
+          return ResponseReceived)
+      resp <- takeMVar var
+      case resp of
+        ResponseBuilder stats hs b ->
+          assert (Builder.toLazyByteString b == "Hello World")
+        _ -> error "Not what we expected"
+
+    it "Streaming response body encoding" $ do
+      var <- newEmptyMVar
+      (brotli defaultSettings $ \req respond -> do
+          respond $ responseStream status200 [("Content-Type", "text/whatever")] $ \send flush -> do
+            send "Hello world\n"
+            flush
+            send "Goodbye world"
+        )
+        (defaultRequest {requestHeaders = [("Accept-Encoding", "br")]})
+        (\resp -> do
+          putMVar var resp
+          return ResponseReceived)
+      resp <- takeMVar var
+      r <- newIORef mempty
+      case resp of
+        ResponseStream stats hs f -> do
+          f (\b -> modifyIORef r (<> b)) (return ())
+          b <- readIORef r
+          assert (decompress (Builder.toLazyByteString b) == "Hello world\nGoodbye world")
+        _ -> error "Not what we expected"
+
+    it "Don't encode non-textual content" $ do
+      let app req respond = respond $ responseLBS status200 [("Content-Type", "application/binary")] bod
+      var <- newEmptyMVar
+      (brotli defaultSettings app)
+        (defaultRequest {requestHeaders = [("Accept-Encoding", "br")]})
+        (\resp -> do
+          putMVar var resp
+          return ResponseReceived)
+      resp <- takeMVar var
+      case resp of
+        ResponseBuilder stats hs b -> do
+          assert (Builder.toLazyByteString b == bod)
+        _ -> error "Not what we expected"
+
+    it "Don't encode if content length is too low" $ do
+      let app req respond = respond $ responseLBS status200 [("Content-Type", "application/json"), ("Content-Length", "2")] "{}"
+      var <- newEmptyMVar
+      (brotli defaultSettings app)
+        (defaultRequest {requestHeaders = [("Accept-Encoding", "br")]})
+        (\resp -> do
+          putMVar var resp
+          return ResponseReceived)
+      resp <- takeMVar var
+      case resp of
+        ResponseBuilder stats hs b -> do
+          assert (Builder.toLazyByteString b == "{}")
+        _ -> error "Not what we expected"
+
+    it "Do encode if content length is long enough" $ do
+      let app req respond = respond $ responseLBS status200 [("Content-Type", "text/whatever"), ("Content-Length", B.pack $ show $ L.length bod)] bod
+      var <- newEmptyMVar
+      (brotli defaultSettings app)
+        (defaultRequest {requestHeaders = [("Accept-Encoding", "br")]})
+        (\resp -> do
+          putMVar var resp
+          return ResponseReceived)
+      resp <- takeMVar var
+      case resp of
+        ResponseBuilder stats hs b -> do
+          assert (decompress (Builder.toLazyByteString b) == bod)
+        _ -> error "Not what we expected"
+
+    it "Performs on-the-fly compression of files if set to compress or requesting appropriate content range size" onTheFlyFileCompressTest
+    it "Serves precomputed compressed files if appropriate & they exist" precompressedTest
+
+  defaultMain $ testGroup "Tests" specs
+
+
+  {-
+
+  putStrLn ""
+
+  putStrLn ""
+
+  putStr ": "
+  putStrLn ""
+
+  putStrLn ""
+
+  putStrLn "Compress fallback response for responseRaw if appropriate: Pending"
+  putStrLn "Don't compress files if set to ignore: Pending"
+  onTheFlyFileCompressTest
+  precompressedTest
+  putStrLn "Create cached file versions in given folder if appropriate: Pending"
+
+  putStrLn "Support flushing compressed data: Pending"
+  putStrLn "Return appropriate error statuses for malformed compressed input: Pending"
+-}
+
+onTheFlyFileCompressTest :: IO ()
+onTheFlyFileCompressTest = do
+  let settings = defaultSettings { brotliFilesBehavior = BrotliCompress, brotliMinimumSize = 5 }
+  let app req respond =
+        respond $
+        responseFile
+          status200
+          [ ("Content-Type", "text/whatever")
+          ]
+          "test/sample.txt"
+          Nothing
+  runSession session $ brotli settings app
+  where
+    session = do
+     resp <- request (defaultRequest {requestHeaders = [("Accept-Encoding", "br")]})
+     liftIO $ assert (decompress (simpleBody resp) @=? "Hello world\n")
+
+precompressedTest :: IO ()
+precompressedTest = do
+  let settings = defaultSettings { brotliFilesBehavior = BrotliPreCompressed BrotliIgnore }
+  let app req respond =
+        respond $
+        responseFile
+          status200
+          [ ("Content-Type", "text/whatever")
+          ]
+          "test/words"
+          Nothing
+  var <- newEmptyMVar
+  (brotli settings app)
+    (defaultRequest {requestHeaders = [("Accept-Encoding", "br")]})
+    (\resp -> do
+       putMVar var resp
+       return ResponseReceived)
+  resp <- takeMVar var
+  case resp of
+    ResponseFile stats hs fp Nothing -> do
+      assert (fp == "test/words.br")
+    _ -> error "Not what we expected"
diff --git a/wai-middleware-brotli.cabal b/wai-middleware-brotli.cabal
new file mode 100644
--- /dev/null
+++ b/wai-middleware-brotli.cabal
@@ -0,0 +1,62 @@
+name:                wai-middleware-brotli
+version:             0.1.0.0
+synopsis:            WAI middleware for brotli compression
+-- description:
+homepage:            https://github.com/iand675/wai-middleware-brotli#readme
+license:             BSD3
+license-file:        LICENSE
+author:              Ian Duncan
+maintainer:          ian@iankduncan.com
+copyright:           Ian Duncan
+category:            Web
+build-type:          Simple
+extra-source-files:  README.md
+cabal-version:       >=1.10
+
+library
+  hs-source-dirs:      src
+  exposed-modules:     Network.Wai.Middleware.Brotli
+  build-depends:       base >= 4.7 && < 5
+                     , wai
+                     , http-types
+                     , hs-brotli
+                     , bytestring
+                     , directory
+                     , filepath
+                     , binary
+                     , unix
+  default-language:    Haskell2010
+
+executable wai-middleware-brotli-server
+  hs-source-dirs:      app
+  main-is:             Main.hs
+  build-depends:       base
+                     , http-types
+                     , warp
+                     , wai-extra
+                     , wai-middleware-brotli
+                     , wai
+                     , wai-app-static
+  default-language:    Haskell2010
+
+test-suite wai-middleware-brotli-test
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Spec.hs
+  build-depends:       base
+                     , hs-brotli
+                     , bytestring
+                     , http-types
+                     , mtl
+                     , tasty
+                     , tasty-hspec
+                     , tasty-hunit
+                     , wai
+                     , wai-extra
+                     , wai-middleware-brotli
+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
+  default-language:    Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/iand675/hs-brotli
