diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8155f906579b801f17b0e2376b5869020f8bcf6f..0d522c7d5c15fb7f4bf3cd4e45924ae57a9bd750 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -160,9 +160,8 @@ install(TARGETS arbor-config-defs EXPORT arbor-targets)
 
 # Interface library `arbor-private-deps` collects dependencies, options etc.
 # for the arbor library.
-
 add_library(arbor-private-deps INTERFACE)
-target_link_libraries(arbor-private-deps INTERFACE arbor-config-defs ext-random123)
+target_link_libraries(arbor-private-deps INTERFACE arbor-config-defs ext-random123 ${CMAKE_DL_LIBS} arbor-compiler-compat)
 install(TARGETS arbor-private-deps EXPORT arbor-targets)
 
 # Interface library `arborenv-private-deps` collects dependencies, options etc.
diff --git a/arbor/arbexcept.cpp b/arbor/arbexcept.cpp
index 2bc11e9274d82eb57a14c440928ed8b194a90f2c..d12883b5fcb1d6f0ad853a6a0dbf308fbdc28f7e 100644
--- a/arbor/arbexcept.cpp
+++ b/arbor/arbexcept.cpp
@@ -15,11 +15,13 @@ bad_cell_probe::bad_cell_probe(cell_kind kind, cell_gid_type gid):
     gid(gid),
     kind(kind)
 {}
+
 bad_cell_description::bad_cell_description(cell_kind kind, cell_gid_type gid):
     arbor_exception(pprintf("recipe::get_cell_kind(gid={}) -> {} does not match the cell type provided by recipe::get_cell_description(gid={})", gid, kind, gid)),
     gid(gid),
     kind(kind)
 {}
+
 bad_target_description::bad_target_description(cell_gid_type gid, cell_size_type rec_val, cell_size_type cell_val):
     arbor_exception(pprintf("Model building error on cell {}: recipe::num_targets(gid={}) = {} is greater than the number of synapses on the cell = {}", gid, gid, rec_val, cell_val)),
     gid(gid), rec_val(rec_val), cell_val(cell_val)
@@ -148,5 +150,16 @@ range_check_failure::range_check_failure(const std::string& whatstr, double valu
     value(value)
 {}
 
+file_not_found_error::file_not_found_error(const std::string &fn)
+    : arbor_exception(pprintf("Could not find file '{}'", fn)),
+      filename{fn}
+{}
+
+bad_catalogue_error::bad_catalogue_error(const std::string &fn, const std::string& call)
+    : arbor_exception(pprintf("Error in '{}' while opening catalogue '{}'", call, fn)),
+      filename{fn},
+      failed_call{call}
+{}
+
 } // namespace arb
 
diff --git a/arbor/include/arbor/arbexcept.hpp b/arbor/include/arbor/arbexcept.hpp
index f9e1a7eb7befc50a5961a95df633190be066113a..f2c49675604bc038267c01ec3652cb70431057e4 100644
--- a/arbor/include/arbor/arbexcept.hpp
+++ b/arbor/include/arbor/arbexcept.hpp
@@ -1,5 +1,6 @@
 #pragma once
 
+#include <any>
 #include <stdexcept>
 #include <string>
 
@@ -170,4 +171,15 @@ struct range_check_failure: arbor_exception {
     double value;
 };
 
+struct file_not_found_error: arbor_exception {
+    file_not_found_error(const std::string& fn);
+    std::string filename;
+};
+
+struct bad_catalogue_error: arbor_exception {
+    bad_catalogue_error(const std::string& fn, const std::string& call);
+    std::string filename;
+    std::string failed_call;
+};
+
 } // namespace arb
diff --git a/arbor/include/arbor/mechcat.hpp b/arbor/include/arbor/mechcat.hpp
index 436edbc2d21d794c2b26e4065c920a271825c914..c10b6f4bd96fc1ada8ad2830e35cf88ad323c309 100644
--- a/arbor/include/arbor/mechcat.hpp
+++ b/arbor/include/arbor/mechcat.hpp
@@ -1,5 +1,6 @@
 #pragma once
 
+#include <filesystem>
 #include <map>
 #include <memory>
 #include <string>
@@ -127,4 +128,8 @@ const mechanism_catalogue& global_default_catalogue();
 const mechanism_catalogue& global_allen_catalogue();
 const mechanism_catalogue& global_bbp_catalogue();
 
+// Load catalogue from disk.
+
+const mechanism_catalogue& load_catalogue(const std::filesystem::path&);
+
 } // namespace arb
diff --git a/arbor/mechcat.cpp b/arbor/mechcat.cpp
index 001ab95eb75dca4b06f9465239328997ac01a1b9..aefb964ff09b3cef03207ac452b5d517c410f049 100644
--- a/arbor/mechcat.cpp
+++ b/arbor/mechcat.cpp
@@ -3,6 +3,9 @@
 #include <memory>
 #include <string>
 #include <vector>
+#include <cassert>
+
+#include <dlfcn.h>
 
 #include <arbor/arbexcept.hpp>
 #include <arbor/mechcat.hpp>
@@ -579,4 +582,29 @@ std::pair<mechanism_ptr, mechanism_overrides> mechanism_catalogue::instance_impl
 
 mechanism_catalogue::~mechanism_catalogue() = default;
 
+static void check_dlerror(const std::string& fn, const std::string& call) {
+    auto error = dlerror();
+    if (error) { throw arb::bad_catalogue_error{fn, call}; }
+}
+
+const mechanism_catalogue& load_catalogue(const std::filesystem::path& fn) {
+    typedef const void* global_catalogue_t();
+
+    if (!std::filesystem::exists(fn)) { throw arb::file_not_found_error{fn}; }
+
+    auto plugin = dlopen(fn.c_str(), RTLD_LAZY);
+    check_dlerror(fn, "dlopen");
+    assert(plugin);
+
+    auto get_catalogue = (global_catalogue_t*)dlsym(plugin, "get_catalogue");
+    check_dlerror(fn, "dlsym");
+
+    /* NOTE We do not free the DSO handle here and accept retaining the handles
+       until termination since the mechanisms provided by the catalogue may have
+       a different lifetime than the actual catalogue itfself. This is not a
+       leak proper as `dlopen` caches handles for us.
+     */
+    return *((const mechanism_catalogue*)get_catalogue());
+}
+
 } // namespace arb
