diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,14 @@
+# Revision history for db-pipe
+
+## 0.1.0.1  2026-06-08
+
+First Hackage release. Previously this package only lived as a
+vendored subtree inside the `hbs2` repository; the source moves to
+`github.com/NCrashed/db-pipe` for independent maintenance.
+
+  - No source changes from the vendored 0.1.0.1.
+  - Cabal metadata filled in: synopsis, description, homepage,
+    bug-reports, source-repository, `tested-with`, explicit version
+    bounds on every dependency.
+  - Maintainer is now `Anton Gushcha <ncrashed@gmail.com>`;
+    original authorship by Dmitry Zuykov is preserved.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2023, Dmitry Zuykov
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * 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.
+
+    * Neither the name of Dmitry Zuykov nor the names of other
+      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
+OWNER 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,24 @@
+# db-pipe
+
+A pipe-like wrapper around [`sqlite-simple`](https://hackage.haskell.org/package/sqlite-simple).
+
+## What it does
+
+`db-pipe` wraps `sqlite-simple` in a small monadic pipe abstraction.
+A long-lived `DBPipeEnv` owns a connection, a transaction handle,
+and a couple of STM channels; individual queries run inside a
+`DBPipeM` monad that batches `select`, `update`, `insert`, and
+`ddl` statements into explicit transactional blocks
+(`transactional`, `transactional_`, `commitAll`).
+
+## Where it came from
+
+Originally written by Dmitry Zuykov (voidlizard) inside the
+[`hbs2`](https://github.com/NCrashed/hbs2) project. This Hackage
+release is published from `github.com/NCrashed/db-pipe` so
+downstream projects can depend on it without vendoring.
+
+## License
+
+BSD-3-Clause; see `LICENSE`. Copyright holders are listed in the
+cabal file.
diff --git a/db-pipe.cabal b/db-pipe.cabal
new file mode 100644
--- /dev/null
+++ b/db-pipe.cabal
@@ -0,0 +1,85 @@
+cabal-version:      3.0
+name:               db-pipe
+version:            0.1.0.1
+synopsis:           A pipe-like wrapper around sqlite-simple
+description:
+    @db-pipe@ wraps @sqlite-simple@ in a small monadic pipe
+    abstraction: a long-lived environment (@DBPipeEnv@) owns a
+    connection, a transaction handle, and a couple of STM channels,
+    and individual queries run inside a @DBPipeM@ monad that batches
+    @select@, @update@, @insert@, and @ddl@ statements into explicit
+    transactional blocks (@transactional@, @transactional_@,
+    @commitAll@). Originally written by Dmitry Zuykov ("voidlizard")
+    for the @hbs2@ project; this Hackage release is published from
+    <https://github.com/NCrashed/db-pipe> so downstream projects can
+    depend on it without vendoring.
+license:            BSD-3-Clause
+license-file:       LICENSE
+author:             Dmitry Zuykov
+maintainer:         Anton Gushcha <ncrashed@gmail.com>
+copyright:          (c) Dmitry Zuykov 2023, (c) Anton Gushcha 2026
+category:           Database
+build-type:         Simple
+tested-with:        GHC == 9.6.6
+extra-doc-files:    CHANGELOG.md
+                    README.md
+homepage:           https://github.com/NCrashed/db-pipe
+bug-reports:        https://github.com/NCrashed/db-pipe/issues
+
+source-repository head
+  type:     git
+  location: https://github.com/NCrashed/db-pipe.git
+
+common common-properties
+  ghc-options: -Wall
+
+  default-language:   GHC2021
+
+  default-extensions:
+                       ApplicativeDo
+                     , BangPatterns
+                     , BlockArguments
+                     , ConstraintKinds
+                     , DataKinds
+                     , DeriveDataTypeable
+                     , DeriveGeneric
+                     , DerivingStrategies
+                     , DerivingVia
+                     , ExtendedDefaultRules
+                     , FlexibleContexts
+                     , FlexibleInstances
+                     , GADTs
+                     , GeneralizedNewtypeDeriving
+                     , ImportQualifiedPost
+                     , LambdaCase
+                     , MultiParamTypeClasses
+                     , OverloadedStrings
+                     , OverloadedLabels
+                     , QuasiQuotes
+                     , RankNTypes
+                     , RecordWildCards
+                     , RecursiveDo
+                     , ScopedTypeVariables
+                     , StandaloneDeriving
+                     , TupleSections
+                     , TypeApplications
+                     , TypeFamilies
+                     , TypeOperators
+
+library
+    import:           common-properties
+    exposed-modules:
+      DBPipe.SQLite
+      DBPipe.SQLite.Types
+      DBPipe.SQLite.Generic
+
+    build-depends:    base                     >= 4.17     && < 5
+                    , clock                    >= 0.8      && < 0.9
+                    , interpolatedstring-perl6 >= 1.0      && < 1.1
+                    , mtl                      >= 2.3      && < 2.4
+                    , stm                      >= 2.5      && < 2.6
+                    , sqlite-simple            >= 0.4.18   && < 0.5
+                    , text                     >= 2.0      && < 2.2
+                    , unliftio                 >= 0.2.24   && < 0.3
+
+    hs-source-dirs:   lib
diff --git a/lib/DBPipe/SQLite.hs b/lib/DBPipe/SQLite.hs
new file mode 100644
--- /dev/null
+++ b/lib/DBPipe/SQLite.hs
@@ -0,0 +1,189 @@
+{-# Language AllowAmbiguousTypes #-}
+module DBPipe.SQLite
+  ( module Database.SQLite.Simple
+  , ToField(..)
+  , FromField(..)
+  , ToRow(..)
+  , FromRow(..)
+  , DBPipeEnv
+  , DBPipeOpts(..)
+  , dbPipeOptsDef
+  , runPipe
+  , newDBPipeEnv
+  , DBPipeM
+  , select, select_
+  , update, update_
+  , insert, insert_
+  , ddl
+  , transactional
+  , transactional_
+  , commitAll
+  , withDB
+  , shutdown
+  ) where
+
+import Control.Concurrent
+import Control.Concurrent.STM (flushTQueue)
+import Control.Monad
+import Control.Monad.Reader
+import Database.SQLite.Simple
+import Database.SQLite.Simple.ToField
+import Database.SQLite.Simple.FromField
+import Data.Fixed
+import System.Clock
+import Text.InterpolatedString.Perl6 (qc)
+import System.IO (hPrint)
+import Data.Kind()
+import Data.String
+import UnliftIO
+
+data DBPipeOpts =
+  DBPipeOpts
+  { dbPipeBatchTime :: Fixed E2
+  , dbLogger        :: String -> IO ()
+  }
+
+data DBPipeEnv =
+  DBPipeEnv
+  { opts         :: DBPipeOpts
+  , connPath     :: FilePath
+  , connection   :: TVar (Maybe Connection)
+  , transNum     :: TVar Int
+  , updates      :: TQueue (IO ())
+  , updatesCount :: TVar Int
+  , updatedLast  :: TVar (Maybe TimeSpec)
+  }
+
+newtype DBPipeM m a = DBPipeM { fromDBPipeM :: ReaderT DBPipeEnv m a }
+                      deriving newtype ( Applicative
+                                       , Functor
+                                       , Monad
+                                       , MonadReader DBPipeEnv
+                                       , MonadIO
+                                       , MonadUnliftIO
+                                       , MonadTrans
+                                       )
+
+dbPipeOptsDef :: DBPipeOpts
+dbPipeOptsDef = DBPipeOpts 1 (liftIO . hPrint stderr)
+
+newDBPipeEnv :: MonadIO m => DBPipeOpts -> FilePath -> m DBPipeEnv
+newDBPipeEnv opts fp = liftIO $ do
+  DBPipeEnv opts fp <$> newTVarIO Nothing
+                    <*> newTVarIO 0
+                    <*> newTQueueIO
+                    <*> newTVarIO 0
+                    <*> newTVarIO Nothing
+
+withDB :: forall a m . MonadIO m => DBPipeEnv -> DBPipeM m a -> m a
+withDB env action = runReaderT (fromDBPipeM action) env
+
+runPipe :: forall m . MonadIO m => DBPipeEnv -> m ()
+runPipe env@(DBPipeEnv{..}) = do
+  forever $ do
+    liftIO $ threadDelay (round (dbPipeBatchTime opts * 1_000_000))
+    _ <- atomically $ peekTQueue updates
+    withDB env commitAll
+
+
+shutdown :: forall m . MonadIO m => Bool -> DBPipeEnv -> m ()
+shutdown doCommit env = do
+  when doCommit $ withDB env commitAll
+  mco <- readTVarIO (connection env)
+  atomically $ writeTVar (connection env) Nothing
+  maybe (pure ()) (liftIO . close) mco
+
+transactional :: forall a m . (MonadUnliftIO m) => DBPipeM m a -> DBPipeM m ()
+transactional what = do
+  conn <- withConn pure
+  env <- ask
+  transactional_ env conn what
+
+transactional_ :: forall a m . MonadUnliftIO m => DBPipeEnv -> Connection -> m a -> m ()
+transactional_ DBPipeEnv{..} conn action = do
+  tnum <- liftIO $ atomically $ stateTVar transNum $ \s -> (s, succ s)
+  let sp = [qc|sp{tnum}|] :: String
+
+  liftIO $ execute_ conn [qc|SAVEPOINT {sp}|]
+
+  try action >>= \case
+
+    Right{} -> do
+      liftIO $ execute_ conn [qc|RELEASE SAVEPOINT {sp}|]
+
+    Left ( e :: SomeException ) -> liftIO do
+      dbLogger opts (show e)
+      execute_ conn [qc|ROLLBACK TO SAVEPOINT {sp}|]
+      throwIO e
+
+class ToQuery a b where
+  toSQL :: a -> String
+
+withConn :: forall a m . MonadIO m => (Connection -> IO a) -> DBPipeM m a
+withConn action = do
+  DBPipeEnv{..} <- ask
+  conn <- readTVarIO connection >>= \case
+      Just conn -> pure conn
+      Nothing -> do
+        conn <- liftIO $ open connPath
+        atomically (writeTVar connection (Just conn))
+        pure conn
+  liftIO $ action conn
+
+
+commitAll :: MonadIO m => DBPipeM m ()
+commitAll = do
+  env@(DBPipeEnv{..}) <- ask
+  ops <- atomically $ flushTQueue updates
+  withDB env $ withConn $ \conn -> do
+    transactional_ env conn $ sequence_ ops
+
+select :: forall b args a m . (ToQuery a b, FromRow b, ToRow args, MonadIO m) => a -> args -> DBPipeM m [b]
+select q wtf = withConn $ \conn -> do
+  liftIO $ query conn (fromString (toSQL @a @b q)) wtf
+
+select_ :: (ToQuery a b, FromRow b, MonadIO m) => a -> DBPipeM m [b]
+select_ a = select a ()
+
+update_ :: forall a m . (ToQuery a (), MonadIO m) => a -> DBPipeM m ()
+update_ a = update @a @() @() a ()
+
+insert_ :: forall a m . (ToQuery a (), MonadIO m) => a -> DBPipeM m ()
+insert_ a = insert @a @() @() a ()
+
+update :: forall a args b m . (ToQuery a b, ToRow args, MonadIO m) => a -> args -> DBPipeM m ()
+update q args = withConn $ \conn -> do
+  execute conn  (fromString (toSQL @a @b q)) args
+
+insert :: forall a args b m . (ToQuery a b, ToRow args, MonadIO m) => a -> args -> DBPipeM m ()
+insert = update @a @_ @b
+
+ddl :: forall a m . (ToQuery a (), MonadIO m) => a -> DBPipeM m ()
+ddl a = update @a @() @() a ()
+
+instance ToQuery String r where
+  toSQL a = a
+
+test1 :: IO ()
+test1 = do
+  env <- newDBPipeEnv dbPipeOptsDef ":memory:"
+
+  a <- async $ runPipe env
+
+  withDB env do
+    ddl "create table wtf (k int primary key, v int)"
+    commitAll
+
+  withDB env $ do
+
+    transactional do
+      update "insert into wtf (k,v) values(1,1)" ()
+
+    commitAll
+
+    wtf <- select @(Int,Int) "select k,v from wtf" ()
+    liftIO $ print wtf
+
+  cancel a
+
+
diff --git a/lib/DBPipe/SQLite/Generic.hs b/lib/DBPipe/SQLite/Generic.hs
new file mode 100644
--- /dev/null
+++ b/lib/DBPipe/SQLite/Generic.hs
@@ -0,0 +1,195 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE UndecidableInstances #-}
+{-# LANGUAGE DefaultSignatures #-}
+module DBPipe.SQLite.Generic where
+
+import DBPipe.SQLite.Types
+import DBPipe.SQLite qualified as SQL
+import DBPipe.SQLite hiding (insert,columnName)
+
+import GHC.Generics
+import Data.Proxy
+import Data.Text qualified as Text
+import Data.Text (Text)
+import Data.String (IsString(..))
+import Text.InterpolatedString.Perl6 (qc)
+import Data.Coerce
+import UnliftIO
+
+newtype SQLName = SQLName Text
+                  deriving stock (Eq,Ord)
+                  deriving newtype (IsString,Monoid,Semigroup,Show)
+
+newtype SQLPart = SQLPart { fromSQL :: Text }
+                  deriving stock (Eq,Ord)
+                  deriving newtype (IsString,Monoid,Semigroup,Show)
+
+data AllColumns a = AllColumns
+                    deriving stock (Generic)
+
+class ToSQL a where
+  toSQL :: a -> SQLPart
+
+instance ToSQL SQLName where
+  toSQL (SQLName a) = SQLPart a
+
+
+class GHasColumnNames f where
+  gColumnNames :: f p -> [SQLName]
+
+class HasTableName a where
+  tableName :: SQLName
+
+class HasColumnNames a where
+  columnNames :: a -> [SQLName]
+  default columnNames :: (Generic a, GHasColumnNames (Rep a)) => a -> [SQLName]
+  columnNames = gColumnNames . from
+
+class HasColumnName a where
+  columnName :: SQLName
+
+instance HasColumnNames [SQLName] where
+  columnNames = id
+
+instance HasColumnNames SQLName where
+  columnNames n = [n]
+
+instance {-# OVERLAPPABLE #-} (Generic a, GHasColumnNames (Rep a)) => HasColumnNames a
+
+instance GHasColumnNames U1 where
+  gColumnNames U1 = []
+
+instance (GHasColumnNames a, GHasColumnNames b) => GHasColumnNames (a :*: b) where
+  gColumnNames (a :*: b) = gColumnNames a <> gColumnNames b
+
+instance (GHasColumnNames a, GHasColumnNames b) => GHasColumnNames (a :+: b) where
+  gColumnNames _ = []  -- Не используется для нашего случая.
+
+instance HasColumnName c => GHasColumnNames (K1 i c) where
+  gColumnNames (K1 c) = [columnName @c]
+
+instance GHasColumnNames a => GHasColumnNames (M1 i t a) where
+  gColumnNames (M1 a) = gColumnNames a
+
+
+class GColumnNames f where
+  gColumnNames1 :: [SQLName]
+
+instance GColumnNames U1 where
+  gColumnNames1 = []
+
+instance (GColumnNames a, GColumnNames b) => GColumnNames (a :+: b) where
+  gColumnNames1 = gColumnNames1 @a ++ gColumnNames1 @b
+
+instance (GColumnNames a, GColumnNames b) => GColumnNames (a :*: b) where
+  gColumnNames1 = gColumnNames1 @a ++ gColumnNames1 @b
+
+instance (Selector s, HasColumnName c) => GColumnNames (M1 S s (K1 i c)) where
+  gColumnNames1 = [columnName @c]
+
+instance GColumnNames a => GColumnNames (M1 D d a) where
+  gColumnNames1 = gColumnNames1 @a
+
+instance GColumnNames a => GColumnNames (M1 C c a) where
+  gColumnNames1 = gColumnNames1 @a
+
+instance (Generic a, GColumnNames (Rep a)) => HasColumnNames (AllColumns a) where
+  columnNames _ = gColumnNames1 @(Rep a)
+
+-- -- Реализация GHasColumnNames для AllColumns a
+-- instance (Generic a, GHasColumnNames (Rep a)) => GHasColumnNames AllColumns where
+--   gColumnNames _ = gColumnNames (from (undefined :: a))
+
+-- -- Функция для получения списка имен колонок через AllColumns
+-- columnNamesForAll :: forall a. (Generic a, GHasColumnNames AllColumns) => [SQLName]
+-- columnNamesForAll = gColumnNames (AllColumns @a)
+
+-- Пример использования этой функции:
+-- myList = columnNamesFor (Proxy :: Proxy GitRepoListEntry)
+
+data Bound = forall a . ToField a => Bound a
+
+class GToBoundList f where
+  gToBoundList :: f p -> [Bound]
+
+instance GToBoundList U1 where
+  gToBoundList U1 = []
+
+instance (GToBoundList a, GToBoundList b) => GToBoundList (a :*: b) where
+  gToBoundList (a :*: b) = gToBoundList a <> gToBoundList b
+
+instance (ToField c) => GToBoundList (K1 i c) where
+  gToBoundList (K1 c) = [Bound c]
+
+instance GToBoundList a => GToBoundList (M1 i t a) where
+  gToBoundList (M1 a) = gToBoundList a
+
+class ToBoundList a where
+  toBoundList :: a -> [Bound]
+  default toBoundList :: (Generic a, GToBoundList (Rep a)) => a -> [Bound]
+  toBoundList = gToBoundList . from
+
+instance (Generic a, GToBoundList (Rep a)) => ToBoundList a where
+  toBoundList = gToBoundList . from
+
+columnListPart :: forall a . HasColumnNames a => a -> SQLPart
+columnListPart w = SQLPart $ Text.intercalate "," [ coerce @_ @Text x | x <- columnNames w ]
+
+bindListPart :: forall a . HasColumnNames a => a -> SQLPart
+bindListPart w = SQLPart $ Text.intercalate "," [ "?" | _ <- columnNames w ]
+
+class HasPrimaryKey t where
+  primaryKey :: [SQLName]
+
+newtype OnCoflictIgnore t r = OnCoflictIgnore r
+                              deriving stock (Generic)
+
+instance (HasPrimaryKey t, HasColumnNames r) => HasColumnNames (OnCoflictIgnore t r) where
+  columnNames (OnCoflictIgnore r) = columnNames r
+
+-- instance (HasColumnNames r) => HasColumnNames (AllColumns r) where
+  -- columnNames _ = gColumnNames @r
+  -- columnNames AllColumns = columnNames r
+
+onConflictIgnore :: (HasTableName t, HasColumnNames r) => r -> OnCoflictIgnore t r
+onConflictIgnore = OnCoflictIgnore
+
+instance ToField Bound where
+  toField (Bound x) = toField x
+
+data BoundQuery =
+  BoundQuery SQLPart [Bound]
+
+class (MonadIO m, HasTableName t, HasColumnNames b) => Insert t b m where
+  insert :: b -> DBPipeM m ()
+
+instance {-# OVERLAPPABLE #-}
+         ( MonadIO m
+         , HasTableName t
+         , HasColumnNames b
+         , ToBoundList b
+         ) => Insert t b m where
+  insert values = do
+    SQL.insert [qc|insert into {tn} values({v}) ({n})|] bound
+    where
+      v = coerce @_ @Text $ bindListPart values
+      n = coerce @_ @Text $ columnListPart values
+      bound = toBoundList values
+      tn = coerce @_ @Text (tableName @t)
+
+instance {-# OVERLAPPABLE #-}
+         ( MonadIO m
+         , HasTableName t
+         , HasPrimaryKey t
+         , HasColumnNames b
+         , ToBoundList b
+         ) => Insert t (OnCoflictIgnore t b) m where
+  insert (OnCoflictIgnore values) = do
+    SQL.insert [qc|insert into {tn} ({n}) values({v}) on conflict ({pk}) do nothing|] bound
+    where
+      v = coerce @_ @Text $ bindListPart values
+      n = coerce @_ @Text $ columnListPart values
+      bound = toBoundList values
+      tn = coerce @_ @Text (tableName @t)
+      pk = coerce @_ @Text $ columnListPart $ primaryKey @t
+
diff --git a/lib/DBPipe/SQLite/Types.hs b/lib/DBPipe/SQLite/Types.hs
new file mode 100644
--- /dev/null
+++ b/lib/DBPipe/SQLite/Types.hs
@@ -0,0 +1,7 @@
+module DBPipe.SQLite.Types
+  ( ToField(..)
+  )where
+
+import Database.SQLite.Simple.ToField
+
+
