diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,12 @@
+# Version 0.0.2
+
+* Added `FromJSON`, `ToJSON` instances for `Name`.
+
+* Improved type-parameter order in `zero`, `one`, `foldM` and similar.
+
+* Added manual transactional migrations suport.
+
+
 # Version 0.0.1
 
 * Initial version.
diff --git a/lib/Sq.hs b/lib/Sq.hs
--- a/lib/Sq.hs
+++ b/lib/Sq.hs
@@ -3,9 +3,6 @@
 
 -- | High-level SQLite client library
 --
--- ⚠️  __This is an early preview release of this library. Use at your own risk.__
---
---
 -- @
 -- import qualified "Sq"
 -- @
@@ -34,6 +31,8 @@
 -- * Type-safe __resource management__ (via 'A.Acquire', see 'new', 'with',
 -- 'uith').
 --
+-- * Manual __transactional migrations__ ('migrate').
+--
 -- * 'Savepoint's.
 --
 -- * A lot of logging.
@@ -42,14 +41,13 @@
 --
 -- * Type-safe 'SQL'.
 --
--- * Manual and automatic migrations solution.
 --
 -- * Probably other things.
 --
 -- If you have questions or suggestions, just say so at
 -- <https://github.com/k0001/hs-sq/issues>.
 --
