diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/example/BuildImpl.hs b/example/BuildImpl.hs
new file mode 100644
--- /dev/null
+++ b/example/BuildImpl.hs
@@ -0,0 +1,49 @@
+module BuildImpl (handlerImpls) where
+
+import Control.Monad.IO.Class (MonadIO)
+import EmptyHandlers (emptySStreamHandler, emptyUnaryHandler)
+import Network.GRPC.Server (ServerStreamHandler, UnaryHandler)
+import Proto.Utxorpc.V1alpha.Build.Build
+import Utxorpc.Server (BuildHandlers (..))
+
+handlerImpls ::
+  (MonadIO m) =>
+  (String -> m ()) ->
+  BuildHandlers m Int
+handlerImpls logF =
+  BuildHandlers
+    (chainTipHandler logF)
+    (chainParamHandler logF)
+    (utxoByAddressHandler logF)
+    (utxoByRefHandler logF)
+    (holdUtxoHandler logF)
+
+chainTipHandler ::
+  (MonadIO m) =>
+  (String -> m ()) ->
+  UnaryHandler m GetChainTipRequest GetChainTipResponse
+chainTipHandler = emptyUnaryHandler
+
+chainParamHandler ::
+  (MonadIO m) =>
+  (String -> m ()) ->
+  UnaryHandler m GetChainParamRequest GetChainParamResponse
+chainParamHandler = emptyUnaryHandler
+
+utxoByAddressHandler ::
+  (MonadIO m) =>
+  (String -> m ()) ->
+  UnaryHandler m GetUtxoByAddressRequest GetUtxoByAddressResponse
+utxoByAddressHandler = emptyUnaryHandler
+
+utxoByRefHandler ::
+  (MonadIO m) =>
+  (String -> m ()) ->
+  UnaryHandler m GetUtxoByRefRequest GetUtxoByRefResponse
+utxoByRefHandler = emptyUnaryHandler
+
+holdUtxoHandler ::
+  (MonadIO m) =>
+  (String -> m ()) ->
+  ServerStreamHandler m HoldUtxoRequest HoldUtxoResponse Int
+holdUtxoHandler = emptySStreamHandler
diff --git a/example/EmptyHandlers.hs b/example/EmptyHandlers.hs
new file mode 100644
--- /dev/null
+++ b/example/EmptyHandlers.hs
@@ -0,0 +1,29 @@
+module EmptyHandlers (emptyUnaryHandler, emptySStreamHandler) where
+
+import Data.ProtoLens (Message (defMessage))
+import Network.GRPC.Server (ServerStream (..))
+import Network.Wai (Request)
+
+emptyUnaryHandler :: (Monad m, Message o) => (String -> m ()) -> Request -> i -> m o
+emptyUnaryHandler logF _ _ = do
+  logF "A unary handler is processing a request and generating a reply..."
+  return defMessage
+
+emptySStreamHandler ::
+  (Monad m, Message o) =>
+  (String -> m ()) ->
+  Request ->
+  i ->
+  m (Int, ServerStream m o Int)
+emptySStreamHandler logF _ _ =
+  return
+    ( 0,
+      ServerStream $ \n ->
+        if n > 3
+          then do
+            logF "The stream handler is ending the stream"
+            return Nothing
+          else do
+            logF "The stream handler is generating a message"
+            return $ Just (n + 1, defMessage)
+    )
diff --git a/example/KatipLogger.hs b/example/KatipLogger.hs
new file mode 100644
--- /dev/null
+++ b/example/KatipLogger.hs
@@ -0,0 +1,91 @@
+{-# LANGUAGE InstanceSigs #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module KatipLogger (katipLogger, unliftKatip, mkLogEnv) where
+
+import Control.Monad.Trans.Reader (runReaderT)
+import Data.Aeson as A
+import qualified Data.ByteString.Char8 as BS
+import Data.UUID (UUID)
+import Katip
+import Katip.Monadic
+import UnliftIO (stdout)
+import Utxorpc.Server (ReplyLogger, RequestLogger, ServerStreamEndLogger, ServerStreamLogger, UtxorpcServiceLogger (..))
+
+katipLogger :: (KatipContext m) => UtxorpcServiceLogger m
+katipLogger =
+  UtxorpcServiceLogger
+    katipRequestLogger
+    katipReplyLogger
+    katipServerStreamLogger
+    katipServerStreamEndLogger
+
+unliftKatip :: LogEnv -> KatipContextT IO a -> IO a
+unliftKatip le ctxT = do
+  let rdr = unKatipContextT ctxT
+  let initState = KatipContextTState le mempty mempty
+  runReaderT rdr initState
+
+mkLogEnv :: IO LogEnv
+mkLogEnv = do
+  handleScribe <- mkHandleScribe ColorIfTerminal stdout (permitItem InfoS) V2
+  le <- initLogEnv "UtxorpcService" "production"
+  registerScribe "stdout" handleScribe defaultScribeSettings le
+
+katipRequestLogger :: (KatipContext m) => RequestLogger m
+katipRequestLogger rpcPath _req uuid i = do
+  addContext rpcPath uuid Nothing Request $
+    $(logTM) InfoS (showLS i)
+  return ()
+
+katipReplyLogger :: (KatipContext m) => ReplyLogger m
+katipReplyLogger path _req uuid reply = do
+  addContext path uuid Nothing Reply $
+    $(logTM) InfoS (showLS reply)
+
+katipServerStreamLogger :: (KatipContext m) => ServerStreamLogger m
+katipServerStreamLogger path _req (uuid, index) msg =
+  addContext path uuid (Just index) ServerStream $
+    $(logTM) InfoS (showLS msg)
+
+katipServerStreamEndLogger :: (KatipContext m) => ServerStreamEndLogger m
+katipServerStreamEndLogger path _req (uuid, index) =
+  addContext path uuid (Just index) ServerStreamEnd $
+    $(logTM) InfoS "End of stream"
+
+addContext :: (KatipContext m) => BS.ByteString -> UUID -> Maybe Int -> GrpcEvent -> m a -> m a
+addContext path uuid index event = katipAddContext $ MsgContext path uuid index event
+
+data MsgContext = MsgContext
+  { _path :: BS.ByteString,
+    _uuid :: UUID,
+    _index :: Maybe Int,
+    _event :: GrpcEvent
+  }
+
+data GrpcEvent = Request | Reply | ServerStream | ServerStreamEnd deriving (Show)
+
+instance ToJSON MsgContext where
+  toJSON :: MsgContext -> Value
+  toJSON MsgContext {_path, _uuid, _index, _event} =
+    A.object
+      ( "path"
+          A..= BS.unpack _path
+          : "UUID"
+          A..= show _uuid
+          : "Event"
+          A..= show _event
+          : indexContext
+      )
+    where
+      indexContext = case _index of
+        Just idx -> ["Message #" A..= show idx]
+        Nothing -> []
+
+instance ToObject MsgContext
+
+instance LogItem MsgContext where
+  payloadKeys :: Verbosity -> MsgContext -> PayloadSelection
+  payloadKeys _ _ = AllKeys
diff --git a/example/Main.hs b/example/Main.hs
new file mode 100644
--- /dev/null
+++ b/example/Main.hs
@@ -0,0 +1,66 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Main (main) where
+
+import qualified BuildImpl
+import Data.List (find, isPrefixOf)
+import Katip (Severity (..), closeScribes, logTM, ls)
+import KatipLogger (katipLogger, mkLogEnv, unliftKatip)
+import Network.GRPC.HTTP2.Encoding (Compression, gzip)
+import Network.Wai.Handler.Warp (Settings, defaultSettings, setPort)
+import Network.Wai.Handler.WarpTLS (TLSSettings, defaultTlsSettings)
+import SimpleLogger (simpleLogger)
+import qualified SubmitImpl
+import qualified SyncImpl
+import System.Environment (getArgs)
+import UnliftIO (MonadIO, bracket)
+import Utxorpc.Server (ServiceConfig (..), UtxorpcHandlers (UtxorpcHandlers), runUtxorpc)
+import qualified WatchImpl
+
+main :: IO ()
+main = do
+  args <- getArgs
+  let port = maybe 3000 (read . drop 2) $ find ("-p" `isPrefixOf`) args
+  putStrLn $ "Starting server on port " ++ show port
+  if "--katip" `elem` args
+    then runKatipExample defaultTlsSettings (setPort port defaultSettings) [gzip]
+    else runSimpleExample
+
+runKatipExample :: TLSSettings -> Settings -> [Compression] -> IO ()
+runKatipExample tlsSettings warpSettings compression =
+  bracket mkLogEnv closeScribes $ \le -> do
+    runUtxorpc $ config le
+  where
+    config le =
+      ServiceConfig
+        tlsSettings
+        warpSettings
+        (handlersImpl katipLogF)
+        (Just katipLogger)
+        (unliftKatip le)
+        compression
+
+    katipLogF str = $(logTM) InfoS (ls str)
+
+runSimpleExample :: IO ()
+runSimpleExample = do
+  runUtxorpc $
+    ServiceConfig
+      defaultTlsSettings
+      defaultSettings
+      (handlersImpl putStrLn)
+      (Just simpleLogger)
+      id
+      [gzip]
+
+handlersImpl :: (MonadIO m) => (String -> m ()) -> UtxorpcHandlers m Int Int Int Int Int
+handlersImpl logF =
+  UtxorpcHandlers
+    (BuildImpl.handlerImpls logF)
+    (SubmitImpl.handlerImpls logF)
+    (SyncImpl.handlerImpls logF)
+    (WatchImpl.handlerImpls logF)
diff --git a/example/SimpleLogger.hs b/example/SimpleLogger.hs
new file mode 100644
--- /dev/null
+++ b/example/SimpleLogger.hs
@@ -0,0 +1,96 @@
+{-# LANGUAGE RankNTypes #-}
+
+module SimpleLogger (simpleLogger) where
+
+import Control.Lens (over, _1)
+import Control.Monad (unless)
+import Control.Monad.IO.Class (MonadIO (liftIO))
+import qualified Data.ByteString.Char8 as BS
+import Data.CaseInsensitive (CI (..))
+import Data.Time (getZonedTime)
+import Data.UUID (UUID)
+import Network.Wai (Request (..))
+import Utxorpc.Server (ServerStreamEndLogger, ServerStreamLogger, UtxorpcServiceLogger (..))
+
+simpleLogger :: (MonadIO m) => UtxorpcServiceLogger m
+simpleLogger =
+  UtxorpcServiceLogger
+    simpleRequestLogger
+    simpleReplyLogger
+    simpleServerStreamLogger
+    simpleServerStreamEndLogger
+
+simpleRequestLogger :: (Show i, MonadIO m) => BS.ByteString -> Request -> UUID -> i -> m ()
+simpleRequestLogger rpcPath req uuid i =
+  liftIO $
+    logEvent "REQUEST" rpcPath req uuid Nothing i
+
+simpleReplyLogger :: (Show o, MonadIO m) => BS.ByteString -> Request -> UUID -> o -> m ()
+simpleReplyLogger rpcPath req uuid o =
+  liftIO $
+    logEvent "REQUEST" rpcPath req uuid Nothing o
+
+simpleServerStreamLogger :: (MonadIO m) => ServerStreamLogger m
+simpleServerStreamLogger rpcPath req (uuid, index) o =
+  liftIO $
+    logEvent "SERVER STREAM" rpcPath req uuid (Just index) o
+
+simpleServerStreamEndLogger :: (MonadIO m) => ServerStreamEndLogger m
+simpleServerStreamEndLogger rpcPath req (uuid, index) =
+  liftIO $
+    logEvent "SERVER STREAM END" rpcPath req uuid (Just index) "End of server stream"
+
+logEvent ::
+  (Show a) =>
+  String ->
+  BS.ByteString ->
+  Request ->
+  UUID ->
+  Maybe Int ->
+  a ->
+  IO ()
+logEvent eventName rpcPath req uuid index msg = do
+  (header, footer) <- headerFooter eventName
+  putStrLn header
+  putStrLn $ "UUID: " ++ show uuid
+  case index of
+    Just i -> putStrLn $ "Message #" ++ show i
+    Nothing -> return ()
+  putStrLn $ "Path: " ++ BS.unpack rpcPath
+  putStrLn $ "Remote: " ++ show (remoteHost req)
+  unless (null $ requestHeaders req) $
+    liftIO . putStr $
+      showCIHdrs "Headers" (requestHeaders req)
+  liftIO . putStrLn $ "Message\n" ++ indent 1 (show msg)
+  putStrLn footer
+
+headerFooter :: String -> IO (String, String)
+headerFooter eventName = do
+  zoned <- getZonedTime
+  return (header zoned, footer)
+  where
+    header zoned = replicate lPadding c ++ " " ++ hdrText ++ " " ++ replicate rPadding c
+      where
+        hdrText = eventName ++ ": " ++ show zoned
+        lPadding = diff `div` 2
+        rPadding = diff `div` 2 + (diff `mod` 2)
+        diff = w - 2 - length hdrText
+
+    footer = replicate w c
+
+    w = 75
+    c = '-'
+
+showHdrs :: String -> [(BS.ByteString, BS.ByteString)] -> String
+showHdrs name hdrs =
+  name
+    ++ ":\n"
+    ++ (if not (null hdrs) then indent 1 . unlines $ map showHdr hdrs else "")
+  where
+    showHdr (k, v) = BS.unpack k ++ ": " ++ BS.unpack v
+
+showCIHdrs :: String -> [(CI BS.ByteString, BS.ByteString)] -> String
+showCIHdrs name hdrs = showHdrs name $ map (over _1 original) hdrs
+
+indent :: Int -> String -> String
+indent n = unlines . map (replicate n '\t' ++) . lines
diff --git a/example/SubmitImpl.hs b/example/SubmitImpl.hs
new file mode 100644
--- /dev/null
+++ b/example/SubmitImpl.hs
@@ -0,0 +1,39 @@
+module SubmitImpl (handlerImpls) where
+
+import Control.Monad.IO.Class (MonadIO)
+import EmptyHandlers (emptySStreamHandler, emptyUnaryHandler)
+import Network.GRPC.Server (ServerStreamHandler, UnaryHandler)
+import Proto.Utxorpc.V1alpha.Submit.Submit
+import Utxorpc.Server (SubmitHandlers (..))
+
+handlerImpls :: (MonadIO m) => (String -> m ()) -> SubmitHandlers m Int Int
+handlerImpls logF =
+  SubmitHandlers
+    (submitTxHandler logF)
+    (readMempoolHandler logF)
+    (waitForTxHandler logF)
+    (watchMempoolHandler logF)
+
+submitTxHandler ::
+  (MonadIO m) =>
+  (String -> m ()) ->
+  UnaryHandler m SubmitTxRequest SubmitTxResponse
+submitTxHandler = emptyUnaryHandler
+
+readMempoolHandler ::
+  (MonadIO m) =>
+  (String -> m ()) ->
+  UnaryHandler m ReadMempoolRequest ReadMempoolResponse
+readMempoolHandler = emptyUnaryHandler
+
+waitForTxHandler ::
+  (MonadIO m) =>
+  (String -> m ()) ->
+  ServerStreamHandler m WaitForTxRequest WaitForTxResponse Int
+waitForTxHandler = emptySStreamHandler
+
+watchMempoolHandler ::
+  (MonadIO m) =>
+  (String -> m ()) ->
+  ServerStreamHandler m WatchMempoolRequest WatchMempoolResponse Int
+watchMempoolHandler = emptySStreamHandler
diff --git a/example/SyncImpl.hs b/example/SyncImpl.hs
new file mode 100644
--- /dev/null
+++ b/example/SyncImpl.hs
@@ -0,0 +1,35 @@
+module SyncImpl (handlerImpls) where
+
+import Control.Monad.IO.Class (MonadIO)
+import EmptyHandlers (emptySStreamHandler, emptyUnaryHandler)
+import Network.GRPC.Server (ServerStreamHandler, UnaryHandler)
+import Proto.Utxorpc.V1alpha.Sync.Sync (DumpHistoryRequest, DumpHistoryResponse, FetchBlockRequest, FetchBlockResponse, FollowTipRequest, FollowTipResponse)
+import Utxorpc.Server (SyncHandlers (..))
+
+handlerImpls ::
+  (MonadIO m) =>
+  (String -> m ()) ->
+  SyncHandlers m Int
+handlerImpls logF =
+  SyncHandlers
+    (fetchBlockHandler logF)
+    (dumpHistoryHandler logF)
+    (followTipHandler logF)
+
+fetchBlockHandler ::
+  (MonadIO m) =>
+  (String -> m ()) ->
+  UnaryHandler m FetchBlockRequest FetchBlockResponse
+fetchBlockHandler = emptyUnaryHandler
+
+dumpHistoryHandler ::
+  (MonadIO m) =>
+  (String -> m ()) ->
+  UnaryHandler m DumpHistoryRequest DumpHistoryResponse
+dumpHistoryHandler = emptyUnaryHandler
+
+followTipHandler ::
+  (MonadIO m) =>
+  (String -> m ()) ->
+  ServerStreamHandler m FollowTipRequest FollowTipResponse Int
+followTipHandler = emptySStreamHandler
diff --git a/example/WatchImpl.hs b/example/WatchImpl.hs
new file mode 100644
--- /dev/null
+++ b/example/WatchImpl.hs
@@ -0,0 +1,18 @@
+module WatchImpl (handlerImpls) where
+
+import Control.Monad.IO.Class (MonadIO)
+import EmptyHandlers (emptySStreamHandler)
+import Network.GRPC.Server (ServerStreamHandler)
+import Proto.Utxorpc.V1alpha.Watch.Watch (WatchTxRequest, WatchTxResponse)
+import Utxorpc.Server (WatchHandlers (..))
+
+handlerImpls :: (MonadIO m) => (String -> m ()) -> WatchHandlers m Int
+handlerImpls logF =
+  WatchHandlers
+    (watchTxHandler logF)
+
+watchTxHandler ::
+  (MonadIO m) =>
+  (String -> m ()) ->
+  ServerStreamHandler m WatchTxRequest WatchTxResponse Int
+watchTxHandler = emptySStreamHandler
diff --git a/src/Utxorpc/Build.hs b/src/Utxorpc/Build.hs
new file mode 100644
--- /dev/null
+++ b/src/Utxorpc/Build.hs
@@ -0,0 +1,34 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE RankNTypes #-}
+
+module Utxorpc.Build (BuildHandlers (..), serviceHandlers) where
+
+import Control.Monad.IO.Class (MonadIO)
+import Network.GRPC.HTTP2.ProtoLens (RPC (RPC))
+import Network.GRPC.Server (ServerStreamHandler, ServiceHandler, UnaryHandler)
+import Proto.Utxorpc.V1alpha.Build.Build
+import Utxorpc.Logged (UtxorpcServiceLogger, loggedSStream, loggedUnary)
+
+data BuildHandlers m a = BuildHandlers
+  { getChainTip :: UnaryHandler m GetChainTipRequest GetChainTipResponse,
+    getChainParam :: UnaryHandler m GetChainParamRequest GetChainParamResponse,
+    getUtxoByAddress :: UnaryHandler m GetUtxoByAddressRequest GetUtxoByAddressResponse,
+    getUtxoByRef :: UnaryHandler m GetUtxoByRefRequest GetUtxoByRefResponse,
+    holdUtxo :: ServerStreamHandler m HoldUtxoRequest HoldUtxoResponse a
+  }
+
+serviceHandlers ::
+  (MonadIO m) =>
+  Maybe (UtxorpcServiceLogger m) ->
+  (forall x. m x -> IO x) ->
+  BuildHandlers m b ->
+  [ServiceHandler]
+serviceHandlers logger f BuildHandlers {getChainTip, getChainParam, getUtxoByAddress, getUtxoByRef, holdUtxo} =
+  [chainTipSH, chainParamSH, byAddressSH, byRefSH, holdSH]
+  where
+    chainTipSH = loggedUnary f (RPC :: RPC LedgerStateService "getChainTip") getChainTip logger
+    chainParamSH = loggedUnary f (RPC :: RPC LedgerStateService "getChainParam") getChainParam logger
+    byAddressSH = loggedUnary f (RPC :: RPC LedgerStateService "getUtxoByAddress") getUtxoByAddress logger
+    byRefSH = loggedUnary f (RPC :: RPC LedgerStateService "getUtxoByRef") getUtxoByRef logger
+    holdSH = loggedSStream f (RPC :: RPC LedgerStateService "holdUtxo") holdUtxo logger
diff --git a/src/Utxorpc/Logged.hs b/src/Utxorpc/Logged.hs
new file mode 100644
--- /dev/null
+++ b/src/Utxorpc/Logged.hs
@@ -0,0 +1,202 @@
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE RankNTypes #-}
+
+module Utxorpc.Logged
+  ( UtxorpcServiceLogger (..),
+    RequestLogger,
+    ReplyLogger,
+    ServerStreamLogger,
+    ServerStreamEndLogger,
+    loggedUnary,
+    loggedUnaryHandler,
+    loggedSStream,
+    loggedSStreamHandler,
+  )
+where
+
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import qualified Data.ByteString.Char8 as BS
+import Data.UUID (UUID)
+import Data.UUID.V4 (nextRandom)
+import Network.GRPC.HTTP2.Encoding (GRPCInput, GRPCOutput)
+import Network.GRPC.HTTP2.Types (IsRPC (..))
+import Network.GRPC.Server (ServiceHandler, UnaryHandler)
+import Network.GRPC.Server.Handlers.Trans (ServerStream (..), ServerStreamHandler, serverStream, unary)
+import Network.Wai (Request (..))
+
+-- | A record of logging functions that runs in the same monad as
+-- the request handlers. Monadic state is passed along throughout the
+-- lifecycle of responding to a request. This means that changes to the
+-- monadic state in the request logger is seen by the stream logger, stream
+-- handler and logger, and reply logger. An `unlift` function to run the monad
+-- in IO is provided to @'runUtxorpc'@.
+data UtxorpcServiceLogger m = UtxorpcServiceLogger
+  { requestLogger :: RequestLogger m,
+    replyLogger :: ReplyLogger m,
+    serverStreamLogger :: ServerStreamLogger m,
+    serverStreamEndLogger :: ServerStreamEndLogger m
+  }
+
+-- | Log incoming requests.
+type RequestLogger m =
+  forall i.
+  (Show i) =>
+  -- | The RPC path
+  BS.ByteString ->
+  -- | Request metadata
+  Request ->
+  -- | A UUID generated for this request and passed to stream and reply loggers.
+  UUID ->
+  -- | The request message
+  i ->
+  m ()
+
+-- | Log outgoing replies.
+type ReplyLogger m =
+  forall o.
+  (Show o) =>
+  -- | The RPC path
+  BS.ByteString ->
+  -- | Request metadata
+  Request ->
+  -- Generated for the request that caused this reply
+  UUID ->
+  -- | The reply message
+  o ->
+  m ()
+
+-- | Log outgoing server stream messages.
+type ServerStreamLogger m =
+  forall o.
+  (Show o) =>
+  -- | The RPC path
+  BS.ByteString ->
+  -- | Request metadata
+  Request ->
+  -- | The UUID generated for the request that generated this stream,
+  -- and the 0-based index of the message in the stream.
+  (UUID, Int) ->
+  -- | The stream message
+  o ->
+  m ()
+
+-- | Log the end of a server stream.
+type ServerStreamEndLogger m =
+  -- | The RPC path
+  BS.ByteString ->
+  -- | Request metadata
+  Request ->
+  -- | The UUID generated for the request that generated this stream,
+  -- and the 0-based index of the message in the stream.
+  (UUID, Int) ->
+  m ()
+
+-- | Creates a ServiceHandler that warp-grpc uses to handle requests
+loggedUnary ::
+  (MonadIO m, GRPCInput r i, GRPCOutput r o, Show i, Show o) =>
+  -- | An `unlift` function for the logger and handler monad
+  -- Monad state is carried through from request logger, to handler, to reply logger,
+  -- So changes to the monad state in the request logger is seen by the handler and reply logger.
+  (forall x. m x -> IO x) ->
+  -- | The RPC
+  r ->
+  -- | Generate a reply from request metadata and a proto Message
+  UnaryHandler m i o ->
+  -- | A logger that runs in the same monad as the handlers
+  Maybe (UtxorpcServiceLogger m) ->
+  ServiceHandler
+loggedUnary unlift rpc handler maybeLogger =
+  unary unlift rpc $ maybe handler loggedHandler maybeLogger
+  where
+    -- Generate UUID here for easier testing of `loggedUnaryHandler`
+    loggedHandler logger req msg = do
+      uuid <- liftIO nextRandom
+      loggedUnaryHandler rpc handler uuid logger req msg
+
+loggedUnaryHandler ::
+  (MonadIO m, Show i, Show o, IsRPC r) =>
+  r ->
+  UnaryHandler m i o ->
+  UUID ->
+  UtxorpcServiceLogger m ->
+  UnaryHandler m i o
+loggedUnaryHandler
+  rpc
+  handler
+  uuid
+  UtxorpcServiceLogger {requestLogger, replyLogger}
+  req
+  msg =
+    do
+      requestLogger (path rpc) req uuid msg
+      reply <- handler req msg
+      replyLogger rpcPath req uuid reply
+      return reply
+    where
+      rpcPath = path rpc
+
+-- | Creates a ServiceHandler that warp-grpc uses to handle stream requests
+loggedSStream ::
+  (MonadIO m, GRPCInput r i, GRPCOutput r o, Show i, Show o) =>
+  -- | An unlift function for the logger and handler
+  -- Monadic state changes are passed from request logger to stream logger and handlers, and so on.
+  -- So changes to the monadic state in the request logger are seen by the handler and other loggers.
+  (forall x. m x -> IO x) ->
+  -- | The RPC
+  r ->
+  -- | A function that, given request metadata and a protobuf Message,
+  -- generates an initial stream state and a function that folds over the stream state to produce
+  -- a stream of messages.
+  ServerStreamHandler m i o a ->
+  -- | A logger that runs in the same monad as the handler
+  Maybe (UtxorpcServiceLogger m) ->
+  ServiceHandler
+loggedSStream unlift rpc handler Nothing = serverStream unlift rpc handler
+loggedSStream unlift rpc handler (Just logger) =
+  serverStream unlift rpc loggedHandler
+  where
+    loggedHandler req msg = do
+      uuid <- liftIO nextRandom
+      loggedSStreamHandler rpc handler uuid logger req msg
+
+loggedSStreamHandler ::
+  (MonadIO m, IsRPC r, Show i, Show o) =>
+  r ->
+  ServerStreamHandler m i o a ->
+  UUID ->
+  UtxorpcServiceLogger m ->
+  ServerStreamHandler m i o (a, Int)
+loggedSStreamHandler
+  rpc
+  handler
+  uuid
+  UtxorpcServiceLogger {requestLogger, serverStreamLogger, serverStreamEndLogger}
+  req
+  msg = do
+    -- Log request
+    requestLogger rpcPath req uuid msg
+    -- Get initial stream state and stream output function
+    (initStreamState, ServerStream {serverStreamNext}) <- handler req msg
+    -- Wrap stream output function with logging
+    let loggedStreamNext = mkLoggedStreamNext serverStreamNext
+    -- The unwrapped handler returns the initial stream state and stream output function
+    -- We add initial log state and return the wrapped stream output function
+    return ((initStreamState, 0), ServerStream loggedStreamNext)
+    where
+      mkLoggedStreamNext getNext (streamState, index) = do
+        -- Get next chunk
+        next <- getNext streamState
+        case next of
+          Nothing -> do
+            -- Log end of stream
+            serverStreamEndLogger rpcPath req (uuid, index)
+            -- Return end of stream
+            return Nothing
+          Just (nextStreamState, replyMsg) -> do
+            -- Log chunk
+            serverStreamLogger rpcPath req (uuid, index) replyMsg
+            -- The unwrapped stream output function returns the next stream state and the message to send
+            -- We add log state
+            return $ Just ((nextStreamState, index + 1), replyMsg)
+
+      rpcPath = path rpc
diff --git a/src/Utxorpc/Server.hs b/src/Utxorpc/Server.hs
new file mode 100644
--- /dev/null
+++ b/src/Utxorpc/Server.hs
@@ -0,0 +1,103 @@
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE RankNTypes #-}
+
+module Utxorpc.Server
+  ( runUtxorpc,
+    ServiceConfig (..),
+    UtxorpcHandlers (..),
+    BuildHandlers (..),
+    SubmitHandlers (..),
+    SyncHandlers (..),
+    WatchHandlers (..),
+    UtxorpcServiceLogger (..),
+    RequestLogger,
+    ReplyLogger,
+    ServerStreamLogger,
+    ServerStreamEndLogger,
+  )
+where
+
+import Control.Monad.IO.Class (MonadIO)
+import Network.GRPC.HTTP2.Encoding (Compression)
+import Network.GRPC.Server
+import Network.Wai.Handler.Warp (Settings)
+import Network.Wai.Handler.WarpTLS (TLSSettings)
+import Utxorpc.Build as Build (BuildHandlers (..), serviceHandlers)
+import Utxorpc.Logged (ReplyLogger, RequestLogger, ServerStreamEndLogger, ServerStreamLogger, UtxorpcServiceLogger (..))
+import Utxorpc.Submit as Submit (SubmitHandlers (..), serviceHandlers)
+import Utxorpc.Sync as Sync (SyncHandlers (..), serviceHandlers)
+import Utxorpc.Watch as Watch (WatchHandlers (..), serviceHandlers)
+
+-- | Run a UTxO RPC service from a @'ServiceConfig'@.
+runUtxorpc ::
+  (MonadIO m) =>
+  -- | Configuration info and method handlers. See @'ServiceConfig'@ for type information.
+  ServiceConfig m a b c d e ->
+  IO ()
+runUtxorpc
+  ServiceConfig
+    { tlsSettings,
+      warpSettings,
+      handlers,
+      logger,
+      unlift,
+      compression
+    } =
+    runGrpc
+      tlsSettings
+      warpSettings
+      (Utxorpc.Server.serviceHandlers logger unlift handlers)
+      compression
+
+-- | Configuration info and method handlers. See @'UtxorpcHandlers'@ for type info.
+data ServiceConfig m a b c d e = ServiceConfig
+  { -- | warp-tls settings for using TLS.
+    tlsSettings :: TLSSettings,
+    -- | warp settings
+    warpSettings :: Settings,
+    -- | A handler for each method in the UTxO RPC specification.
+    handlers :: UtxorpcHandlers m a b c d e,
+    -- | Log each RPC event.
+    logger :: Maybe (UtxorpcServiceLogger m),
+    -- | An unlift function for the handlers and logger. Allows the handler and logger to be run in any monad, but they must be the same monad.
+    unlift :: forall x. m x -> IO x,
+    -- | A list of compressions to accept and use.
+    compression :: [Compression]
+  }
+
+-- | A handler for each method in the UTxO RPC specification.
+-- @'ServerStreamHandler'@s require a type variable representing the "stream state" (a value that the stream processes/folds over).
+-- The type variables here (other than @`m`@) are the type variables of each stream handler in the record.
+data
+  UtxorpcHandlers
+    m -- Monad of the handler functions
+    a -- Stream state of `holdUtxo`
+    b -- Stream state of `waitForTx`
+    c -- Stream state of `watchMempool`
+    d -- Stream state of `followTip`
+    e -- Stream state of `watchTx`
+  = UtxorpcHandlers
+  { -- | Handlers for the Build module.
+    buildHandlers :: BuildHandlers m a,
+    -- | Handlers for the Submit module.
+    submitHandlers :: SubmitHandlers m b c,
+    -- | Handlers for the Sync module.
+    syncHandlers :: SyncHandlers m d,
+    -- | Handlers for the Watch module.
+    watchHandlers :: WatchHandlers m e
+  }
+
+serviceHandlers ::
+  (MonadIO m) =>
+  Maybe (UtxorpcServiceLogger m) ->
+  (forall x. m x -> IO x) ->
+  UtxorpcHandlers m a b c d e ->
+  [ServiceHandler]
+serviceHandlers
+  logger
+  unlift
+  UtxorpcHandlers {buildHandlers, submitHandlers, syncHandlers, watchHandlers} =
+    Build.serviceHandlers logger unlift buildHandlers
+      <> Submit.serviceHandlers logger unlift submitHandlers
+      <> Sync.serviceHandlers logger unlift syncHandlers
+      <> Watch.serviceHandlers logger unlift watchHandlers
diff --git a/src/Utxorpc/Submit.hs b/src/Utxorpc/Submit.hs
new file mode 100644
--- /dev/null
+++ b/src/Utxorpc/Submit.hs
@@ -0,0 +1,32 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE RankNTypes #-}
+
+module Utxorpc.Submit (SubmitHandlers (..), serviceHandlers) where
+
+import Control.Monad.IO.Class (MonadIO)
+import Network.GRPC.HTTP2.ProtoLens (RPC (RPC))
+import Network.GRPC.Server (ServerStreamHandler, ServiceHandler, UnaryHandler)
+import Proto.Utxorpc.V1alpha.Submit.Submit
+import Utxorpc.Logged (UtxorpcServiceLogger, loggedSStream, loggedUnary)
+
+data SubmitHandlers m a b = SubmitHandlers
+  { submitTx :: UnaryHandler m SubmitTxRequest SubmitTxResponse,
+    readMempool :: UnaryHandler m ReadMempoolRequest ReadMempoolResponse,
+    waitForTx :: ServerStreamHandler m WaitForTxRequest WaitForTxResponse a,
+    watchMempool :: ServerStreamHandler m WatchMempoolRequest WatchMempoolResponse b
+  }
+
+serviceHandlers ::
+  (MonadIO m) =>
+  Maybe (UtxorpcServiceLogger m) ->
+  (forall x. m x -> IO x) ->
+  SubmitHandlers m b c ->
+  [ServiceHandler]
+serviceHandlers logger f SubmitHandlers {submitTx, readMempool, waitForTx, watchMempool} =
+  [submitTxSH, readMempoolSH, waitForTxSH, watchMempoolSH]
+  where
+    submitTxSH = loggedUnary f (RPC :: RPC SubmitService "submitTx") submitTx logger
+    readMempoolSH = loggedUnary f (RPC :: RPC SubmitService "readMempool") readMempool logger
+    waitForTxSH = loggedSStream f (RPC :: RPC SubmitService "waitForTx") waitForTx logger
+    watchMempoolSH = loggedSStream f (RPC :: RPC SubmitService "watchMempool") watchMempool logger
diff --git a/src/Utxorpc/Sync.hs b/src/Utxorpc/Sync.hs
new file mode 100644
--- /dev/null
+++ b/src/Utxorpc/Sync.hs
@@ -0,0 +1,30 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE RankNTypes #-}
+
+module Utxorpc.Sync (SyncHandlers (..), serviceHandlers) where
+
+import Control.Monad.IO.Class (MonadIO)
+import Network.GRPC.HTTP2.ProtoLens (RPC (RPC))
+import Network.GRPC.Server (ServerStreamHandler, ServiceHandler, UnaryHandler)
+import Proto.Utxorpc.V1alpha.Sync.Sync
+import Utxorpc.Logged (UtxorpcServiceLogger, loggedSStream, loggedUnary)
+
+data SyncHandlers m a = SyncHandlers
+  { fetchBlock :: UnaryHandler m FetchBlockRequest FetchBlockResponse,
+    dumpHistory :: UnaryHandler m DumpHistoryRequest DumpHistoryResponse,
+    followTip :: ServerStreamHandler m FollowTipRequest FollowTipResponse a
+  }
+
+serviceHandlers ::
+  (MonadIO m) =>
+  Maybe (UtxorpcServiceLogger m) ->
+  (forall x. m x -> IO x) ->
+  SyncHandlers m b ->
+  [ServiceHandler]
+serviceHandlers logger f SyncHandlers {fetchBlock, dumpHistory, followTip} =
+  [fetchBlockSH, dumpHistorySH, followTipSH]
+  where
+    fetchBlockSH = loggedUnary f (RPC :: RPC ChainSyncService "fetchBlock") fetchBlock logger
+    dumpHistorySH = loggedUnary f (RPC :: RPC ChainSyncService "dumpHistory") dumpHistory logger
+    followTipSH = loggedSStream f (RPC :: RPC ChainSyncService "followTip") followTip logger
diff --git a/src/Utxorpc/Watch.hs b/src/Utxorpc/Watch.hs
new file mode 100644
--- /dev/null
+++ b/src/Utxorpc/Watch.hs
@@ -0,0 +1,26 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE RankNTypes #-}
+
+module Utxorpc.Watch (WatchHandlers (..), serviceHandlers) where
+
+import Control.Monad.IO.Class (MonadIO)
+import Network.GRPC.HTTP2.ProtoLens (RPC (RPC))
+import Network.GRPC.Server (ServerStreamHandler, ServiceHandler)
+import Proto.Utxorpc.V1alpha.Watch.Watch
+import Utxorpc.Logged (UtxorpcServiceLogger, loggedSStream)
+
+newtype WatchHandlers m a = WatchHandlers
+  { watchTx :: ServerStreamHandler m WatchTxRequest WatchTxResponse a
+  }
+
+serviceHandlers ::
+  (MonadIO m) =>
+  Maybe (UtxorpcServiceLogger m) ->
+  (forall x. m x -> IO x) ->
+  WatchHandlers m b ->
+  [ServiceHandler]
+serviceHandlers logger f WatchHandlers {watchTx} =
+  [watchTxSH]
+  where
+    watchTxSH = loggedSStream f (RPC :: RPC WatchService "watchTx") watchTx logger
diff --git a/test/logged_test.hs b/test/logged_test.hs
new file mode 100644
--- /dev/null
+++ b/test/logged_test.hs
@@ -0,0 +1,230 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE RankNTypes #-}
+
+module Main (main) where
+
+import Control.Monad.Trans.Class (lift)
+import Control.Monad.Trans.Writer.CPS (WriterT, runWriterT, tell)
+import Data.List (foldl')
+import Data.ProtoLens.Message (Message, defMessage)
+import Data.UUID (UUID)
+import Data.UUID.V4 (nextRandom)
+import Network.GRPC.HTTP2.ProtoLens (RPC (RPC))
+import Network.GRPC.Server (ServerStream (..), ServerStreamHandler, UnaryHandler)
+import Proto.Utxorpc.V1alpha.Sync.Sync
+import System.Directory (removeFile)
+import System.IO (Handle, IOMode (..), hPutStr, readFile', withFile)
+import Test.Hspec
+import Utxorpc.Logged
+  ( ReplyLogger,
+    RequestLogger,
+    ServerStreamEndLogger,
+    ServerStreamLogger,
+    UtxorpcServiceLogger (..),
+    loggedSStreamHandler,
+    loggedUnaryHandler,
+  )
+
+logFilePath :: String
+logFilePath = "test/test-logs.txt"
+
+mockRequestLogger :: (String -> IO ()) -> RequestLogger (WriterT String IO)
+mockRequestLogger logF _ _ uuid msg = do
+  lift . logF $ requestOut uuid msg
+  tell $ requestOut uuid msg
+
+requestOut :: (Show a) => UUID -> a -> String
+requestOut uuid msg = "REQUEST_LOGGER: " ++ "[" ++ show uuid ++ "]" ++ show msg ++ "\n"
+
+mockReplyLogger :: (String -> IO ()) -> ReplyLogger (WriterT String IO)
+mockReplyLogger logF _ _ uuid msg = do
+  lift . logF $ replyOut uuid msg
+  tell $ replyOut uuid msg
+
+replyOut :: (Show a) => UUID -> a -> String
+replyOut uuid msg = "REPLY_LOGGER: [" ++ show uuid ++ "] " ++ show msg ++ "\n"
+
+mockServerStreamLogger :: (String -> IO ()) -> ServerStreamLogger (WriterT String IO)
+mockServerStreamLogger logF _ _ (uuid, index) _ = do
+  lift . logF $ serverStreamOut (uuid, index)
+  tell $ serverStreamOut (uuid, index)
+
+serverStreamOut :: (UUID, Int) -> String
+serverStreamOut (uuid, int) = "SERVER_STREAM_LOGGER: " ++ show (uuid, int) ++ "\n"
+
+mockServerStreamEndLogger :: (String -> IO ()) -> ServerStreamEndLogger (WriterT String IO)
+mockServerStreamEndLogger logF _ _ (uuid, index) = do
+  lift . logF $ serverStreamEndOut (uuid, index)
+  tell $ serverStreamEndOut (uuid, index)
+
+serverStreamEndOut :: (UUID, Int) -> String
+serverStreamEndOut (uuid, index) = "SERVER_STREAM_END_LOGGER: " ++ show (uuid, index) ++ "\n"
+
+mockLogger :: (String -> IO ()) -> UtxorpcServiceLogger (WriterT String IO)
+mockLogger logF =
+  UtxorpcServiceLogger
+    (mockRequestLogger logF)
+    (mockReplyLogger logF)
+    (mockServerStreamLogger logF)
+    (mockServerStreamEndLogger logF)
+
+mockUnaryHandler :: (Message o) => (String -> IO ()) -> UnaryHandler (WriterT String IO) i o
+mockUnaryHandler logF _ _ = do
+  lift $ logF unaryHandlerOut
+  tell unaryHandlerOut
+  return defMessage
+
+unaryHandlerOut :: String
+unaryHandlerOut = "UNARY_HANDLER\n"
+
+unaryRpc :: RPC ChainSyncService "fetchBlock"
+unaryRpc = RPC
+
+unaryReq :: FetchBlockRequest
+unaryReq = defMessage
+
+type UnaryReply = FetchBlockResponse
+
+mockSStreamHandler :: (Message o) => (String -> IO ()) -> ServerStreamHandler (WriterT String IO) i o Int
+mockSStreamHandler logF _ _ = do
+  lift $ logF generateStreamOut
+  tell generateStreamOut
+  return (0, stream)
+  where
+    stream = ServerStream $ \index ->
+      if index > 3
+        then do
+          lift $ logF $ streamHandlerEndOut index
+          tell $ streamHandlerEndOut index
+          return Nothing
+        else do
+          lift $ logF $ streamHandlerOut index
+          tell $ streamHandlerOut index
+          return $ Just (index + 1, defMessage)
+
+generateStreamOut :: String
+generateStreamOut = "GENERATE_STREAM\n"
+
+streamHandlerOut :: Int -> String
+streamHandlerOut index = "STREAM_HANDLER " ++ show index ++ "\n"
+
+streamHandlerEndOut :: Int -> String
+streamHandlerEndOut index = "STREAM_HANDLER_END " ++ show index ++ "\n"
+
+sStreamRpc :: RPC ChainSyncService "followTip"
+sStreamRpc = RPC
+
+type SStreamReq = FollowTipRequest
+
+sStreamReq :: SStreamReq
+sStreamReq = defMessage
+
+type SStreamReply = FollowTipResponse
+
+testUnary :: IO ()
+testUnary = do
+  uuid <- nextRandom
+  (reply, _mState) <- withFile logFilePath WriteMode (runWriterT . handleReq)
+  out <- readFile' logFilePath
+
+  (loggedReply, loggedMState) <- withFile logFilePath WriteMode (runWriterT . handleReqLogged uuid)
+  loggedOut <- readFile' logFilePath
+
+  hspec $ describe "Unary Logging" $ do
+    it "Logged reply should equal unlogged reply" $ do
+      reply `shouldBe` loggedReply
+    -- We know that the IO effects are accumulating, so if the WriterT state equals the IO effect,
+    -- we know the monadic state was passed from one logger/handler to the next
+    it "Monad state should accumulate" $ do
+      loggedOut `shouldBe` loggedMState
+    it "Execution should occur in the right order" $ do
+      targetLoggedOut uuid out reply `shouldBe` loggedOut
+
+  removeFile logFilePath
+  where
+    handleReq :: Handle -> WriterT String IO UnaryReply
+    handleReq fileHandle = mockUnaryHandler (hPutStr fileHandle) undefined unaryReq
+
+    handleReqLogged :: UUID -> Handle -> WriterT String IO UnaryReply
+    handleReqLogged uuid fileHandle = loggedHandler uuid (hPutStr fileHandle) undefined unaryReq
+    loggedHandler uuid logF = loggedUnaryHandler unaryRpc (mockUnaryHandler logF) uuid (mockLogger logF)
+
+    targetLoggedOut uuid unloggedOut reply =
+      requestOut uuid unaryReq ++ unloggedOut ++ replyOut uuid reply
+
+testSStream :: IO ()
+testSStream = do
+  -- Run unlogged
+  let withLogFile = withFile logFilePath WriteMode
+  ((_streamStates, msgs), _monadState) <-
+    withLogFile $ runWriterT . run . handlerWith
+  _out <- readFile' logFilePath
+
+  -- Run logged
+  uuid <- nextRandom
+  ((loggedStreamStates, loggedMsgs), loggedMonadState) <-
+    withLogFile $ runWriterT . run . loggedHandlerWith uuid
+  loggedOut <- readFile' logFilePath
+
+  hspec $ describe "Stream Logging" $ do
+    it "Logged messages should equal unlogged messages" $ do
+      msgs `shouldBe` loggedMsgs
+    -- We know that the IO effects are accumulating, so if the WriterT state equals the IO effect,
+    -- we know the monadic state was passed from one logger/handler to the next
+    it "Monad state should accumulate" $ do
+      loggedOut `shouldBe` loggedMonadState
+    it "Execution should occur in the right order" $ do
+      targetLoggedOut uuid loggedStreamStates `shouldBe` loggedOut
+
+  removeFile logFilePath
+  where
+    handlerWith ::
+      Handle ->
+      ServerStreamHandler (WriterT String IO) SStreamReq SStreamReply Int
+    handlerWith = mockSStreamHandler . hPutStr
+
+    loggedHandlerWith ::
+      UUID ->
+      Handle ->
+      ServerStreamHandler (WriterT String IO) SStreamReq SStreamReply (Int, Int)
+    loggedHandlerWith uuid h =
+      loggedSStreamHandler
+        sStreamRpc
+        (mockSStreamHandler $ hPutStr h)
+        uuid
+        (mockLogger $ hPutStr h)
+
+    run :: (Monad m) => ServerStreamHandler m SStreamReq SStreamReply a -> m ([a], [SStreamReply])
+    run handler = do
+      (initState, ServerStream {serverStreamNext}) <- handler undefined sStreamReq
+      runStream serverStreamNext initState
+      where
+        runStream stream state = do
+          next <- stream state
+          case next of
+            Just (state', msg) -> do
+              (states, msgs) <- runStream stream state'
+              return (state : states, msg : msgs)
+            Nothing ->
+              return ([], [])
+
+    targetLoggedOut uuid loggedStreamStates =
+      requestOut uuid unaryReq -- log request
+        ++ generateStreamOut -- generate stream
+        -- Output for all stream handling and logs
+        ++ foldl' (\out streamState -> out ++ targetOut streamState) "" loggedStreamStates
+        -- end of stream handler
+        ++ streamHandlerEndOut (length loggedStreamStates)
+        -- end of stream logger
+        ++ serverStreamEndOut (uuid, length loggedStreamStates)
+      where
+        -- output for a single stream message
+        targetOut (index, _) =
+          streamHandlerOut index
+            ++ serverStreamOut (uuid, index)
+
+main :: IO ()
+main = do
+  testUnary
+  testSStream
diff --git a/utxorpc-server.cabal b/utxorpc-server.cabal
new file mode 100644
--- /dev/null
+++ b/utxorpc-server.cabal
@@ -0,0 +1,134 @@
+cabal-version: 3.0
+
+name:           utxorpc-server
+version:        0.0.1.0
+synopsis:       An SDK for UTxO RPC services.
+description:    This SDK includes helper functions for creating a UTxO RPC service with automated logging.
+category:       Network, Blockchain, Cardano
+homepage:       https://github.com/utxorpc/haskell-sdk#readme
+bug-reports:    https://github.com/utxorpc/haskell-sdk/issues
+author:         Dominic Mayhew
+maintainer:     dominic.j.mayhew@gmail.com
+license:        Apache-2.0
+license-file:   LICENSE
+build-type:     Simple
+
+source-repository head
+  type: git
+  location: https://github.com/utxorpc/haskell-sdk
+
+library
+  exposed-modules:
+      Utxorpc.Server
+  other-modules:
+      Utxorpc.Build
+      Utxorpc.Submit
+      Utxorpc.Sync
+      Utxorpc.Watch
+      Utxorpc.Logged
+  hs-source-dirs:
+      src
+  ghc-options:
+    -Wall
+    -Wcompat
+    -Widentities
+    -Wincomplete-record-updates
+    -Wincomplete-uni-patterns
+    -Wmissing-export-lists
+    -Wmissing-home-modules
+    -Wpartial-fields
+    -Wredundant-constraints
+    -Wunused-packages
+  build-depends:
+    , base >=4.7 && <5
+    , bytestring < 0.13
+    , http2-grpc-proto-lens < 0.2
+    , http2-grpc-types >= 0.5.0.0 && < 0.6
+    , utxorpc >= 0.0.3.0 && < 0.1
+    , uuid < 1.4
+    , wai < 3.3
+    , warp  < 3.5
+    , warp-grpc < 0.5
+    , warp-tls < 3.5
+  default-language: Haskell2010
+
+executable server-example
+  main-is:
+    Main.hs
+  other-modules:
+    BuildImpl
+    KatipLogger
+    EmptyHandlers
+    SimpleLogger
+    SubmitImpl
+    SyncImpl
+    WatchImpl
+  hs-source-dirs:
+    example
+  ghc-options:
+    -Wall
+    -Wcompat
+    -Widentities
+    -Wincomplete-record-updates
+    -Wincomplete-uni-patterns
+    -Wmissing-export-lists
+    -Wmissing-home-modules
+    -Wpartial-fields
+    -Wredundant-constraints
+    -Wunused-packages
+  build-depends:
+    aeson < 2.3
+    , base >= 4.7 && < 5
+    , bytestring < 0.13
+    , case-insensitive < 1.3
+    , http2-grpc-types < 0.6
+    , katip < 0.9
+    , lens < 5.3
+    , proto-lens < 0.8
+    , time < 1.13
+    , transformers < 0.7
+    , unliftio < 0.3
+    , utxorpc >= 0.0.3 && < 0.1
+    , utxorpc-server
+    , uuid < 1.4
+    , wai < 3.3
+    , warp  < 3.5
+    , warp-grpc < 0.5
+    , warp-tls < 3.5
+  default-language: Haskell2010
+
+test-suite logged_test_server
+  type:
+    exitcode-stdio-1.0
+  main-is:
+    logged_test.hs
+  other-modules:
+    Utxorpc.Logged
+  hs-source-dirs:
+    test
+    src
+  ghc-options:
+    -Wall
+    -Wcompat
+    -Widentities
+    -Wincomplete-record-updates
+    -Wincomplete-uni-patterns
+    -Wmissing-export-lists
+    -Wmissing-home-modules
+    -Wpartial-fields
+    -Wredundant-constraints
+    -Wunused-packages
+  build-depends:
+    , base >=4.7 && <5
+    , bytestring < 0.13
+    , directory >= 1.3.8 && < 1.4
+    , http2-grpc-proto-lens < 0.2
+    , http2-grpc-types < 0.6
+    , hspec >= 2.11.7 && < 2.12
+    , proto-lens < 0.8
+    , transformers < 0.7
+    , utxorpc >= 0.0.3 && < 0.1
+    , uuid < 1.4
+    , wai < 3.3
+    , warp-grpc < 0.5
+  default-language: Haskell2010
