diff --git a/doc/cpp_common.rst b/doc/cpp_common.rst
new file mode 100644
index 0000000000000000000000000000000000000000..d0e4e8b3b85c3522cdff98ff820d5546ebf9e2bc
--- /dev/null
+++ b/doc/cpp_common.rst
@@ -0,0 +1,115 @@
+Common Types
+============
+
+.. cpp:namespace:: arb
+
+Cell Identifiers and Indexes
+----------------------------
+
+These types, defined in ``common_types.hpp``, are used as identifiers for
+cells and members of cell-local collections.
+
+.. Note::
+    Arbor uses ``std::unit32_t`` for :cpp:type:`cell_gid_type`,
+    :cpp:type:`cell_size_type`, :cpp:type:`cell_lid_type`, and
+    :cpp:type:`cell_local_size_type` at the time of writing, however
+    this could change, e.g. to handle models that cell gid that don't
+    fit into a 32 bit unsigned integer.
+    It is thus recomended that these type aliases be used whenever identifying
+    or counting cells and cell members.
+
+
+.. cpp:type:: cell_gid_type
+
+    An integer type used for identifying cells globally.
+
+
+.. cpp:type::  cell_size_type
+
+    An unsigned integer for sizes of collections of cells.
+    Unsigned type for counting :cpp:type:`cell_gid_type`.
+
+
+.. cpp:type::  cell_lid_type
+
+    For indexes into cell-local data.
+    Local indices for items within a particular cell-local collection should be
+    zero-based and numbered contiguously.
+
+
+.. cpp:type::  cell_local_size_type
+
+    An unsigned integer for for counts of cell-local data.
+
+
+.. cpp:class:: cell_member_type
+
+    For global identification of an item of cell local data.
+    Items of :cpp:type:`cell_member_type` must:
+
+        * be associated with a unique cell, identified by the member
+          :cpp:member:`gid`;
+        * identify an item within a cell-local collection by the member
+          :cpp:member:`index`.
+
+    An example is uniquely identifying a synapse in the model.
+    Each synapse has a post-synaptic cell (:cpp:member:`gid`), and an index
+    (:cpp:member:`index`) into the set of synapses on the post-synaptic cell.
+
+    Lexographically ordered by :cpp:member:`gid`,
+    then :cpp:member:`index`.
+
+    .. cpp:member:: cell_gid_type   gid
+
+        Global identifier of the cell containing/associated with the item.
+
+    .. cpp:member:: cell_lid_type   index
+
+        The index of the item in a cell-local collection.
+
+
+.. cpp:enum-class:: cell_kind
+
+    Enumeration used to indentify the cell type/kind, used by the model to
+    group equal kinds in the same cell group.
+
+    .. cpp:enumerator:: cable1d_neuron
+
+        A cell with morphology described by branching 1D cable segments.
+
+    .. cpp:enumerator:: lif_neuron
+
+        Leaky-integrate and fire neuron.
+
+    .. cpp:enumerator:: regular_spike_source
+
+        Regular spiking source.
+
+    .. cpp:enumerator:: data_spike_source
+
+        Spike source from values inserted via description.
+
+Probes
+------
+
+.. cpp:type:: probe_tag = int
+
+    Extra contextual information associated with a probe.
+
+.. cpp:class:: probe_info
+
+    Probes are specified in the recipe objects that are used to initialize a
+    model; the specification of the item or value that is subjected to a
+    probe will be specific to a particular cell type.
+
+    .. cpp:member:: cell_member_type id
+
+           Cell gid, index of probe.
+
+    .. cpp:member:: probe_tag tag
+
+           Opaque key, returned in sample record.
+
+    .. cpp:member:: util::any address
+
+           Cell-type specific location info, specific to cell kind of ``id.gid``.
diff --git a/doc/cpp_intro.rst b/doc/cpp_intro.rst
new file mode 100644
index 0000000000000000000000000000000000000000..6fbba414d2289136a634cd3bdc77d112a6e7c543
--- /dev/null
+++ b/doc/cpp_intro.rst
@@ -0,0 +1,11 @@
+Overview
+=========
+
+The C++ API for is the main interface through which application developers will
+access Arbor, though it is designed to be usable for power users to
+implement models.
+
+Arbor makes a distinction between the **description** of a model, and the
+**execution** of a model.
+
+A :cpp:type:`arb::recipe` describes a model.
diff --git a/doc/cpp_recipe.rst b/doc/cpp_recipe.rst
new file mode 100644
index 0000000000000000000000000000000000000000..6edf46cbc05298577331766e169307643c1294ef
--- /dev/null
+++ b/doc/cpp_recipe.rst
@@ -0,0 +1,230 @@
+Recipes
+===============
+
+An Arbor recipe is a description of a model. The recipe is queried during the model
+building phase to provide cell information, such as:
+
+  * the number of cells in the model;
+  * the type of a cell;
+  * a description of a cell;
+  * incoming network connections on a cell.
+
+The :cpp:class:`arb::recipe` class documentation is below.
+
+Why Recipes?
+--------------
+
+The interface and design of Arbor recipes was motivated by the following aims:
+
+    * Building a simulation from a recipe description must be possible in a
+      distributed system efficiently with minimal communication.
+    * To minimise the amount of memory used in model building, to make it
+      possible to build and run simulations in one run.
+
+Recipe descriptions are cell-oriented, in order that the building phase can
+be efficiently distributed and that the model can be built independently of any
+runtime execution environment.
+
+During model building, the recipe is queried first by a load balancer,
+then later when building the low-level cell groups and communication network.
+The cell-centered recipe interface, whereby cell and network properties are
+specified "per-cell", facilitates this.
+
+The steps of building a simulation from a recipe are:
+
+.. topic:: 1. Load balancing
+
+    First, the cells are partitioned over MPI ranks, and each rank parses
+    the cells assigned to it to build a cost model.
+    The ranks then coordinate to redistribute cells over MPI ranks so that
+    each rank has a balanced workload. Finally, each rank groups its local
+    cells into :cpp:type:`cell_group` s that balance the work over threads (and
+    GPU accelerators if available).
+
+.. topic:: 2. Model building
+
+    The model building phase takes the cells assigned to the local rank, and builds the
+    local cell groups and the part of the communication network by querying the recipe
+    for more information about the cells assigned to it.
+
+.. _recipe_best_practice:
+
+Best Practices
+--------------
+
+Here is a set of rules of tumb to keep in mind when making recipes. The first is
+mandatory, and following the others as closely as possible will lead to better
+performance.
+
+.. topic:: Stay thread safe
+
+    The load balancing and model construction are multithreaded, that is
+    multiple threads query the recipe simultaneously.
+    Hence calls to a recipe member should not have side effects, and should use
+    lazy evaluation when possible (see `Be lazy <recipe_lazy_>`_).
+
+.. _recipe_lazy:
+
+.. topic:: Be lazy
+
+    A recipe does not have to contain a complete description of the model in
+    memory; it should precompute as little as possible, and use
+    `lazy evaluation <https://en.wikipedia.org/wiki/Lazy_evaluation>`_ to generate
+    information only when requested.
+    This has multiple benefits, including:
+
+        * thread safety;
+        * minimising memory footprint of recipe.
+
+.. topic:: Think of the cells
+
+    When formulating a model, think cell-first, and try to formulate the model and
+    the associated workflow from a cell-centered perspective. If this isn't possible,
+    please contact the developers, because we would like to develop tools that help
+    make this simpler.
+
+.. topic:: Be reproducible
+
+    Arbor is designed to give reproduceable results when the same model is run on a
+    different number of MPI ranks or threads, or on different hardware (e.g. GPUs).
+    This only holds when a recipe provides a reproducible model description, which
+    can be a challenge when a description uses random numbers, e.g. to pick incoming
+    connections to a cell from a random subset of a cell population.
+    To get a reproduceable model, use the cell `gid` (or a hash based on the `gid`)
+    to seed random number generators, including those for :cpp:type:`event_generator` s.
+
+
+Class Documentation
+-------------------
+
+.. cpp:namespace:: arb
+
+.. cpp:class:: recipe
+
+    A description of a model, describing the cells and network, without any
+    information about how the model is to be represented or executed.
+
+    All recipes derive from this abstract base class, defined in ``src/recipe.hpp``.
+
+    Recipes provide a cell-centric interface for describing a model. This means that
+    model properties, such as connections, are queried using the global identifier
+    (`gid`) of a cell. In the description below, the term `gid` is used as shorthand
+    for "the cell with global identifier `gid`".
+
+
+    .. Warning::
+        All member functions must be **thread safe**, because the recipe is used
+        by the multithreaded model builing stage. In practice, this means that
+        multiple threads should be able to call member functions of a recipe
+        simultaneously. Model building is multithreaded to reduce model building times,
+        so recipe implementations should avoid using locks and mutexes to introduce
+        thread safety. See `recipe best practices <recipe_best_practice_>`_ for more
+        information.
+
+    **Required Member Functions**
+
+    The following member functions must be implemented by every recipe:
+
+    .. cpp:function:: virtual cell_size_type num_cells() const = 0
+
+        The number of cells in the model.
+
+    .. cpp:function:: virtual cell_kind get_cell_kind(cell_gid_type gid) const = 0
+
+        The kind of `gid` (see :cpp:type:`arb::cell_kind`).
+
+    .. cpp:function:: virtual util::unique_any get_cell_description(cell_gid_type gid) const = 0
+
+        A description of the cell `gid`, for example the morphology, synapses
+        and ion channels required to build a multi-compartment neuron.
+
+        The type used to describe a cell depends on the kind of the cell.
+        The interface for querying the kind and description of a cell are
+        seperate to allow the the cell type to be provided without building
+        a full cell description, which can be very expensive.
+
+    **Optional Member Functions**
+
+    .. cpp:function:: virtual std::vector<cell_connection> connections_on(cell_gid_type gid) const
+
+        Returns a list of all the **incoming** connections for `gid` .
+        Each connection ``con`` should have post-synaptic target ``con.dest.gid`` that matches
+        the argument :cpp:var:`gid`, and a valid synapse id ``con.dest.index`` on `gid`.
+        See :cpp:type:`cell_connection`.
+
+        By default returns an empty list.
+
+    .. cpp:function:: virtual std::vector<event_generator> event_generators(cell_gid_type gid) const
+
+        Returns a list of all the event generators that are attached to `gid`.
+
+        By default returns an empty list.
+
+    .. cpp:function:: virtual cell_size_type num_sources(cell_gid_type gid) const
+
+        Returns the number of spike sources on `gid`. This corresponds to the number
+        of spike detectors on a multi-compartment cell. Typically there is one detector
+        at the soma of the cell, however it is possible to attache multiple detectors
+        at arbitrary locations.
+
+        By default returns 0.
+
+    .. cpp:function:: virtual cell_size_type num_targets(cell_gid_type gid) const
+
+        The number of post-synaptic sites on `gid`, which corresponds to the number
+        of synapses.
+
+        By default returns 0.
+
+    .. cpp:function:: virtual cell_size_type num_probes(cell_gid_type gid) const
+
+        The number of probes attached to the cell.
+
+        By default returns 0.
+
+    .. cpp:function:: virtual probe_info get_probe(cell_member_type) const
+
+        Intended for use by cell group implementations to set up sampling data
+        structures ahead of time and for putting in place any structures or
+        information in the concrete cell implementations to allow monitoring.
+
+        By default throws :cpp:type:`std::logic_error`. If ``arb::recipe::num_probes``
+        returns a non-zero value, this must also be overriden.
+
+    .. cpp:function:: virtual util::any get_global_properties(cell_kind) const
+
+        Global property type will be specific to given cell kind.
+
+        By default returns an empty container.
+
+.. cpp:class:: cell_connection
+
+    Describes a connection between two cells: a pre-synaptic source and a
+    post-synaptic destination. The source is typically a threshold detector on
+    a cell or a spike source. The destination is a synapse on the post-synaptic cell.
+
+    .. cpp:type:: cell_connection_endpoint = cell_member_type
+
+        Connection end-points are represented by pairs
+        (cell index, source/target index on cell).
+
+    .. cpp:member:: cell_connection_endpoint source
+
+        Source end point.
+
+    .. cpp:member:: cell_connection_endpoint dest
+
+        Destination end point.
+
+    .. cpp:member:: float weight
+
+        The weight delivered to the target synapse.
+        The weight is dimensionless, and its interpretation is
+        specific to the synapse type of the target. For example,
+        the `expsyn` synapse interprets it as a conductance
+        with units μS (micro-Siemens).
+
+    .. cpp:member:: float delay
+
+        Delay of the connection (milliseconds).
+
diff --git a/doc/index.rst b/doc/index.rst
index b8d8e03e67ac059139481649a784cce7aa99e7a7..6e04975dd42836c5d76f1c5570cc8970a248551b 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -26,7 +26,7 @@ Arbor is designed from the ground up for **many core**  architectures:
 Features
 --------
 
