diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,13 @@
 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).
 
+## 0.6.1
+### Changed
+- Documentation: the README explains the purpose and design of the library,
+  with installation steps and a tutorial on modelling a schema, and the
+  Haddock of every function says why it works the way it does.
+- Build against rocksdb-haskell-jprupp 2.6.1 and Stackage nightly-2026-11-07.
+
 ## 0.6.0
 ### Changed
 - Get rid of MonadIO and operate directly on IO monad.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,3 +1,412 @@
-# RocksDB Query
+# rocksdb-query
 
-Helper functions to perform queries on RocksDB databases using serialized Haskell datatypes.
+[![Hackage](https://img.shields.io/hackage/v/rocksdb-query.svg)](https://hackage.haskell.org/package/rocksdb-query)
+[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jprupp/rocksdb-query/blob/master/LICENSE)
+
+rocksdb-query stores and queries Haskell data types in
+[RocksDB](https://rocksdb.org). It sits on top of
+[rocksdb-haskell-jprupp](https://github.com/jprupp/rocksdb-haskell) and adds
+three things:
+
+- Keys and values are any types with a
+  [cereal](https://hackage.haskell.org/package/cereal) `Serialize` instance.
+- Empty type classes tie each key type to its value type, so the compiler stops
+  you from reading a value of the wrong type.
+- Prefix queries fetch every record whose key starts with a short key, as a list
+  or as a [conduit](https://hackage.haskell.org/package/conduit) stream.
+
+- [API documentation](https://hackage.haskell.org/package/rocksdb-query)
+- [Changelog](https://github.com/jprupp/rocksdb-query/blob/master/CHANGELOG.md)
+- [Report a bug](https://github.com/jprupp/rocksdb-query/issues)
+
+## Contents
+
+- [Why rocksdb-query](#why-rocksdb-query)
+- [Design principles](#design-principles)
+- [Installation](#installation)
+- [Quick start](#quick-start)
+- [Tutorial: modelling a schema](#tutorial-modelling-a-schema)
+- [Concepts](#concepts)
+- [Usage guide](#usage-guide)
+- [API overview](#api-overview)
+- [Designing keys](#designing-keys)
+- [Development](#development)
+- [Related projects](#related-projects)
+- [License](#license)
+
+## Why rocksdb-query
+
+RocksDB is a key-value store of bytes. Real programs store records, and the code
+in between is repetitive: encode the key, call `get`, decode the value, and
+remember which kind of value lives under which kind of key. That last part is
+where bugs hide. If you read a key as the wrong type, the bytes may still
+decode, into nonsense, and you find out much later.
+
+rocksdb-query turns that knowledge into types. You declare once that orders live
+under order keys, and from then on the compiler checks every read and write.
+It also gives you prefix queries, which is how a key-value store answers
+questions like "all the orders of this customer" without a query language.
+
+It was written for [Haskoin](https://github.com/jprupp/haskoin-store), whose
+indices of the Bitcoin block chain are all modelled this way. It is small on
+purpose: about thirty functions, all thin wrappers over
+[rocksdb-haskell-jprupp](https://github.com/jprupp/rocksdb-haskell).
+
+## Design principles
+
+- **The schema is in the types.** A `KeyValue key value` instance says what is
+  stored under a key type. Reads and writes that disagree do not compile.
+- **You own the bytes.** Serialization is a `Serialize` instance that you
+  control. That matters, because the byte order of keys decides what a prefix
+  query returns and in which order.
+- **Sorted keys instead of indices.** RocksDB has no secondary indices. A key
+  that starts with what you look records up by turns a lookup into a seek and a
+  short scan. Prefix queries make that pattern a one-liner.
+- **Corrupt data is a bug.** If your program cannot decode data it wrote, it
+  stops with `error` instead of handing you a `Left` to ignore. Carrying on
+  would only spread the damage.
+- **Stay thin.** Opening, tuning, snapshots and iterators remain
+  `rocksdb-haskell-jprupp`'s job. This library adds types, not a new way to use
+  RocksDB.
+
+## Installation
+
+### System library
+
+You need the RocksDB C library, as for
+[rocksdb-haskell-jprupp](https://github.com/jprupp/rocksdb-haskell#system-library):
+
+```sh
+sudo apt-get install librocksdb-dev   # Debian and Ubuntu
+sudo dnf install rocksdb-devel        # Fedora
+brew install rocksdb                  # macOS with Homebrew
+```
+
+### Step by step with Stack
+
+1. Install [Stack](https://docs.haskellstack.org/) and RocksDB (above).
+2. Create a project and enter it:
+
+   ```sh
+   stack new orders simple
+   cd orders
+   ```
+
+3. Add `cereal`, `data-default`, `rocksdb-haskell-jprupp` and `rocksdb-query`
+   to `build-depends` in `orders.cabal`.
+4. Put the [quick start](#quick-start) program in `src/Main.hs` and run it with
+   `stack run`. It creates a database in `/tmp/orders` and prints the records it
+   reads back.
+
+### With Cabal
+
+```cabal
+build-depends:
+    base
+  , cereal
+  , data-default
+  , rocksdb-haskell-jprupp
+  , rocksdb-query
+```
+
+You open and close the database with `rocksdb-haskell-jprupp`, and write the
+`Serialize` instances with `cereal`.
+
+### With Stack
+
+The package is in Stackage nightly snapshots. Otherwise add it to `stack.yaml`:
+
+```yaml
+extra-deps:
+  - rocksdb-query-0.6.1
+  - rocksdb-haskell-jprupp-2.6.1
+```
+
+## Quick start
+
+Orders keyed by customer, then by order number:
+
+```haskell
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+
+import Data.Default (def)
+import Data.Serialize (Serialize (..))
+import Data.Word (Word32)
+import Database.RocksDB (Config (..), withDB)
+import Database.RocksDB.Query
+import GHC.Generics (Generic)
+
+data OrderKey
+  = OrderKey Word32 Word32 -- customer, order
+  | OrderKeyS Word32 -- customer only: a prefix of OrderKey
+  deriving (Show, Generic)
+
+data Order = Order {item :: String, qty :: Int}
+  deriving (Show, Generic)
+
+instance Serialize Order
+
+-- The short key must serialize to a prefix of the full key, so write this
+-- instance by hand instead of deriving it.
+instance Serialize OrderKey where
+  put (OrderKey c o) = put c >> put o
+  put (OrderKeyS c) = put c
+  get = OrderKey <$> get <*> get
+
+instance Key OrderKey
+
+instance KeyValue OrderKey Order
+
+main :: IO ()
+main = withDB "/tmp/orders" def {createIfMissing = True} $ \db -> do
+  insert db (OrderKey 1 100) (Order "apple" 3)
+  writeBatch
+    db
+    [ insertOp (OrderKey 1 101) (Order "pear" 1),
+      insertOp (OrderKey 2 200) (Order "plum" 7)
+    ]
+
+  retrieve db (OrderKey 1 100) >>= print
+  -- Just (Order {item = "apple", qty = 3})
+
+  matchingAsList db (OrderKeyS 1) >>= mapM_ print
+  -- (OrderKey 1 100,Order {item = "apple", qty = 3})
+  -- (OrderKey 1 101,Order {item = "pear", qty = 1})
+
+  remove db (OrderKey 2 200)
+```
+
+## Tutorial: modelling a schema
+
+A small library system stores books and loans. This tutorial goes through the
+decisions behind its keys.
+
+### 1. List the questions
+
+Before writing any type, list how the data will be read:
+
+- get a book by its id;
+- list the loans of a member, newest first;
+- list every book on a shelf.
+
+Each question becomes a key whose leading fields are what you look things up
+by.
+
+### 2. One tag byte per key type
+
+The keys share one column family, so each type starts with its own byte, which
+guarantees that a prefix of one type never matches keys of another:
+
+```haskell
+data BookKey = BookKey Word32 -- book id
+
+instance Serialize BookKey where
+  put (BookKey b) = putWord8 0x01 >> put b
+  get = getWord8 >>= guard . (== 0x01) >> BookKey <$> get
+
+instance Key BookKey
+instance KeyValue BookKey Book
+```
+
+### 3. Keys that sort the way you read
+
+Loans are listed per member, newest first. The key starts with the member, then
+an inverted time, so that a larger time sorts first:
+
+```haskell
+data LoanKey
+  = LoanKey Word32 Word64 Word32 -- member, maxBound - time, book
+  | LoanKeyS Word32 -- member only
+
+instance Serialize LoanKey where
+  put (LoanKey m t b) = putWord8 0x02 >> put m >> put t >> put b
+  put (LoanKeyS m) = putWord8 0x02 >> put m
+  get = getWord8 >>= guard . (== 0x02) >> LoanKey <$> get <*> get <*> get
+
+instance Key LoanKey
+instance KeyValue LoanKey ()
+```
+
+The value is `()`: everything is in the key. That is common for indices.
+
+### 4. Keep data and indices in step
+
+A loan changes the book and adds an index entry. Write both in one batch, so a
+crash can never leave one without the other:
+
+```haskell
+lend :: DB -> Word32 -> Word32 -> Word64 -> Book -> IO ()
+lend db member book now b =
+  writeBatch db
+    [ insertOp (BookKey book) b {onLoan = True},
+      insertOp (LoanKey member (maxBound - now) book) ()
+    ]
+```
+
+### 5. Read
+
+```haskell
+loans <- matchingAsList db (LoanKeyS member)    -- newest first
+page2 <- take 20 <$> matchingSkipAsList db (LoanKeyS member) lastKey
+```
+
+The same approach answers the shelf question with a `ShelfKey shelf book`
+index.
+
+## Concepts
+
+### Typed keys
+
+`Key` and `KeyValue` have no methods. They are declarations:
+
+```haskell
+instance Key OrderKey             -- OrderKey can be deleted
+instance KeyValue OrderKey Order  -- values under OrderKey are Orders
+```
+
+`retrieve`, `insert` and the queries need `KeyValue`; `remove` and `deleteOp`
+need only `Key`. Reading a key as a value type you did not declare for it is
+a type error, rather than a decoding failure at run time.
+
+### Prefix queries
+
+RocksDB keeps keys sorted by their bytes, so all keys that start with the same
+bytes sit next to each other. A query with a short key seeks to the first key
+at or after its encoding and reads until a key no longer starts with it.
+
+cereal encodes unsigned integers (`Word32`, `Word64`) in big-endian order, so
+those key parts sort in numeric order. Signed integers do not: negative numbers
+sort after positive ones.
+
+### Decoding errors
+
+If a key or value in the database cannot be decoded, these functions call
+`error`. That means the instance used to read disagrees with the one used to
+write, or two key types share the same prefix.
+
+## Usage guide
+
+### Reading and writing
+
+```haskell
+insert db key value
+mvalue <- retrieve db key
+remove db key
+```
+
+### Batches
+
+All the operations in a batch are applied atomically:
+
+```haskell
+writeBatch db [insertOp k1 v1, insertOp k2 v2, deleteOp k3]
+```
+
+### Prefix queries
+
+| Function            | Returns                                      |
+| ------------------- | -------------------------------------------- |
+| `matchingAsList`    | every match, as a list                       |
+| `firstMatching`     | the first match                              |
+| `matching`          | every match, as a conduit, on your iterator  |
+
+The conduit version reads lazily, so it suits prefixes with many matches:
+
+```haskell
+import Conduit
+import Database.RocksDB (withIter)
+
+total <- withIter db $ \it ->
+  runConduit $ matching it (OrderKeyS 1) .| mapC (qty . snd) .| sumC
+```
+
+### Pagination
+
+The `Skip` variants start inside the prefix at a given key, or the next one
+after it:
+
+```haskell
+page2 <- take 20 <$> matchingSkipAsList db (OrderKeyS 1) (OrderKey 1 lastSeen)
+```
+
+### Column families
+
+Each function has a `CF` variant that takes a `ColumnFamily`:
+
+```haskell
+withDBCF dir cfg [("orders", cfg)] $ \db -> do
+  let [orders] = columnFamilies db
+  insertCF db orders (OrderKey 1 100) (Order "apple" 3)
+  matchingAsListCF db orders (OrderKeyS 1)
+```
+
+## API overview
+
+| Default column family                  | Column family variant                      |
+| -------------------------------------- | ------------------------------------------ |
+| `retrieve`                             | `retrieveCF`                               |
+| `insert`, `remove`                     | `insertCF`, `removeCF`                     |
+| `insertOp`, `deleteOp`                 | `insertOpCF`, `deleteOpCF`                 |
+| `matchingAsList`, `matchingSkipAsList` | `matchingAsListCF`, `matchingSkipAsListCF` |
+| `firstMatching`, `firstMatchingSkip`   | `firstMatchingCF`, `firstMatchingSkipCF`   |
+| `matching`, `matchingSkip`             | use an iterator from `withIterCF`          |
+
+`writeBatch` takes operations for any column family. Everything is in
+`Database.RocksDB.Query`.
+
+## Designing keys
+
+- Start every key type with a distinct tag byte when several types share a
+  column family, so that their prefixes never overlap:
+
+  ```haskell
+  put (OrderKey c o) = putWord8 0x01 >> put c >> put o
+  put (OrderKeyS c) = putWord8 0x01 >> put c
+  ```
+
+- Give the short key the same leading fields, in the same order, as the full
+  key. `get` only needs to decode the full key.
+- Use fixed-width, big-endian numbers (`Word32`, `Word64`) for parts you sort
+  or range over. Variable-length encodings do not sort numerically.
+- To read newest first, store an inverted value, such as `maxBound - time`.
+
+## Development
+
+```sh
+git clone https://github.com/jprupp/rocksdb-query.git
+cd rocksdb-query
+
+stack build                        # build the library
+stack test                         # run the tests
+stack haddock --no-haddock-deps    # build the API documentation
+```
+
+With `stack --nix`, the `nix` section of `stack.yaml` provides RocksDB.
+
+`package.yaml` is the source of the package description. Change it rather than
+`rocksdb-query.cabal`, which [hpack](https://github.com/sol/hpack) generates.
+
+The tests are in `test/Spec.hs` and use [hspec](https://hspec.github.io/).
+
+### Contributing
+
+Pull requests are welcome.
+
+- Keep the style of the code around your change.
+- Add tests for the behaviour you change.
+- Describe the change in `CHANGELOG.md`, which follows
+  [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+
+## Related projects
+
+- [rocksdb-haskell-jprupp](https://github.com/jprupp/rocksdb-haskell) is the
+  RocksDB binding underneath.
+- [haskoin-node](https://github.com/jprupp/haskoin-node) and
+  [haskoin-store](https://github.com/jprupp/haskoin-store) store their headers
+  and indices with this library.
+
+## License
+
+rocksdb-query is released under the
+[MIT License](https://github.com/jprupp/rocksdb-query/blob/master/LICENSE).
diff --git a/rocksdb-query.cabal b/rocksdb-query.cabal
--- a/rocksdb-query.cabal
+++ b/rocksdb-query.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           rocksdb-query
-version:        0.6.0
+version:        0.6.1
 synopsis:       RocksDB database querying library for Haskell
 description:    Please see the README on GitHub at <https://github.com/jprupp/rocksdb-query#readme>
 category:       Database
diff --git a/src/Database/RocksDB/Query.hs b/src/Database/RocksDB/Query.hs
--- a/src/Database/RocksDB/Query.hs
+++ b/src/Database/RocksDB/Query.hs
@@ -4,13 +4,82 @@
 
 -- |
 -- Module      : Database.RocksDB.Query
--- Copyright   : No rights reserved
--- License     : UNLICENSE
--- Maintainer  : xenog@protonmail.com
+-- Copyright   : Haskoin Developers
+-- License     : MIT
+-- Maintainer  : jprupp@protonmail.ch
 -- Stability   : experimental
 -- Portability : POSIX
 --
 -- Query functions to make interaction with RocksDB stores easier and safer.
+--
+-- = Purpose
+--
+-- RocksDB stores bytes. A program stores records: users, orders,
+-- transactions. Between the two sits a lot of repetitive and error-prone code
+-- that encodes keys, decodes values, and remembers which kind of value lives
+-- under which kind of key. Getting that last part wrong is a silent bug: the
+-- bytes decode into nonsense, or into the wrong record, long after they were
+-- written.
+--
+-- This module removes that code and makes the mapping from key types to value
+-- types something the compiler checks. It is deliberately small, and leaves
+-- opening, tuning and closing the database to "Database.RocksDB".
+--
+-- = Design
+--
+-- * __Types describe the schema.__ A key type and the instance
+--   @'KeyValue' Key Value@ together say what is stored where. Reading a key as
+--   a type you did not declare for it does not compile.
+--
+-- * __Serialization is cereal.__ Its encodings of fixed-width unsigned
+--   integers are big-endian, so numeric key parts sort numerically, which makes
+--   range scans work. You keep full control over the bytes by writing the
+--   'Serialize' instance yourself when the order matters.
+--
+-- * __Prefix queries are the main way to read sets.__ RocksDB has no
+--   secondary indices or queries, but it keeps keys sorted. A schema whose keys
+--   start with what you look records up by turns "all orders of a customer"
+--   into one seek and a short scan.
+--
+-- * __Corrupt data is a bug, not an outcome.__ Decoding failures call 'error'
+--   rather than returning 'Maybe' or 'Either'. If data written by your own
+--   program cannot be read back, the program is wrong, and carrying on would
+--   only spread the damage.
+--
+-- Keys and values are Haskell types with 'Serialize' instances from the
+-- @cereal@ package. They are encoded with 'encode' before being written and
+-- decoded with 'decode' after being read. The empty classes 'Key' and
+-- 'KeyValue' tie key types to value types, so that the type checker prevents
+-- reading a value of the wrong type for a key:
+--
+-- @
+-- data UserKey = UserKey Word32 deriving (Generic)
+-- instance 'Serialize' UserKey
+-- instance 'Key' UserKey
+-- instance 'KeyValue' UserKey User
+--
+-- getUser :: DB -> Word32 -> IO (Maybe User)
+-- getUser db = 'retrieve' db . UserKey
+-- @
+--
+-- = Prefix queries
+--
+-- Because RocksDB keeps keys sorted by their bytes, all keys that start with
+-- the same bytes are stored next to each other. The 'matching' family of
+-- functions takes a short key whose encoding is a prefix of the encoding of
+-- full keys, and returns every entry whose key starts with that prefix. See
+-- 'matching' for an example.
+--
+-- = Decoding errors
+--
+-- If a stored key or value cannot be decoded, these functions call 'error'.
+-- That indicates a bug, such as a mismatch between the 'Serialize' instance
+-- used to write and to read, or two key types sharing a prefix.
+--
+-- = Column families
+--
+-- Functions ending in @CF@ act on a given column family. The others act on the
+-- default column family.
 module Database.RocksDB.Query where
 
 import Conduit
@@ -19,13 +88,25 @@
 import Data.Serialize as S
 import Database.RocksDB as R
 
--- | Class for types that are database keys.
+-- | Class for types that are database keys. It has no methods; an instance
+-- marks a type as allowed to be used as a key in 'remove', 'deleteOp' and
+-- related functions.
+--
+-- Deletion needs to know only that a type is a key, not what it maps to, which
+-- is why it is a separate class.
 class Key key
 
--- | Class for types that are corresponding database key and value.
+-- | Class for types that are corresponding database key and value. It has no
+-- methods; an instance states that values of type @value@ are stored under keys
+-- of type @key@. Functions that read or write values require it.
+--
+-- The class has no methods because its job is purely to be checked by the
+-- compiler: it records a decision about your schema in a place where every read
+-- and write must agree with it.
 class KeyValue key value
 
--- | Read a value from the database, or 'Nothing' if not found.
+-- | Read a value from the default column family, or 'Nothing' if not found.
+-- Calls 'error' if the stored value cannot be decoded.
 retrieve ::
   (KeyValue key value, Serialize key, Serialize value) =>
   DB ->
@@ -33,6 +114,7 @@
   IO (Maybe value)
 retrieve db = retrieveCommon db Nothing
 
+-- | Like 'retrieve', but read from the given column family.
 retrieveCF ::
   (KeyValue key value, Serialize key, Serialize value) =>
   DB ->
@@ -41,7 +123,8 @@
   IO (Maybe value)
 retrieveCF db cf = retrieveCommon db (Just cf)
 
--- | Read a value from the database, or 'Nothing' if not found.
+-- | Read a value from the default column family, or from a column family if
+-- one is given. Returns 'Nothing' if not found.
 retrieveCommon ::
   (KeyValue key value, Serialize key, Serialize value) =>
   DB ->
@@ -60,6 +143,9 @@
       Just cf -> R.getCF db cf (encode key)
       Nothing -> R.get db (encode key)
 
+-- | Internal function to collect, from the current position of the iterator,
+-- every entry whose key starts with the encoding of the given key. Stops at the
+-- first entry that does not match.
 matchRecursiveList ::
   (KeyValue key value, Serialize key, Serialize value) =>
   key ->
@@ -80,7 +166,8 @@
             else return []
     base_bytes = encode base
 
--- | Internal function for recursively matching a key.
+-- | Internal function for recursively matching a key. Like
+-- 'matchRecursiveList', but streams the entries through a conduit.
 matchRecursive ::
   ( KeyValue key value,
     Serialize key,
@@ -113,18 +200,28 @@
 -- >   put (FullKey a b) = put a >> put b
 -- >   get = FullKey <$> get <*> get
 -- > instance KeyValue MyKey String
--- > main = do
--- >   db <- open "test-db" defaultOptions {createIfMissing = True}
--- >   insert db (FullKey "hello" "world") "despite all my rage"
--- >   Just record <- runResourceT . runConduit $
--- >     matching db def (ShortKey "hello") .| headC
--- >   print (record :: (MyKey, String))
--- >   -- (Fullkey "hello" "world","despite all my rage")
+-- > main =
+-- >   withDB "test-db" def {createIfMissing = True} $ \db -> do
+-- >     insert db (FullKey "hello" "world") "despite all my rage"
+-- >     Just record <- withIter db $ \it ->
+-- >       runConduit $ matching it (ShortKey "hello") .| headC
+-- >     print (record :: (MyKey, String))
+-- >     -- (FullKey "hello" "world","despite all my rage")
 --
 -- In this example the @ShortKey@ is serialized to the prefix of the only
 -- element in the database, which is then returned. Since the 'get' function of
 -- the 'Serialize' instance for @MyKey@ only understands how to deserialize a
 -- @FullKey@, then that is what is returned.
+--
+-- The iterator is moved to the first key at or after the prefix, and the
+-- conduit yields entries until it finds a key that does not start with the
+-- prefix. The iterator must stay open while the conduit runs. See
+-- 'matchingAsList' and 'firstMatching' for versions that manage the iterator
+-- themselves.
+--
+-- The conduit version exists for sets too large to hold in memory, and for
+-- consumers that stop early: only the entries that are actually consumed are read
+-- from the database.
 matching ::
   ( KeyValue key value,
     Serialize key,
@@ -139,6 +236,15 @@
 
 -- | Like 'matching', but skip to the second key passed as argument, or after if
 -- there is no entry for the second key.
+--
+-- This is useful for pagination: pass the last key seen in the previous page
+-- (or the key just after it) as the second argument. The second key should
+-- start with the same prefix as the first; if it sorts before the prefix, no
+-- entries are returned.
+--
+-- Starting from a key instead of an offset is what makes deep pagination cheap:
+-- the database seeks straight to the page instead of reading and discarding
+-- every entry before it.
 matchingSkip ::
   ( KeyValue key value,
     Serialize key,
@@ -152,7 +258,8 @@
   liftIO (iterSeek it (encode start))
   matchRecursive base it
 
--- | Insert a record into the database.
+-- | Insert a record into the default column family, replacing any previous
+-- value for the key.
 insert ::
   (KeyValue key value, Serialize key, Serialize value) =>
   DB ->
@@ -161,7 +268,7 @@
   IO ()
 insert db key value = R.put db (encode key) (encode value)
 
--- | Insert a record into the database.
+-- | Like 'insert', but write to the given column family.
 insertCF ::
   (KeyValue key value, Serialize key, Serialize value) =>
   DB ->
@@ -171,17 +278,19 @@
   IO ()
 insertCF db cf key value = R.putCF db cf (encode key) (encode value)
 
--- | Delete a record from the database.
+-- | Delete a record from the default column family. Deleting a key that does
+-- not exist is not an error.
 remove :: (Key key, Serialize key) => DB -> key -> IO ()
 remove db key = delete db (encode key)
 
--- | Delete a record from the database.
+-- | Like 'remove', but delete from the given column family.
 removeCF ::
   (Key key, Serialize key) =>
   DB -> ColumnFamily -> key -> IO ()
 removeCF db cf key = deleteCF db cf (encode key)
 
--- | Get the 'BatchOp' to insert a record in the database.
+-- | Get the 'BatchOp' to insert a record in the default column family. Use with
+-- 'writeBatch' to apply several changes atomically.
 insertOp ::
   (KeyValue key value, Serialize key, Serialize value) =>
   key ->
@@ -189,7 +298,7 @@
   BatchOp
 insertOp key value = R.Put (encode key) (encode value)
 
--- | Get the 'BatchOp' to insert a record in the database.
+-- | Like 'insertOp', but for the given column family.
 insertOpCF ::
   (KeyValue key value, Serialize key, Serialize value) =>
   ColumnFamily ->
@@ -198,19 +307,25 @@
   BatchOp
 insertOpCF cf key value = R.PutCF cf (encode key) (encode value)
 
--- | Get the 'BatchOp' to delete a record from the database.
+-- | Get the 'BatchOp' to delete a record from the default column family. Use
+-- with 'writeBatch' to apply several changes atomically.
 deleteOp :: (Key key, Serialize key) => key -> BatchOp
 deleteOp key = Del (encode key)
 
--- | Get the 'BatchOp' to delete a record from the database.
+-- | Like 'deleteOp', but for the given column family.
 deleteOpCF :: (Key key, Serialize key) => ColumnFamily -> key -> BatchOp
 deleteOpCF cf key = DelCF cf (encode key)
 
--- | Write a batch to the database.
+-- | Write a batch to the database atomically. Synonym for 'write'.
+--
+-- @
+-- 'writeBatch' db ['insertOp' k1 v1, 'deleteOp' k2]
+-- @
 writeBatch :: DB -> [BatchOp] -> IO ()
 writeBatch = write
 
--- | Like 'matching' but return the first element only.
+-- | Like 'matching' but return the first element only, or 'Nothing' if no key
+-- starts with the prefix. Opens and closes its own iterator.
 firstMatching ::
   ( KeyValue key value,
     Serialize key,
@@ -222,7 +337,7 @@
 firstMatching db base =
   withIter db $ \it -> runConduit $ matching it base .| headC
 
--- | Like 'matching' but return the first element only.
+-- | Like 'firstMatching', but read from the given column family.
 firstMatchingCF ::
   ( KeyValue key value,
     Serialize key,
@@ -235,7 +350,8 @@
 firstMatchingCF db cf base =
   withIterCF db cf $ \it -> runConduit $ matching it base .| headC
 
--- | Like 'matchingSkip', but return the first element only.
+-- | Like 'matchingSkip', but return the first element only. Opens and closes
+-- its own iterator.
 firstMatchingSkip ::
   ( KeyValue key value,
     Serialize key,
@@ -250,7 +366,7 @@
     runConduit $
       matchingSkip it base start .| headC
 
--- | Like 'matchingSkip', but return the first element only.
+-- | Like 'firstMatchingSkip', but read from the given column family.
 firstMatchingSkipCF ::
   ( KeyValue key value,
     Serialize key,
@@ -266,7 +382,9 @@
     runConduit $
       matchingSkip it base start .| headC
 
--- | Like 'matching' but return a list.
+-- | Like 'matching' but return a list. Opens and closes its own iterator. All
+-- matching entries are loaded into memory, so use 'matching' for prefixes that
+-- may match many entries.
 matchingAsList ::
   ( KeyValue key value,
     Serialize key,
@@ -280,7 +398,7 @@
     iterSeek it (encode base)
     matchRecursiveList base it
 
--- | Like 'matching' but return a list.
+-- | Like 'matchingAsList', but read from the given column family.
 matchingAsListCF ::
   ( KeyValue key value,
     Serialize key,
@@ -295,7 +413,7 @@
     iterSeek it (encode base)
     matchRecursiveList base it
 
--- | Like 'matchingSkip', but return a list.
+-- | Like 'matchingSkip', but return a list. Opens and closes its own iterator.
 matchingSkipAsList ::
   ( KeyValue key value,
     Serialize key,
@@ -310,7 +428,7 @@
     iterSeek it (encode start)
     matchRecursiveList base it
 
--- | Like 'matchingSkip', but return a list.
+-- | Like 'matchingSkipAsList', but read from the given column family.
 matchingSkipAsListCF ::
   ( KeyValue key value,
     Serialize key,
