diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.6.1
+
+ * Documentation, lots of it, with detailed explanations.
+
 ## 2.6.0
 
  * Add `multiGet` and `multiGetCF`, which read the values of many keys in
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,22 +1,532 @@
-This library provides Haskell bindings to
-[RocksDB](http://rocksdb.org)
+# rocksdb-haskell-jprupp
 
+[![Hackage](https://img.shields.io/hackage/v/rocksdb-haskell-jprupp.svg)](https://hackage.haskell.org/package/rocksdb-haskell-jprupp)
+[![License: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](https://github.com/jprupp/rocksdb-haskell/blob/master/LICENSE)
+
+Haskell bindings to [RocksDB](https://rocksdb.org), the embedded persistent
+key-value store. The package on Hackage is `rocksdb-haskell-jprupp`; the module
+to import is `Database.RocksDB`.
+
+The bindings follow the RocksDB C API closely. On top of it they add
+bracket-style resource management, so that databases, snapshots and iterators
+are always released, even when an exception is thrown.
+
+- [API documentation](https://hackage.haskell.org/package/rocksdb-haskell-jprupp)
+- [Changelog](https://github.com/jprupp/rocksdb-haskell/blob/master/CHANGELOG.md)
+- [Report a bug](https://github.com/jprupp/rocksdb-haskell/issues)
+
+## Contents
+
+- [Why these bindings](#why-these-bindings)
+- [Design principles](#design-principles)
+- [Features](#features)
+- [Installation](#installation)
+- [Quick start](#quick-start)
+- [Tutorial: an event log](#tutorial-an-event-log)
+- [Concepts](#concepts)
+- [Usage guide](#usage-guide)
+- [Configuration](#configuration)
+- [Module overview](#module-overview)
+- [Things to watch out for](#things-to-watch-out-for)
+- [Development](#development)
+- [Related projects](#related-projects)
+- [License](#license)
+
+## Why these bindings
+
+RocksDB is a storage engine: a library that keeps sorted keys and values in
+files on local disk, with fast writes, fast point lookups and fast ordered
+scans. There is no server to run. It is what you reach for when SQLite's
+relational model is more than you need, and an in-memory map is not durable or
+big enough.
+
+These bindings exist because the Haskoin Bitcoin indexer needed them. It keeps
+the whole Bitcoin block chain indexed by address, transaction and block, which
+is hundreds of gigabytes, and serves queries from many threads while new
+blocks are written. That use shaped the library:
+
+- it must be safe to share one database between many threads;
+- resources must never leak, because the process runs for a long time;
+- memory outside the Haskell heap must be boundable, because RocksDB's defaults
+  let it grow with the size of the database;
+- the hot paths, such as reading many keys at once, must cost as little as the
+  C API allows.
+
+The library is a fork of `leveldb-haskell` and of earlier RocksDB bindings,
+maintained as `rocksdb-haskell-jprupp`.
+
+## Design principles
+
+- **Stay close to the C API.** Functions map nearly one to one to RocksDB's C
+  functions, so RocksDB's own documentation and tuning guides apply directly.
+  Conveniences such as typed keys live in separate libraries
+  ([rocksdb-query](https://github.com/jprupp/rocksdb-query)).
+- **Scope every resource.** Databases, snapshots and iterators are C objects
+  that must be freed once and never touched again. `withDB`, `withSnapshot` and
+  `withIter` free them when their block ends, even on an exception.
+- **Plain `IO`.** No monad transformer constraints. Lift into your own monad
+  with `liftIO`.
+- **Bytes in, bytes out.** Keys and values are strict `ByteString`s, which is
+  what RocksDB stores. How you serialize them is your choice.
+- **Don't crash in C.** RocksDB crashes the process if you misuse an iterator.
+  The Haskell side checks first and turns those cases into no-ops or
+  `Nothing`.
+- **Expose what bounds memory.** The few settings that turn RocksDB's memory
+  use into a fixed budget are first-class fields of `Config`.
+
+## Features
+
+| Area           | What you get                                                                   |
+| -------------- | ------------------------------------------------------------------------------ |
+| Basic access   | `get`, `put`, `delete` on strict `ByteString` keys and values                  |
+| Batches        | Atomic `write` of many puts and deletes, across column families                |
+| Multi-get      | `multiGet` reads many keys in one call to RocksDB's `MultiGet`                 |
+| Column families| Open a database with several independent key spaces; add new ones on open     |
+| Snapshots      | Consistent point-in-time reads across many queries and iterators               |
+| Iterators      | Ordered scans, forwards and backwards, optionally pinned to a snapshot         |
+| Tuning         | Bloom filters, fixed-length prefix extractor, shared LRU block cache, memtable and WAL limits, open file limit |
+| Administration | Properties (memory use, statistics, SST files), approximate sizes, destroy, repair |
+
+### What is not included
+
+- No transactions (`TransactionDB`, `OptimisticTransactionDB`), merge
+  operators, TTL databases, backups or checkpoints.
+- No manual compaction, `DeleteRange` or `SingleDelete`.
+- No custom comparators: keys always sort by their bytes.
+
+Pull requests that add bindings for these are welcome.
+
+## Installation
+
+### System library
+
+You need the RocksDB C library and its headers.
+
+```sh
+# Debian and Ubuntu
+sudo apt-get install librocksdb-dev
+
+# Fedora
+sudo dnf install rocksdb-devel
+
+# macOS with Homebrew
+brew install rocksdb
+
+# Nix
+nix-shell -p rocksdb
+```
+
+The library links against `-lrocksdb`. If RocksDB is installed in a
+non-standard place, pass its directories with `--extra-include-dirs` and
+`--extra-lib-dirs` (Cabal or Stack).
+
+### Check the library is found
+
+```sh
+ls /usr/include/rocksdb/c.h        # or $(brew --prefix)/include/rocksdb/c.h
+pkg-config --modversion rocksdb    # prints the installed version, if registered
+```
+
+### Step by step with Stack
+
+1. Install [Stack](https://docs.haskellstack.org/) and the RocksDB library
+   (above).
+2. Create a project:
+
+   ```sh
+   stack new rocks-demo simple
+   cd rocks-demo
+   ```
+
+3. Add `rocksdb-haskell-jprupp`, `bytestring` and `data-default` to
+   `build-depends` in `rocks-demo.cabal`.
+4. Put the [quick start](#quick-start) program in `src/Main.hs` and run it:
+
+   ```sh
+   stack run
+   ```
+
+   It creates a database in `/tmp/example` and prints what it reads back.
+
+If the build fails with `cannot find -lrocksdb` or `rocksdb/c.h: No such file`,
+the C library is not where the compiler looks. Install the development package,
+or pass `--extra-include-dirs` and `--extra-lib-dirs`.
+
+### With Cabal
+
+```cabal
+build-depends:
+    base
+  , bytestring
+  , data-default
+  , rocksdb-haskell-jprupp
+```
+
+`data-default` gives you `def` for the default `Config`.
+
+### With Stack
+
+The package is in Stackage nightly snapshots. If your resolver does not have
+it, or has an older version, add it to `stack.yaml`:
+
+```yaml
+extra-deps:
+  - rocksdb-haskell-jprupp-2.6.0
+```
+
+With `stack --nix`, add RocksDB to the Nix packages:
+
+```yaml
+nix:
+  packages:
+    - rocksdb
+```
+
+## Quick start
+
+```haskell
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+import Data.Default (def)
+import Database.RocksDB
+
+main :: IO ()
+main = do
+  let cfg = def {createIfMissing = True, bloomFilter = True}
+  withDB "/tmp/example" cfg $ \db -> do
+    put db "hello" "world"
+    get db "hello" >>= print -- Just "world"
+
+    -- atomic batch
+    write db [Put "a" "1", Put "b" "2", Del "hello"]
+
+    -- many keys at once
+    multiGet db ["a", "b", "c"] >>= print -- [Just "1",Just "2",Nothing]
+
+    -- every entry, in key order
+    withIter db $ \it -> do
+      iterFirst it
+      let loop =
+            iterEntry it >>= \case
+              Nothing -> return ()
+              Just kv -> print kv >> iterNext it >> loop
+      loop
+```
+
+## Tutorial: an event log
+
+This builds a small append-only event log, to show how a RocksDB schema is
+designed around sorted keys.
+
+### 1. Design the keys
+
+RocksDB sorts keys by their bytes. Records read together should therefore sort
+together. Events are read per user and in time order, so the key is the user
+followed by a big-endian timestamp, which sorts numerically:
+
+```haskell
+{-# LANGUAGE OverloadedStrings #-}
+
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Builder as BB
+import qualified Data.ByteString.Lazy as BL
+import Data.Word (Word64)
+
+eventKey :: ByteString -> Word64 -> ByteString
+eventKey user time =
+  BL.toStrict . BB.toLazyByteString $
+    BB.byteString user <> BB.char7 '/' <> BB.word64BE time
+```
+
+### 2. Write atomically
+
+An event and a per-user counter must stay consistent, so they go in one batch:
+
+```haskell
+import Database.RocksDB
+
+logEvent :: DB -> ByteString -> Word64 -> ByteString -> Int -> IO ()
+logEvent db user time payload count =
+  write db
+    [ Put (eventKey user time) payload,
+      Put ("count/" <> user) (BL.toStrict (BB.toLazyByteString (BB.intDec count)))
+    ]
+```
+
+If the program dies halfway, either both writes are there or neither is.
+
+### 3. Read a range with an iterator
+
+All events of a user sort together, so one seek and a short scan read them:
+
+```haskell
+eventsOf :: DB -> ByteString -> IO [ByteString]
+eventsOf db user = withIter db $ \it -> do
+  let prefix = user <> "/"
+  iterSeek it prefix
+  let go acc = do
+        mkv <- iterEntry it
+        case mkv of
+          Just (k, v) | prefix `B.isPrefixOf` k -> iterNext it >> go (v : acc)
+          _ -> return (reverse acc)
+  go []
+```
+
+### 4. Read consistently with a snapshot
+
+If another thread logs events while you read the counter and then the events,
+the two can disagree. Read both through a snapshot:
+
+```haskell
+report :: DB -> ByteString -> IO (Maybe ByteString, [ByteString])
+report db user = withSnapshot db $ \snap ->
+  (,) <$> get snap ("count/" <> user) <*> eventsOf snap user
+```
+
+### 5. Tune for size
+
+Once the log holds millions of events, open it with a memory budget and a Bloom
+filter for faster point lookups:
+
+```haskell
+let cfg =
+      def
+        { createIfMissing = True,
+          bloomFilter = True,
+          maxFiles = Just 1000,
+          blockCacheSize = Just (512 * 1024 * 1024),
+          cacheIndexAndFilterBlocks = True
+        }
+```
+
+## Concepts
+
+### The database handle
+
+`withDB` and `withDBCF` open a database, pass a `DB` to your function, and close
+it when the function returns or throws. The `DB` must not escape that function.
+It is safe to use from many threads at the same time.
+
+### Keys and values
+
+Keys and values are strict `ByteString`s. RocksDB sorts keys by their bytes,
+which is the order iterators walk them in. To store Haskell types, serialize
+them yourself, or use [rocksdb-query](https://github.com/jprupp/rocksdb-query).
+
+### Column families
+
+A database can hold several key spaces called column families, each with its
+own configuration. Functions ending in `CF` act on a given column family; the
+others act on the default one. The handles are in `columnFamilies db`, in the
+order you listed them when opening.
+
+### Snapshots
+
+`withSnapshot db f` passes `f` a `DB` that reads from a frozen view of the
+database. Reads and iterators through it do not see later writes. Writes
+through it still go to the live database.
+
+### Errors
+
+Failures reported by RocksDB are thrown as `IOError` (built with `userError`).
+The message starts with the operation, such as `put: ...` or `open: ...`.
+
+## Usage guide
+
+### Column families
+
+```haskell
+withDBCF "/tmp/example-cf" cfg [("users", cfg), ("orders", cfg)] $ \db -> do
+  let [users, orders] = columnFamilies db
+  putCF db users "alice" "..."
+  getCF db orders "1234" >>= print
+  write db [PutCF users "bob" "...", DelCF orders "1234"]
+```
+
+On an empty directory, `withDBCF` creates the database and all the column
+families. On an existing database, RocksDB needs every column family that
+exists to be listed. To add a new one, list it and set
+`createMissingColumnFamilies = True` in the database `Config`.
+
+### Snapshots
+
+```haskell
+withSnapshot db $ \snap -> do
+  a <- get snap "a"
+  b <- get snap "b" -- same point in time as "a"
+  withIter snap $ \it -> iterFirst it >> iterEntry it >>= print
+```
+
+When the snapshot's lifetime does not fit a scope, use `createSnapshot` and
+`releaseSnapshot`. `withIterSnap` and `withIterSnapCF` iterate over a raw
+`Snapshot`.
+
+### Iterators
+
+An iterator starts out unpositioned. Move it with `iterFirst`, `iterLast` or
+`iterSeek`, then step with `iterNext` and `iterPrev`. `iterKey`, `iterValue` and
+`iterEntry` return `Nothing` once it moves past either end.
+
+```haskell
+-- every key that starts with "user:"
+withIter db $ \it -> do
+  iterSeek it "user:"
+  let go acc =
+        iterEntry it >>= \case
+          Just (k, v) | "user:" `B.isPrefixOf` k -> iterNext it >> go ((k, v) : acc)
+          _ -> return (reverse acc)
+  users <- go []
+  iterGetError it >>= mapM_ (fail . show) -- tell the end apart from an error
+  return users
+```
+
+`createIterator`, `createIteratorSnap` and `destroyIterator` manage iterators
+by hand.
+
+### Properties and maintenance
+
+```haskell
+getProperty db Stats >>= mapM_ BS.putStr
+getProperty db BlockCacheUsage
+getPropertyCF db cf CurSizeAllMemTables
+approximateSize db ("a", "z")
+```
+
+`destroy` deletes a closed database and `repair` tries to recover a damaged one.
+Both take a raw `Options` pointer, which the public API does not let you build
+yet, so in practice they are only usable from inside the library.
+
+## Configuration
+
+`Config` has a `Default` instance: all flags off and all sizes left to RocksDB.
+
+| Field                         | Scope           | Meaning                                                         |
+| ----------------------------- | --------------- | --------------------------------------------------------------- |
+| `createIfMissing`             | database        | Create the database if it does not exist                        |
+| `errorIfExists`               | database        | Fail if the database already exists                             |
+| `paranoidChecks`              | database        | Check data aggressively and stop on corruption                  |
+| `createMissingColumnFamilies` | database        | Create column families given to `withDBCF` that are missing     |
+| `maxFiles`                    | database        | Maximum number of open files (default unlimited)                |
+| `dbWriteBufferSize`           | database        | Bytes of all memtables together (default unlimited)             |
+| `blockCacheSize`              | database        | Bytes of an LRU block cache shared by all column families       |
+| `maxTotalWalSize`             | database        | Bytes of write-ahead log that trigger flushes                   |
+| `writeBufferSize`             | column family   | Bytes per memtable (RocksDB default 64 MiB)                     |
+| `bloomFilter`                 | column family   | Full Bloom filter with 10 bits per key                          |
+| `prefixLength`                | column family   | Length of a fixed key prefix for the prefix extractor           |
+| `cacheIndexAndFilterBlocks`   | column family   | Keep index and filter blocks in the block cache                 |
+
+Database-wide fields are ignored in the configuration of a column family.
+
+### Choosing a configuration
+
+RocksDB's defaults are tuned for small databases. Its memory use outside the
+Haskell heap comes from three places:
+
+1. **Memtables**, where recent writes collect before being flushed to disk.
+   Capped per column family by `writeBufferSize` and in total by
+   `dbWriteBufferSize`.
+2. **Index and filter blocks** of every open table file. Without
+   `cacheIndexAndFilterBlocks` they live outside any cache and grow with the
+   number of files; `maxFiles` limits how many are open.
+3. **The block cache** of recently read data. `blockCacheSize` makes one cache
+   shared by all column families, so memory goes where reads are.
+
+A reasonable starting point for a large database is a block cache of a few
+hundred megabytes to a few gigabytes, `cacheIndexAndFilterBlocks = True`,
+`maxFiles` of a few thousand, and `dbWriteBufferSize` of a few hundred
+megabytes. Enable `bloomFilter` when most reads are point lookups of keys that
+may not exist.
+
+For a large database, setting `maxFiles`, `blockCacheSize`,
+`cacheIndexAndFilterBlocks` and `dbWriteBufferSize` puts a ceiling on the memory
+RocksDB uses outside the Haskell heap. Without them, index and filter blocks of
+every open file and every memtable can grow without bound.
+
+## Module overview
+
+`Database.RocksDB` is the only exposed module. It re-exports:
+
+| Part                     | Contents                                                       |
+| ------------------------ | -------------------------------------------------------------- |
+| Types                    | `DB`, `Config`, `BatchOp`, `ColumnFamily`, `Snapshot`, `Iterator`, `Property`, `Range` |
+| Opening                  | `withDB`, `withDBCF`                                           |
+| Reading and writing      | `get`, `getCF`, `multiGet`, `multiGetCF`, `put`, `putCF`, `delete`, `deleteCF`, `write` |
+| Snapshots                | `withSnapshot`, `createSnapshot`, `releaseSnapshot`            |
+| Iteration                | `withIter`, `withIterCF`, `withIterSnap`, `withIterSnapCF`, `iterSeek`, `iterFirst`, `iterLast`, `iterNext`, `iterPrev`, `iterKey`, `iterValue`, `iterEntry`, `iterValid`, `iterGetError` |
+| Administration           | `getProperty`, `getPropertyCF`, `approximateSize`, `destroy`, `repair` |
+
+The FFI bindings (`Database.RocksDB.C`) and helpers (`Database.RocksDB.Internal`)
+are hidden modules.
+
+## Things to watch out for
+
+- Do not use a `DB`, snapshot or iterator after the function that gave it to you
+  has returned. The C object is gone and RocksDB will crash.
+- Only one process can open a database at a time. A second open fails with a
+  lock error.
+- Moving an invalid iterator does nothing. Reposition it with `iterSeek`,
+  `iterFirst` or `iterLast`.
+- An iterator that becomes invalid because of corruption looks like one that
+  reached the end. Check `iterGetError`.
+- Calls into RocksDB are `safe` foreign calls, so they do not block other
+  Haskell threads, but each one costs a little more than an `unsafe` call.
+
 ## Development
 
 ### Stack
 
-```bash
-stack build
-stack test
+```sh
+git clone https://github.com/jprupp/rocksdb-haskell.git
+cd rocksdb-haskell
+
+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.
+
 ### Nix
 
-```bash
+```sh
 nix develop
 cabal build all
 cabal test all
 ```
 
-The shell includes GHC 9.8.4, cabal, haskell-language-server, fourmolu,
-hlint, and hoogle.
+The shell includes GHC 9.8.4, cabal, haskell-language-server, fourmolu, hlint
+and hoogle.
+
+`package.yaml` is the source of the package description. Change it rather than
+`rocksdb-haskell-jprupp.cabal`, which [hpack](https://github.com/sol/hpack)
+generates.
+
+The tests are in `test/Spec.hs` and use [hspec](https://hspec.github.io/) and
+QuickCheck. Each test runs against a fresh database in a temporary directory.
+
+### Contributing
+
+Pull requests are welcome.
+
+- Format new code with fourmolu, which the Nix shell provides.
+- Add tests for the behaviour you change.
+- Describe the change in `CHANGELOG.md`. Versions follow the
+  [Haskell Package Versioning Policy](https://pvp.haskell.org/).
+
+## Related projects
+
+- [rocksdb-query](https://github.com/jprupp/rocksdb-query) stores and queries
+  Haskell types with these bindings.
+- [haskoin-store](https://github.com/jprupp/haskoin-store) is a Bitcoin block
+  chain indexer built on them.
+
+This library started as a fork of
+[leveldb-haskell](https://github.com/kim/leveldb-haskell) and of earlier RocksDB
+bindings. See [AUTHORS](https://github.com/jprupp/rocksdb-haskell/blob/master/AUTHORS).
+
+## License
+
+rocksdb-haskell-jprupp is released under the
+[BSD 3-Clause License](https://github.com/jprupp/rocksdb-haskell/blob/master/LICENSE).
diff --git a/rocksdb-haskell-jprupp.cabal b/rocksdb-haskell-jprupp.cabal
--- a/rocksdb-haskell-jprupp.cabal
+++ b/rocksdb-haskell-jprupp.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           rocksdb-haskell-jprupp
-version:        2.6.0
+version:        2.6.1
 synopsis:       Haskell bindings for RocksDB
 description:    See README at <https://github.com/jprupp/rocksdb-haskell#readme>
 category:       Database, FFI
diff --git a/src/Database/RocksDB.hs b/src/Database/RocksDB.hs
--- a/src/Database/RocksDB.hs
+++ b/src/Database/RocksDB.hs
@@ -1,3 +1,88 @@
+-- |
+-- Module      : Database.RocksDB
+-- Copyright   : (c) 2012-2013 The leveldb-haskell Authors
+--               (c) 2014-2020 The rocksdb-haskell Authors
+--               (c) 2025 JP Rupp
+-- License     : BSD3
+-- Maintainer  : jprupp@protonmail.ch
+-- Stability   : experimental
+-- Portability : non-portable
+--
+-- Haskell bindings to <https://rocksdb.org RocksDB>, an embedded persistent
+-- key-value store.
+--
+-- = Purpose
+--
+-- RocksDB is a fast embedded key-value store: a library, not a server, that
+-- keeps sorted keys and values in files on local disk. These bindings let a
+-- Haskell program use it as its storage engine. They were written for, and are
+-- used by, the Haskoin Bitcoin indexer, which keeps hundreds of
+-- gigabytes in RocksDB and reads them concurrently from many threads.
+--
+-- = Design
+--
+-- * __Close to the C API.__ The functions map almost one to one to RocksDB's C
+--   functions, so the RocksDB documentation applies directly and nothing is
+--   hidden behind an abstraction you would have to learn separately. Higher
+--   level conveniences, such as typed keys and values, belong in libraries on
+--   top, like @rocksdb-query@.
+--
+-- * __Resources are scoped.__ A database, a snapshot and an iterator are C
+--   objects that must be freed exactly once and never used afterwards. Every
+--   such resource has a @with@ function that frees it when the scope ends, even
+--   on an exception. Manual @create@ and @destroy@ functions exist for the cases
+--   where a lifetime cannot be a scope.
+--
+-- * __Plain 'IO'.__ Every function runs in 'IO'. Lifting into an application
+--   monad is left to the caller, which keeps the bindings free of monad
+--   transformer dependencies and keeps their types simple.
+--
+-- * __Bytes in, bytes out.__ Keys and values are strict
+--   'Data.ByteString.ByteString's, the representation RocksDB itself uses.
+--   Serialization is a separate concern.
+--
+-- * __Memory you can bound.__ RocksDB keeps a lot of memory outside the Haskell
+--   heap. 'Config' exposes the settings that put a ceiling on it, because an
+--   indexer that grows until the machine runs out of memory is not usable.
+--
+-- This is the only module that needs to be imported. It re-exports
+-- "Database.RocksDB.Base", which in turn re-exports the iterator functions.
+--
+-- = Quick start
+--
+-- @
+-- -- with OverloadedStrings
+-- import Data.Default (def)
+-- import Database.RocksDB
+--
+-- main :: IO ()
+-- main =
+--     'withDB' "\/tmp\/example" def{'createIfMissing' = True} $ \\db -> do
+--         'put' db "hello" "world"
+--         'get' db "hello" >>= print            -- Just "world"
+--         'write' db ['Put' "a" "1", 'Put' "b" "2", 'Del' "hello"]
+--         'withIter' db $ \\it -> do
+--             'iterFirst' it
+--             'iterEntry' it >>= print          -- Just ("a","1")
+-- @
+--
+-- = Keys and values
+--
+-- Keys and values are strict 'Data.ByteString.ByteString's. RocksDB orders keys
+-- lexicographically by their bytes, which is the order used by iterators.
+--
+-- = Column families
+--
+-- A database can hold several independent key spaces called column families.
+-- Open such a database with 'withDBCF'. The handles for the extra column
+-- families are found in 'columnFamilies', in the same order as they were passed
+-- to 'withDBCF'. Functions ending in @CF@ act on a given column family; the
+-- others act on the default column family.
+--
+-- = Errors
+--
+-- Errors reported by RocksDB are thrown as 'IOError' exceptions created with
+-- 'userError', with a message that names the failing operation.
 module Database.RocksDB (module Base) where
 
 import Database.RocksDB.Base as Base
diff --git a/src/Database/RocksDB/Base.hs b/src/Database/RocksDB/Base.hs
--- a/src/Database/RocksDB/Base.hs
+++ b/src/Database/RocksDB/Base.hs
@@ -10,7 +10,14 @@
 --
 -- RocksDB Haskell binding.
 --
--- The API closely follows the C-API of RocksDB.
+-- The API closely follows the C-API of RocksDB. All resources (databases,
+-- snapshots, iterators) are managed with bracket-style functions such as
+-- 'withDB', 'withSnapshot' and 'withIter', which release them when the
+-- supplied action returns or throws. Manual variants such as 'createSnapshot'
+-- and 'createIterator' exist for cases where the lifetime of a resource cannot
+-- be expressed as a scope.
+--
+-- A 'DB' handle is safe to use from many threads at once.
 
 module Database.RocksDB.Base
     ( -- * Exported Types
@@ -69,10 +76,15 @@
 import           Foreign.Storable
 import           System.Directory
 
--- | Properties exposed by RocksDB
+-- | Properties exposed by RocksDB. Read them with 'getProperty' or
+-- 'getPropertyCF'. Every value is returned by RocksDB as text.
 data Property
+    -- | @rocksdb.num-files-at-level\<N\>@: number of SST files at level @N@
     = NumFilesAtLevel Int
+    -- | @rocksdb.stats@: multi-line human-readable statistics
     | Stats
+    -- | @rocksdb.sstables@: description of the SST files that make up the
+    -- database
     | SSTables
     -- | @rocksdb.estimate-table-readers-mem@: memory for index and filter
     -- blocks of open table files, outside the block cache
@@ -85,16 +97,36 @@
     | PropertyName String
     deriving (Eq, Show)
 
+-- | A single operation in an atomic batch written with 'write'.
+--
+-- Batches are how a program keeps several keys consistent with each other: an
+-- index and the data it points to, for example, must change together or not at
+-- all. RocksDB applies a batch atomically and in one write to its log, so a batch
+-- is also much faster than the same writes made one by one.
 data BatchOp = Put !ByteString !ByteString
+               -- ^ set a key to a value in the default column family
              | Del !ByteString
+               -- ^ delete a key from the default column family
              | PutCF !ColumnFamily !ByteString !ByteString
+               -- ^ set a key to a value in a column family
              | DelCF !ColumnFamily !ByteString
+               -- ^ delete a key from a column family
              deriving (Eq, Show)
 
--- | Open a database.
+-- | Open a database at the given path and pass its handle to a function.
 --
--- The returned handle will be automatically released with 'close'
--- when the function exits.
+-- The handle is closed automatically when the function returns or throws, and
+-- must not be used afterwards. If 'createIfMissing' is set, the directory is
+-- created first if needed. Throws an 'IOError' if the database cannot be
+-- opened, for example if it does not exist and 'createIfMissing' is not set,
+-- or if it is locked by another process.
+--
+-- Only the default column family is opened. A database that has other column
+-- families must be opened with 'withDBCF'.
+--
+-- Opening is expensive and a database can only be opened by one process at a
+-- time, so open it once at program start and share the 'DB' between threads for
+-- the life of the program.
 withDB :: FilePath -> Config -> (DB -> IO a) -> IO a
 withDB path config f =
     withBlockCache config $ \cache ->
@@ -116,6 +148,25 @@
                       , writeOpts = write_opts
                       }
 
+-- | Open a database with column families and pass its handle to a function.
+--
+-- The second argument configures the database and its default column family.
+-- The third argument lists the other column families by name, each with its own
+-- 'Config'. Their handles are stored in 'columnFamilies', in the same order.
+--
+-- If the database directory is empty, a new database is created along with all
+-- the listed column families. Otherwise the existing database is opened with
+-- those column families; if some of them do not exist yet, the open fails unless
+-- 'createMissingColumnFamilies' is set in the database 'Config'. RocksDB also
+-- requires every column family that exists in the database to be listed.
+--
+-- The handle and all column family handles are released when the function
+-- returns or throws.
+--
+-- Column families are separate key spaces in one database. They share the
+-- write-ahead log, so a batch can change several of them atomically, but each
+-- has its own files, memtables and tuning. Use them to keep data of different
+-- shapes apart, such as an index with short keys and a table of large values.
 withDBCF :: FilePath
          -> Config
          -> [(String, Config)]
@@ -197,6 +248,16 @@
 
 -- | Run an action with a snapshot of the database.
 -- The 'DB' object is not valid after the action ends.
+--
+-- The 'DB' passed to the action reads from a consistent point-in-time view of
+-- the database: 'get', 'multiGet' and iterators created from it do not see
+-- writes made after the snapshot was taken. Writes through it still go to the
+-- live database.
+--
+-- Without a snapshot, two reads made one after the other can see different
+-- states of the database if another thread writes in between. Anything that
+-- reads several related keys and expects them to agree, such as an index and the
+-- records it points to, should read through a snapshot.
 withSnapshot :: DB -> (DB -> IO a) -> IO a
 withSnapshot db f =
     bracket (createSnapshot db) releaseSnapshot (f . fst)
@@ -217,7 +278,8 @@
   destroyReadOpts read_opts
   c_rocksdb_release_snapshot db_ptr snap_ptr
 
--- | Get a DB property.
+-- | Get a property of the default column family, or of the database as a
+-- whole. Returns 'Nothing' if RocksDB does not know the property.
 getProperty :: DB -> Property -> IO (Maybe ByteString)
 getProperty DB{rocksDB = db_ptr} p =
     withCString (propertyName p) $ \name ->
@@ -230,6 +292,8 @@
     withCString (propertyName p) $ \name ->
     c_rocksdb_property_value_cf db_ptr cf name >>= propertyResult
 
+-- | Turn a property value returned by RocksDB into a 'ByteString', freeing the
+-- C string.
 propertyResult :: CString -> IO (Maybe ByteString)
 propertyResult val_ptr
     | val_ptr == nullPtr = return Nothing
@@ -238,6 +302,7 @@
           freeCString val_ptr
           return (Just res)
 
+-- | The name RocksDB uses for a property.
 propertyName :: Property -> String
 propertyName = \case
     NumFilesAtLevel i       -> "rocksdb.num-files-at-level" ++ show i
@@ -248,23 +313,31 @@
     BlockCacheUsage         -> "rocksdb.block-cache-usage"
     PropertyName n          -> n
 
--- | Destroy the given RocksDB database.
+-- | Destroy the given RocksDB database, deleting all of its files. The
+-- database must not be open. Throws an 'IOError' on failure.
 destroy :: FilePath -> Options -> IO ()
 destroy path opts_ptr =
     withCString path $ \path_ptr ->
     throwIfErr "destroy" $ c_rocksdb_destroy_db opts_ptr path_ptr
 
--- | Repair the given RocksDB database.
+-- | Try to recover as much data as possible from a corrupted database. Some
+-- data may be lost. The database must not be open. Throws an 'IOError' on
+-- failure.
 repair :: FilePath -> Options -> IO ()
 repair path opts_ptr =
     withCString path $ \path_ptr ->
     throwIfErr "repair" $ c_rocksdb_repair_db opts_ptr path_ptr
 
 
+-- | A key range given as @(start, limit)@. The start key is included and the
+-- limit key is excluded.
+--
 -- TODO: support [Range], like C API does
 type Range  = (ByteString, ByteString)
 
--- | Inspect the approximate sizes of the different levels.
+-- | Approximate number of bytes of file system space used by the keys in a
+-- 'Range' of the default column family. Data still in memtables may not be
+-- counted.
 approximateSize :: DB -> Range -> IO Int64
 approximateSize DB{rocksDB = db_ptr} (from, to) =
     BU.unsafeUseAsCStringLen from $ \(from_ptr, flen) ->
@@ -283,13 +356,17 @@
     where
         toInt64 = return . fromIntegral
 
--- | Write a key/value pair.
+-- | Write a key/value pair to the default column family, replacing any
+-- previous value. Throws an 'IOError' on failure.
 put :: DB -> ByteString -> ByteString -> IO ()
 put db = putCommon db Nothing
 
+-- | Like 'put', but write to the given column family.
 putCF :: DB -> ColumnFamily -> ByteString -> ByteString -> IO ()
 putCF db cf = putCommon db (Just cf)
 
+-- | Write a key/value pair to the default column family, or to a column family
+-- if one is given.
 putCommon :: DB -> Maybe ColumnFamily -> ByteString -> ByteString -> IO ()
 putCommon DB{rocksDB = db_ptr, writeOpts = write_opts} mcf key value =
     BU.unsafeUseAsCStringLen key   $ \(key_ptr, klen) ->
@@ -304,13 +381,17 @@
                       key_ptr (intToCSize klen)
                       val_ptr (intToCSize vlen)
 
--- | Read a value by key.
+-- | Read a value by key from the default column family. Returns 'Nothing' if
+-- the key is not found. Throws an 'IOError' on failure.
 get :: DB -> ByteString -> IO (Maybe ByteString)
 get db = getCommon db Nothing
 
+-- | Like 'get', but read from the given column family.
 getCF :: DB -> ColumnFamily -> ByteString -> IO (Maybe ByteString)
 getCF db cf = getCommon db (Just cf)
 
+-- | Read a value from the default column family, or from a column family if one
+-- is given.
 getCommon :: DB -> Maybe ColumnFamily -> ByteString -> IO (Maybe ByteString)
 getCommon DB{rocksDB = db_ptr, readOpts = read_opts} mcf key =
     BU.unsafeUseAsCStringLen key $ \(key_ptr, klen) ->
@@ -333,7 +414,12 @@
 -- | Read the values of many keys in one call. RocksDB looks the keys up
 -- together, sharing the work of finding the files and blocks that hold
 -- them, which is faster than reading them one by one. The values are in the
--- order of the keys.
+-- order of the keys, with 'Nothing' for keys that are not found. Duplicate keys
+-- are allowed. Throws an 'IOError' if any of the lookups fails.
+--
+-- For a query that needs hundreds of keys at once, such as the balances of many
+-- addresses, this cuts the time spent finding blocks and crossing into C to a
+-- fraction of separate 'get' calls.
 multiGet :: DB -> [ByteString] -> IO [Maybe ByteString]
 multiGet db = multiGetCommon db Nothing
 
@@ -341,6 +427,8 @@
 multiGetCF :: DB -> ColumnFamily -> [ByteString] -> IO [Maybe ByteString]
 multiGetCF db cf = multiGetCommon db (Just cf)
 
+-- | Read many values from the default column family, or from a column family if
+-- one is given.
 multiGetCommon :: DB -> Maybe ColumnFamily -> [ByteString] -> IO [Maybe ByteString]
 multiGetCommon _ _ [] = return []
 multiGetCommon DB{rocksDB = db_ptr, readOpts = read_opts} mcf keys =
@@ -380,13 +468,17 @@
             m : _ -> throwIO $ userError $ "multi_get: " ++ m
             [] -> return res
 
+-- | Delete a key from the default column family. Deleting a key that does not
+-- exist is not an error. Throws an 'IOError' on failure.
 delete :: DB -> ByteString -> IO ()
 delete db = deleteCommon db Nothing
 
+-- | Like 'delete', but delete from the given column family.
 deleteCF :: DB -> ColumnFamily -> ByteString -> IO ()
 deleteCF db cf = deleteCommon db (Just cf)
 
--- | Delete a key/value pair.
+-- | Delete a key/value pair from the default column family, or from a column
+-- family if one is given.
 deleteCommon :: DB -> Maybe ColumnFamily -> ByteString -> IO ()
 deleteCommon DB{rocksDB = db_ptr, writeOpts = write_opts} mcf key =
     BU.unsafeUseAsCStringLen key $ \(key_ptr, klen) ->
@@ -394,7 +486,13 @@
     Just cf -> c_rocksdb_delete_cf db_ptr write_opts cf key_ptr (intToCSize klen)
     Nothing -> c_rocksdb_delete db_ptr write_opts key_ptr (intToCSize klen)
 
--- | Perform a batch mutation.
+-- | Perform a batch mutation. All operations in the list are applied atomically:
+-- either all of them are written or none are. They are applied in order, so a
+-- later operation on a key overrides an earlier one. Throws an 'IOError' on
+-- failure.
+--
+-- The keys and values are kept alive until RocksDB has copied them, because
+-- they are handed to C as pointers into Haskell memory.
 write :: DB -> [BatchOp] -> IO ()
 write DB{rocksDB = db_ptr, writeOpts = write_opts} batch =
     bracket
@@ -443,6 +541,8 @@
     touch (DelCF _ (PS p _ _)) =
         touchForeignPtr p
 
+-- | Marshal a list of strings into C strings that are valid while the function
+-- runs.
 withStrings :: [String] -> ([CString] -> IO a) -> IO a
 withStrings ss f =
     go [] ss
diff --git a/src/Database/RocksDB/C.hs b/src/Database/RocksDB/C.hs
--- a/src/Database/RocksDB/C.hs
+++ b/src/Database/RocksDB/C.hs
@@ -9,6 +9,18 @@
 -- Stability   : experimental
 -- Portability : non-portable
 --
+-- Raw FFI bindings to the RocksDB C API declared in @rocksdb/c.h@. This module
+-- is internal; use "Database.RocksDB" instead.
+--
+-- Each @c_rocksdb_*@ function is a direct binding to the C function of the same
+-- name without the @c_@ prefix. Functions that take an 'ErrPtr' report failure
+-- by storing a @malloc@-allocated error message in it, which the caller must
+-- free. See 'Database.RocksDB.Internal.throwIfErr'.
+--
+-- The bindings are @safe@ foreign calls. RocksDB calls can take a long time,
+-- for example when they wait for disk or for a compaction to make room, and a
+-- @safe@ call lets other Haskell threads keep running meanwhile. The small
+-- extra cost per call is worth that in a concurrent program.
 
 module Database.RocksDB.C
     ( RocksDB
@@ -105,41 +117,72 @@
 import           Foreign.C.String
 import           Foreign.C.Types
 
+-- | Opaque C database object.
 data LRocksDB
+-- | Opaque C column family handle.
 data LColumnFamily
+-- | Opaque C iterator.
 data LIterator
+-- | Opaque C database or column family options.
 data LOptions
+-- | Opaque C block-based table options.
 data LBlockBasedOptions
+-- | Opaque C read options.
 data LReadOpts
+-- | Opaque C snapshot.
 data LSnapshot
+-- | Opaque C write batch.
 data LWriteBatch
+-- | Opaque C write options.
 data LWriteOpts
+-- | Opaque C filter policy.
 data LFilterPolicy
+-- | Opaque C slice transform (prefix extractor).
 data LSliceTransform
+-- | Opaque C block cache.
 data LCache
 
+-- | Pointer to an open database.
 type RocksDB            = Ptr LRocksDB
+-- | Pointer to a column family handle.
 type ColumnFamily       = Ptr LColumnFamily
+-- | Pointer to database or column family options.
 type Options            = Ptr LOptions
+-- | Pointer to a batch of writes applied atomically.
 type WriteBatch         = Ptr LWriteBatch
+-- | Pointer to a key transformation, used to extract prefixes.
 type SliceTransform     = Ptr LSliceTransform
+-- | Pointer to options for reads and iterators.
 type ReadOpts           = Ptr LReadOpts
+-- | Pointer to options for writes.
 type WriteOpts          = Ptr LWriteOpts
+-- | Pointer to a consistent point-in-time view of a database.
 type Snapshot           = Ptr LSnapshot
+-- | Pointer to an iterator over keys.
 type Iterator           = Ptr LIterator
+-- | Pointer to a filter policy, such as a bloom filter.
 type FilterPolicy       = Ptr LFilterPolicy
+-- | Pointer to block-based table options.
 type BlockBasedOptions  = Ptr LBlockBasedOptions
+-- | Pointer to a block cache.
 type Cache              = Ptr LCache
 
+-- | Pointer to a location where a C function stores an error message, or leaves @NULL@ on success.
 type ErrPtr           = Ptr CString
+-- | Path of a database, as a C string.
 type DBName           = CString
+-- | Name of a column family, as a C string.
 type CFName           = CString
+-- | Pointer to the bytes of a key. Its length is passed separately.
 type Key              = CString
+-- | Pointer to the bytes of a value. Its length is passed separately.
 type Val              = CString
 
+-- | Binding to @rocksdb_open@.
 foreign import ccall safe "rocksdb/c.h rocksdb_open"
   c_rocksdb_open :: Options -> DBName -> ErrPtr -> IO RocksDB
 
+-- | Binding to @rocksdb_open_column_families@.
 foreign import ccall safe "rocksdb/c.h rocksdb_open_column_families"
   c_rocksdb_open_column_families :: Options
                                  -> DBName
@@ -150,9 +193,11 @@
                                  -> ErrPtr
                                  -> IO RocksDB
 
+-- | Binding to @rocksdb_close@.
 foreign import ccall safe "rocksdb/c.h rocksdb_close"
   c_rocksdb_close :: RocksDB -> IO ()
 
+-- | Binding to @rocksdb_create_column_family@.
 foreign import ccall safe "rocksdb/c.h rocksdb_create_column_family"
   c_rocksdb_create_column_family :: RocksDB
                                  -> Options
@@ -160,15 +205,18 @@
                                  -> ErrPtr
                                  -> IO ColumnFamily
 
+-- | Binding to @rocksdb_drop_column_family@.
 foreign import ccall safe "rocksdb/c.h rocksdb_drop_column_family"
   c_rocksdb_drop_column_family :: RocksDB
                                -> ColumnFamily
                                -> ErrPtr
                                -> IO ()
 
+-- | Binding to @rocksdb_column_family_handle_destroy@.
 foreign import ccall safe "rocksdb/c.h rocksdb_column_family_handle_destroy"
   c_rocksdb_column_family_handle_destroy :: ColumnFamily -> IO ()
 
+-- | Binding to @rocksdb_put@.
 foreign import ccall safe "rocksdb/c.h rocksdb_put"
   c_rocksdb_put :: RocksDB
                 -> WriteOpts
@@ -177,6 +225,7 @@
                 -> ErrPtr
                 -> IO ()
 
+-- | Binding to @rocksdb_put_cf@.
 foreign import ccall safe "rocksdb/c.h rocksdb_put_cf"
   c_rocksdb_put_cf :: RocksDB
                    -> WriteOpts
@@ -186,6 +235,7 @@
                    -> ErrPtr
                    -> IO ()
 
+-- | Binding to @rocksdb_delete@.
 foreign import ccall safe "rocksdb/c.h rocksdb_delete"
   c_rocksdb_delete :: RocksDB
                    -> WriteOpts
@@ -193,6 +243,7 @@
                    -> ErrPtr
                    -> IO ()
 
+-- | Binding to @rocksdb_delete_cf@.
 foreign import ccall safe "rocksdb/c.h rocksdb_delete_cf"
   c_rocksdb_delete_cf :: RocksDB
                       -> WriteOpts
@@ -201,6 +252,7 @@
                       -> ErrPtr
                       -> IO ()
 
+-- | Binding to @rocksdb_write@.
 foreign import ccall safe "rocksdb/c.h rocksdb_write"
   c_rocksdb_write :: RocksDB
                   -> WriteOpts
@@ -210,6 +262,8 @@
 
 -- | Returns NULL if not found. A malloc()ed array otherwise.
 -- Stores the length of the array in *vallen.
+--
+-- Binding to @rocksdb_get@.
 foreign import ccall safe "rocksdb/c.h rocksdb_get"
   c_rocksdb_get :: RocksDB
                 -> ReadOpts
@@ -218,6 +272,7 @@
                 -> ErrPtr
                 -> IO CString
 
+-- | Binding to @rocksdb_get_cf@.
 foreign import ccall safe "rocksdb/c.h rocksdb_get_cf"
   c_rocksdb_get_cf :: RocksDB
                    -> ReadOpts
@@ -227,6 +282,7 @@
                    -> ErrPtr
                    -> IO CString
 
+-- | Binding to @rocksdb_multi_get@.
 foreign import ccall safe "rocksdb/c.h rocksdb_multi_get"
   c_rocksdb_multi_get :: RocksDB
                       -> ReadOpts
@@ -238,6 +294,7 @@
                       -> ErrPtr
                       -> IO ()
 
+-- | Binding to @rocksdb_multi_get_cf@.
 foreign import ccall safe "rocksdb/c.h rocksdb_multi_get_cf"
   c_rocksdb_multi_get_cf :: RocksDB
                          -> ReadOpts
@@ -250,20 +307,26 @@
                          -> ErrPtr
                          -> IO ()
 
+-- | Binding to @rocksdb_create_snapshot@.
 foreign import ccall safe "rocksdb/c.h rocksdb_create_snapshot"
   c_rocksdb_create_snapshot :: RocksDB -> IO Snapshot
 
+-- | Binding to @rocksdb_release_snapshot@.
 foreign import ccall safe "rocksdb/c.h rocksdb_release_snapshot"
   c_rocksdb_release_snapshot :: RocksDB -> Snapshot -> IO ()
 
 -- | Returns NULL if property name is unknown. Else returns a pointer to a
 -- malloc()-ed null-terminated value.
+--
+-- Binding to @rocksdb_property_value@.
 foreign import ccall safe "rocksdb/c.h rocksdb_property_value"
   c_rocksdb_property_value :: RocksDB -> CString -> IO CString
 
+-- | Binding to @rocksdb_property_value_cf@.
 foreign import ccall safe "rocksdb/c.h rocksdb_property_value_cf"
   c_rocksdb_property_value_cf :: RocksDB -> ColumnFamily -> CString -> IO CString
 
+-- | Binding to @rocksdb_approximate_sizes@.
 foreign import ccall safe "rocksdb/c.h rocksdb_approximate_sizes"
   c_rocksdb_approximate_sizes :: RocksDB
                               -> CInt
@@ -276,9 +339,11 @@
                                  -- ^ array of approx. sizes of ranges
                               -> IO ()
 
+-- | Binding to @rocksdb_destroy_db@.
 foreign import ccall safe "rocksdb/c.h rocksdb_destroy_db"
   c_rocksdb_destroy_db :: Options -> DBName -> ErrPtr -> IO ()
 
+-- | Binding to @rocksdb_repair_db@.
 foreign import ccall safe "rocksdb/c.h rocksdb_repair_db"
   c_rocksdb_repair_db :: Options -> DBName -> ErrPtr -> IO ()
 
@@ -287,44 +352,56 @@
 -- Iterator
 --
 
+-- | Binding to @rocksdb_create_iterator@.
 foreign import ccall safe "rocksdb/c.h rocksdb_create_iterator"
   c_rocksdb_create_iterator :: RocksDB
                             -> ReadOpts
                             -> IO Iterator
 
+-- | Binding to @rocksdb_create_iterator_cf@.
 foreign import ccall safe "rocksdb/c.h rocksdb_create_iterator_cf"
   c_rocksdb_create_iterator_cf :: RocksDB
                                -> ReadOpts
                                -> ColumnFamily
                                -> IO Iterator
 
+-- | Binding to @rocksdb_iter_destroy@.
 foreign import ccall safe "rocksdb/c.h rocksdb_iter_destroy"
   c_rocksdb_iter_destroy :: Iterator -> IO ()
 
+-- | Binding to @rocksdb_iter_valid@.
 foreign import ccall safe "rocksdb/c.h rocksdb_iter_valid"
   c_rocksdb_iter_valid :: Iterator -> IO CUChar
 
+-- | Binding to @rocksdb_iter_seek_to_first@.
 foreign import ccall safe "rocksdb/c.h rocksdb_iter_seek_to_first"
   c_rocksdb_iter_seek_to_first :: Iterator -> IO ()
 
+-- | Binding to @rocksdb_iter_seek_to_last@.
 foreign import ccall safe "rocksdb/c.h rocksdb_iter_seek_to_last"
   c_rocksdb_iter_seek_to_last :: Iterator -> IO ()
 
+-- | Binding to @rocksdb_iter_seek@.
 foreign import ccall safe "rocksdb/c.h rocksdb_iter_seek"
   c_rocksdb_iter_seek :: Iterator -> Key -> CSize -> IO ()
 
+-- | Binding to @rocksdb_iter_next@.
 foreign import ccall safe "rocksdb/c.h rocksdb_iter_next"
   c_rocksdb_iter_next :: Iterator -> IO ()
 
+-- | Binding to @rocksdb_iter_prev@.
 foreign import ccall safe "rocksdb/c.h rocksdb_iter_prev"
   c_rocksdb_iter_prev :: Iterator -> IO ()
 
+-- | Binding to @rocksdb_iter_key@.
 foreign import ccall safe "rocksdb/c.h rocksdb_iter_key"
   c_rocksdb_iter_key :: Iterator -> Ptr CSize -> IO Key
 
+-- | Binding to @rocksdb_iter_value@.
 foreign import ccall safe "rocksdb/c.h rocksdb_iter_value"
   c_rocksdb_iter_value :: Iterator -> Ptr CSize -> IO Val
 
+-- | Binding to @rocksdb_iter_get_error@.
 foreign import ccall safe "rocksdb/c.h rocksdb_iter_get_error"
   c_rocksdb_iter_get_error :: Iterator -> ErrPtr -> IO ()
 
@@ -333,21 +410,26 @@
 -- Write batch
 --
 
+-- | Binding to @rocksdb_writebatch_create@.
 foreign import ccall safe "rocksdb/c.h rocksdb_writebatch_create"
   c_rocksdb_writebatch_create :: IO WriteBatch
 
+-- | Binding to @rocksdb_writebatch_destroy@.
 foreign import ccall safe "rocksdb/c.h rocksdb_writebatch_destroy"
   c_rocksdb_writebatch_destroy :: WriteBatch -> IO ()
 
+-- | Binding to @rocksdb_writebatch_clear@.
 foreign import ccall safe "rocksdb/c.h rocksdb_writebatch_clear"
   c_rocksdb_writebatch_clear :: WriteBatch -> IO ()
 
+-- | Binding to @rocksdb_writebatch_put@.
 foreign import ccall safe "rocksdb/c.h rocksdb_writebatch_put"
   c_rocksdb_writebatch_put :: WriteBatch
                            -> Key -> CSize
                            -> Val -> CSize
                            -> IO ()
 
+-- | Binding to @rocksdb_writebatch_put_cf@.
 foreign import ccall safe "rocksdb/c.h rocksdb_writebatch_put_cf"
   c_rocksdb_writebatch_put_cf :: WriteBatch
                               -> ColumnFamily
@@ -355,10 +437,12 @@
                               -> Val -> CSize
                               -> IO ()
 
+-- | Binding to @rocksdb_writebatch_delete@.
 foreign import ccall safe "rocksdb/c.h rocksdb_writebatch_delete"
   c_rocksdb_writebatch_delete :: WriteBatch
                               -> Key -> CSize -> IO ()
 
+-- | Binding to @rocksdb_writebatch_delete_cf@.
 foreign import ccall safe "rocksdb/c.h rocksdb_writebatch_delete_cf"
   c_rocksdb_writebatch_delete_cf :: WriteBatch
                                  -> ColumnFamily
@@ -370,36 +454,47 @@
 -- Options
 --
 
+-- | Binding to @rocksdb_options_create@.
 foreign import ccall safe "rocksdb/c.h rocksdb_options_create"
   c_rocksdb_options_create :: IO Options
 
+-- | Binding to @rocksdb_options_destroy@.
 foreign import ccall "rocksdb/c.h rocksdb_options_destroy"
   c_rocksdb_options_destroy :: Options -> IO ()
 
+-- | Binding to @rocksdb_options_set_create_if_missing@.
 foreign import ccall safe "rocksdb/c.h rocksdb_options_set_create_if_missing"
   c_rocksdb_options_set_create_if_missing :: Options -> CBool -> IO ()
 
+-- | Binding to @rocksdb_options_set_error_if_exists@.
 foreign import ccall safe "rocksdb/c.h rocksdb_options_set_error_if_exists"
   c_rocksdb_options_set_error_if_exists :: Options -> CBool -> IO ()
 
+-- | Binding to @rocksdb_options_set_paranoid_checks@.
 foreign import ccall safe "rocksdb/c.h rocksdb_options_set_paranoid_checks"
   c_rocksdb_options_set_paranoid_checks :: Options -> CBool -> IO ()
 
+-- | Binding to @rocksdb_options_set_max_open_files@.
 foreign import ccall safe "rocksdb/c.h rocksdb_options_set_max_open_files"
   c_rocksdb_options_set_max_open_files :: Options -> CInt -> IO ()
 
+-- | Binding to @rocksdb_options_set_write_buffer_size@.
 foreign import ccall safe "rocksdb/c.h rocksdb_options_set_write_buffer_size"
   c_rocksdb_options_set_write_buffer_size :: Options -> CSize -> IO ()
 
+-- | Binding to @rocksdb_options_set_db_write_buffer_size@.
 foreign import ccall safe "rocksdb/c.h rocksdb_options_set_db_write_buffer_size"
   c_rocksdb_options_set_db_write_buffer_size :: Options -> CSize -> IO ()
 
+-- | Binding to @rocksdb_options_set_max_total_wal_size@.
 foreign import ccall safe "rocksdb/c.h rocksdb_options_set_max_total_wal_size"
   c_rocksdb_options_set_max_total_wal_size :: Options -> Word64 -> IO ()
 
+-- | Binding to @rocksdb_options_set_create_missing_column_families@.
 foreign import ccall safe "rocksdb/c.h rocksdb_options_set_create_missing_column_families"
   c_rocksdb_options_set_create_missing_column_families :: Options -> CBool -> IO ()
 
+-- | Binding to @rocksdb_options_set_prefix_extractor@.
 foreign import ccall safe "rocksdb/c.h rocksdb_options_set_prefix_extractor"
   c_rocksdb_options_set_prefix_extractor :: Options
                                          -> SliceTransform
@@ -409,38 +504,46 @@
 -- Block-Based Options
 --
 
+-- | Binding to @rocksdb_block_based_options_create@.
 foreign import ccall safe "rocksdb/c.h rocksdb_block_based_options_create"
   c_rocksdb_block_based_options_create :: IO BlockBasedOptions
 
+-- | Binding to @rocksdb_block_based_options_destroy@.
 foreign import ccall safe "rocksdb/c.h rocksdb_block_based_options_destroy"
   c_rocksdb_block_based_options_destroy :: BlockBasedOptions
                                         -> IO ()
 
+-- | Binding to @rocksdb_block_based_options_set_filter_policy@.
 foreign import ccall safe "rocksdb/c.h rocksdb_block_based_options_set_filter_policy"
   c_rocksdb_block_based_options_set_filter_policy :: BlockBasedOptions
                                                   -> FilterPolicy
                                                   -> IO ()
 
+-- | Binding to @rocksdb_block_based_options_set_block_cache@.
 foreign import ccall safe "rocksdb/c.h rocksdb_block_based_options_set_block_cache"
   c_rocksdb_block_based_options_set_block_cache :: BlockBasedOptions
                                                 -> Cache
                                                 -> IO ()
 
+-- | Binding to @rocksdb_block_based_options_set_cache_index_and_filter_blocks@.
 foreign import ccall safe "rocksdb/c.h rocksdb_block_based_options_set_cache_index_and_filter_blocks"
   c_rocksdb_block_based_options_set_cache_index_and_filter_blocks :: BlockBasedOptions
                                                                   -> CUChar
                                                                   -> IO ()
 
+-- | Binding to @rocksdb_block_based_options_set_cache_index_and_filter_blocks_with_high_priority@.
 foreign import ccall safe "rocksdb/c.h rocksdb_block_based_options_set_cache_index_and_filter_blocks_with_high_priority"
   c_rocksdb_block_based_options_set_cache_index_and_filter_blocks_with_high_priority :: BlockBasedOptions
                                                                                      -> CUChar
                                                                                      -> IO ()
 
+-- | Binding to @rocksdb_block_based_options_set_pin_l0_filter_and_index_blocks_in_cache@.
 foreign import ccall safe "rocksdb/c.h rocksdb_block_based_options_set_pin_l0_filter_and_index_blocks_in_cache"
   c_rocksdb_block_based_options_set_pin_l0_filter_and_index_blocks_in_cache :: BlockBasedOptions
                                                                             -> CUChar
                                                                             -> IO ()
 
+-- | Binding to @rocksdb_options_set_block_based_table_factory@.
 foreign import ccall safe "rocksdb/c.h rocksdb_options_set_block_based_table_factory"
   c_rocksdb_options_set_block_based_table_factory :: Options
                                                   -> BlockBasedOptions
@@ -450,9 +553,11 @@
 -- Block Cache
 --
 
+-- | Binding to @rocksdb_cache_create_lru@.
 foreign import ccall safe "rocksdb/c.h rocksdb_cache_create_lru"
   c_rocksdb_cache_create_lru :: CSize -> IO Cache
 
+-- | Binding to @rocksdb_cache_destroy@.
 foreign import ccall safe "rocksdb/c.h rocksdb_cache_destroy"
   c_rocksdb_cache_destroy :: Cache -> IO ()
 
@@ -460,9 +565,11 @@
 -- Prefix Extractor
 --
 
+-- | Binding to @rocksdb_slicetransform_create_fixed_prefix@.
 foreign import ccall safe "rocksdb/c.h rocksdb_slicetransform_create_fixed_prefix"
   c_rocksdb_slicetransform_create_fixed_prefix :: CSize -> IO SliceTransform
 
+-- | Binding to @rocksdb_slicetransform_destroy@.
 foreign import ccall safe "rocksdb/c.h rocksdb_slicetransform_destroy"
   c_rocksdb_slicetransform_destroy :: SliceTransform -> IO ()
 
@@ -470,9 +577,11 @@
 -- Bloom Filter
 --
 
+-- | Binding to @rocksdb_filterpolicy_destroy@.
 foreign import ccall safe "rocksdb/c.h rocksdb_filterpolicy_destroy"
   c_rocksdb_filterpolicy_destroy :: FilterPolicy -> IO ()
 
+-- | Binding to @rocksdb_filterpolicy_create_bloom_full@.
 foreign import ccall safe "rocksdb/c.h rocksdb_filterpolicy_create_bloom_full"
   c_rocksdb_filterpolicy_create_bloom_full :: CInt -> IO FilterPolicy
 
@@ -480,12 +589,15 @@
 -- Read options
 --
 
+-- | Binding to @rocksdb_readoptions_create@.
 foreign import ccall safe "rocksdb/c.h rocksdb_readoptions_create"
   c_rocksdb_readoptions_create :: IO ReadOpts
 
+-- | Binding to @rocksdb_readoptions_destroy@.
 foreign import ccall safe "rocksdb/c.h rocksdb_readoptions_destroy"
   c_rocksdb_readoptions_destroy :: ReadOpts -> IO ()
 
+-- | Binding to @rocksdb_readoptions_set_snapshot@.
 foreign import ccall safe "rocksdb/c.h rocksdb_readoptions_set_snapshot"
   c_rocksdb_readoptions_set_snapshot :: ReadOpts -> Snapshot -> IO ()
 
@@ -494,9 +606,11 @@
 -- Write options
 --
 
+-- | Binding to @rocksdb_writeoptions_create@.
 foreign import ccall safe "rocksdb/c.h rocksdb_writeoptions_create"
   c_rocksdb_writeoptions_create :: IO WriteOpts
 
+-- | Binding to @rocksdb_writeoptions_destroy@.
 foreign import ccall safe "rocksdb/c.h rocksdb_writeoptions_destroy"
   c_rocksdb_writeoptions_destroy :: WriteOpts -> IO ()
 
@@ -504,5 +618,6 @@
 -- Free
 --
 
+-- | Binding to @rocksdb_free@.
 foreign import ccall safe "rocksdb/c.h rocksdb_free"
   c_rocksdb_free :: CString -> IO ()
diff --git a/src/Database/RocksDB/Internal.hs b/src/Database/RocksDB/Internal.hs
--- a/src/Database/RocksDB/Internal.hs
+++ b/src/Database/RocksDB/Internal.hs
@@ -8,6 +8,9 @@
 -- Stability   : experimental
 -- Portability : non-portable
 --
+-- Internal types and helpers shared by the other modules. The 'DB' and 'Config'
+-- types are re-exported by "Database.RocksDB"; everything else is an
+-- implementation detail.
 
 module Database.RocksDB.Internal
     ( Config (..)
@@ -43,21 +46,57 @@
 import           Foreign.C.String
 import           Foreign.C.Types
 
+-- | Handle to an open database. Obtain one with 'Database.RocksDB.withDB' or
+-- 'Database.RocksDB.withDBCF'. It is only valid inside the function passed to
+-- those, and can be shared between threads.
 data DB = DB { rocksDB        :: !RocksDB
+               -- ^ pointer to the C database object
              , columnFamilies :: ![ColumnFamily]
+               -- ^ handles of the column families opened with
+               -- 'Database.RocksDB.withDBCF', in the order they were given;
+               -- empty for 'Database.RocksDB.withDB'
              , readOpts       :: !ReadOpts
+               -- ^ read options used by reads and iterators; set to read from
+               -- a snapshot by 'Database.RocksDB.withSnapshot'
              , writeOpts      :: !WriteOpts
+               -- ^ write options used by all writes
              }
 
+-- | Configuration of a database or column family. Use 'def' for the defaults
+-- and override the fields you need:
+--
+-- @
+-- def { 'createIfMissing' = True, 'bloomFilter' = True }
+-- @
+--
+-- Some fields only apply to the database as a whole and are ignored in the
+-- configuration of a column family. This is noted on each field.
+--
+-- Most fields exist to control memory. RocksDB's defaults suit a small database:
+-- each column family gets its own cache, the index and filter blocks of every
+-- open file stay in memory, and memtables are not capped as a whole. On a large
+-- database these grow without bound. Setting 'maxFiles', 'blockCacheSize',
+-- 'cacheIndexAndFilterBlocks' and 'dbWriteBufferSize' turns that into a fixed
+-- budget.
 data Config = Config { createIfMissing :: !Bool
+                       -- ^ create the database if it does not exist
                      , errorIfExists   :: !Bool
+                       -- ^ fail to open if the database already exists
                      , paranoidChecks  :: !Bool
+                       -- ^ check data aggressively and stop early on any
+                       -- corruption
                      , createMissingColumnFamilies :: !Bool
                        -- ^ create the column families passed to 'withDBCF'
                        -- that an existing database does not have yet
                      , maxFiles        :: !(Maybe Int)
+                       -- ^ maximum number of files RocksDB keeps open
+                       -- ('Nothing': RocksDB default, unlimited)
                      , prefixLength    :: !(Maybe Int)
+                       -- ^ length in bytes of a fixed key prefix, used by
+                       -- bloom filters and prefix seeks
                      , bloomFilter     :: !Bool
+                       -- ^ use a full bloom filter with 10 bits per key to
+                       -- skip files that cannot contain a key
                      , writeBufferSize :: !(Maybe Int)
                        -- ^ bytes per memtable (RocksDB default 64 MiB);
                        -- per column family
@@ -84,6 +123,7 @@
                        -- configs
                      } deriving (Eq, Show)
 
+-- | All flags off and all sizes left to RocksDB defaults.
 instance Default Config where
     def = Config { createIfMissing  = False
                  , errorIfExists    = False
@@ -99,11 +139,13 @@
                  , maxTotalWalSize  = Nothing
                  }
 
+-- | Create RocksDB options from a 'Config' and pass them to a function. The
+-- options are destroyed when it returns.
 withOptions :: Config -> (Options -> IO a) -> IO a
 withOptions = withOptionsCache Nothing
 
 -- | Options of a database or column family that use this block cache, if
--- any.
+-- any. The options are destroyed when the function returns.
 withOptionsCache :: Maybe Cache -> Config -> (Options -> IO a) -> IO a
 withOptionsCache cache Config {..} f = with_opts $ \opts -> do
     when (bloomFilter || isJust cache || cacheIndexAndFilterBlocks) $
@@ -146,6 +188,8 @@
         c_rocksdb_options_destroy
 
 
+-- | Create options for several column families at once, in the same order as
+-- the configurations.
 withOptionsCF :: [Config] -> ([Options] -> IO a) -> IO a
 withOptionsCF = withOptionsCFCache Nothing
 
@@ -159,6 +203,9 @@
 
 -- | The block cache of 'blockCacheSize', if it is set. The database and
 -- column families that use it keep it alive after this returns.
+--
+-- A single cache for the whole database lets RocksDB give memory to whichever
+-- column family is busy, instead of fixing a share for each.
 withBlockCache :: Config -> (Maybe Cache -> IO a) -> IO a
 withBlockCache Config {blockCacheSize = Nothing} f = f Nothing
 withBlockCache Config {blockCacheSize = Just n} f =
@@ -166,6 +213,8 @@
             c_rocksdb_cache_destroy
             (f . Just)
 
+-- | Create read options, reading from a snapshot if one is given, and pass them
+-- to a function. They are destroyed when it returns.
 withReadOpts :: Maybe Snapshot -> (ReadOpts -> IO a) -> IO a
 withReadOpts maybe_snap_ptr =
     bracket
@@ -184,15 +233,25 @@
 destroyReadOpts :: ReadOpts -> IO ()
 destroyReadOpts = c_rocksdb_readoptions_destroy
 
+-- | Create default write options and pass them to a function. They are
+-- destroyed when it returns.
 withWriteOpts :: (WriteOpts -> IO a) -> IO a
 withWriteOpts =
     bracket
     c_rocksdb_writeoptions_create
     c_rocksdb_writeoptions_destroy
 
+-- | Free a C string allocated by RocksDB.
 freeCString :: CString -> IO ()
 freeCString = c_rocksdb_free
 
+-- | Run a C function that takes an error pointer as its last argument. If the
+-- function sets an error, free it and throw an 'IOError' whose message starts
+-- with the given operation name.
+--
+-- Every C function that can fail reports it this way, so this is the one place
+-- where RocksDB errors become Haskell exceptions, and where their messages are
+-- freed.
 throwIfErr :: String -> (ErrPtr -> IO a) -> IO a
 throwIfErr s f = alloca $ \err_ptr -> do
     poke err_ptr nullPtr
@@ -204,27 +263,33 @@
         throwIO $ userError $ s ++ ": " ++ err
     return res
 
+-- | Convert a 'Bool' to a C boolean.
 boolToCBool :: Bool -> CBool
 boolToCBool True  = 1
 boolToCBool False = 0
 {-# INLINE boolToCBool #-}
 
+-- | Convert a 'CSize' to an 'Int'.
 cSizeToInt :: CSize -> Int
 cSizeToInt = fromIntegral
 {-# INLINE cSizeToInt #-}
 
+-- | Convert an 'Int' to a 'CSize'.
 intToCSize :: Int -> CSize
 intToCSize = fromIntegral
 {-# INLINE intToCSize #-}
 
+-- | Convert an 'Int' to a 'CInt'.
 intToCInt :: Int -> CInt
 intToCInt = fromIntegral
 {-# INLINE intToCInt #-}
 
+-- | Convert a 'CInt' to an 'Int'.
 cIntToInt :: CInt -> Int
 cIntToInt = fromIntegral
 {-# INLINE cIntToInt #-}
 
+-- | Convert a 'Bool' to @1@ or @0@.
 boolToNum :: Num b => Bool -> b
 boolToNum True  = fromIntegral (1 :: Int)
 boolToNum False = fromIntegral (0 :: Int)
diff --git a/src/Database/RocksDB/Iterator.hs b/src/Database/RocksDB/Iterator.hs
--- a/src/Database/RocksDB/Iterator.hs
+++ b/src/Database/RocksDB/Iterator.hs
@@ -11,6 +11,41 @@
 -- Portability : non-portable
 --
 -- Iterating over key ranges.
+--
+-- An 'Iterator' walks the keys of a database or column family in ascending
+-- byte order. It starts out not positioned anywhere; call 'iterFirst',
+-- 'iterLast' or 'iterSeek' to position it, then 'iterNext' or 'iterPrev' to
+-- move. Read the current entry with 'iterKey', 'iterValue' or 'iterEntry',
+-- which return 'Nothing' once the iterator has moved past either end.
+--
+-- @
+-- -- all entries whose key starts with "user:"
+-- usersFrom :: DB -> IO [(ByteString, ByteString)]
+-- usersFrom db = 'withIter' db $ \\it -> do
+--     'iterSeek' it "user:"
+--     let go acc = 'iterEntry' it >>= \\case
+--             Just (k, v) | "user:" \`B.isPrefixOf\` k -> 'iterNext' it >> go ((k, v) : acc)
+--             _ -> return (reverse acc)
+--     go []
+-- @
+--
+-- After iterating, check 'iterGetError' to tell the end of the data apart from
+-- an error such as corruption.
+--
+-- = Why iterators
+--
+-- Sorted keys are what make RocksDB more than a hash table. Keys that share a
+-- prefix are stored next to each other, so all the records of one user, or all
+-- the transactions of one address, can be read with one seek and a short scan.
+-- Designing keys so that the records you read together sort together is the
+-- most important performance decision in a RocksDB schema.
+--
+-- = Safety
+--
+-- The C API crashes the process if an invalid iterator is moved, and the bytes
+-- it returns are only valid until it moves again. This module guards against
+-- both: 'iterNext' and 'iterPrev' do nothing on an invalid iterator, and keys
+-- and values are copied into Haskell memory before they are returned.
 module Database.RocksDB.Iterator
   ( Iterator,
     -- * Bracket-style iteration
@@ -59,6 +94,7 @@
 withIter :: DB -> (Iterator -> IO a) -> IO a
 withIter db = withIterCommon db Nothing Nothing
 
+-- | Like 'withIter', but iterate over the given column family.
 withIterCF :: DB -> ColumnFamily -> (Iterator -> IO a) -> IO a
 withIterCF db cf = withIterCommon db Nothing (Just cf)
 
@@ -88,6 +124,8 @@
     -> IO a
 withIterSnapCF db msnap cf = withIterCommon db msnap (Just cf)
 
+-- | Create an iterator over the default column family, or over a column family
+-- if one is given, reading from a snapshot if one is given.
 withIterCommon
     :: DB
     -> Maybe Snapshot
@@ -116,7 +154,11 @@
             Just cf -> c_rocksdb_create_iterator_cf rocks_db ro cf
             Nothing -> c_rocksdb_create_iterator rocks_db ro
 
--- | Manually create unmanaged iterator.
+-- | Manually create unmanaged iterator, over the default column family or over
+-- a column family if one is given. It uses the 'readOpts' of the 'DB', so if
+-- the 'DB' comes from 'Database.RocksDB.withSnapshot' it reads from that
+-- snapshot. It must be released with 'destroyIterator', and must not outlive
+-- the 'DB'.
 createIterator :: DB -> Maybe ColumnFamily -> IO Iterator
 createIterator DB{rocksDB = rocks_db, readOpts = read_opts} mcf =
     throwErrnoIfNull "create_iterator" $ case mcf of
@@ -157,7 +199,8 @@
         Just cf -> c_rocksdb_create_iterator_cf rocks_db ro cf
         Nothing -> c_rocksdb_create_iterator rocks_db ro
 
--- | Destroy unmanaged iterator.
+-- | Destroy unmanaged iterator created with 'createIterator' or
+-- 'createIteratorSnap'. It must not be used afterwards.
 destroyIterator :: Iterator -> IO ()
 destroyIterator = c_rocksdb_iter_destroy
 
@@ -192,6 +235,9 @@
 -- If the iterator is not valid, this function does nothing: RocksDB does not
 -- allow moving an invalid iterator, so reposition it with 'iterSeek',
 -- 'iterFirst' or 'iterLast' instead.
+--
+-- The check costs one cheap call and turns a crash in C into a harmless no-op,
+-- which is the right trade for a Haskell binding.
 iterNext :: Iterator -> IO ()
 iterNext it = do
     valid <- iterValid it
@@ -210,6 +256,9 @@
 
 -- | Return the key for the current entry if the iterator is currently
 -- positioned at an entry, ie. 'iterValid'.
+--
+-- The key is copied, because RocksDB reuses the memory behind it as soon as the
+-- iterator moves.
 iterKey :: Iterator -> IO (Maybe ByteString)
 iterKey it = iterString it c_rocksdb_iter_key
 
@@ -229,6 +278,9 @@
 -- | Check for errors
 --
 -- Note that this captures somewhat severe errors such as a corrupted database.
+-- An iterator that becomes invalid because of an error looks the same as one
+-- that reached the end of the data, so call this after iterating to tell them
+-- apart. Returns 'Nothing' if there was no error.
 iterGetError :: Iterator -> IO (Maybe ByteString)
 iterGetError iter_ptr = alloca $ \err_ptr -> do
     poke err_ptr nullPtr
@@ -242,6 +294,8 @@
 -- Internal
 --
 
+-- | Read a C string from the iterator using the given accessor, if the
+-- iterator is valid. The string is copied, since it belongs to the iterator.
 iterString ::
     Iterator ->
     (Iterator -> Ptr CSize -> IO CString) ->