-We are actively developing Arbor, improving performance and adding features.
+We are actively developing `Arbor <https://github.com/eth-cscs/arbor>`_, improving performance and adding features.
 Some key features include:
 
     * Optimized back ends for CUDA, KNL and AVX2 intrinsics.
@@ -39,19 +39,23 @@ Some key features include:
 
 .. toctree::
    :caption: Getting Stared:
-   :maxdepth: 1
 
    install
 
 .. toctree::
    :caption: Users:
-   :maxdepth: 1
 
    users
 
+.. toctree::
+   :caption: C++ API:
+
+   cpp_intro
+   cpp_common
+   cpp_recipe
+
 .. toctree::
    :caption: Developers:
-   :maxdepth: 1
 
    library
    simd_api
diff --git a/doc/install.rst b/doc/install.rst
index 7ddce436cf8f625f90160f452b97034ba86efd3c..8cd30e7587ae56b40c82863ac1781aa443a8f66a 100644
--- a/doc/install.rst
+++ b/doc/install.rst
@@ -159,7 +159,8 @@ Building Arbor
 Once the Arbor code has been checked out, it can be built by first running CMake to configure the build, then running make.
 
 Below is a simple workflow for: **1)** getting the source; **2)** configuring the build;
-**3)** building then; **4)** running tests.
+**3)** building; **4)** then running tests.
+
 For more detailed build configuration options, see the `quick start <quickstart_>`_ guide.
 
 .. code-block:: bash
