diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,7 @@
+Copyright 2022 Andre Marianiello
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/hasql-transaction-io.cabal b/hasql-transaction-io.cabal
new file mode 100644
--- /dev/null
+++ b/hasql-transaction-io.cabal
@@ -0,0 +1,56 @@
+cabal-version: 2.4
+name:          hasql-transaction-io
+version:       0.1.0.0
+synopsis:      Perform IO actions during transactions for Hasql
+homepage:      https://github.com/andremarianiello/hasql-transaction-io
+bug-reports:
+  https://github.com/andremarianiello/hasql-transaction-io/issues
+
+license:       MIT
+license-file:  LICENSE
+author:        Andre Marianiello
+maintainer:    andremarianiello@users.noreply.github.com
+copyright:     (c) 2022, Andre Marianiello
+category:      Database, PostgreSQL, Hasql
+
+source-repository head
+  type:     git
+  location:
+    https://github.com/andremarianiello/hasql-transaction-io.git
+
+library
+  exposed-modules:
+    Hasql.CursorTransactionIO
+    Hasql.CursorTransactionIO.TransactionIO
+    Hasql.TransactionIO
+    Hasql.TransactionIO.Sessions
+
+  other-modules:
+    Hasql.Private.CursorTransactionIO
+    Hasql.Private.Session.UnliftIO
+    Hasql.Private.Statements
+    Hasql.Private.TransactionIO
+    Hasql.Private.Types
+
+  default-extensions:
+    DeriveFunctor
+    FlexibleContexts
+    GeneralizedNewtypeDeriving
+    LambdaCase
+    NamedFieldPuns
+    RankNTypes
+
+  other-extensions:   OverloadedStrings
+  build-depends:
+    , base                     >=4.14 && <4.17
+    , bytestring               >=0.10
+    , bytestring-tree-builder  ^>=0.2
+    , hasql                    ^>=1.5
+    , mtl                      ^>=2.2
+    , resourcet                ^>=1.2
+    , safe-exceptions          ^>=0.1
+    , transformers             ^>=0.5
+    , unliftio-core            ^>=0.2
+
+  hs-source-dirs:     library
+  default-language:   Haskell2010
diff --git a/library/Hasql/CursorTransactionIO.hs b/library/Hasql/CursorTransactionIO.hs
new file mode 100644
--- /dev/null
+++ b/library/Hasql/CursorTransactionIO.hs
@@ -0,0 +1,9 @@
+module Hasql.CursorTransactionIO (
+  CursorTransactionIO,
+  Cursor,
+
+  declareCursorFor,
+  fetchWithCursor,
+) where
+
+import Hasql.Private.CursorTransactionIO
diff --git a/library/Hasql/CursorTransactionIO/TransactionIO.hs b/library/Hasql/CursorTransactionIO/TransactionIO.hs
new file mode 100644
--- /dev/null
+++ b/library/Hasql/CursorTransactionIO/TransactionIO.hs
@@ -0,0 +1,10 @@
+module Hasql.CursorTransactionIO.TransactionIO (
+  cursorTransactionIO,
+) where
+
+import Hasql.Private.TransactionIO (TransactionIO)
+import Hasql.Private.CursorTransactionIO
+
+-- | Run a `CursorTransactionIO` to produce a `TransactionIO` that manages the lifespan of all allocated `Cursor`s
+cursorTransactionIO :: (forall s. CursorTransactionIO s a) -> TransactionIO a
+cursorTransactionIO = run
diff --git a/library/Hasql/Private/CursorTransactionIO.hs b/library/Hasql/Private/CursorTransactionIO.hs
new file mode 100644
--- /dev/null
+++ b/library/Hasql/Private/CursorTransactionIO.hs
@@ -0,0 +1,92 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasql.Private.CursorTransactionIO where
+
+-- bytestring
+import Data.ByteString (ByteString)
+
+-- bytestring-tree-builder
+import ByteString.TreeBuilder
+
+-- transformers
+import Control.Monad.Error.Class
+
+-- mtl
+import Control.Monad.Reader
+import Control.Monad.State
+
+-- unliftio-core
+import Control.Monad.IO.Unlift
+
+-- resourcet
+import Control.Monad.Trans.Resource
+
+-- hasql
+import Hasql.Encoders (noParams)
+import Hasql.Decoders (Result, noResult)
+import Hasql.Session hiding (statement)
+import Hasql.Statement
+
+-- hasql-streaming
+import Hasql.Private.Session.UnliftIO
+import Hasql.TransactionIO hiding (statement)
+import qualified Hasql.TransactionIO as TransactionIO
+
+-- | A PostgresSQL cursor that produces results of type @a@ when fetched
+data Cursor s a = Cursor 
+  { cursorVar :: ByteString
+  , decoder :: Result a
+  }
+  deriving (Functor)
+
+-- | A `TransactionIO` that also manages creation and deletion of `Cursor`s
+newtype CursorTransactionIO s a = CursorTransactionIO
+  ( StateT Int (ResourceT TransactionIO) a )
+  deriving (Functor, Applicative, Monad, MonadIO, MonadResource, MonadState Int)
+
+run :: (forall s. CursorTransactionIO s a) -> TransactionIO a
+run (CursorTransactionIO ctxio) = runResourceT . flip evalStateT 0 $ ctxio
+
+-- | Like `Session.sql` but in a `CursorTransactionIO`. It should not attempt any statements that cannot be safely run inside a transaction.
+sql :: ByteString -> CursorTransactionIO s ()
+sql = CursorTransactionIO . lift . lift . TransactionIO.sql
+
+-- | Like `Session.statement` but in a `CursorTransactionIO`. It should not any statements that cannot be safely run inside a transaction.
+statement :: params -> Statement params result -> CursorTransactionIO s result
+statement params stmt = CursorTransactionIO . lift . lift $ TransactionIO.statement params stmt
+
+ignoreFailedTransactionError :: MonadError QueryError m => m () -> m ()
+ignoreFailedTransactionError sess =
+  catchError sess $ \qe -> case qe of
+    QueryError _ _ (ResultError (ServerError "25P02" _ _ _)) -> pure ()
+    _ -> throwError qe
+
+-- | Run a `Statement` using a cursor
+declareCursorFor :: params -> Statement params result -> CursorTransactionIO s (Cursor s result)
+declareCursorFor params stmt = do
+  UnliftIO runInIO <- CursorTransactionIO . lift . lift $ askUnliftIO
+  cursorIx <- get
+  let cursorVar = toByteString $ "Hasql_CursorTransactionIO_" <> asciiIntegral cursorIx
+  modify' (+1)
+  (_, cursor) <- allocate
+    (runInIO $ newCursor cursorVar params stmt)
+    (runInIO . ignoreFailedTransactionError . closeCursor)
+  pure cursor
+
+newCursor :: ByteString -> params -> Statement params result -> TransactionIO (Cursor s result)
+newCursor cursorVar params (Statement query encoder decoder prepare) = do
+  let cursorQuery = 
+        "DECLARE " <> cursorVar <> " NO SCROLL CURSOR FOR " <> query
+  TransactionIO.statement params (Statement cursorQuery encoder noResult prepare)
+  pure $ Cursor cursorVar decoder
+
+closeCursor :: Cursor s a -> TransactionIO ()
+closeCursor (Cursor cursorVar _) = do
+  let closeQuery = "CLOSE " <> cursorVar
+  TransactionIO.statement () (Statement closeQuery noParams noResult True)
+
+-- | Fetch results from a cursor
+fetchWithCursor :: Cursor s a -> CursorTransactionIO s a
+fetchWithCursor (Cursor cursorVar decoder) = do
+  let fetchQuery = "FETCH " <> cursorVar
+  statement () (Statement fetchQuery noParams decoder True)
diff --git a/library/Hasql/Private/Session/UnliftIO.hs b/library/Hasql/Private/Session/UnliftIO.hs
new file mode 100644
--- /dev/null
+++ b/library/Hasql/Private/Session/UnliftIO.hs
@@ -0,0 +1,23 @@
+module Hasql.Private.Session.UnliftIO where
+
+-- mtl
+import Control.Monad.Reader.Class (ask)
+import Control.Monad.Error.Class (throwError)
+
+-- unliftio-core
+import Control.Monad.IO.Unlift
+
+-- safe-exceptions
+import Control.Exception.Safe
+
+--hasql
+import Hasql.Session
+
+instance MonadUnliftIO Session where
+  withRunInIO inner = do
+    conn <- ask
+    res <- liftIO $ try $ inner $ \sess -> do
+      run sess conn >>= either throwIO pure
+    case res of
+      Left e -> throwError e
+      Right a -> pure a
diff --git a/library/Hasql/Private/Statements.hs b/library/Hasql/Private/Statements.hs
new file mode 100644
--- /dev/null
+++ b/library/Hasql/Private/Statements.hs
@@ -0,0 +1,30 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasql.Private.Statements where
+
+-- bytestring-tree-builder
+import ByteString.TreeBuilder (toByteString)
+
+-- hasql
+import Hasql.Statement
+import Hasql.Encoders
+import Hasql.Decoders
+
+-- hasql-transaction-io
+import Hasql.Private.Types
+
+startTransaction :: IsolationLevel -> Mode -> Deferrable -> Bool -> Statement () ()
+startTransaction isolation mode deferrable = 
+  Statement stmt noParams noResult
+  where
+    stmt = toByteString $
+      "START TRANSACTION ISOLATION LEVEL " <>
+      isolationLevelToSQL isolation <> " " <>
+      modeToSQL mode <> " " <>
+      deferrableToSQL deferrable
+
+commitTransaction :: Bool -> Statement () ()
+commitTransaction = Statement "COMMIT" noParams noResult
+
+rollbackTransaction :: Bool -> Statement () ()
+rollbackTransaction = Statement "ROLLBACK" noParams noResult
diff --git a/library/Hasql/Private/TransactionIO.hs b/library/Hasql/Private/TransactionIO.hs
new file mode 100644
--- /dev/null
+++ b/library/Hasql/Private/TransactionIO.hs
@@ -0,0 +1,82 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasql.Private.TransactionIO where
+
+-- base
+import Control.Applicative
+
+-- bytestring
+import Data.ByteString (ByteString)
+
+-- bytestring-tree-builder
+import ByteString.TreeBuilder
+
+-- transformers
+import Control.Monad.Trans.Reader
+import Control.Monad.Trans.Class
+
+-- mtl
+import Control.Monad.Error.Class
+
+-- unliftio-core
+import Control.Monad.IO.Unlift
+
+-- resourcet
+import Data.Acquire
+import Control.Monad.Trans.Resource
+
+-- hasql
+import Hasql.Statement
+import Hasql.Session
+import qualified Hasql.Session as Session
+
+-- hasql-streaming
+import Hasql.Private.Session.UnliftIO
+import Hasql.Private.Types
+import qualified Hasql.Private.Statements as Statements
+
+-- | A mixture of Hasql statements and arbitrary IO that is all performed during a single transaction
+newtype TransactionIO a = TransactionIO (ReaderT Transaction Session a)
+  deriving (Functor, Applicative, Monad, MonadIO, MonadError QueryError, MonadUnliftIO)
+
+data Transaction = Transaction
+
+instance Semigroup a => Semigroup (TransactionIO a) where
+  (<>) = liftA2 (<>)
+
+instance Monoid a => Monoid (TransactionIO a) where
+  mempty = pure mempty
+
+run :: TransactionIO a -> IsolationLevel -> Mode -> Deferrable -> Bool -> Session a
+run (TransactionIO txio) isolation mode deferrable preparable = runResourceT $ do
+  UnliftIO runInIO <- lift askUnliftIO
+  let acq = mkAcquireType (runInIO $ startTransaction isolation mode deferrable preparable) ((runInIO .) . endTransaction preparable)
+  (_, tx) <- allocateAcquire acq
+  lift $ runReaderT txio tx
+
+-- | Like `Session.sql` but in a `TransactionIO`. It should not attempt any statements that cannot be safely run inside a transaction.
+sql :: ByteString -> TransactionIO ()
+sql = TransactionIO . lift . Session.sql
+
+-- | Like `Session.statement` but in a `TransactionIO`. It should not attempt any statements that cannot be safely run inside a transaction.
+statement :: params -> Statement params result -> TransactionIO result
+statement params stmt = TransactionIO . lift $ Session.statement params stmt
+
+startTransaction :: IsolationLevel -> Mode -> Deferrable -> Bool -> Session Transaction
+startTransaction isolation mode deferrable prepare = do
+  Session.statement () (Statements.startTransaction isolation mode deferrable prepare)
+  pure Transaction
+
+endTransaction :: Bool -> Transaction -> ReleaseType -> Session ()
+endTransaction prepare tx = \case
+  ReleaseEarly -> commitTransaction prepare tx
+  ReleaseNormal -> commitTransaction prepare tx
+  ReleaseException -> rollbackTransaction prepare tx
+
+commitTransaction :: Bool -> Transaction -> Session ()
+commitTransaction prepare Transaction = do
+  Session.statement () (Statements.commitTransaction prepare)
+
+rollbackTransaction :: Bool -> Transaction -> Session ()
+rollbackTransaction prepare Transaction = do
+  Session.statement () (Statements.rollbackTransaction prepare)
diff --git a/library/Hasql/Private/Types.hs b/library/Hasql/Private/Types.hs
new file mode 100644
--- /dev/null
+++ b/library/Hasql/Private/Types.hs
@@ -0,0 +1,37 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasql.Private.Types where
+
+-- bytestring-tree-builder
+import ByteString.TreeBuilder (Builder)
+
+-- | A PostgreSQL transaction isolation level
+data IsolationLevel
+  = ReadCommitted
+  | RepeatableRead
+  | Serializable
+  deriving (Show, Eq)
+
+isolationLevelToSQL :: IsolationLevel -> Builder
+isolationLevelToSQL = \case
+  ReadCommitted -> "READ COMMITTED"
+  RepeatableRead -> "REPEATABLE READ"
+  Serializable -> "SERIALIZABLE"
+
+-- | A PostgreSQL transaction mode
+data Mode = ReadWrite | ReadOnly
+  deriving (Show, Eq)
+  
+modeToSQL :: Mode -> Builder
+modeToSQL = \case
+  ReadWrite -> "READ WRITE"
+  ReadOnly -> "READ ONLY"
+
+-- | A PostgreSQL transaction deferrability designation
+data Deferrable = Deferrable | NotDeferrable
+  deriving (Show, Eq)
+
+deferrableToSQL :: Deferrable -> Builder
+deferrableToSQL = \case
+  Deferrable -> "DEFERRABLE"
+  NotDeferrable -> "NOT DEFERRABLE"
diff --git a/library/Hasql/TransactionIO.hs b/library/Hasql/TransactionIO.hs
new file mode 100644
--- /dev/null
+++ b/library/Hasql/TransactionIO.hs
@@ -0,0 +1,8 @@
+module Hasql.TransactionIO (
+  TransactionIO,
+  statement,
+  sql,
+) where
+
+-- hasql-transaction-io
+import Hasql.Private.TransactionIO
diff --git a/library/Hasql/TransactionIO/Sessions.hs b/library/Hasql/TransactionIO/Sessions.hs
new file mode 100644
--- /dev/null
+++ b/library/Hasql/TransactionIO/Sessions.hs
@@ -0,0 +1,22 @@
+module Hasql.TransactionIO.Sessions (
+  transactionIO,
+  unpreparedTransactionIO,
+  IsolationLevel(..),
+  Mode(..),
+  Deferrable(..),
+) where
+
+-- hasql
+import Hasql.Session (Session)
+
+-- hasql-streaming
+import Hasql.Private.TransactionIO
+import Hasql.Private.Types
+
+-- | Run a prepared transaction with the given properties
+transactionIO :: IsolationLevel -> Mode -> Deferrable -> TransactionIO a -> Session a
+transactionIO isolation mode deferrable txio = run txio isolation mode deferrable True
+
+-- | Run an unprepared transaction with the given properties
+unpreparedTransactionIO :: IsolationLevel -> Mode -> Deferrable -> TransactionIO a -> Session a
+unpreparedTransactionIO isolation mode deferrable txio = run txio isolation mode deferrable False
