diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,15 @@
+# Changelog
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [0.1.0.0] - 2026-04-25
+
+Initial release.
+[Effectful](https://hackage.haskell.org/package/effectful) adapter for
+[`valiant`](https://hackage.haskell.org/package/valiant). Provides a
+dynamic `Valiant` effect with pool-based (`runValiant`) and custom
+(`runValiantWith`) handlers, plus smart constructors covering query,
+command, and transaction operations.
+
+[0.1.0.0]: https://github.com/joshburgess/valiant/releases/tag/v0.1.0.0
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2026 Josh Burgess
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+   this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from this
+   software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,43 @@
+# valiant-effectful
+
+[Effectful](https://hackage.haskell.org/package/effectful) adapter for
+[`valiant`](https://hackage.haskell.org/package/valiant).
+
+Provides a dynamic `Valiant` effect that any `Eff` action can request
+via `Valiant :> es`, plus a pool-based handler.
+
+## Quick start
+
+```haskell
+import Effectful
+import Valiant (newPool, defaultPoolConfig, poolConnString)
+import Valiant.Effectful
+
+myApp :: (Valiant :> es, IOE :> es) => Eff es [User]
+myApp = do
+  users <- fetchAllEff listUsers ()
+  count <- fetchScalarEff countUsers ()
+  liftIO $ putStrLn $ "got " <> show count <> " users"
+  pure users
+
+main :: IO ()
+main = do
+  pool <- newPool defaultPoolConfig { poolConnString = "postgres://..." }
+  users <- runEff . runValiant pool $ myApp
+  print users
+```
+
+## What you get
+
+- Effect: `data Valiant :: Effect`
+- Handlers: `runValiant pool` and `runValiantWith` (custom handler,
+  useful for tests)
+- Query operations: `fetchOneEff`, `fetchAllEff`, `fetchScalarEff`,
+  `fetchOneOrThrowEff`, `fetchExistsEff`
+- Command operations: `executeEff`, `executeReturningEff`,
+  `executeBatchEff`
+- Transactions: `withTransactionEff`, `withTransactionLevelEff`
+- Raw access: `withConnectionEff`
+
+See the [valiant tutorial](https://github.com/joshburgess/valiant/blob/main/docs/TUTORIAL.md)
+for the underlying `Statement` and `valiant prepare` workflow.
diff --git a/src/Valiant/Effectful.hs b/src/Valiant/Effectful.hs
new file mode 100644
--- /dev/null
+++ b/src/Valiant/Effectful.hs
@@ -0,0 +1,198 @@
+-- | Effectful adapter for valiant.
+--
+-- Provides an @Valiant@ effect for database operations, with a pool-based
+-- handler that manages connection acquisition and release automatically.
+--
+-- @
+-- import Effectful
+-- import Valiant.Effectful
+--
+-- myApp :: (Valiant :> es, IOE :> es) => Eff es [User]
+-- myApp = do
+--   users <- fetchAllEff listUsers ()
+--   count <- fetchScalarEff countUsers ()
+--   pure users
+--
+-- main :: IO ()
+-- main = do
+--   pool <- newPool defaultPoolConfig { poolConnString = "..." }
+--   result <- runEff . runValiant pool $ myApp
+--   print result
+-- @
+module Valiant.Effectful
+  ( -- * Effect
+    Valiant (..)
+
+    -- * Handler
+  , runValiant
+  , runValiantWith
+
+    -- * Query operations
+  , fetchOneEff
+  , fetchAllEff
+  , fetchScalarEff
+  , fetchOneOrThrowEff
+  , fetchExistsEff
+
+    -- * Command operations
+  , executeEff
+  , executeReturningEff
+  , executeBatchEff
+
+    -- * Transaction operations
+  , withTransactionEff
+  , withTransactionLevelEff
+
+    -- * Raw access
+  , withConnectionEff
+  ) where
+
+import Data.Int (Int64)
+import Effectful
+import Effectful.Dispatch.Dynamic
+import Valiant (Connection, IsolationLevel, Pool, Statement, Transaction (..))
+import Valiant qualified
+import PgWire.Pool (withResource)
+
+------------------------------------------------------------------------
+-- Effect definition
+------------------------------------------------------------------------
+
+-- | The Valiant database effect.
+--
+-- All operations acquire a connection from the pool, execute, and release.
+-- Transactions hold a connection for the duration of the callback.
+data Valiant :: Effect where
+  FetchOneEff :: Statement p r -> p -> Valiant m (Maybe r)
+  FetchAllEff :: Statement p r -> p -> Valiant m [r]
+  FetchScalarEff :: Statement p r -> p -> Valiant m r
+  FetchOneOrThrowEff :: Statement p r -> p -> Valiant m r
+  FetchExistsEff :: Statement p r -> p -> Valiant m Bool
+  ExecuteEff :: Statement p () -> p -> Valiant m Int64
+  ExecuteReturningEff :: Statement p r -> p -> Valiant m (Int64, [r])
+  ExecuteBatchEff :: Statement p () -> [p] -> Valiant m Int64
+  WithTransactionEff :: (Transaction -> IO a) -> Valiant m a
+  WithTransactionLevelEff :: IsolationLevel -> (Transaction -> IO a) -> Valiant m a
+  WithConnectionEff :: (Connection -> IO a) -> Valiant m a
+
+type instance DispatchOf Valiant = 'Dynamic
+
+------------------------------------------------------------------------
+-- Handler
+------------------------------------------------------------------------
+
+-- | Run the Valiant effect with a connection pool.
+--
+-- @
+-- main = do
+--   pool <- newPool defaultPoolConfig { poolConnString = "..." }
+--   runEff . runValiant pool $ do
+--     users <- fetchAllEff listUsers ()
+--     liftIO $ print users
+-- @
+runValiant :: (IOE :> es) => Pool -> Eff (Valiant : es) a -> Eff es a
+runValiant pool = interpret $ \_ -> \case
+  FetchOneEff stmt params ->
+    liftIO $ withResource pool $ \conn -> Valiant.fetchOne conn stmt params
+  FetchAllEff stmt params ->
+    liftIO $ withResource pool $ \conn -> Valiant.fetchAll conn stmt params
+  FetchScalarEff stmt params ->
+    liftIO $ withResource pool $ \conn -> Valiant.fetchScalar conn stmt params
+  FetchOneOrThrowEff stmt params ->
+    liftIO $ withResource pool $ \conn -> Valiant.fetchOneOrThrow conn stmt params
+  FetchExistsEff stmt params ->
+    liftIO $ withResource pool $ \conn -> Valiant.fetchExists conn stmt params
+  ExecuteEff stmt params ->
+    liftIO $ withResource pool $ \conn -> Valiant.execute conn stmt params
+  ExecuteReturningEff stmt params ->
+    liftIO $ withResource pool $ \conn -> Valiant.executeReturning conn stmt params
+  ExecuteBatchEff stmt paramsList ->
+    liftIO $ withResource pool $ \conn -> Valiant.executeBatch conn stmt paramsList
+  WithTransactionEff action ->
+    liftIO $ Valiant.withTransaction pool action
+  WithTransactionLevelEff level action ->
+    liftIO $ Valiant.withTransactionLevel level pool action
+  WithConnectionEff action ->
+    liftIO $ withResource pool action
+
+-- | Run the Valiant effect with a custom handler function.
+-- Useful for testing with mock connections.
+runValiantWith
+  :: (IOE :> es)
+  => (forall x. (Connection -> IO x) -> IO x)
+  -- ^ Connection provider (e.g., 'withResource pool' or a mock)
+  -> (forall x. (Transaction -> IO x) -> IO x)
+  -- ^ Transaction provider
+  -> Eff (Valiant : es) a
+  -> Eff es a
+runValiantWith withConn withTxn = interpret $ \_ -> \case
+  FetchOneEff stmt params ->
+    liftIO $ withConn $ \conn -> Valiant.fetchOne conn stmt params
+  FetchAllEff stmt params ->
+    liftIO $ withConn $ \conn -> Valiant.fetchAll conn stmt params
+  FetchScalarEff stmt params ->
+    liftIO $ withConn $ \conn -> Valiant.fetchScalar conn stmt params
+  FetchOneOrThrowEff stmt params ->
+    liftIO $ withConn $ \conn -> Valiant.fetchOneOrThrow conn stmt params
+  FetchExistsEff stmt params ->
+    liftIO $ withConn $ \conn -> Valiant.fetchExists conn stmt params
+  ExecuteEff stmt params ->
+    liftIO $ withConn $ \conn -> Valiant.execute conn stmt params
+  ExecuteReturningEff stmt params ->
+    liftIO $ withConn $ \conn -> Valiant.executeReturning conn stmt params
+  ExecuteBatchEff stmt paramsList ->
+    liftIO $ withConn $ \conn -> Valiant.executeBatch conn stmt paramsList
+  WithTransactionEff action ->
+    liftIO $ withTxn action
+  WithTransactionLevelEff _level action ->
+    liftIO $ withTxn action  -- level ignored in custom handler
+  WithConnectionEff action ->
+    liftIO $ withConn action
+
+------------------------------------------------------------------------
+-- Smart constructors
+------------------------------------------------------------------------
+
+-- | Fetch zero or one row.
+fetchOneEff :: (Valiant :> es) => Statement p r -> p -> Eff es (Maybe r)
+fetchOneEff stmt params = send (FetchOneEff stmt params)
+
+-- | Fetch all rows.
+fetchAllEff :: (Valiant :> es) => Statement p r -> p -> Eff es [r]
+fetchAllEff stmt params = send (FetchAllEff stmt params)
+
+-- | Fetch exactly one scalar value. Throws on zero or multiple rows.
+fetchScalarEff :: (Valiant :> es) => Statement p r -> p -> Eff es r
+fetchScalarEff stmt params = send (FetchScalarEff stmt params)
+
+-- | Fetch one row, throwing if none returned.
+fetchOneOrThrowEff :: (Valiant :> es) => Statement p r -> p -> Eff es r
+fetchOneOrThrowEff stmt params = send (FetchOneOrThrowEff stmt params)
+
+-- | Check if a query returns any rows.
+fetchExistsEff :: (Valiant :> es) => Statement p r -> p -> Eff es Bool
+fetchExistsEff stmt params = send (FetchExistsEff stmt params)
+
+-- | Execute a command. Returns rows affected.
+executeEff :: (Valiant :> es) => Statement p () -> p -> Eff es Int64
+executeEff stmt params = send (ExecuteEff stmt params)
+
+-- | Execute with RETURNING. Returns rows affected and decoded rows.
+executeReturningEff :: (Valiant :> es) => Statement p r -> p -> Eff es (Int64, [r])
+executeReturningEff stmt params = send (ExecuteReturningEff stmt params)
+
+-- | Execute a batch of commands (pipelined). Returns total rows affected.
+executeBatchEff :: (Valiant :> es) => Statement p () -> [p] -> Eff es Int64
+executeBatchEff stmt paramsList = send (ExecuteBatchEff stmt paramsList)
+
+-- | Run an action in a transaction.
+withTransactionEff :: (Valiant :> es) => (Transaction -> IO a) -> Eff es a
+withTransactionEff action = send (WithTransactionEff action)
+
+-- | Run an action in a transaction with a specific isolation level.
+withTransactionLevelEff :: (Valiant :> es) => IsolationLevel -> (Transaction -> IO a) -> Eff es a
+withTransactionLevelEff level action = send (WithTransactionLevelEff level action)
+
+-- | Run an action with a raw connection from the pool.
+withConnectionEff :: (Valiant :> es) => (Connection -> IO a) -> Eff es a
+withConnectionEff action = send (WithConnectionEff action)
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,88 @@
+module Main where
+
+import Data.ByteString (ByteString)
+import Data.ByteString.Char8 qualified as BS8
+import Data.Int (Int32)
+import Data.Text (Text)
+import Effectful
+import Valiant (defaultPoolConfig, newPool, closePool, txConn)
+import Valiant qualified
+import Valiant.Effectful
+import PgWire.Pool.Config (PoolConfig (..))
+import System.Environment (lookupEnv)
+import Test.Hspec
+import TestSupport
+
+main :: IO ()
+main = hspec $ do
+  describe "Valiant.Effectful" $ do
+    describe "runValiant" $ do
+      it "fetchAllEff returns all rows" $ do
+        withTestConnection $ \conn -> withSchema conn $ do
+          insertTestUsers conn
+          pool <- mkPool
+          result <- runEff . runValiant pool $ fetchAllEff stmtListAll ()
+          closePool pool
+          length result `shouldBe` 5
+
+      it "fetchOneEff returns Just for existing row" $ do
+        withTestConnection $ \conn -> withSchema conn $ do
+          insertTestUsers conn
+          pool <- mkPool
+          result <- runEff . runValiant pool $ fetchOneEff stmtSelectOne 1
+          closePool pool
+          case result of
+            Just (_, name, _) -> name `shouldBe` "Alice"
+            Nothing -> expectationFailure "Expected a row"
+
+      it "fetchOneEff returns Nothing for missing row" $ do
+        withTestConnection $ \conn -> withSchema conn $ do
+          pool <- mkPool
+          result <- runEff . runValiant pool $ fetchOneEff stmtSelectOne (-1)
+          closePool pool
+          result `shouldBe` (Nothing :: Maybe (Int32, Text, Maybe Text))
+
+      it "executeEff returns rows affected" $ do
+        withTestConnection $ \conn -> withSchema conn $ do
+          insertTestUsers conn
+          pool <- mkPool
+          let stmtDelete = Valiant.mkStatement "DELETE FROM users_effectful WHERE id = $1" [23] [] "<test>" :: Valiant.Statement Int32 ()
+          n <- runEff . runValiant pool $ executeEff stmtDelete (1 :: Int32)
+          closePool pool
+          n `shouldBe` 1
+
+      it "withTransactionEff commits" $ do
+        withTestConnection $ \conn -> withSchema conn $ do
+          pool <- mkPool
+          let stmtInsert = Valiant.mkStatement "INSERT INTO users_effectful (name, email) VALUES ($1, $2)" [25, 25] [] "<test>"
+              stmtCount = Valiant.mkStatement "SELECT count(*)::int4 FROM users_effectful" [] ["count"] "<test>" :: Valiant.Statement () Int32
+          _ <- runEff . runValiant pool $
+            withTransactionEff $ \tx ->
+              Valiant.execute (txConn tx) stmtInsert ("EffUser" :: Text, Just ("eff@test.com" :: Text))
+          count <- runEff . runValiant pool $ fetchScalarEff stmtCount ()
+          closePool pool
+          count `shouldBe` 1
+
+      it "composes multiple operations" $ do
+        withTestConnection $ \conn -> withSchema conn $ do
+          insertTestUsers conn
+          pool <- mkPool
+          (users, exists) <- runEff . runValiant pool $ do
+            us <- fetchAllEff stmtListAll ()
+            ex <- fetchExistsEff stmtSelectOne 1
+            pure (us, ex)
+          closePool pool
+          length users `shouldBe` 5
+          exists `shouldBe` True
+
+mkPool :: IO Valiant.Pool
+mkPool = do
+  url <- requireDatabaseUrl'
+  newPool defaultPoolConfig { poolConnString = url, poolSize = 2, poolAcquireTimeout = 5 }
+
+requireDatabaseUrl' :: IO ByteString
+requireDatabaseUrl' = do
+  mUrl <- lookupEnv "DATABASE_URL"
+  case mUrl of
+    Just url -> pure (BS8.pack url)
+    Nothing -> error "DATABASE_URL is not set."
diff --git a/test/TestSupport.hs b/test/TestSupport.hs
new file mode 100644
--- /dev/null
+++ b/test/TestSupport.hs
@@ -0,0 +1,63 @@
+module TestSupport
+  ( withTestConnection
+  , withSchema
+  , insertTestUsers
+  , stmtListAll
+  , stmtSelectOne
+  ) where
+
+import Data.ByteString (ByteString)
+import Data.ByteString.Char8 qualified as BS8
+import Data.Int (Int32)
+import Data.Text (Text)
+import Valiant
+import System.Environment (lookupEnv)
+
+requireDatabaseUrl :: IO ByteString
+requireDatabaseUrl = do
+  mUrl <- lookupEnv "DATABASE_URL"
+  case mUrl of
+    Just url -> pure (BS8.pack url)
+    Nothing -> error "DATABASE_URL is not set."
+
+withTestConnection :: (Connection -> IO a) -> IO a
+withTestConnection action = do
+  url <- requireDatabaseUrl
+  conn <- connectString url
+  result <- action conn
+  close conn
+  pure result
+
+withSchema :: Connection -> IO a -> IO a
+withSchema conn action = do
+  _ <- simpleQuery conn "DROP TABLE IF EXISTS users_effectful CASCADE"
+  _ <- simpleQuery conn
+    "CREATE TABLE users_effectful (\
+    \  id SERIAL PRIMARY KEY,\
+    \  name TEXT NOT NULL,\
+    \  email TEXT\
+    \)"
+  result <- action
+  _ <- simpleQuery conn "DROP TABLE IF EXISTS users_effectful CASCADE"
+  pure result
+
+insertTestUsers :: Connection -> IO ()
+insertTestUsers conn = do
+  _ <- simpleQuery conn
+    "INSERT INTO users_effectful (name, email) VALUES \
+    \('Alice', 'alice@example.com'),\
+    \('Bob', 'bob@example.com'),\
+    \('Carol', NULL),\
+    \('Dave', 'dave@example.com'),\
+    \('Eve', 'eve@example.com')"
+  pure ()
+
+stmtListAll :: Statement () (Int32, Text)
+stmtListAll = mkStatement
+  "SELECT id, name FROM users_effectful ORDER BY id"
+  [] ["id", "name"] "<test>"
+
+stmtSelectOne :: Statement Int32 (Int32, Text, Maybe Text)
+stmtSelectOne = mkStatement
+  "SELECT id, name, email FROM users_effectful WHERE id = $1"
+  [23] ["id", "name", "email"] "<test>"
diff --git a/valiant-effectful.cabal b/valiant-effectful.cabal
new file mode 100644
--- /dev/null
+++ b/valiant-effectful.cabal
@@ -0,0 +1,100 @@
+cabal-version:   3.0
+name:            valiant-effectful
+version:         0.1.0.0
+synopsis:        Effectful adapter for valiant
+description:
+  Provides an @Valiant@ effect for the @effectful@ effect system, enabling
+  compile-time checked SQL queries as an effect with pool-based handlers.
+
+  @
+  import Valiant.Effectful
+
+  myApp :: (Valiant :> es, IOE :> es) => Eff es [User]
+  myApp = do
+    users <- fetchAllEff listUsers ()
+    forM_ users $ \\u -> executeEff updateLastSeen (userId u)
+    pure users
+
+  main :: IO ()
+  main = do
+    pool <- newPool defaultPoolConfig { poolConnString = "..." }
+    runEff . runValiant pool $ myApp
+  @
+license:         BSD-3-Clause
+license-file:    LICENSE
+author:          Josh Burgess
+maintainer:      joshburgess.webdev@gmail.com
+category:        Database
+homepage:        https://github.com/joshburgess/valiant
+bug-reports:     https://github.com/joshburgess/valiant/issues
+build-type:      Simple
+extra-doc-files:
+  README.md
+  CHANGELOG.md
+tested-with:     GHC ==9.10.3
+
+source-repository head
+  type:     git
+  location: https://github.com/joshburgess/valiant
+  subdir:   adapters/valiant-effectful
+
+flag werror
+  description: Enable -Werror for development builds.
+  default:     False
+  manual:      True
+
+common warnings
+  ghc-options: -Wall -Wcompat -Wno-unticked-promoted-constructors -funbox-strict-fields -fspecialise-aggressively
+  if flag(werror)
+    ghc-options: -Werror
+
+library
+  import:           warnings
+  hs-source-dirs:   src
+  default-language: GHC2021
+  default-extensions:
+    DataKinds
+    DerivingStrategies
+    GADTs
+    LambdaCase
+    OverloadedStrings
+    StrictData
+    TypeFamilies
+    TypeOperators
+
+  exposed-modules:
+    Valiant.Effectful
+
+  build-depends:
+    , base            >=4.17 && <5
+    , effectful-core  >=2.3  && <2.6
+    , valiant         >=0.1  && <0.2
+    , pg-wire         >=0.1  && <0.2
+    , vector          >=0.12 && <0.14
+
+test-suite valiant-effectful-test
+  import:           warnings
+  type:             exitcode-stdio-1.0
+  hs-source-dirs:   test
+  main-is:          Main.hs
+  default-language: GHC2021
+  default-extensions:
+    DataKinds
+    GADTs
+    OverloadedStrings
+    TypeOperators
+
+  ghc-options: -rtsopts "-with-rtsopts=-K8K"
+
+  build-depends:
+    , base            >=4.17 && <5
+    , bytestring      >=0.11 && <0.13
+    , effectful-core   >=2.3  && <2.6
+    , hspec           >=2.11 && <2.13
+    , valiant
+    , valiant-effectful
+    , pg-wire
+    , text            >=2.0  && <2.2
+
+  other-modules:
+    TestSupport
