diff --git a/doc/internals/export.rst b/doc/internals/export.rst
new file mode 100644
index 0000000000000000000000000000000000000000..7e997f364d43898098c23d2d8b5075ddd69d4971
--- /dev/null
+++ b/doc/internals/export.rst
@@ -0,0 +1,134 @@
+.. _export:
+
+Exporting Symbols
+=================
+
+The Arbor libraries are compiled with `hidden visibility <https://gcc.gnu.org/wiki/Visibility>`_ by
+default which strips a compiled library of all symbols not explicitly marked as visible. Note that
+the term *visibility* here refers to the symbol visibility exposed to the linker and not to language
+*access* specifier such as public/private. Arbor provides a couple of macros to make functions and
+classes visible which are defined in the header file ``export.hpp`` in each library's include
+directory, i.e. ``include/arbor/export.hpp``.  These header files are generated at configure time
+based on the build variant, compiler and platform.
+
+By default, hidden/visible symbols will affect **shared** libraries directly. Since the linker is
+responsible for making symbols hidden, the visibility settings are not applied to **static**
+libraries immediately, as static libraries are generated by bundling object files with the archiver
+(standard in cmake for ``STATIC`` libraries).  In principle, the linker would be able to generate
+relocatable output instead (``ld -r``) by incrementally linking object files into one output file
+(sometimes called partial linking) which would apply proper visibility to static libraries.
+However, we currently do not handle this case in our build scripts as it is not not nativley
+`supported by cmake yet <https://gitlab.kitware.com/cmake/cmake/-/issues/16977>`_.
+
+.. Note::
+    When linking an application with **static** Arbor libraries the linker may issue warnings (particularly on macos).  Thus, if you encounter problems, try building shared Arbor libraries (cmake option ``-DBUILD_SHARED_LIBS=ON``) instead.
+
+Macro Descripiton
+-----------------
+
+.. c:macro:: ARB_LIBNAME_API
+
+    Here "``LIBNAME``" is a placeholder for the library's name: ``ARB_ARBOR_API`` for the core Arbor
+    library, ``ARB_ARBORIO_API`` for Arborio, etc. This macro is intended to annotate functions,
+    classes and structs which need to be accessible when interfacing with the library. Note that it
+    expands to different values when Arbor is being built vs. when Arbor is being used by an
+    application.
+    
+    Below we list the places where the macro needs to be added or can be safely omitted (we assume
+    all of the symbols below are part of the public/user-facing API).  In general, annotation is
+    required
+    
+    * for **declarations in header files** which are not definitions
+    * for **definitions** of functions, friend functions and (extern) variables **in source files**
+        
+    Members and member functions of already visible classes will be visible, as well, without
+    further annotation, except for friend function (see below).
+
+    Implementation details and internal APIs may not need annotation as long as they do not require
+    visibility across the library boundary (though some annotations are required for unit test
+    purposes). Exception classes and type-erased objects need special annotation, see
+    :c:macro:`ARB_SYMBOL_VISIBLE`.
+
+    .. code-block:: cpp
+        :caption: header.hpp
+
+        #include <arbor/export.hpp>
+
+        // free function declaration
+        ARB_ARBOR_API void foo();
+
+        // free function definition
+        inline void bar(int i) { /* ... */ }
+
+        // function template definition
+        template<typename T>
+        void baz(T i) { /* ... */ }
+
+        // class definition
+        // class member declaration
+        class ARB_ARBOR_API A {
+            A();
+            friend std::ostream& operator<<(std::ostream& o, A const & a);
+        };
+
+        // class definition
+        // class member definition
+        class B {
+            B() { /* ... */ }
+        };
+
+        // template class definition
+        // class member definition
+        template<typename T>
+        class C {
+            C() { /* ... */ }
+        };
+
+        // (extern) variable declarations
+        ARB_ARBOR_API int g;
+        ARB_ARBOR_API extern int h;
+
+
+    .. code-block:: cpp
+        :caption: source.cpp
+
+        // free function definition
+        ARB_ARBOR_API void foo() { /* ... */ }
+
+        // class member definition (will be visible since A is visible)
+        A::A() { /* ... */ }
+
+        // friend function definition
+        ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, A const& a) { /* ... */ }
+
+        // (extern) variable definitions
+        ARB_ARBOR_API int g = 10;
+        ARB_ARBOR_API int h = 11;
+
+
+.. c:macro:: ARB_SYMBOL_VISIBLE
+
+    Objects which are type-erased and passed across the library boundaries sometimes need runtime
+    type information (rtti). In particular, exception classes and classes stored in ``std::any`` or
+    similar need to have the correct runtime information attached. Hidden visibility strips away
+    this information which leads to all kind of unexpected behaviour. Therefore, all such classes
+    must be annotated with this macro which guarantees that the symbol is always visible. This also
+    applies for classes (or structs) which are entirely defined inline. Note, one must not use
+    :c:macro:`ARB_LIBNAME_API` for these cases.
+
+    .. code-block:: cpp
+        :caption: header.hpp
+
+        #include <arbor/export.hpp>
+
+        // exception class defintion and class member definition
+        class ARB_SYMBOL_VISIBLE some_error : public std::runtime_error {
+            some_error() { /* ... */ }
+        };
+
+        // class defintion and member defintion
+        // class D will be type-erased and restored by an any_cast or similar
+        class ARB_SYMBOL_VISIBLE D {
+            D() { /* ... */ }
+        };
+
diff --git a/doc/internals/index.rst b/doc/internals/index.rst
index cb7a844720465c16c951c226144a08c2263439e2..dbbfdbdf5ab72d026b6dffefc14604bd28affa92 100644
--- a/doc/internals/index.rst
+++ b/doc/internals/index.rst
@@ -9,6 +9,7 @@ Here we document internal components of Arbor. These pages can be useful if you'
    :caption: Arbor Internals:
    :maxdepth: 2
 
+   export
    util
    simd_api
    extending_catalogues