diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,12 +1,14 @@
 # Changelog for warp-grpc
 
-## Unreleased changes
+## 0.4.0.0
 
-## 0.3.0.0
+Support for handlers in transformers stacks, and in monads which support [`unliftio`](http://hackage.haskell.org/package/unliftio).
 
+## 0.3.0.0
+
 Support for warp-3.3.x .
 
-## 0.2.0.0
+## 0.2.0.0
 
 Support for different Protocol Buffers serialization libraries.
 
diff --git a/src/Network/GRPC/Server/Handlers.hs b/src/Network/GRPC/Server/Handlers.hs
--- a/src/Network/GRPC/Server/Handlers.hs
+++ b/src/Network/GRPC/Server/Handlers.hs
@@ -1,299 +1,51 @@
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TypeFamilies #-}
-{-# LANGUAGE CPP #-}
-module Network.GRPC.Server.Handlers where
+module Network.GRPC.Server.Handlers (
+  H.UnaryHandler
+, unary
+, H.ServerStreamHandler, H.ServerStream(..)
+, serverStream
+, H.ClientStreamHandler, H.ClientStream(..)
+, clientStream
+, H.BiDiStreamHandler, H.BiDiStep(..), H.BiDiStream(..)
+, bidiStream
+, H.GeneralStreamHandler, H.IncomingStream(..), H.OutgoingStream(..)
+, generalStream
+) where
 
-import           Control.Concurrent.Async (concurrently)
-import           Control.Monad (void, (>=>))
-import           Data.Binary.Get (pushChunk, Decoder(..))
-import qualified Data.ByteString.Char8 as ByteString
-import           Data.ByteString.Char8 (ByteString)
-import           Data.ByteString.Lazy (toStrict)
 import           Network.GRPC.HTTP2.Encoding
-import           Network.GRPC.HTTP2.Types (path, GRPCStatus(..), GRPCStatusCode(..))
-#if MIN_VERSION_wai(3,2,2)
-import           Network.Wai (Request, getRequestBodyChunk, strictRequestBody)
-#else
-import           Network.Wai (Request, requestBody, strictRequestBody)
-#endif
-
-#if MIN_VERSION_base(4,11,0)
-#else
-import Data.Monoid ((<>))
-#endif
-
-import Network.GRPC.Server.Wai (WaiHandler, ServiceHandler(..), closeEarly)
-
-#if !MIN_VERSION_wai(3,2,2)
-getRequestBodyChunk :: Request -> IO ByteString
-getRequestBodyChunk = requestBody
-#endif
-
--- | Handy type to refer to Handler for 'unary' RPCs handler.
-type UnaryHandler i o = Request -> i -> IO o
-
--- | Handy type for 'server-streaming' RPCs.
---
--- We expect an implementation to:
--- - read the input request
--- - return an initial state and an state-passing action that the server code will call to fetch the output to send to the client (or close an a Nothing)
--- See 'ServerStream' for the type which embodies these requirements.
-type ServerStreamHandler i o a = Request -> i -> IO (a, ServerStream o a)
-
-newtype ServerStream o a = ServerStream {
-    serverStreamNext :: a -> IO (Maybe (a, o))
-  }
-
--- | Handy type for 'client-streaming' RPCs.
---
--- We expect an implementation to:
--- - acknowledge a the new client stream by returning an initial state and two functions:
--- - a state-passing handler for new client message
--- - a state-aware handler for answering the client when it is ending its stream
--- See 'ClientStream' for the type which embodies these requirements.
-type ClientStreamHandler i o a = Request -> IO (a, ClientStream i o a)
-
-data ClientStream i o a = ClientStream {
-    clientStreamHandler   :: a -> i -> IO a
-  , clientStreamFinalizer :: a -> IO o
-  }
-
--- | Handy type for 'bidirectional-streaming' RPCs.
---
--- We expect an implementation to:
--- - acknowlege a new bidirection stream by returning an initial state and one functions:
--- - a state-passing function that returns a single action step
--- The action may be to
--- - stop immediately
--- - wait and handle some input with a callback and a finalizer (if the client closes the stream on its side) that may change the state
--- - return a value and a new state
---
--- There is no way to stop locally (that would mean sending HTTP2 trailers) and
--- keep receiving messages from the client.
-type BiDiStreamHandler i o a = Request -> IO (a, BiDiStream i o a)
-
-data BiDiStep i o a
-  = Abort
-  | WaitInput !(a -> i -> IO a) !(a -> IO a)
-  | WriteOutput !a o
-
-newtype BiDiStream i o a = BiDiStream {
-    bidirNextStep :: a -> IO (BiDiStep i o a)
-  }
+import qualified Network.GRPC.Server.Handlers.Trans as H
+import           Network.GRPC.Server.Wai (ServiceHandler)
 
--- | Construct a handler for handling a unary RPC.
 unary
   :: (GRPCInput r i, GRPCOutput r o)
   => r
-  -> UnaryHandler i o
+  -> H.UnaryHandler IO i o
   -> ServiceHandler
-unary rpc handler =
-    ServiceHandler (path rpc) (handleUnary rpc handler)
+unary = H.unary id
 
--- | Construct a handler for handling a server-streaming RPC.
 serverStream
   :: (GRPCInput r i, GRPCOutput r o)
   => r
-  -> ServerStreamHandler i o a
+  -> H.ServerStreamHandler IO i o a
   -> ServiceHandler
-serverStream rpc handler =
-    ServiceHandler (path rpc) (handleServerStream rpc handler)
+serverStream = H.serverStream id
 
--- | Construct a handler for handling a client-streaming RPC.
 clientStream
   :: (GRPCInput r i, GRPCOutput r o)
   => r
-  -> ClientStreamHandler i o a
+  -> H.ClientStreamHandler IO i o a
   -> ServiceHandler
-clientStream rpc handler =
-    ServiceHandler (path rpc) (handleClientStream rpc handler)
+clientStream = H.clientStream id
 
--- | Construct a handler for handling a bidirectional-streaming RPC.
 bidiStream
   :: (GRPCInput r i, GRPCOutput r o)
   => r
-  -> BiDiStreamHandler i o a
+  -> H.BiDiStreamHandler IO i o a
   -> ServiceHandler
-bidiStream rpc handler =
-    ServiceHandler (path rpc) (handleBiDiStream rpc handler)
+bidiStream = H.bidiStream id
 
--- | Construct a handler for handling a bidirectional-streaming RPC.
 generalStream
   :: (GRPCInput r i, GRPCOutput r o)
   => r
-  -> GeneralStreamHandler i o a b
+  -> H.GeneralStreamHandler IO i o a b
   -> ServiceHandler
-generalStream rpc handler =
-    ServiceHandler (path rpc) (handleGeneralStream rpc handler)
-
--- | Handle unary RPCs.
-handleUnary
-  :: (GRPCInput r i, GRPCOutput r o)
-  => r
-  -> UnaryHandler i o
-  -> WaiHandler
-handleUnary rpc handler decoding encoding req write flush =
-    handleRequestChunksLoop (decodeInput rpc $ _getDecodingCompression decoding)
-                            handleMsg handleEof nextChunk
-  where
-    nextChunk = toStrict <$> strictRequestBody req
-    handleMsg = errorOnLeftOver (handler req >=> reply)
-    handleEof = closeEarly (GRPCStatus INVALID_ARGUMENT "early end of request body")
-    reply msg = write (encodeOutput rpc (_getEncodingCompression encoding) msg) >> flush
-
--- | Handle Server-Streaming RPCs.
-handleServerStream
-  :: (GRPCInput r i, GRPCOutput r o)
-  => r
-  -> ServerStreamHandler i o a
-  -> WaiHandler
-handleServerStream rpc handler decoding encoding req write flush =
-    handleRequestChunksLoop (decodeInput rpc $ _getDecodingCompression decoding)
-                            handleMsg handleEof nextChunk
-  where
-    nextChunk = toStrict <$> strictRequestBody req
-    handleMsg = errorOnLeftOver (handler req >=> replyN)
-    handleEof = closeEarly (GRPCStatus INVALID_ARGUMENT "early end of request body")
-    replyN (v, sStream) = do
-        let go v1 = serverStreamNext sStream v1 >>= \case
-                Just (v2, msg) -> do
-                    write (encodeOutput rpc (_getEncodingCompression encoding) msg) >> flush
-                    go v2
-                Nothing -> pure ()
-        go v
-
--- | Handle Client-Streaming RPCs.
-handleClientStream
-  :: (GRPCInput r i, GRPCOutput r o)
-  => r
-  -> ClientStreamHandler i o a
-  -> WaiHandler
-handleClientStream rpc handler0 decoding encoding req write flush =
-    handler0 req >>= go
-  where
-    go (v, cStream) = handleRequestChunksLoop (decodeInput rpc $ _getDecodingCompression decoding)
-                                              (handleMsg v) (handleEof v) nextChunk
-      where
-        nextChunk = getRequestBodyChunk req
-        handleMsg v0 dat msg = clientStreamHandler cStream v0 msg >>= \v1 -> loop dat v1
-        handleEof v0 = clientStreamFinalizer cStream v0 >>= reply
-        reply msg = write (encodeOutput rpc (_getEncodingCompression encoding) msg) >> flush
-        loop chunk v1 = handleRequestChunksLoop
-                          (flip pushChunk chunk $ decodeInput rpc (_getDecodingCompression decoding))
-                          (handleMsg v1) (handleEof v1) nextChunk
-
--- | Handle Bidirectional-Streaming RPCs.
-handleBiDiStream
-  :: (GRPCInput r i, GRPCOutput r o)
-  => r
-  -> BiDiStreamHandler i o a
-  -> WaiHandler
-handleBiDiStream rpc handler0 decoding encoding req write flush =
-    handler0 req >>= go ""
-  where
-    nextChunk = getRequestBodyChunk req
-    reply msg = write (encodeOutput rpc (_getEncodingCompression encoding) msg) >> flush
-    go chunk (v0, bStream) = do
-        let cont dat v1 = go dat (v1, bStream)
-        step <- bidirNextStep bStream v0
-        case step of
-            WaitInput handleMsg handleEof ->
-                handleRequestChunksLoop (flip pushChunk chunk
-                                         $ decodeInput rpc
-                                         $ _getDecodingCompression decoding)
-                                        (\dat msg -> handleMsg v0 msg >>= cont dat)
-                                        (handleEof v0 >>= cont "")
-                                        nextChunk
-            WriteOutput v1 msg -> do
-                reply msg
-                cont "" v1
-            Abort -> return ()
-
--- | A GeneralStreamHandler combining server and client asynchronous streams.
-type GeneralStreamHandler i o a b =
-    Request -> IO (a, IncomingStream i a, b, OutgoingStream o b)
-
--- | Pair of handlers for reacting to incoming messages.
-data IncomingStream i a = IncomingStream {
-    incomingStreamHandler   :: a -> i -> IO a
-  , incomingStreamFinalizer :: a -> IO ()
-  }
-
--- | Handler to decide on the next message (if any) to return.
-newtype OutgoingStream o a = OutgoingStream {
-    outgoingStreamNext  :: a -> IO (Maybe (a, o))
-  }
-
--- | Handler for the somewhat general case where two threads behave concurrently:
--- - one reads messages from the client
--- - one returns messages to the client
-handleGeneralStream
-  :: (GRPCInput r i, GRPCOutput r o)
-  => r
-  -> GeneralStreamHandler i o a b
-  -> WaiHandler
-handleGeneralStream rpc handler0 decoding encoding req write flush = void $
-    handler0 req >>= go
-  where
-    newDecoder = decodeInput rpc $ _getDecodingCompression decoding
-    nextChunk = getRequestBodyChunk req
-    reply msg = write (encodeOutput rpc (_getEncodingCompression encoding) msg) >> flush
-
-    go (in0, instream, out0, outstream) = concurrently
-        (incomingLoop newDecoder in0 instream)
-        (replyLoop out0 outstream)
-
-    replyLoop v0 sstream@(OutgoingStream next) =
-        next v0 >>= \case
-            Nothing          -> return v0
-            (Just (v1, msg)) -> reply msg >> replyLoop v1 sstream
-
-    incomingLoop decode v0 cstream = do
-        let handleMsg dat msg = do
-                v1 <- incomingStreamHandler cstream v0 msg
-                incomingLoop (pushChunk newDecoder dat) v1 cstream
-        let handleEof = incomingStreamFinalizer cstream v0 >> pure v0
-        handleRequestChunksLoop decode handleMsg handleEof nextChunk
-
-
--- | Helpers to consume input in chunks.
-handleRequestChunksLoop
-  :: Decoder (Either String a)
-  -- ^ Message decoder.
-  -> (ByteString -> a -> IO b)
-  -- ^ Handler for a single message.
-  -- The ByteString corresponds to leftover data.
-  -> IO b
-  -- ^ Handler for handling end-of-streams.
-  -> IO ByteString
-  -- ^ Action to retrieve the next chunk.
-  -> IO b
-{-# INLINEABLE handleRequestChunksLoop #-}
-handleRequestChunksLoop decoder handleMsg handleEof nextChunk =
-    case decoder of
-        (Done unusedDat _ (Right val)) ->
-            handleMsg unusedDat val
-        (Done _ _ (Left err)) ->
-            closeEarly (GRPCStatus INVALID_ARGUMENT (ByteString.pack $ "done-error: " ++ err))
-        (Fail _ _ err)         ->
-            closeEarly (GRPCStatus INVALID_ARGUMENT (ByteString.pack $ "fail-error: " ++ err))
-        partial@(Partial _)    -> do
-            chunk <- nextChunk
-            if ByteString.null chunk
-            then
-                handleEof
-            else
-                handleRequestChunksLoop (pushChunk partial chunk) handleMsg handleEof nextChunk
-
--- | Combinator around message handler to error on left overs.
---
--- This combinator ensures that, unless for client stream, an unparsed piece of
--- data with a correctly-read message is treated as an error.
-errorOnLeftOver :: (a -> IO b) -> ByteString -> a -> IO b
-errorOnLeftOver f rest
-  | ByteString.null rest = f
-  | otherwise            = const $ putStrLn "left-over" >> closeEarly (GRPCStatus INVALID_ARGUMENT ("left-overs: " <> rest))
+generalStream = H.generalStream id
diff --git a/src/Network/GRPC/Server/Handlers/Trans.hs b/src/Network/GRPC/Server/Handlers/Trans.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/GRPC/Server/Handlers/Trans.hs
@@ -0,0 +1,324 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE CPP #-}
+module Network.GRPC.Server.Handlers.Trans where
+
+import           Control.Concurrent.Async (concurrently)
+import           Control.Monad (void, (>=>))
+import           Control.Monad.IO.Class
+import           Data.Binary.Get (pushChunk, Decoder(..))
+import qualified Data.ByteString.Char8 as ByteString
+import           Data.ByteString.Char8 (ByteString)
+import           Data.ByteString.Lazy (toStrict)
+import           Network.GRPC.HTTP2.Encoding
+import           Network.GRPC.HTTP2.Types (path, GRPCStatus(..), GRPCStatusCode(..))
+#if MIN_VERSION_wai(3,2,2)
+import           Network.Wai (Request, getRequestBodyChunk, strictRequestBody)
+#else
+import           Network.Wai (Request, requestBody, strictRequestBody)
+#endif
+
+#if MIN_VERSION_base(4,11,0)
+#else
+import Data.Monoid ((<>))
+#endif
+
+import Network.GRPC.Server.Wai (WaiHandler, ServiceHandler(..), closeEarly)
+
+#if !MIN_VERSION_wai(3,2,2)
+getRequestBodyChunk :: Request -> IO ByteString
+getRequestBodyChunk = requestBody
+#endif
+
+-- | Handy type to refer to Handler for 'unary' RPCs handler.
+type UnaryHandler m i o = Request -> i -> m o
+
+-- | Handy type for 'server-streaming' RPCs.
+--
+-- We expect an implementation to:
+-- - read the input request
+-- - return an initial state and an state-passing action that the server code will call to fetch the output to send to the client (or close an a Nothing)
+-- See 'ServerStream' for the type which embodies these requirements.
+type ServerStreamHandler m i o a = Request -> i -> m (a, ServerStream m o a)
+
+newtype ServerStream m o a = ServerStream {
+    serverStreamNext :: a -> m (Maybe (a, o))
+  }
+
+-- | Handy type for 'client-streaming' RPCs.
+--
+-- We expect an implementation to:
+-- - acknowledge a the new client stream by returning an initial state and two functions:
+-- - a state-passing handler for new client message
+-- - a state-aware handler for answering the client when it is ending its stream
+-- See 'ClientStream' for the type which embodies these requirements.
+type ClientStreamHandler m i o a = Request -> m (a, ClientStream m i o a)
+
+data ClientStream m i o a = ClientStream {
+    clientStreamHandler   :: a -> i -> m a
+  , clientStreamFinalizer :: a -> m o
+  }
+
+-- | Handy type for 'bidirectional-streaming' RPCs.
+--
+-- We expect an implementation to:
+-- - acknowlege a new bidirection stream by returning an initial state and one functions:
+-- - a state-passing function that returns a single action step
+-- The action may be to
+-- - stop immediately
+-- - wait and handle some input with a callback and a finalizer (if the client closes the stream on its side) that may change the state
+-- - return a value and a new state
+--
+-- There is no way to stop locally (that would mean sending HTTP2 trailers) and
+-- keep receiving messages from the client.
+type BiDiStreamHandler m i o a = Request -> m (a, BiDiStream m i o a)
+
+data BiDiStep m i o a
+  = Abort
+  | WaitInput !(a -> i -> m a) !(a -> m a)
+  | WriteOutput !a o
+
+newtype BiDiStream m i o a = BiDiStream {
+    bidirNextStep :: a -> m (BiDiStep m i o a)
+  }
+
+-- | Construct a handler for handling a unary RPC.
+unary
+  :: (MonadIO m, GRPCInput r i, GRPCOutput r o)
+  => (forall x. m x -> IO x)
+  -> r
+  -> UnaryHandler m i o
+  -> ServiceHandler
+unary f rpc handler =
+    ServiceHandler (path rpc) (handleUnary f rpc handler)
+
+-- | Construct a handler for handling a server-streaming RPC.
+serverStream
+  :: (MonadIO m, GRPCInput r i, GRPCOutput r o)
+  => (forall x. m x -> IO x)
+  -> r
+  -> ServerStreamHandler m i o a
+  -> ServiceHandler
+serverStream f rpc handler =
+    ServiceHandler (path rpc) (handleServerStream f rpc handler)
+
+-- | Construct a handler for handling a client-streaming RPC.
+clientStream
+  :: (MonadIO m, GRPCInput r i, GRPCOutput r o)
+  => (forall x. m x -> IO x)
+  -> r
+  -> ClientStreamHandler m i o a
+  -> ServiceHandler
+clientStream f rpc handler =
+    ServiceHandler (path rpc) (handleClientStream f rpc handler)
+
+-- | Construct a handler for handling a bidirectional-streaming RPC.
+bidiStream
+  :: (MonadIO m, GRPCInput r i, GRPCOutput r o)
+  => (forall x. m x -> IO x)
+  -> r
+  -> BiDiStreamHandler m i o a
+  -> ServiceHandler
+bidiStream f rpc handler =
+    ServiceHandler (path rpc) (handleBiDiStream f rpc handler)
+
+-- | Construct a handler for handling a bidirectional-streaming RPC.
+generalStream
+  :: (MonadIO m, GRPCInput r i, GRPCOutput r o)
+  => (forall x. m x -> IO x)
+  -> r
+  -> GeneralStreamHandler m i o a b
+  -> ServiceHandler
+generalStream f rpc handler =
+    ServiceHandler (path rpc) (handleGeneralStream f rpc handler)
+
+-- | Handle unary RPCs.
+handleUnary
+  :: (MonadIO m, GRPCInput r i, GRPCOutput r o)
+  => (forall x. m x -> IO x)
+  -> r
+  -> UnaryHandler m i o
+  -> WaiHandler
+handleUnary f rpc handler decoding encoding req write flush = f $
+    handleRequestChunksLoop (decodeInput rpc $ _getDecodingCompression decoding)
+                            handleMsg handleEof nextChunk
+  where
+    nextChunk = toStrict <$> strictRequestBody req
+    handleMsg = errorOnLeftOver (handler req >=> liftIO . reply)
+    handleEof = closeEarly (GRPCStatus INVALID_ARGUMENT "early end of request body")
+    reply msg = write (encodeOutput rpc (_getEncodingCompression encoding) msg) >> flush
+
+-- | Handle Server-Streaming RPCs.
+handleServerStream
+  :: (MonadIO m, GRPCInput r i, GRPCOutput r o)
+  => (forall x. m x -> IO x)
+  -> r
+  -> ServerStreamHandler m i o a
+  -> WaiHandler
+handleServerStream f rpc handler decoding encoding req write flush = f $
+    handleRequestChunksLoop (decodeInput rpc $ _getDecodingCompression decoding)
+                            handleMsg handleEof nextChunk
+  where
+    nextChunk = toStrict <$> strictRequestBody req
+    handleMsg = errorOnLeftOver (handler req >=> replyN)
+    handleEof = closeEarly (GRPCStatus INVALID_ARGUMENT "early end of request body")
+    replyN (v, sStream) = do
+        let go v1 = serverStreamNext sStream v1 >>= \case
+                Just (v2, msg) -> do
+                    liftIO $ write (encodeOutput rpc (_getEncodingCompression encoding) msg)
+                    liftIO flush
+                    go v2
+                Nothing -> pure ()
+        go v
+
+-- | Handle Client-Streaming RPCs.
+handleClientStream
+  :: forall m r i o a.
+     (MonadIO m, GRPCInput r i, GRPCOutput r o)
+  => (forall x. m x -> IO x)
+  -> r
+  -> ClientStreamHandler m i o a
+  -> WaiHandler
+handleClientStream f rpc handler0 decoding encoding req write flush =
+    f $ handler0 req >>= go
+  where
+    go :: (a, ClientStream m i o a) -> m ()
+    go (v, cStream) = handleRequestChunksLoop (decodeInput rpc $ _getDecodingCompression decoding)
+                                              (handleMsg v) (handleEof v) nextChunk
+      where
+        nextChunk = getRequestBodyChunk req
+        handleMsg v0 dat msg = clientStreamHandler cStream v0 msg >>= \v1 -> loop dat v1
+        handleEof v0 = clientStreamFinalizer cStream v0 >>= reply
+        reply msg = do
+          liftIO $ write (encodeOutput rpc (_getEncodingCompression encoding) msg)
+          liftIO flush
+        loop chunk v1 = handleRequestChunksLoop
+                          (flip pushChunk chunk $ decodeInput rpc (_getDecodingCompression decoding))
+                          (handleMsg v1) (handleEof v1) nextChunk
+
+-- | Handle Bidirectional-Streaming RPCs.
+handleBiDiStream
+  :: forall m r i o a.
+     (MonadIO m, GRPCInput r i, GRPCOutput r o)
+  => (forall x. m x -> IO x)
+  -> r
+  -> BiDiStreamHandler m i o a
+  -> WaiHandler
+handleBiDiStream f rpc handler0 decoding encoding req write flush =
+    f $ handler0 req >>= go ""
+  where
+    nextChunk = getRequestBodyChunk req
+    reply msg = write (encodeOutput rpc (_getEncodingCompression encoding) msg) >> flush
+    go :: ByteString -> (a, BiDiStream m i o a) -> m ()
+    go chunk (v0, bStream) = do
+        let cont dat v1 = go dat (v1, bStream)
+        step <- bidirNextStep bStream v0
+        case step of
+            WaitInput handleMsg handleEof ->
+                handleRequestChunksLoop (flip pushChunk chunk
+                                         $ decodeInput rpc
+                                         $ _getDecodingCompression decoding)
+                                        (\dat msg -> handleMsg v0 msg >>= cont dat)
+                                        (handleEof v0 >>= cont "")
+                                        nextChunk
+            WriteOutput v1 msg -> do
+                liftIO $ reply msg
+                cont "" v1
+            Abort -> return ()
+
+-- | A GeneralStreamHandler combining server and client asynchronous streams.
+type GeneralStreamHandler m i o a b =
+    Request -> m (a, IncomingStream m i a, b, OutgoingStream m o b)
+
+-- | Pair of handlers for reacting to incoming messages.
+data IncomingStream m i a = IncomingStream {
+    incomingStreamHandler   :: a -> i -> m a
+  , incomingStreamFinalizer :: a -> m ()
+  }
+
+-- | Handler to decide on the next message (if any) to return.
+newtype OutgoingStream m o a = OutgoingStream {
+    outgoingStreamNext  :: a -> m (Maybe (a, o))
+  }
+
+-- | Handler for the somewhat general case where two threads behave concurrently:
+-- - one reads messages from the client
+-- - one returns messages to the client
+handleGeneralStream
+  :: forall m r i o a b.
+     (MonadIO m, GRPCInput r i, GRPCOutput r o)
+  => (forall x. m x -> IO x)
+  -> r
+  -> GeneralStreamHandler m i o a b
+  -> WaiHandler
+handleGeneralStream f rpc handler0 decoding encoding req write flush = void $
+    f $ handler0 req >>= go
+  where
+    newDecoder = decodeInput rpc $ _getDecodingCompression decoding
+    nextChunk = getRequestBodyChunk req
+    reply msg = write (encodeOutput rpc (_getEncodingCompression encoding) msg) >> flush
+
+    go :: (a, IncomingStream m i a, b, OutgoingStream m o b) -> m (a, b)
+    go (in0, instream, out0, outstream) = liftIO $ concurrently
+        (f $ incomingLoop newDecoder in0 instream)
+        (f $ replyLoop out0 outstream)
+
+    replyLoop :: b -> OutgoingStream m o b -> m b
+    replyLoop v0 sstream@(OutgoingStream next) =
+        next v0 >>= \case
+            Nothing          -> return v0
+            (Just (v1, msg)) -> liftIO (reply msg) >> replyLoop v1 sstream
+
+    incomingLoop :: Decoder (Either String i) -> a -> IncomingStream m i a -> m a
+    incomingLoop decode v0 cstream = do
+        let handleMsg dat msg = do
+                v1 <- incomingStreamHandler cstream v0 msg
+                incomingLoop (pushChunk newDecoder dat) v1 cstream
+        let handleEof = incomingStreamFinalizer cstream v0 >> pure v0
+        handleRequestChunksLoop decode handleMsg handleEof nextChunk
+
+
+-- | Helpers to consume input in chunks.
+handleRequestChunksLoop
+  :: (MonadIO m)
+  => Decoder (Either String a)
+  -- ^ Message decoder.
+  -> (ByteString -> a -> m b)
+  -- ^ Handler for a single message.
+  -- The ByteString corresponds to leftover data.
+  -> m b
+  -- ^ Handler for handling end-of-streams.
+  -> IO ByteString
+  -- ^ Action to retrieve the next chunk.
+  -> m b
+{-# INLINEABLE handleRequestChunksLoop #-}
+handleRequestChunksLoop decoder handleMsg handleEof nextChunk =
+    case decoder of
+        (Done unusedDat _ (Right val)) ->
+            handleMsg unusedDat val
+        (Done _ _ (Left err)) ->
+            closeEarly (GRPCStatus INVALID_ARGUMENT (ByteString.pack $ "done-error: " ++ err))
+        (Fail _ _ err)         ->
+            closeEarly (GRPCStatus INVALID_ARGUMENT (ByteString.pack $ "fail-error: " ++ err))
+        partial@(Partial _)    -> do
+            chunk <- liftIO nextChunk
+            if ByteString.null chunk
+            then
+                handleEof
+            else
+                handleRequestChunksLoop (pushChunk partial chunk) handleMsg handleEof nextChunk
+
+-- | Combinator around message handler to error on left overs.
+--
+-- This combinator ensures that, unless for client stream, an unparsed piece of
+-- data with a correctly-read message is treated as an error.
+errorOnLeftOver :: MonadIO m => (a -> m b) -> ByteString -> a -> m b
+errorOnLeftOver f rest
+  | ByteString.null rest = f
+  | otherwise            = const $ do
+     liftIO (putStrLn "left-over")
+     closeEarly (GRPCStatus INVALID_ARGUMENT ("left-overs: " <> rest))
diff --git a/src/Network/GRPC/Server/Handlers/Unlift.hs b/src/Network/GRPC/Server/Handlers/Unlift.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/GRPC/Server/Handlers/Unlift.hs
@@ -0,0 +1,57 @@
+module Network.GRPC.Server.Handlers.Unlift (
+  H.UnaryHandler
+, unary
+, H.ServerStreamHandler, H.ServerStream(..)
+, serverStream
+, H.ClientStreamHandler, H.ClientStream(..)
+, clientStream
+, H.BiDiStreamHandler, H.BiDiStep(..), H.BiDiStream(..)
+, bidiStream
+, H.GeneralStreamHandler, H.IncomingStream(..), H.OutgoingStream(..)
+, generalStream
+) where
+
+import           Control.Monad.IO.Unlift
+import           Network.GRPC.HTTP2.Encoding
+import qualified Network.GRPC.Server.Handlers.Trans as H
+import           Network.GRPC.Server.Wai (ServiceHandler)
+
+unary
+  :: (MonadUnliftIO m, GRPCInput r i, GRPCOutput r o)
+  => r
+  -> H.UnaryHandler m i o
+  -> m ServiceHandler
+unary rpc handler
+  = withRunInIO (\f -> return $ H.unary f rpc handler)
+
+serverStream
+  :: (MonadUnliftIO m, GRPCInput r i, GRPCOutput r o)
+  => r
+  -> H.ServerStreamHandler m i o a
+  -> m ServiceHandler
+serverStream rpc handler
+  = withRunInIO (\f -> return $ H.serverStream f rpc handler)
+
+clientStream
+  :: (MonadUnliftIO m, GRPCInput r i, GRPCOutput r o)
+  => r
+  -> H.ClientStreamHandler m i o a
+  -> m ServiceHandler
+clientStream rpc handler
+  = withRunInIO (\f -> return $ H.clientStream f rpc handler)
+
+bidiStream
+  :: (MonadUnliftIO m, GRPCInput r i, GRPCOutput r o)
+  => r
+  -> H.BiDiStreamHandler m i o a
+  -> m ServiceHandler
+bidiStream rpc handler
+  = withRunInIO (\f -> return $ H.bidiStream f rpc handler)
+
+generalStream
+  :: (MonadUnliftIO m, GRPCInput r i, GRPCOutput r o)
+  => r
+  -> H.GeneralStreamHandler m i o a b
+  -> m ServiceHandler
+generalStream rpc handler
+  = withRunInIO (\f -> return $ H.generalStream f rpc handler)
diff --git a/src/Network/GRPC/Server/Wai.hs b/src/Network/GRPC/Server/Wai.hs
--- a/src/Network/GRPC/Server/Wai.hs
+++ b/src/Network/GRPC/Server/Wai.hs
@@ -5,6 +5,7 @@
 module Network.GRPC.Server.Wai where
 
 import           Control.Exception (Handler(..), catches, SomeException, throwIO)
+import           Control.Monad.IO.Class
 import           Data.ByteString.Char8 (ByteString)
 import qualified Data.ByteString.Char8 as ByteString
 import           Data.ByteString.Lazy (fromStrict)
@@ -64,8 +65,8 @@
         rep $ responseLBS status404 [] $ fromStrict ("not found: " <> rawPathInfo req)
 
 -- | Aborts a GRPC handler with a given GRPCStatus.
-closeEarly :: GRPCStatus -> IO a
-closeEarly = throwIO
+closeEarly :: MonadIO m => GRPCStatus -> m a
+closeEarly = liftIO . throwIO
 
 -- | Build a WAI 'Middleware' from a list of ServiceHandler.
 --
diff --git a/warp-grpc.cabal b/warp-grpc.cabal
--- a/warp-grpc.cabal
+++ b/warp-grpc.cabal
@@ -4,10 +4,10 @@
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 794fc7769141a0b3e1da1800fa7e22733da3750f11b1d80f2558372cf578b90e
+-- hash: e655a88509754d2322f32ede1f4ca7a39ab5ba25b54ef1d9f2e79ad6521ec110
 
 name:           warp-grpc
-version:        0.3.0.0
+version:        0.4.0.0
 synopsis:       A minimal gRPC server on top of Warp.
 description:    Please see the README on Github at <https://github.com/haskell-grpc-native/http2-grpc-haskell/blob/master/warp-grpc/README.md>
 category:       Networking
@@ -15,7 +15,7 @@
 bug-reports:    https://github.com/haskell-grpc-native/http2-grpc-haskell/issues
 author:         Lucas DiCioccio, Alejandro Serrano
 maintainer:     lucas@dicioccio.fr
-copyright:      2017 - 2019 Lucas DiCioccio, Alejandro Serrano
+copyright:      2017 - 2020 Lucas DiCioccio, Alejandro Serrano
 license:        BSD3
 license-file:   LICENSE
 build-type:     Simple
@@ -31,6 +31,8 @@
   exposed-modules:
       Network.GRPC.Server
       Network.GRPC.Server.Handlers
+      Network.GRPC.Server.Handlers.Trans
+      Network.GRPC.Server.Handlers.Unlift
       Network.GRPC.Server.Helpers
       Network.GRPC.Server.Wai
   other-modules:
@@ -47,6 +49,7 @@
     , http-types >=0.12 && <0.13
     , http2 >=1.6 && <3
     , http2-grpc-types >=0.5 && <0.6
+    , unliftio-core >=0.1 && <0.3
     , wai >=3.2 && <3.4
     , warp >=3.2.23 && <3.4
     , warp-tls >=3.2 && <3.4