diff --git a/cmake/CompilerOptions.cmake b/cmake/CompilerOptions.cmake
index 9780d534e5b8c1b241f3649a7b8e6b215cf07cea..cf8b248fcf8cfb181791e40c1070f05e75769065 100644
--- a/cmake/CompilerOptions.cmake
+++ b/cmake/CompilerOptions.cmake
@@ -17,6 +17,23 @@ if(${ARBDEV_COLOR})
     add_compile_options("$<$<COMPILE_LANGUAGE:CXX>:${colorflags}>")
 endif()
 
+# A library to collect compiler-specific linking adjustments.
+
+add_library(arbor-compiler-compat INTERFACE)
+# TODO Remove when upgrading GCC.
+if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
+  if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9.1)
+    target_link_libraries(arbor-compiler-compat INTERFACE stdc++fs)
+  endif()
+endif()
+# TODO Remove when upgrading Clang
+if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
+  if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9.0)
+    target_link_libraries(arbor-compiler-compat INTERFACE stdc++fs)
+  endif()
+endif()
+install(TARGETS arbor-compiler-compat EXPORT arbor-targets)
+
 # Warning options: disable specific spurious warnings as required.
 
 set(CXXOPT_WALL
diff --git a/cmake/arbor-config.cmake.in b/cmake/arbor-config.cmake.in
index 246d92d51d52a6e55bc1f642b74a1d95e8341668..d551de69d3108bfc7ff2f8553f0e6ae3bcc256db 100644
--- a/cmake/arbor-config.cmake.in
+++ b/cmake/arbor-config.cmake.in
@@ -44,6 +44,13 @@ function(_append_property target property)
     endif()
 endfunction()
 
+set(ARB_VECTORIZE @ARB_VECTORIZE@)
+set(ARB_ARCH @ARB_ARCH@)
+set(ARB_MODCC_FLAGS @ARB_MODCC_FLAGS@)
+set(ARB_CXX @CMAKE_CXX_COMPILER@)
+set(ARB_CXX_FLAGS @CMAKE_CXX_FLAGS@)
+set(ARB_CXXOPT_ARCH @ARB_CXXOPT_ARCH@)
+
 _append_property(arbor::arbor INTERFACE_LINK_LIBRARIES @arbor_add_import_libs@)
 _append_property(arbor::arborenv INTERFACE_LINK_LIBRARIES @arborenv_add_import_libs@)
 _append_property(arbor::arbornml INTERFACE_LINK_LIBRARIES @arbornml_add_import_libs@)
diff --git a/doc/concepts/mechanisms.rst b/doc/concepts/mechanisms.rst
index e507443090c2d7af47befa0ba0b564695ff5d2d4..b67ea243d9bf6d5464ecb6c7ebe0e0a746a96ce4 100644
--- a/doc/concepts/mechanisms.rst
+++ b/doc/concepts/mechanisms.rst
@@ -51,19 +51,33 @@ used by the `Allen Institute <https://alleninstitute.org/>`_ and the `Blue Brain
 the `Allen institute mechanisms <https://github.com/arbor-sim/arbor/tree/master/mechanisms/allen>`_ and
 the `BBP mechanisms <https://github.com/arbor-sim/arbor/tree/master/mechanisms/bbp>`_ at the provided links.)
 
-Default catalogue
-'''''''''''''''''
+Built-in Catalogues
+'''''''''''''''''''
 
-Arbor provides a default catalogue with the following mechanisms:
+Arbor provides the *default* catalogue with the following mechanisms:
 
 * *pas*: Leaky current (:ref:`density mechanism <mechanisms-density>`).
-* *hh*:  Classic Hodgkin-Huxley dynamics (:ref:`density mechanism <mechanisms-density>`).
-* *nernst*: Calculate reversal potential for an ionic species using the Nernst equation (:ref:`reversal potential mechanism <mechanisms-revpot>`)
-* *expsyn*: Synapse with discontinuous change in conductance at an event followed by an exponential decay (:ref:`point mechanism <mechanisms-point>`).
-* *exp2syn*: Bi-exponential conductance synapse described by two time constants: rise and decay (:ref:`point mechanism <mechanisms-point>`).
+* *hh*: Classic Hodgkin-Huxley dynamics (:ref:`density mechanism
+  <mechanisms-density>`).
+* *nernst*: Calculate reversal potential for an ionic species using the Nernst
+  equation (:ref:`reversal potential mechanism <mechanisms-revpot>`). **NB**
+  This is not meant to be used directly
+* *expsyn*: Synapse with discontinuous change in conductance at an event
+  followed by an exponential decay (:ref:`point mechanism <mechanisms-point>`).
+* *exp2syn*: Bi-exponential conductance synapse described by two time constants:
+  rise and decay (:ref:`point mechanism <mechanisms-point>`).
 
 With the exception of *nernst*, these mechanisms are the same as those available in NEURON.
 
+Two catalogues are provided that collect mechanisms associated with specific projects and model databases:
+
+* *bbp* For models published by the Blue Brain Project (BBP).
+* *allen* For models published on the Allen Brain Atlas Database.
+
+Further catalogues can be added by extending the list of built-in catalogues in
+the arbor source tree or by compiling a dynamically loadable catalogue
+(:ref:`extending catalogues <extending-catalogues>`).
+
 Parameters
 ''''''''''
 