--- ⚠️  __This is an early preview release of this library. Use at your own risk.__
+-- Note: This library is young and needs more testing.
 module Sq
    ( -- * Statement
     Statement
@@ -148,6 +146,14 @@
    , savepointRollback
    , savepointRelease
 
+    -- * Migrations
+    -- $migrations
+   , migrate
+   , migration
+   , Migration
+   , MigrationId
+   , MigrationsTable
+
     -- * Miscellaneuos
    , Retry (..)
    , BindingName
@@ -170,15 +176,12 @@
 where
 
 import Control.Exception.Safe qualified as Ex
-import Control.Foldl qualified as F
 import Control.Monad hiding (foldM)
 import Control.Monad.IO.Class
 import Control.Monad.Trans.Resource qualified as R
 import Control.Monad.Trans.Resource.Extra qualified as R
 import Data.Acquire qualified as A
 import Data.Function
-import Data.Int
-import Data.List.NonEmpty (NonEmpty)
 import Database.SQLite3 qualified as S
 import Di.Df1 qualified as Di
 import System.FilePath
@@ -188,6 +191,7 @@
 import Sq.Decoders
 import Sq.Encoders
 import Sq.Input
+import Sq.Migrations
 import Sq.Mode
 import Sq.Names
 import Sq.Null
@@ -290,63 +294,6 @@
 
 --------------------------------------------------------------------------------
 
--- | Executes a 'Statement' expected to return __zero or one__ rows.
---
--- * Throws 'ErrRows_TooMany' if more than one row.
-maybe :: (SubMode t s) => Statement s i o -> i -> Transactional g r t (Maybe o)
-maybe = foldM $ foldMaybeM ErrRows_TooMany
-{-# INLINE maybe #-}
-
--- | Executes a 'Statement' expected to return exactly __one__ row.
---
--- * Throws 'ErrRows_TooFew' if zero rows, 'ErrRows_TooMany' if more than one row.
-one :: (SubMode t s) => Statement s i o -> i -> Transactional g r t o
-one = foldM $ foldOneM ErrRows_TooFew ErrRows_TooMany
-{-# INLINE one #-}
-
--- | Executes a 'Statement' expected to return exactly __zero__ rows.
---
--- * Throws 'ErrRows_TooMany' if more than zero rows.
-zero :: (SubMode t s) => Statement s i o -> i -> Transactional g r t ()
-zero = foldM $ foldZeroM ErrRows_TooMany
-{-# INLINE zero #-}
-
--- | Executes a 'Statement' expected to return __one or more__ rows.
---
--- * Returns the length of the 'NonEmpty' list, too.
---
--- * Throws 'ErrRows_TooFew' if zero rows.
-some
-   :: (SubMode t s)
-   => Statement s i o
-   -> i
-   -> Transactional g r t (Int64, NonEmpty o)
-some = foldM $ foldNonEmptyM ErrRows_TooFew
-{-# INLINE some #-}
-
--- | Executes a 'Statement' expected to return __zero or more__ rows.
---
--- * Returns the length of the list, too.
-list
-   :: (SubMode t s)
-   => Statement s i o
-   -> i
-   -> Transactional g r t (Int64, [o])
-list = fold foldList
-{-# INLINE list #-}
-
--- | __Purely fold__ all the output rows.
-fold
-   :: (SubMode t s)
-   => F.Fold o z
-   -> Statement s i o
-   -> i
-   -> Transactional g r t z
-fold = foldM . F.generalize
-{-# INLINE fold #-}
-
---------------------------------------------------------------------------------
-
 -- | Execute a 'Read'-only 'Transactional' in a fresh 'Transaction' that will
 -- be automatically released when done.
 read
@@ -378,3 +325,41 @@
    -> m a
 rollback p = transactionalRetry $ rollbackTransaction p
 {-# INLINE rollback #-}
+
+--------------------------------------------------------------------------------
+
+-- $migrations
+--
+-- 1. List all the 'Migration's in chronological order.
+--    Each 'Migration' is a 'Transactional' action on the database, identified
+--    by a unique 'MigrationId'. Construct with 'migration'.
+--
+--     @
+--     __migrations__ :: ["Sq".'Migration']
+--     __migrations__ =
+--        [ "Sq".'migration' /\"create users table\"/ createUsersTable
+--        , "Sq".'migration' /\"add email column to users\"/ addUserEmailColumn
+--        , "Sq".'migration' /\"create articles table\"/ createArticlesTable
+--        , / ... more migrations ... /
+--        ]
+--     @
+--
+-- 2. Run any 'Migration's that haven't been run yet, if necessary, by performing
+--    'migrate' once as soon as you obtain your 'Write' connection 'Pool'.
+--    'migrate' will enforce that the 'MigrationId's, be unique, and will
+--    make sure that any migration history in the 'MigrationsTable' is
+--    compatible with the specified 'Migration's.
+--
+--     @
+--     "Sq".'migrate' pool /\"migrations\"/ __migrations__ \\case
+--        []   -> /... No migrations will run. .../
+--        mIds -> /... Some migrations will run. Maybe backup things here? .../
+--     @
+--
+-- 3. __Don't change your 'MigrationId's over time__. If you do, then the
+--    history in 'MigrationsTable' will become unrecognizable by 'migrate'.
+--    Also, avoid having the 'Transactional' code in each 'Migration' use your
+--    domain types and functions, as doing so may force you to alter past
+--    'Transactional' if your domain types and functions change. Ideally,
+--    you should write each 'Migration' in such a way that you never /have/
+--    to modify them in the future.
diff --git a/lib/Sq/Connection.hs b/lib/Sq/Connection.hs
--- a/lib/Sq/Connection.hs
+++ b/lib/Sq/Connection.hs
@@ -561,7 +561,8 @@
 -- Note: This could be defined in terms of 'streamIO', but this implementation
 -- is faster because we avoid per-row resource management.
 foldIO
-   :: (MonadIO m, Ex.MonadMask m, SubMode t s)
+   :: forall o z i t s m
+    . (MonadIO m, Ex.MonadMask m, SubMode t s)
    => F.FoldM m o z
    -> A.Acquire (Transaction t)
    -- ^ How to acquire the 'Transaction' once the @m@ is executed,
@@ -596,7 +597,8 @@
 -- @m@ by means of 'R.runResourceT' or similar as soon as possible in order to
 -- release the 'Transaction' lock.
 streamIO
-   :: (R.MonadResource m, SubMode t s)
+   :: forall o i t s m
+    . (R.MonadResource m, SubMode t s)
    => A.Acquire (Transaction t)
    -- ^ How to acquire the 'Transaction' once the 'Z.Stream' starts
    -- being consumed, and how to release it when it's not needed anymore.
diff --git a/lib/Sq/Migrations.hs b/lib/Sq/Migrations.hs
new file mode 100644
--- /dev/null
+++ b/lib/Sq/Migrations.hs
@@ -0,0 +1,175 @@
+module Sq.Migrations {--}
+   ( migrate
+   , migration
+   , Migration
+   , MigrationId
+   , MigrationsTable
+   ) -- }
+where
+
+import Control.Exception.Safe qualified as Ex
+import Control.Monad
+import Control.Monad.IO.Class
+import Control.Monad.Trans.Resource.Extra qualified as R
+import Data.Aeson qualified as Ae
+import Data.List qualified as List
+import Data.Set (Set)
+import Data.Set qualified as Set
+import Data.String
+import Data.Text qualified as T
+import Data.Time qualified as Time
+import GHC.Records
+
+import Sq.Connection
+import Sq.Decoders
+import Sq.Encoders
+import Sq.Mode
+import Sq.Names
+import Sq.Pool
+import Sq.Statement
+import Sq.Transactional
+
+-- | Name of the database table keeping a registry of executed 'Migration's, by
+-- their 'MigrationId'.
+--
+-- * Same syntax rules as 'Name'.
+--
+-- * This table will be created and updated by 'migrate' as necessary.
+newtype MigrationsTable = MigrationsTable Name
+   deriving newtype (Eq, Ord, Show, IsString, Ae.FromJSON, Ae.ToJSON)
+
+instance HasField "text" MigrationsTable T.Text where
+   getField (MigrationsTable x) = x.text
+
+-- | A single 'Migration' consisting of a 'Transactional' action uniquely
+-- identified by a 'MigrationId'.
+--
+-- * Construct with 'migration'.
+--
+-- * Run through 'migrate'.
+data Migration = Migration MigrationId (Transaction 'Write -> IO ())
+
+instance HasField "id" Migration MigrationId where
+   getField (Migration x _) = x
+
+-- | 'Just' if at least one 'MigrationId' is duplicate.
+duplicateMigrationId :: [Migration] -> Maybe MigrationId
+duplicateMigrationId = go mempty
+  where
+   go :: Set MigrationId -> [Migration] -> Maybe MigrationId
+   go !mIds = \case
+      Migration mId _ : ms
+         | Set.member mId mIds -> Just mId
+         | otherwise -> go (Set.insert mId mIds) ms
+      [] -> Nothing
+
+-- | Define a single 'Migration' that, when executed, will perform
+-- the given 'Transactional'.
+--
+-- * See 'Migration'.
+migration
+   :: MigrationId
+   -> (forall g. Transactional g 'NoRetry 'Write ())
+   -- ^ Notice the 'NoRetry'. In other words, this 'Transactional'
+   -- can't perform 'retry' nor any 'Control.Applicative.Alternative'
+   -- nor 'MonadPlus' features.
+   -> Migration
+migration mId ta = Migration mId \tx -> embed tx ta
+
+-- | Unique identifier for a 'Migration' within a 'MigrationsTable'.
+--
+-- * You are supposed to type these statically, so construct a 'MigrationId'
+-- by typing down the literal string.
+newtype MigrationId = MigrationId T.Text
+   deriving newtype (Eq, Ord, IsString, Show, EncodeDefault, DecodeDefault)
+
+instance HasField "text" MigrationId T.Text where
+   getField (MigrationId x) = x
+
+--------------------------------------------------------------------------------
+
+createMigrationsTable :: MigrationsTable -> Statement 'Write () ()
+createMigrationsTable tbl =
+   -- We are storing the timestamp in case we need it in the future.
+   -- We aren't really using it now.
+   writeStatement mempty mempty $
+      fromString $
+         "CREATE TABLE IF NOT EXISTS "
+            <> show tbl
+            <> " (ord INTEGER PRIMARY KEY NOT NULL CHECK (ord >= 0)"
+            <> ", id TEXT UNIQUE NOT NULL"
+            <> ", time TEXT NOT NULL)"
+
+getMigrations
+   :: MigrationsTable -> Statement 'Read () (Time.UTCTime, MigrationId)
+getMigrations tbl =
+   readStatement mempty (liftA2 (,) "time" "id") $
+      fromString $
+         "SELECT time, id FROM " <> show tbl <> " ORDER BY ord ASC"
+
+pushMigration :: MigrationsTable -> Statement 'Write MigrationId Time.UTCTime
+pushMigration tbl =
+   writeStatement "id" "time" $
+      fromString $
+         "INSERT INTO "
+            <> show tbl
+            <> " (ord, time, id)"
+            <> " SELECT t.ord"
+            <> ", strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now', 'subsecond')"
+            <> ", $id"
+            <> " FROM (SELECT coalesce(max(ord) + 1, 0) AS ord FROM "
+            <> show tbl
+            <> ") AS t"
+            <> " RETURNING time"
+
+--------------------------------------------------------------------------------
+
+-- | Run all the migrations in 'Migration's that haven't been run yet.
+--
+-- * If the 'MigrationId's are not compatible with the current migration
+-- history as reported by 'MigrationsTable', there will be an exception.
+--
+-- * If 'MigrationsTable' doesn't exist, it will be created.
+--
+-- * All the changes are run in a single 'Transaction', including those to
+-- 'MigrationsTable'.
+migrate
+   :: forall a m
+    . (MonadIO m, Ex.MonadMask m)
+   => Pool 'Write
+   -- ^ Connection 'Pool' to the database to migrate.
+   -> MigrationsTable
+   -- ^ Name of the table where the registry of ran 'Migration's is kept.
+   -> [Migration]
+   -- ^ 'Migration's to apply to the database, if necessary, in chronological
+   -- order.
+   -> ([MigrationId] -> m a)
+   -- ^ This will be performed __while the write transaction is active__,
+   -- letting you know which 'MigrationId's are to be performed, and in which
+   -- order.
+   --
+   -- This can be a good place to perform a backup of the database if
+   -- necessary. Presumably, 'migrate' is being run during the initialization
+   -- of your program, suggesting that nobody else is trying to write to the
+   -- database at this time, so it's OK if this code takes a while to run.
+   --
+   -- Don't try to acquire a 'Write' transaction here, it will deadlock.
+   -- It's OK to interact with the 'Pool' through 'Read'-only means.
+   -> m a
+migrate p tbl want k
+   | Just mId <- duplicateMigrationId want =
+      Ex.throwString $ "Duplicate migration id: " <> show mId
+   | otherwise = R.withAcquire (commitTransaction p) \tx -> do
+      pending <- embed tx do
+         zero (createMigrationsTable tbl) ()
+         (nran, ran) <- list (getMigrations tbl) ()
+         case List.stripPrefix (fmap snd ran) (fmap (.id) want) of
+            Just _ -> pure $ List.drop (fromIntegral nran) want
+            Nothing ->
+               Ex.throwString $
+                  "Incompatible migration history: " <> show (fmap snd ran)
+      a <- k $ fmap (.id) pending
+      forM_ pending \(Migration mId f) -> do
+         liftIO $ f tx
+         void $ embed tx $ one (pushMigration tbl) mId
+      pure a
diff --git a/lib/Sq/Names.hs b/lib/Sq/Names.hs
--- a/lib/Sq/Names.hs
+++ b/lib/Sq/Names.hs
@@ -14,10 +14,11 @@
 import Control.Applicative
 import Control.DeepSeq
 import Control.Monad
-import Data.List.NonEmpty (NonEmpty(..))
+import Data.Aeson qualified as Ae
 import Data.Attoparsec.Text qualified as AT
 import Data.Char qualified as Ch
 import Data.Coerce
+import Data.List.NonEmpty (NonEmpty (..))
 import Data.String
 import Data.Text qualified as T
 import GHC.Records
@@ -29,10 +30,13 @@
 --
 -- Construct with 'name' or 'IsString'.
 newtype Name = Name T.Text
-   deriving newtype (Eq, Ord, Show, NFData)
+   deriving newtype (Eq, Ord, Show, NFData, Ae.ToJSON)
 
 instance IsString Name where
    fromString = either error id . name . T.pack
+
+instance Ae.FromJSON Name where
+   parseJSON = Ae.withText "Name" (either fail pure . name)
 
 instance HasField "text" Name T.Text where getField = coerce
 
diff --git a/lib/Sq/Transactional.hs b/lib/Sq/Transactional.hs
--- a/lib/Sq/Transactional.hs
+++ b/lib/Sq/Transactional.hs
@@ -2,6 +2,12 @@
    ( Transactional
    , embed
    , transactionalRetry
+   , one
+   , maybe
+   , zero
+   , some
+   , list
+   , fold
    , foldM
    , Ref
    , Retry (..)
@@ -9,7 +15,7 @@
    , orElse
    ) where
 
-import Control.Applicative
+import Control.Applicative hiding (some)
 import Control.Concurrent
 import Control.Concurrent.STM hiding (orElse, retry)
 import Control.Exception.Safe qualified as Ex
@@ -24,9 +30,12 @@
 import Control.Monad.Trans.Resource.Extra qualified as R hiding (runResourceT)
 import Data.Acquire qualified as A
 import Data.Coerce
+import Data.Int
 import Data.IntMap.Strict (IntMap)
 import Data.IntMap.Strict qualified as IntMap
 import Data.Kind
+import Data.List.NonEmpty (NonEmpty)
+import Prelude hiding (Read, maybe, read)
 
 import Sq.Connection
 import Sq.Mode
@@ -216,7 +225,8 @@
 --
 -- * For a non-'Transactional' version of this function, see 'Sq.foldIO'.
 foldM
-   :: (SubMode t s)
+   :: forall o z i t s g r
+    . (SubMode t s)
    => F.FoldM (Transactional g r t) o z
    -> Statement s i o
    -> i
@@ -388,3 +398,78 @@
                writeTVar tv $! Just a1
                pure b
             Nothing -> Ex.throwM $ resourceVanishedWithCallStack "Ref"
+
+--------------------------------------------------------------------------------
+
+-- | Executes a 'Statement' expected to return __zero or one__ rows.
+--
+-- * Throws 'ErrRows_TooMany' if more than one row.
+maybe
+   :: forall o i t s g r
+    . (SubMode t s)
+   => Statement s i o
+   -> i
+   -> Transactional g r t (Maybe o)
+maybe = foldM $ foldMaybeM ErrRows_TooMany
+{-# INLINE maybe #-}
+
+-- | Executes a 'Statement' expected to return exactly __one__ row.
+--
+-- * Throws 'ErrRows_TooFew' if zero rows, 'ErrRows_TooMany' if more than one row.
+one
+   :: forall o i t s g r
+    . (SubMode t s)
+   => Statement s i o
+   -> i
+   -> Transactional g r t o
+one = foldM $ foldOneM ErrRows_TooFew ErrRows_TooMany
+{-# INLINE one #-}
+
+-- | Executes a 'Statement' expected to return exactly __zero__ rows.
+--
+-- * Throws 'ErrRows_TooMany' if more than zero rows.
+zero
+   :: forall o i t s g r
+    . (SubMode t s)
+   => Statement s i o
+   -> i
+   -> Transactional g r t ()
+zero = foldM $ foldZeroM ErrRows_TooMany
+{-# INLINE zero #-}
+
+-- | Executes a 'Statement' expected to return __one or more__ rows.
+--
+-- * Returns the length of the 'NonEmpty' list, too.
+--
+-- * Throws 'ErrRows_TooFew' if zero rows.
+some
+   :: forall o i t s g r
+    . (SubMode t s)
+   => Statement s i o
+   -> i
+   -> Transactional g r t (Int64, NonEmpty o)
+some = foldM $ foldNonEmptyM ErrRows_TooFew
+{-# INLINE some #-}
+
+-- | Executes a 'Statement' expected to return __zero or more__ rows.
+--
+-- * Returns the length of the list, too.
+list
+   :: forall o i t s g r
+    . (SubMode t s)
+   => Statement s i o
+   -> i
+   -> Transactional g r t (Int64, [o])
+list = fold foldList
+{-# INLINE list #-}
+
+-- | __Purely fold__ all the output rows.
+fold
+   :: forall o z i t s g r
+    . (SubMode t s)
+   => F.Fold o z
+   -> Statement s i o
+   -> i
+   -> Transactional g r t z
+fold = foldM . F.generalize
+{-# INLINE fold #-}
diff --git a/sq.cabal b/sq.cabal
--- a/sq.cabal
+++ b/sq.cabal
@@ -1,6 +1,6 @@
 cabal-version: 2.4
 name: sq
-version: 0.0.1
+version: 0.0.2
 license: Apache-2.0
 license-file: LICENSE
 extra-source-files: README.md CHANGELOG.md
@@ -45,6 +45,7 @@
     Sq.Decoders
     Sq.Encoders
     Sq.Input
+    Sq.Migrations
     Sq.Mode
     Sq.Names
     Sq.Null
diff --git a/test/Sq/Test.hs b/test/Sq/Test.hs
--- a/test/Sq/Test.hs
+++ b/test/Sq/Test.hs
@@ -1,6 +1,5 @@
 module Sq.Test (tree) where
 
-import Control.Applicative
 import Control.Concurrent
 import Control.Concurrent.Async qualified as Async
 import Control.Exception.Safe qualified as Ex
@@ -10,18 +9,17 @@
 import Control.Monad.Trans.Resource.Extra qualified as R
 import Data.Acquire.Internal qualified as A
 import Data.Foldable
-import Data.Text qualified as T
 import Di qualified
 import Hedgehog qualified as H
 import Hedgehog.Gen qualified as H
 import Hedgehog.Range qualified as HR
-import Sq qualified
 import System.Timeout
 import Test.Tasty (testGroup, withResource)
 import Test.Tasty.HUnit (testCase, (@?=))
 import Test.Tasty.Hedgehog (testProperty)
 import Test.Tasty.Runners (TestTree)
 
+import Sq qualified
 import Sq.Test.Codec qualified
 
 --------------------------------------------------------------------------------
@@ -55,6 +53,7 @@
          ysLen H.=== fromIntegral (length ys)
          xs H.=== ys
       , testExample1 di
+      , testMigs di
       ]
 
 withAcquire :: A.Acquire a -> (IO a -> TestTree) -> TestTree
@@ -132,3 +131,45 @@
             timeout 500_000 (example1 pool) >>= \case
                Just () -> pure ()
                Nothing -> fail "example1: timeout!"
+
+--------------------------------------------------------------------------------
+
+testMigs :: Di.Df1 -> TestTree
+testMigs di0 = testCase "migs" do
+   Sq.with (Sq.tempPool di0) \pool -> do
+      Sq.migrate pool "migs" migsAB \ids -> ids @?= ["A", "B"]
+      Sq.migrate pool "migs" migsAB \ids -> ids @?= []
+      for_ [migsCD, []] \migs ->
+         Ex.catchJust
+            ( \case
+               Ex.StringException m _ ->
+                  guard $ m == "Incompatible migration history: [\"A\",\"B\"]"
+            )
+            (Sq.migrate pool "migs" migs mempty)
+            pure
+      Sq.migrate pool "migs" migsAB \ids -> ids @?= []
+      Sq.migrate pool "migs" (migsAB <> migsCD) \ids -> ids @?= ["C", "D"]
+      Sq.migrate pool "migs" (migsAB <> migsCD) \ids -> ids @?= []
+      Sq.read pool do
+         (2, [7, 8 :: Int]) <-
+            Sq.list
+               (Sq.readStatement mempty "x" "SELECT x FROM t ORDER BY x ASC")
+               ()
+         pure ()
+  where
+   migsAB :: [Sq.Migration]
+   migsAB =
+      [ Sq.migration "A" (pure ())
+      , Sq.migration "B" (pure ())
+      ]
+   migsCD :: [Sq.Migration]
+   migsCD =
+      [ Sq.migration "C" do
+         Sq.zero @()
+            (Sq.writeStatement mempty mempty "CREATE TABLE t (x INTEGER)")
+            ()
+      , Sq.migration "D" do
+         Sq.zero @()
+            (Sq.writeStatement mempty mempty "INSERT INTO t (x) VALUES (7), (8)")
+            ()
+      ]