diff --git a/example/miniapp/miniapp.cpp b/example/miniapp/miniapp.cpp
index 969b6aedf9d6f3b8527ff24504029b1d1c6a64d9..2ef4ef722edb5a335536eabe69ec5a97ec204ad1 100644
--- a/example/miniapp/miniapp.cpp
+++ b/example/miniapp/miniapp.cpp
@@ -107,7 +107,7 @@ int main(int argc, char** argv) {
         // by command line options.
         std::vector<sample_trace> sample_traces;
         for (const auto& g: decomp.groups) {
-            if (g.kind==cable1d_neuron) {
+            if (g.kind==cell_kind::cable1d_neuron) {
                 for (auto gid: g.gids) {
                     if (options.trace_max_gid && gid>*options.trace_max_gid) {
                         continue;
diff --git a/src/common_types.hpp b/src/common_types.hpp
index 41284522a6c12286a0dbc25024f6531d0d505642..79f77232fdd67b986e723e122462c9a0fa3acdc2 100644
--- a/src/common_types.hpp
+++ b/src/common_types.hpp
@@ -67,7 +67,7 @@ using sample_size_type = std::int32_t;
 // Enumeration used to indentify the cell type/kind, used by the model to
 // group equal kinds in the same cell group.
 
-enum cell_kind {
+enum class cell_kind {
     cable1d_neuron,           // Our own special mc neuron
     lif_neuron,               // Leaky-integrate and fire neuron
     regular_spike_source,     // Regular spiking source
diff --git a/src/fvm_multicell.hpp b/src/fvm_multicell.hpp
index 4d76fc83a99bb080345fcc51c8545df1a661fb57..2049c2942e969dfb0dac98f3aa4e5f1bdab1d1c3 100644
--- a/src/fvm_multicell.hpp
+++ b/src/fvm_multicell.hpp
@@ -630,7 +630,7 @@ void fvm_multicell<Backend>::initialize(
     // Handle any global parameters for these cell groups.
     // (Currently: just specialized mechanisms).
     std::map<std::string, specialized_mechanism> special_mechs;
-    util::any gprops = rec.get_global_properties(cable1d_neuron);
+    util::any gprops = rec.get_global_properties(cell_kind::cable1d_neuron);
     if (gprops.has_value()) {
         special_mechs = util::any_cast<cell_global_properties&>(gprops).special_mechs;
     }
diff --git a/src/partition_load_balance.cpp b/src/partition_load_balance.cpp
index 32f202aad8d3284c6dd743b7740bfae52106cae5..f3ec4f91dfe8e3958e739ecee76d9d4ae21eaf8b 100644
--- a/src/partition_load_balance.cpp
+++ b/src/partition_load_balance.cpp
@@ -2,6 +2,7 @@
 #include <domain_decomposition.hpp>
 #include <hardware/node_info.hpp>
 #include <recipe.hpp>
+#include <util/enumhash.hpp>
 
 namespace arb {
 
@@ -19,7 +20,6 @@ domain_decomposition partition_load_balance(const recipe& rec, hw::node_info nd)
         const std::vector<cell_gid_type> gid_divisions;
     };
 
-    using kind_type = std::underlying_type<cell_kind>::type;
     using util::make_span;
 
     unsigned num_domains = communication::global_policy::size();
@@ -40,7 +40,8 @@ domain_decomposition partition_load_balance(const recipe& rec, hw::node_info nd)
 
     // Local load balance
 
-    std::unordered_map<kind_type, std::vector<cell_gid_type>> kind_lists;
+    std::unordered_map<cell_kind, std::vector<cell_gid_type>, arb::util::enum_hash>
+        kind_lists;
     for (auto gid: make_span(gid_part[domain_id])) {
         kind_lists[rec.get_cell_kind(gid)].push_back(gid);
     }
diff --git a/src/recipe.hpp b/src/recipe.hpp
index 0a193548ea637e72e4001e2543b965099b9d7aa9..caaddaddb69a2a8c4c4154808b656bac54365a5c 100644
--- a/src/recipe.hpp
+++ b/src/recipe.hpp
@@ -27,13 +27,9 @@ public:
 
 /* Recipe descriptions are cell-oriented: in order that the building
  * phase can be done distributedly and in order that the recipe
- * description can be built indepdently of any runtime execution
- * environment, connection end-points are represented by pairs
- * (cell index, source/target index on cell).
+ * description can be built indepdently of any runtime execution environment.
  */
 
-using cell_connection_endpoint = cell_member_type;
-
 // Note: `cell_connection` and `connection` have essentially the same data
 // and represent the same thing conceptually. `cell_connection` objects
 // are notionally described in terms of external cell identifiers instead
@@ -41,6 +37,10 @@ using cell_connection_endpoint = cell_member_type;
 // two in the current code. These two types could well be merged.
 
 struct cell_connection {
+    // Connection end-points are represented by pairs
+    // (cell index, source/target index on cell).
+    using cell_connection_endpoint = cell_member_type;
+
     cell_connection_endpoint source;
     cell_connection_endpoint dest;
 
diff --git a/src/util/enumhash.hpp b/src/util/enumhash.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..e50d7bf8a26d108fa08fa401c1903e105062ea02
--- /dev/null
+++ b/src/util/enumhash.hpp
@@ -0,0 +1,20 @@
+#pragma once
+
+#include <functional>
+#include <type_traits>
+
+// Work around for C++11 defect #2148: hashing enums should be supported directly by std::hash.
+// Fixed in C++14.
+
+namespace arb {
+namespace util {
+
+struct enum_hash {
+    template <typename E, typename V = typename std::underlying_type<E>::type>
+    std::size_t operator()(E e) const noexcept {
+        return std::hash<V>{}(static_cast<V>(e));
+    }
+};
+
+} // namespace util
+} // namespace arb
diff --git a/tests/simple_recipes.hpp b/tests/simple_recipes.hpp
index 8e7218c0570d8a5ecd6637270b22a5dd43a50617..ef6e04fca3bd43111d9d874c7f57a1b27d60335e 100644
--- a/tests/simple_recipes.hpp
+++ b/tests/simple_recipes.hpp
@@ -37,7 +37,7 @@ public:
 
     util::any get_global_properties(cell_kind k) const override {
         switch (k) {
-        case cable1d_neuron:
+            case cell_kind::cable1d_neuron:
             return cell_gprop;
         default:
             return util::any{};