diff --git a/doc/internals/extending-catalogues.rst b/doc/internals/extending-catalogues.rst
new file mode 100644
index 0000000000000000000000000000000000000000..5ac0374815f347059f174cbbcd924ec42df99020
--- /dev/null
+++ b/doc/internals/extending-catalogues.rst
@@ -0,0 +1,57 @@
+.. _extending-catalogues:
+
+Adding Catalogues to Arbor
+==========================
+
+There are two ways new mechanisms catalogues can be added to Arbor, statically
+or dynamically. None is considered to be part of the stable user-facing API at
+the moment, although the dynamic approach is aligned with our eventual goals.
+
+Both require a copy of the Arbor source tree and the compiler toolchain used to
+build Arbor in addition to the installed library.
+
+Static Extensions
+'''''''''''''''''
+
+This will produce a catalogue of the same level of integration as the built-in
+catalogues (*default*, *bbp*, and *allen*). The required steps are as follows
+
+1. Go to the Arbor source tree.
+2. Create a new directory under *mechanisms*.
+3. Add your .mod files.
+4. Edit *mechanisms/CMakeLists.txt* to add a definition like this
+
+   .. code-block :: cmake
+
+     make_catalogue(
+       NAME <catalogue-name>                             # Name of your catalogue
+       SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/<directory>" # Directory name (added above)
+       OUTPUT "<output-name>"                            # Variable name to output to
+       MECHS <names>)                                    # Space separated list of mechanism
+                                                         # names w/o .mod suffix.
+
+5. Add your `output-name` to the `arbor_mechanism_sources` list.
+6. Add a `global_NAME_catalogue` function in `mechcat.hpp` and `mechcat.cpp`
+7. Bind this function in `python/mechanisms.cpp`.
+
+All steps can be more or less copied from the surrounding code.
+
+Dynamic Extensions
+''''''''''''''''''
+
+This will produce a catalogue loadable at runtime by calling `load_catalogue`
+with a filename in both C++ and Python. The steps are
+
+1. Prepare a directory containing your NMODL files (.mod suffixes required)
+2. Call `build_catalogue` from the `scripts` directory
+
+   .. code-block :: bash
+
+     build-catalogue <name> <path/to/nmodl>
+
+All files with the suffix `.mod` located in `<path/to/nmodl>` will be baked into
+a catalogue named `lib<name>-catalogue.so` and placed into your current working
+directory. Note that these files are platform-specific and should only be used
+on the combination of OS, compiler, arbor, and machine they were built with.
+
+See the demonstration in `python/example/dynamic-catalogue.py` for an example.
diff --git a/mechanisms/BuildModules.cmake b/mechanisms/BuildModules.cmake
index 945671f21d565ad2ca487979f56d74d9baf95915..d35a361b2c64314d5c4730d2281c01d8c845ff6e 100644
--- a/mechanisms/BuildModules.cmake
+++ b/mechanisms/BuildModules.cmake
@@ -56,3 +56,81 @@ function(build_modules)
         add_custom_target(${build_modules_TARGET} DEPENDS ${depends})
     endif()
 endfunction()
+
+function("make_catalogue")
+  cmake_parse_arguments(MK_CAT "" "NAME;SOURCES;OUTPUT;ARBOR;STANDALONE" "MECHS" ${ARGN})
+  set(MK_CAT_OUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated/${MK_CAT_NAME}")
+
+  # Need to set ARB_WITH_EXTERNAL_MODCC *and* modcc
+  set(external_modcc)
+  if(ARB_WITH_EXTERNAL_MODCC)
+    set(external_modcc MODCC ${modcc})
+  endif()
+
+  message("Catalogue name:       ${MK_CAT_NAME}")
+  message("Catalogue mechanisms: ${MK_CAT_MECHS}")
+  message("Catalogue sources:    ${MK_CAT_SOURCES}")
+  message("Catalogue output:     ${MK_CAT_OUT_DIR}")
+  message("Arbor source tree:    ${MK_CAT_ARBOR}")
+  message("Build as standalone:  ${MK_CAT_STANDALONE}")
+  message("Source directory:     ${PROJECT_SOURCE_DIR}")    
+  message("Arbor arch:           ${ARB_CXXOPT_ARCH}")    
+
+  file(MAKE_DIRECTORY "${MK_CAT_OUT_DIR}")
+
+  if (NOT TARGET build_all_mods)
+    add_custom_target(build_all_mods)
+  endif()
+
+  build_modules(
+    ${MK_CAT_MECHS}
+    SOURCE_DIR "${MK_CAT_SOURCES}"
+    DEST_DIR "${MK_CAT_OUT_DIR}"
+    ${external_modcc} # NB: expands to 'MODCC <binary>' to add an optional argument
+    MODCC_FLAGS -t cpu -t gpu ${ARB_MODCC_FLAGS} -N arb::${MK_CAT_NAME}_catalogue
+    GENERATES .hpp _cpu.cpp _gpu.cpp _gpu.cu
+    TARGET build_catalogue_${MK_CAT_NAME}_mods)
+
+  set(catalogue_${MK_CAT_NAME}_source ${CMAKE_CURRENT_BINARY_DIR}/${MK_CAT_NAME}_catalogue.cpp)
+  set(catalogue_${MK_CAT_NAME}_options -A arbor -I ${MK_CAT_OUT_DIR} -o ${catalogue_${MK_CAT_NAME}_source} -B multicore -C ${MK_CAT_NAME} -N arb::${MK_CAT_NAME}_catalogue)
+  if(ARB_WITH_GPU)
+    list(APPEND catalogue_${MK_CAT_NAME}_options -B gpu)
+  endif()
+
+  add_custom_command(
+    OUTPUT ${catalogue_${MK_CAT_NAME}_source}
+    COMMAND ${PROJECT_SOURCE_DIR}/mechanisms/generate_catalogue ${catalogue_${MK_CAT_NAME}_options} ${MK_CAT_MECHS}
+    COMMENT "Building catalogue ${MK_CAT_NAME}"
+    DEPENDS ${PROJECT_SOURCE_DIR}/mechanisms/generate_catalogue)
+
+  add_custom_target(${MK_CAT_NAME}_catalogue_cpp_target DEPENDS ${catalogue_${MK_CAT_NAME}_source})
+  add_dependencies(build_catalogue_${MK_CAT_NAME}_mods ${MK_CAT_NAME}_catalogue_cpp_target)
+  add_dependencies(build_all_mods build_catalogue_${MK_CAT_NAME}_mods)
+
+  foreach(mech ${MK_CAT_MECHS})
+    list(APPEND catalogue_${MK_CAT_NAME}_source ${MK_CAT_OUT_DIR}/${mech}_cpu.cpp)
+    if(ARB_WITH_GPU)
+      list(APPEND catalogue_${MK_CAT_NAME}_source ${MK_CAT_OUT_DIR}/${mech}_gpu.cpp ${MK_CAT_OUT_DIR}/${mech}_gpu.cu)
+    endif()
+  endforeach()
+  set(${MK_CAT_OUTPUT} ${catalogue_${MK_CAT_NAME}_source} PARENT_SCOPE)
+
+  set_source_files_properties(${catalogue_${MK_CAT_NAME}_source} COMPILE_FLAGS ${ARB_CXXOPT_ARCH})
+
+  if(${MK_CAT_STANDALONE})
+    add_library(${MK_CAT_NAME}-catalogue SHARED ${catalogue_${MK_CAT_NAME}_source})
+    target_compile_definitions(${MK_CAT_NAME}-catalogue PUBLIC STANDALONE=1)
+    set_target_properties(${MK_CAT_NAME}-catalogue
+      PROPERTIES
+      SUFFIX ".so"
+      PREFIX ""
+      CXX_STANDARD 17)
+    target_include_directories(${MK_CAT_NAME}-catalogue PUBLIC "${MK_CAT_ARBOR}/arbor/")
+
+    if(TARGET arbor)
+      target_link_libraries(${MK_CAT_NAME}-catalogue PRIVATE arbor)
+    else()
+      target_link_libraries(${MK_CAT_NAME}-catalogue PRIVATE arbor::arbor)
+    endif()
+  endif()
+endfunction()
diff --git a/mechanisms/CMakeLists.txt b/mechanisms/CMakeLists.txt
index 032adf1244614217b5cad45498b20dbf9813903f..bb56bfc25c89e7cb5b15b8e03eb064341ffbbedb 100644
--- a/mechanisms/CMakeLists.txt
+++ b/mechanisms/CMakeLists.txt
@@ -1,135 +1,37 @@
 include(BuildModules.cmake)
 
-set(external_modcc)
-if(ARB_WITH_EXTERNAL_MODCC)
-    set(external_modcc MODCC ${modcc})
-endif()
-
-add_custom_target(build_all_mods)
-
-set(mech_sources "")
-
-# BBP
-set(bbp_mechanisms CaDynamics_E2 Ca_HVA Ca_LVAst Ih Im K_Pst K_Tst Nap_Et2 NaTa_t NaTs2_t SK_E2 SKv3_1)
-set(bbp_mod_srcdir "${CMAKE_CURRENT_SOURCE_DIR}/bbp")
-set(mech_dir "${CMAKE_CURRENT_BINARY_DIR}/generated/bbp")
-file(MAKE_DIRECTORY "${mech_dir}")
-
-build_modules(
-        ${bbp_mechanisms}
-        SOURCE_DIR "${bbp_mod_srcdir}"
-        DEST_DIR "${mech_dir}"
-        ${external_modcc}
-        MODCC_FLAGS -t cpu -t gpu ${ARB_MODCC_FLAGS} -N arb::bbp_catalogue
-        GENERATES .hpp _cpu.cpp _gpu.cpp _gpu.cu
-        TARGET build_catalogue_bbp_mods)
-
-set(bbp_catalogue_source ${CMAKE_CURRENT_BINARY_DIR}/bbp_catalogue.cpp)
-set(bbp_catalogue_options -A arbor -I ${mech_dir} -o ${bbp_catalogue_source} -B multicore -C bbp -N arb::bbp_catalogue)
-if(ARB_WITH_GPU)
-    list(APPEND bbp_catalogue_options -B gpu)
-endif()
-
-add_custom_command(
-        OUTPUT ${bbp_catalogue_source}
-        COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/generate_catalogue ${bbp_catalogue_options} ${bbp_mechanisms}
-        DEPENDS generate_catalogue
-)
-
-add_custom_target(bbp_catalogue_cpp_target DEPENDS ${bbp_catalogue_source})
-add_dependencies(build_catalogue_bbp_mods bbp_catalogue_cpp_target)
-add_dependencies(build_all_mods build_catalogue_bbp_mods)
-
-list(APPEND mech_sources ${bbp_catalogue_source})
-foreach(mech ${bbp_mechanisms})
-    list(APPEND mech_sources ${mech_dir}/${mech}_cpu.cpp)
-    if(ARB_WITH_GPU)
-        list(APPEND mech_sources ${mech_dir}/${mech}_gpu.cpp)
-        list(APPEND mech_sources ${mech_dir}/${mech}_gpu.cu)
-    endif()
-endforeach()
-
-# ALLEN
-set(allen_mechanisms CaDynamics Ca_HVA Ca_LVA Ih Im Im_v2 K_P K_T Kd Kv2like Kv3_1 NaTa NaTs NaV Nap SK)
-set(allen_mod_srcdir "${CMAKE_CURRENT_SOURCE_DIR}/allen")
-set(mech_dir "${CMAKE_CURRENT_BINARY_DIR}/generated/allen")
-file(MAKE_DIRECTORY "${mech_dir}")
-
-build_modules(
-    ${allen_mechanisms}
-    SOURCE_DIR "${allen_mod_srcdir}"
-    DEST_DIR "${mech_dir}"
-    ${external_modcc}
-    MODCC_FLAGS -t cpu -t gpu ${ARB_MODCC_FLAGS} -N arb::allen_catalogue
-    GENERATES .hpp _cpu.cpp _gpu.cpp _gpu.cu
-    TARGET build_catalogue_allen_mods)
-
-set(allen_catalogue_source ${CMAKE_CURRENT_BINARY_DIR}/allen_catalogue.cpp)
-set(allen_catalogue_options -A arbor -I ${mech_dir} -o ${allen_catalogue_source} -B multicore -C allen -N arb::allen_catalogue)
-if(ARB_WITH_GPU)
-    list(APPEND allen_catalogue_options -B gpu)
-endif()
-
-add_custom_command(
-    OUTPUT ${allen_catalogue_source}
-    COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/generate_catalogue ${allen_catalogue_options} ${allen_mechanisms}
-    DEPENDS generate_catalogue
-)
-
-add_custom_target(allen_catalogue_cpp_target DEPENDS ${allen_catalogue_source})
-add_dependencies(build_catalogue_allen_mods allen_catalogue_cpp_target)
-add_dependencies(build_all_mods build_catalogue_allen_mods)
-
-list(APPEND mech_sources ${allen_catalogue_source})
-foreach(mech ${allen_mechanisms})
-    list(APPEND mech_sources ${mech_dir}/${mech}_cpu.cpp)
-    if(ARB_WITH_GPU)
-        list(APPEND mech_sources ${mech_dir}/${mech}_gpu.cpp)
-        list(APPEND mech_sources ${mech_dir}/${mech}_gpu.cu)
-    endif()
-endforeach()
-
-# DEFAULT
-set(default_mechanisms exp2syn expsyn hh kamt kdrmt nax nernst pas)
-set(default_mod_srcdir "${CMAKE_CURRENT_SOURCE_DIR}/default")
-set(mech_dir "${CMAKE_CURRENT_BINARY_DIR}/generated/default")
-file(MAKE_DIRECTORY "${mech_dir}")
-
-build_modules(
-    ${default_mechanisms}
-    SOURCE_DIR "${default_mod_srcdir}"
-    DEST_DIR "${mech_dir}"
-    ${external_modcc}
-    MODCC_FLAGS -t cpu -t gpu ${ARB_MODCC_FLAGS} -N arb::default_catalogue
-    GENERATES .hpp _cpu.cpp _gpu.cpp _gpu.cu
-    TARGET build_catalogue_default_mods)
-
-set(default_catalogue_source ${CMAKE_CURRENT_BINARY_DIR}/default_catalogue.cpp)
-set(default_catalogue_options -A arbor -I ${mech_dir} -o ${default_catalogue_source} -B multicore -C default -N arb::default_catalogue)
-if(ARB_WITH_GPU)
-    list(APPEND default_catalogue_options -B gpu)
-endif()
-
-add_custom_command(
-    OUTPUT ${default_catalogue_source}
-    COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/generate_catalogue ${default_catalogue_options} ${default_mechanisms}
-    DEPENDS generate_catalogue)
-
-add_custom_target(default_catalogue_cpp_target DEPENDS ${default_catalogue_source})
-add_dependencies(build_catalogue_default_mods default_catalogue_cpp_target)
-add_dependencies(build_all_mods build_catalogue_default_mods)
-
-list(APPEND mech_sources ${default_catalogue_source})
-foreach(mech ${default_mechanisms})
-    list(APPEND mech_sources ${mech_dir}/${mech}_cpu.cpp)
-    if(ARB_WITH_GPU)
-        list(APPEND mech_sources ${mech_dir}/${mech}_gpu.cpp)
-        list(APPEND mech_sources ${mech_dir}/${mech}_gpu.cu)
-    endif()
-endforeach()
+# Define catalogues
+make_catalogue(
+  NAME bbp
+  SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/bbp"
+  OUTPUT "CAT_BBP_SOURCES"
+  MECHS CaDynamics_E2 Ca_HVA Ca_LVAst Ih Im K_Pst K_Tst Nap_Et2 NaTa_t NaTs2_t SK_E2 SKv3_1
+  ARBOR "${PROJECT_SOURCE_DIR}"
+  STANDALONE FALSE)
+
+make_catalogue(
+  NAME allen
+  SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/allen"
+  OUTPUT "CAT_ALLEN_SOURCES"
+  MECHS CaDynamics Ca_HVA Ca_LVA Ih Im Im_v2 K_P K_T Kd Kv2like Kv3_1 NaTa NaTs NaV Nap SK
+  ARBOR "${PROJECT_SOURCE_DIR}"
+  STANDALONE FALSE)
+
+make_catalogue(
+  NAME default
+  SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/default"
+  OUTPUT "CAT_DEFAULT_SOURCES"
+  MECHS exp2syn expsyn hh kamt kdrmt nax nernst pas
+  ARBOR "${PROJECT_SOURCE_DIR}"
+  STANDALONE FALSE)
+
+# Join sources
+set(arbor_mechanism_sources
+  ${CAT_BBP_SOURCES}
+  ${CAT_ALLEN_SOURCES}
+  ${CAT_DEFAULT_SOURCES}
+  PARENT_SCOPE)
 
-# 
-set(arbor_mechanism_sources ${mech_sources} PARENT_SCOPE)
 if(ARB_WITH_CUDA_CLANG OR ARB_WITH_HIP_CLANG)
     set_source_files_properties(${arbor_mechanism_sources} PROPERTIES LANGUAGE CXX)
 endif()
diff --git a/mechanisms/generate_catalogue b/mechanisms/generate_catalogue
index a4ad3cfd05f6a0f073169c67c54d9e21db12faf7..8e8df375cd9e3d45b3897b5f355907e9b5be9d7d 100755
--- a/mechanisms/generate_catalogue
+++ b/mechanisms/generate_catalogue
@@ -116,7 +116,17 @@ const mechanism_catalogue& global_${catalogue}_catalogue() {
     return cat;
 }
 
-} // namespace arb''')
+} // namespace arb
+
+#ifdef STANDALONE
+extern "C" {
+    const void* get_catalogue() {
+        static auto cat = arb::build_${catalogue}_catalogue();
+        return (void*)&cat;
+    }
+}
+#endif
+''')
 
     def indent(n, lines):
         return '{{:<{0!s}}}'.format(n+1).format('\n').join(lines)
diff --git a/python/example/cat/dummy.mod b/python/example/cat/dummy.mod
new file mode 100644
index 0000000000000000000000000000000000000000..df712b023df091c60cb826a00066ccd0485d2231
--- /dev/null
+++ b/python/example/cat/dummy.mod
@@ -0,0 +1,54 @@
+: Reference: Adams et al. 1982 - M-currents and other potassium currents in bullfrog sympathetic neurones
+: Comment:   corrected rates using q10 = 2.3, target temperature 34, orginal 21
+
+NEURON {
+    SUFFIX dummy
+    USEION k READ ek WRITE ik
+    RANGE gImbar
+}
+
+UNITS {
+    (S) = (siemens)
+    (mV) = (millivolt)
+    (mA) = (milliamp)
+}
+
+PARAMETER {
+    gImbar = 0.00001 (S/cm2)
+}
+
+STATE {
+    m
+}
+
+BREAKPOINT {
+    SOLVE states METHOD cnexp
+    ik = gImbar*m*(v - ek)
+}
+
+DERIVATIVE states {
+    LOCAL qt, mAlpha, mBeta
+
+    qt     = 2.3^((34-21)/10)
+    mAlpha = m_alpha(v)
+    mBeta  = m_beta(v)
+
+    m'     = qt*(mAlpha - m*(mAlpha + mBeta))
+}
+
+INITIAL {
+    LOCAL mAlpha, mBeta
+
+    mAlpha = m_alpha(v)
+    mBeta  = m_beta(v)
+
+    m = mAlpha/(mAlpha + mBeta)
+}
+
+FUNCTION m_alpha(v) {
+    m_alpha = 3.3e-3*exp( 2.5*0.04*(v + 35))
+}
+FUNCTION m_beta(v) {
+    m_beta  = 3.3e-3*exp(-2.5*0.04*(v + 35))
+}
+
diff --git a/python/example/dynamic-catalogue.py b/python/example/dynamic-catalogue.py
new file mode 100644
index 0000000000000000000000000000000000000000..d8da71925fbeca6c7e9706fc63760efdce2b374f
--- /dev/null
+++ b/python/example/dynamic-catalogue.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+
+from pathlib import Path
+
+import arbor as arb
+
+cat = 'cat-catalogue.so'
+
+class recipe(arb.recipe):
+    def __init__(self):
+        arb.recipe.__init__(self)
+        self.tree = arb.segment_tree()
+        self.tree.append(arb.mnpos, (0, 0, 0, 10), (1, 0, 0, 10), 1)
+        self.props = arb.neuron_cable_properties()
+        self.cat = arb.load_catalogue(cat)
+        self.props.register(self.cat)
+        d = arb.decor()
+        d.paint('(all)', 'dummy')
+        d.set_property(Vm=0.0)
+        self.cell = arb.cable_cell(self.tree, arb.label_dict(), d)
+
+    def global_properties(self, _):
+        return self.props
+
+    def num_cells(self):
+        return 1
+
+    def num_targets(self, gid):
+        return 0
+
+    def num_sources(self, gid):
+        return 0
+
+    def cell_kind(self, gid):
+        return arb.cell_kind.cable
+
+    def cell_description(self, gid):
+        return self.cell
+
+if not Path(cat).is_file():
+    print("""Catalogue not found in this directory.
+Please ensure it has been compiled by calling")
+  <arbor>/scripts/build-catalogue cat <arbor>/python/examples/cat
+where <arbor> is the location of the arbor source tree.""")
+    exit(1)
+
+rcp = recipe()
+ctx = arb.context()
+dom = arb.partition_load_balance(rcp, ctx)
+sim = arb.simulation(rcp, dom, ctx)
+sim.run(tfinal=30)
diff --git a/python/mechanism.cpp b/python/mechanism.cpp
index a1ac7a9d8f86120fa3d762dd8304f29b0193fc95..a2c93be2c0a6e15cb2258f84226dd134578cdf3a 100644
--- a/python/mechanism.cpp
+++ b/python/mechanism.cpp
@@ -156,6 +156,7 @@ void register_mechanisms(pybind11::module& m) {
     m.def("default_catalogue", [](){return arb::global_default_catalogue();});
     m.def("allen_catalogue", [](){return arb::global_allen_catalogue();});
     m.def("bbp_catalogue", [](){return arb::global_bbp_catalogue();});
+    m.def("load_catalogue", [](const std::string& fn){return arb::load_catalogue(fn);});
 
     // arb::mechanism_desc
     // For specifying a mechanism in the cable_cell interface.
diff --git a/python/test/unit/runner.py b/python/test/unit/runner.py
index b727c46647d251114cfef81d85c36d2703cf9cad..fb796792b6de99c5958cb778d2f655960083109f 100644
--- a/python/test/unit/runner.py
+++ b/python/test/unit/runner.py
@@ -18,10 +18,12 @@ try:
     import test_schedules
     import test_cable_probes
     import test_morphology
+    import test_catalogues
     # add more if needed
 except ModuleNotFoundError:
     from test import options
     from test.unit import test_contexts
+    from test.unit import test_catalogues
     from test.unit import test_domain_decompositions
     from test.unit import test_event_generators
     from test.unit import test_identifiers
@@ -32,6 +34,7 @@ except ModuleNotFoundError:
 
 test_modules = [\
     test_contexts,\
+    test_catalogues,\
     test_domain_decompositions,\
     test_event_generators,\
     test_identifiers,\
diff --git a/python/test/unit/test_catalogues.py b/python/test/unit/test_catalogues.py
new file mode 100644
index 0000000000000000000000000000000000000000..eac665c1d0d3ce3052ddf58e7d8c4eca53745747
--- /dev/null
+++ b/python/test/unit/test_catalogues.py
@@ -0,0 +1,91 @@
+import unittest
+
+import arbor as arb
+
+# to be able to run .py file from child directory
+import sys, os
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
+
+try:
+    import options
+except ModuleNotFoundError:
+    from test import options
+
+"""
+tests for (dynamically loaded) catalogues
+"""
+
+class recipe(arb.recipe):
+    def __init__(self):
+        arb.recipe.__init__(self)
+        self.tree = arb.segment_tree()
+        self.tree.append(arb.mnpos, (0, 0, 0, 10), (1, 0, 0, 10), 1)
+        self.props = arb.neuron_cable_properties()
+        try:
+            self.cat = arb.default_catalogue()
+            self.props.register(self.cat)
+        except:
+            print("Catalogue not found. Are you running from build directory?")
+            raise
+
+        d = arb.decor()
+        d.paint('(all)', 'pas')
+        d.set_property(Vm=0.0)
+        self.cell = arb.cable_cell(self.tree, arb.label_dict(), d)
+
+    def global_properties(self, _):
+        return self.props
+
+    def num_cells(self):
+        return 1
+
+    def num_targets(self, gid):
+        return 0
+
+    def num_sources(self, gid):
+        return 0
+
+    def cell_kind(self, gid):
+        return arb.cell_kind.cable
+
+    def cell_description(self, gid):
+        return self.cell
+
+
+class Catalogues(unittest.TestCase):
+    def test_nonexistent(self):
+        with self.assertRaises(RuntimeError):
+            arb.load_catalogue("_NO_EXIST_.so")
+
+    def test_shared_catalogue(self):
+        try:
+            cat = arb.load_catalogue("lib/dummy-catalogue.so")
+        except:
+            print("BBP catalogue not found. Are you running from build directory?")
+            raise
+        nms = [m for m in cat]
+        self.assertEqual(nms, ['dummy'], "Expected equal names.")
+        for nm in nms:
+            prm = list(cat[nm].parameters.keys())
+            self.assertEqual(prm, ['gImbar'], "Expected equal parameters on mechanism '{}'.".format(nm))
+
+    def test_simulation(self):
+        rcp = recipe()
+        ctx = arb.context()
+        dom = arb.partition_load_balance(rcp, ctx)
+        sim = arb.simulation(rcp, dom, ctx)
+        sim.run(tfinal=30)
+
+
+def suite():
+    # specify class and test functions in tuple (here: all tests starting with 'test' from class Contexts
+    suite = unittest.makeSuite(Catalogues, ('test'))
+    return suite
+
+def run():
+    v = options.parse_arguments().verbosity
+    runner = unittest.TextTestRunner(verbosity = v)
+    runner.run(suite())
+
+if __name__ == "__main__":
+    run()
diff --git a/scripts/build-catalogue b/scripts/build-catalogue
new file mode 100755
index 0000000000000000000000000000000000000000..034b2079d571c39fa9cf90986dfbd9bf2659dbcf
--- /dev/null
+++ b/scripts/build-catalogue
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+
+import subprocess as sp
+import sys
+from tempfile import TemporaryDirectory
+import os
+from pathlib import Path
+import shutil
+import stat
+import string
+import argparse
+
+def parse_arguments():
+    def append_slash(s):
+        return s+'/' if s and not s.endswith('/') else s
+
+    class ConciseHelpFormatter(argparse.HelpFormatter):
+        def __init__(self, **kwargs):
+            super(ConciseHelpFormatter, self).__init__(max_help_position=20, **kwargs)
+
+        def _format_action_invocation(self, action):
+            if not action.option_strings:
+                return super(ConciseHelpFormatter, self)._format_action_invocation(action)
+            else:
+                optstr = ', '.join(action.option_strings)
+                if action.nargs==0:
+                    return optstr
+                else:
+                    return optstr+' '+self._format_args(action, action.dest.upper())
+
+    parser = argparse.ArgumentParser(
+        description = 'Generate dynamic catalogue and build it into a shared object.',
+        usage = '%(prog)s catalogue_name mod_source_dir',
+        add_help = False,
+        formatter_class = ConciseHelpFormatter)
+
+    parser.add_argument('name',
+                        metavar='name',
+                        type=str,
+                        help='Catalogue name.')
+
+    parser.add_argument('-s', '--source',
+                        metavar='source',
+                        type=str,
+                        default=Path(__file__).parents[1].resolve(),
+                        help='Path to arbor sources; defaults to parent of this script\'s directory')
+
+    parser.add_argument('modpfx',
+                        metavar='modpfx',
+                        type=str,
+                        help='Directory name where *.mod files live.')
+
+    parser.add_argument('-v', '--verbose',
+                        action='store_true',
+                        help='Verbose.')
+
+
+    parser.add_argument('-h', '--help',
+                        action='help',
+                        help='display this help and exit')
+
+    return vars(parser.parse_args())
+
+args    = parse_arguments()
+pwd     = Path.cwd()
+name    = args['name']
+mod_dir = pwd / Path(args['modpfx'])
+mods    = [ f[:-4] for f in os.listdir(mod_dir) if f.endswith('.mod') ]
+verbose = args['verbose']
+arb     = args['source']
+
+cmake = """
+cmake_minimum_required(VERSION 3.9)
+project({catalogue}-cat LANGUAGES CXX)
+
+find_package(arbor REQUIRED)
+
+include(BuildModules.cmake)
+
+set(ARB_WITH_EXTERNAL_MODCC true)
+find_program(modcc NAMES modcc)
+
+make_catalogue(
+  NAME {catalogue}
+  SOURCES "${{CMAKE_CURRENT_SOURCE_DIR}}/mod"
+  OUTPUT "CAT_{catalogue}_SOURCES"
+  MECHS {mods}
+  ARBOR {arb})
+"""
+
+print(f"Building catalogue '{name}' from mechanisms in {mod_dir}")
+for m in mods:
+    print(" *", m)
+
+with TemporaryDirectory() as tmp:
+    print(f"Setting up temporary build directory: {tmp}")
+    tmp = Path(tmp)
+    shutil.copytree(mod_dir, tmp / 'mod')
+    os.mkdir(tmp / 'build')
+    os.chdir(tmp / 'build')
+    with open(tmp / 'CMakeLists.txt', 'w') as fd:
+        fd.write(cmake.format(catalogue=name, mods=' '.join(mods), arb=arb))
+    shutil.copy2(f'{arb}/mechanisms/BuildModules.cmake', tmp)
+    shutil.copy2(f'{arb}/mechanisms/generate_catalogue', tmp)
+    print("Configuring...")
+    sp.run('cmake ..', shell=True, check=True, capture_output=not verbose)
+    print("Building...")
+    sp.run('make',     shell=True, capture_output=not verbose)
+    print("Cleaning-up...")
+    shutil.copy2(f'{name}-catalogue.so', pwd)
+    print(f'Catalogue has been built and copied to {pwd}/{name}-catalogue.so')
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index 7a0abc1d8ad016724f22bca074635d26a2326775..040752618a9af914f1131b527d42d18a85d060a5 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -173,6 +173,16 @@ set(unit_sources
     unit_test_catalogue.cpp
 )
 
+
+# for unit testing
+make_catalogue(
+  NAME dummy
+  SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/dummy"
+  OUTPUT "CAT_DUMMY_SOURCES"
+  MECHS dummy
+  ARBOR "${PROJECT_SOURCE_DIR}"
+  STANDALONE ON)
+
 if(ARB_WITH_GPU)
     list(APPEND unit_sources
         test_intrin.cu
@@ -204,6 +214,7 @@ endif()
 add_executable(unit EXCLUDE_FROM_ALL ${unit_sources} ${test_mech_sources})
 add_dependencies(unit build_test_mods)
 add_dependencies(tests unit)
+add_dependencies(unit dummy-catalogue)
 
 if(ARB_WITH_NVCC)
     target_compile_options(unit PRIVATE -DARB_CUDA)
@@ -221,6 +232,7 @@ endif()
 
 target_compile_options(unit PRIVATE ${ARB_CXXOPT_ARCH})
 target_compile_definitions(unit PRIVATE "-DDATADIR=\"${CMAKE_CURRENT_SOURCE_DIR}/swc\"")
+target_compile_definitions(unit PRIVATE "-DLIBDIR=\"${PROJECT_BINARY_DIR}/lib\"")
 target_include_directories(unit PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
 target_link_libraries(unit PRIVATE gtest arbor arborenv arborio arbor-private-headers arbor-sup)
 
diff --git a/test/unit/dummy/dummy.mod b/test/unit/dummy/dummy.mod
new file mode 100644
index 0000000000000000000000000000000000000000..df712b023df091c60cb826a00066ccd0485d2231
--- /dev/null
+++ b/test/unit/dummy/dummy.mod
@@ -0,0 +1,54 @@
+: Reference: Adams et al. 1982 - M-currents and other potassium currents in bullfrog sympathetic neurones
+: Comment:   corrected rates using q10 = 2.3, target temperature 34, orginal 21
+
+NEURON {
+    SUFFIX dummy
+    USEION k READ ek WRITE ik
+    RANGE gImbar
+}
+
+UNITS {
+    (S) = (siemens)
+    (mV) = (millivolt)
+    (mA) = (milliamp)
+}
+
+PARAMETER {
+    gImbar = 0.00001 (S/cm2)
+}
+
+STATE {
+    m
+}
+
+BREAKPOINT {
+    SOLVE states METHOD cnexp
+    ik = gImbar*m*(v - ek)
+}
+
+DERIVATIVE states {
+    LOCAL qt, mAlpha, mBeta
+
+    qt     = 2.3^((34-21)/10)
+    mAlpha = m_alpha(v)
+    mBeta  = m_beta(v)
+
+    m'     = qt*(mAlpha - m*(mAlpha + mBeta))
+}
+
+INITIAL {
+    LOCAL mAlpha, mBeta
+
+    mAlpha = m_alpha(v)
+    mBeta  = m_beta(v)
+
+    m = mAlpha/(mAlpha + mBeta)
+}
+
+FUNCTION m_alpha(v) {
+    m_alpha = 3.3e-3*exp( 2.5*0.04*(v + 35))
+}
+FUNCTION m_beta(v) {
+    m_beta  = 3.3e-3*exp(-2.5*0.04*(v + 35))
+}
+
diff --git a/test/unit/test_mechcat.cpp b/test/unit/test_mechcat.cpp
index 1b3656765d1fe35bb812f3f309ecbd7938fb679b..2b6b1a6465f0ac85559e8c809ed3be3daa19e51d 100644
--- a/test/unit/test_mechcat.cpp
+++ b/test/unit/test_mechcat.cpp
@@ -8,6 +8,11 @@
 
 #include "common.hpp"
 
+#ifndef LIBDIR
+#warning "LIBDIR not set; defaulting to '.'"
+#define LIBDIR "."
+#endif
+
 using namespace std::string_literals;
 using namespace arb;
 
@@ -286,6 +291,15 @@ TEST(mechcat, names) {
     }
 }
 
+TEST(mechcat, loading) {
+    EXPECT_THROW(load_catalogue(LIBDIR "/does-not-exist-catalogue.so"), file_not_found_error);
+    EXPECT_THROW(load_catalogue(LIBDIR "/libarbor.a"), bad_catalogue_error);
+    const mechanism_catalogue* cat = nullptr;
+    EXPECT_NO_THROW(cat = &load_catalogue(LIBDIR "/dummy-catalogue.so"));
+    ASSERT_NE(cat, nullptr);
+    EXPECT_EQ(std::vector<std::string>{"dummy"}, cat->mechanism_names());
+}
+
 TEST(mechcat, derived_info) {
     auto cat = build_fake_catalogue();