diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,12 @@
+# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
+and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+### Added
+- Typeclasses for key/value pairs, base keys and key seekers.
+- Functions for creating, updating, deleting records.
+- Functions for retrieving multiple records.
+- Fuunctions for streaming records.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# RocksDB Query
+
+Helper functions to perform queries on RocksDB databases using serialized Haskell datatypes.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/UNLICENSE b/UNLICENSE
new file mode 100644
--- /dev/null
+++ b/UNLICENSE
@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+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 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.
+
+For more information, please refer to <http://unlicense.org/>
diff --git a/rocksdb-query.cabal b/rocksdb-query.cabal
new file mode 100644
--- /dev/null
+++ b/rocksdb-query.cabal
@@ -0,0 +1,64 @@
+-- This file has been generated from package.yaml by hpack version 0.28.2.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 22fa4385a9bbab4e0eb723156fce25ead14fbd6fd70ba1743a4d6895415b3062
+
+name:           rocksdb-query
+version:        0.1.0
+synopsis:       RocksDB database querying library for Haskell
+description:    Please see the README on GitHub at <https://github.com/xenog/rocksdb-query#readme>
+category:       Database
+homepage:       https://github.com/xenog/rocksdb-query#readme
+bug-reports:    https://github.com/xenog/rocksdb-query/issues
+author:         Jean-Pierre Rupp
+maintainer:     xenog@protonmail.com
+copyright:      No Rights Reserved
+license:        PublicDomain
+license-file:   UNLICENSE
+build-type:     Simple
+cabal-version:  >= 1.10
+extra-source-files:
+    CHANGELOG.md
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/xenog/rocksdb-query
+
+library
+  hs-source-dirs:
+      src
+  build-depends:
+      base >=4.7 && <5
+    , bytestring
+    , cereal
+    , conduit
+    , resourcet
+    , rocksdb-haskell
+    , unliftio
+  exposed-modules:
+      Database.RocksDB.Query
+  other-modules:
+      Paths_rocksdb_query
+  default-language: Haskell2010
+
+test-suite rocksdb-query-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      base >=4.7 && <5
+    , bytestring
+    , cereal
+    , conduit
+    , hspec
+    , resourcet
+    , rocksdb-haskell
+    , rocksdb-query
+    , unliftio
+  other-modules:
+      Paths_rocksdb_query
+  default-language: Haskell2010
diff --git a/src/Database/RocksDB/Query.hs b/src/Database/RocksDB/Query.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/RocksDB/Query.hs
@@ -0,0 +1,168 @@
+{-# LANGUAGE LambdaCase            #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+module Database.RocksDB.Query where
+
+import qualified Data.ByteString          as B
+import           Data.Conduit
+import qualified Data.Conduit.Combinators as CC
+import           Data.Serialize           as S
+import           Database.RocksDB         as R
+import           UnliftIO
+import           UnliftIO.Resource
+
+class Key key
+class KeyValue key value
+
+retrieve ::
+       (MonadIO m, KeyValue key value, Serialize key, Serialize value)
+    => DB
+    -> Maybe Snapshot
+    -> key
+    -> m (Maybe value)
+retrieve db snapshot key = do
+    let opts = defaultReadOptions {useSnapshot = snapshot}
+    R.get db opts (encode key) >>= \case
+        Nothing -> return Nothing
+        Just bytes ->
+            case decode bytes of
+                Left e  -> throwString e
+                Right x -> return (Just x)
+
+matchRecursive ::
+       ( MonadIO m
+       , KeyValue key value
+       , Serialize key
+       , Serialize value
+       )
+    => key
+    -> Iterator
+    -> ConduitT () (key, value) m ()
+matchRecursive base it =
+    iterEntry it >>= \case
+        Nothing -> return ()
+        Just (key_bytes, value_bytes) -> do
+            let start_bytes = B.take (B.length base_bytes) key_bytes
+            if start_bytes /= base_bytes
+                then return ()
+                else do
+                    key <- either throwString return (decode key_bytes)
+                    value <- either throwString return (decode value_bytes)
+                    yield (key, value)
+                    iterNext it
+                    matchRecursive base it
+  where
+    base_bytes = encode base
+
+matching ::
+       ( MonadResource m
+       , KeyValue key value
+       , Serialize key
+       , Serialize value
+       )
+    => DB
+    -> Maybe Snapshot
+    -> key
+    -> ConduitT () (key, value) m ()
+matching db snapshot base = do
+    let opts = defaultReadOptions {useSnapshot = snapshot}
+    withIterator db opts $ \it -> do
+        iterSeek it (encode base)
+        matchRecursive base it
+
+matchingSkip ::
+       ( MonadResource m
+       , KeyValue key value
+       , Serialize key
+       , Serialize value
+       )
+    => DB
+    -> Maybe Snapshot
+    -> key
+    -> key
+    -> ConduitT () (key, value) m ()
+matchingSkip db snapshot base start = do
+    let opts = defaultReadOptions {useSnapshot = snapshot}
+    withIterator db opts $ \it -> do
+        iterSeek it (encode start)
+        matchRecursive base it
+
+insert ::
+       (MonadIO m, KeyValue key value, Serialize key, Serialize value)
+    => DB
+    -> key
+    -> value
+    -> m ()
+insert db key value = R.put db defaultWriteOptions (encode key) (encode value)
+
+remove :: (MonadIO m, Key key, Serialize key) => DB -> key -> m ()
+remove db key = delete db defaultWriteOptions (encode key)
+
+insertOp ::
+       (KeyValue key value, Serialize key, Serialize value)
+    => key
+    -> value
+    -> BatchOp
+insertOp key value = R.Put (encode key) (encode value)
+
+deleteOp :: (Key key, Serialize key) => key -> BatchOp
+deleteOp key = Del (encode key)
+
+writeBatch :: MonadIO m => DB -> WriteBatch -> m ()
+writeBatch db = write db defaultWriteOptions
+
+firstMatching ::
+       ( MonadUnliftIO m
+       , KeyValue key value
+       , Serialize key
+       , Serialize value
+       )
+    => DB
+    -> Maybe Snapshot
+    -> key
+    -> m (Maybe (key, value))
+firstMatching db snapshot base =
+    runResourceT . runConduit $ matching db snapshot base .| CC.head
+
+firstMatchingSkip ::
+       ( MonadUnliftIO m
+       , KeyValue key value
+       , Serialize key
+       , Serialize value
+       )
+    => DB
+    -> Maybe Snapshot
+    -> key
+    -> key
+    -> m (Maybe (key, value))
+firstMatchingSkip db snapshot base start =
+    runResourceT . runConduit $
+    matchingSkip db snapshot base start .| CC.head
+
+matchingAsList ::
+       ( MonadUnliftIO m
+       , KeyValue key value
+       , Serialize key
+       , Serialize value
+       )
+    => DB
+    -> Maybe Snapshot
+    -> key
+    -> m [(key, value)]
+matchingAsList db snapshot base =
+    runResourceT . runConduit $
+    matching db snapshot base .| CC.sinkList
+
+matchingSkipAsList ::
+       ( MonadUnliftIO m
+       , KeyValue key value
+       , Serialize key
+       , Serialize value
+       )
+    => DB
+    -> Maybe Snapshot
+    -> key
+    -> key
+    -> m [(key, value)]
+matchingSkipAsList db snapshot base start =
+    runResourceT . runConduit $
+    matchingSkip db snapshot base start .| CC.sinkList
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,84 @@
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+import           Control.Applicative
+import           Control.Monad
+import           Data.Serialize         as S
+import           Data.Word
+import           Database.RocksDB       as R
+import           Database.RocksDB.Query
+import           Test.Hspec
+import           UnliftIO
+
+newtype KeyOne = KeyOne Word32 deriving (Show, Eq)
+data KeyTwo
+    = KeyTwo Word32
+             Word32
+    | KeyTwoBase Word32
+    deriving (Show, Eq)
+
+instance KeyValue KeyTwo String
+instance KeyValue KeyOne String
+
+instance Serialize KeyOne where
+    put (KeyOne x) = do
+        putWord8 0x01
+        S.put x
+    get = do
+        getWord8 >>= guard . (== 0x01)
+        KeyOne <$> S.get
+
+instance Serialize KeyTwo where
+    put k = do
+        putWord8 0x02
+        case k of
+            KeyTwoBase x -> S.put x
+            KeyTwo x y   -> S.put x >> S.put y
+    get =
+        (guard . (== 0x02) =<< getWord8) >>
+        KeyTwo <$> S.get <*> S.get <|> KeyTwoBase <$> S.get
+
+main :: IO ()
+main =
+    setup $ \db ->
+        describe "database" $ do
+            it "reads a record" $ do
+                r <- retrieve db Nothing (KeyTwo 1 2)
+                r `shouldBe` Just "Hello First World Again!"
+            it "reads two records at the end" $ do
+                let ls =
+                        [ (KeyTwo 2 1, "Hello Second World!")
+                        , (KeyTwo 2 2, "Hello Second World Again!")
+                        ]
+                rs <- matchingAsList db Nothing (KeyTwoBase 2)
+                rs `shouldBe` ls
+            it "reads two records in the middle" $ do
+                let ls =
+                        [ (KeyTwo 1 1, "Hello First World!")
+                        , (KeyTwo 1 2, "Hello First World Again!")
+                        ]
+                rs <- matchingAsList db Nothing (KeyTwoBase 1)
+                rs `shouldBe` ls
+            it "query and skip" $ do
+                let ex = (KeyTwo 2 2, "Hello Second World Again!")
+                rs <-
+                    matchingSkipAsList
+                        db
+                        Nothing
+                        (KeyTwoBase 2)
+                        (KeyTwo 2 2)
+                rs `shouldBe` [ex]
+  where
+    setup f =
+        withSystemTempDirectory "rocksdb-query-test-" $ \d -> do
+            db <- open d defaultOptions {createIfMissing = True}
+            insertTestRecords db
+            hspec $ f db
+
+insertTestRecords :: MonadIO m => DB -> m ()
+insertTestRecords db = do
+    insert db (KeyOne 1) "Hello World!"
+    insert db (KeyOne 2) "Hello World Again!"
+    insert db (KeyTwo 1 1) "Hello First World!"
+    insert db (KeyTwo 1 2) "Hello First World Again!"
+    insert db (KeyTwo 2 1) "Hello Second World!"
+    insert db (KeyTwo 2 2) "Hello Second World Again!"
