diff --git a/.gitignore b/.gitignore
index ef18ef29c682c0c471e5ecc4c974c7f9fe602763..c970c0ef7370d24590b25d9e42386114300323f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,5 +21,8 @@ dist
 htmlcov
 coverage.xml                
 
+# cmake
+CMakeUserPresets.json
+
 # scratch
 scratch
\ No newline at end of file
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bf5e6fa193d4df6336eee83fc8a1def861ae5c46
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,29 @@
+stages:
+ - Documentation
+ - Deploy
+
+
+build-user-manual:
+  image: i10git.cs.fau.de:5005/pycodegen/pycodegen/nox:alpine
+  tags:
+    - docker
+  stage: "Documentation"
+  needs: []
+  script:
+    - nox --session user_manual
+  artifacts:
+    paths:
+      - user_manual/_sphinx_build/html
+
+pages:
+  image: alpine:latest
+  stage: "Deploy"
+  script:
+    - mv user_manual/_sphinx_build/html public  # folder has to be named "public" for gitlab to publish it
+  artifacts:
+    paths:
+      - public
+  tags:
+    - docker
+  only:
+    - master@da15siwa/sfg-walberla
diff --git a/CMakeLists.txt b/CMakeLists.txt
index bab7c8fd4560a1c0e8359d6ba0cb4e7307468d02..15cdc03ad250bf7d9b197a8801a867f204ae4113 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,8 +1,11 @@
 cmake_minimum_required( VERSION 3.24 )
 project ( sfg-walberla )
 
-SET ( CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake )
-find_package( PystencilsSfg REQUIRED )
+set(sfg_walberla_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
+
+list (APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake )
+
+include( PrepareSFG )
 
 add_library( sfg_walberla INTERFACE )
 
diff --git a/cmake/CodegenConfig.template.py b/cmake/CodegenConfig.template.py
new file mode 100644
index 0000000000000000000000000000000000000000..07f29998fd188b7f4f95a2bd5409cb02f77f6c8d
--- /dev/null
+++ b/cmake/CodegenConfig.template.py
@@ -0,0 +1,31 @@
+from pystencilssfg import SfgConfig
+from sfg_walberla import WalberlaBuildConfig
+
+
+def configure_sfg(cfg: SfgConfig):
+    cfg.extensions.header = "hpp"
+    cfg.extensions.impl = "cpp"
+
+
+def project_info() -> WalberlaBuildConfig:
+    from sfg_walberla.build_config import cmake_parse_bool
+
+    return WalberlaBuildConfig(
+        c_compiler_id="${CMAKE_C_COMPILER_ID}",
+        cxx_compiler_id="${CMAKE_CXX_COMPILER_ID}",
+        use_double_precision=cmake_parse_bool("${WALBERLA_DOUBLE_ACCURACY}"),
+        optimize_for_localhost=cmake_parse_bool("${WALBERLA_OPTIMIZE_FOR_LOCALHOST}"),
+        mpi_enabled=cmake_parse_bool("${WALBERLA_BUILD_WITH_MPI}"),
+        openmp_enabled=cmake_parse_bool("${WALBERLA_BUILD_WITH_OPENMP}"),
+        cuda_enabled=cmake_parse_bool("${WALBERLA_BUILD_WITH_CUDA}"),
+        hip_enabled=cmake_parse_bool("${WALBERLA_BUILD_WITH_HIP}"),
+        likwid_enabled=cmake_parse_bool("${WALBERLA_BUILD_WITH_LIKWID_MARKERS}"),
+    )
+
+
+def validate():
+    _ = project_info()
+
+
+if __name__ == "__main__":
+    validate()
diff --git a/cmake/FindPystencilsSfg.cmake b/cmake/FindPystencilsSfg.cmake
index a5e7b11d09ddb55da6802291a28be77d6a44f4f6..20a3fd596d99cf2db18e346609b8060bf86d32bc 100644
--- a/cmake/FindPystencilsSfg.cmake
+++ b/cmake/FindPystencilsSfg.cmake
@@ -1,16 +1,42 @@
-set( PystencilsSfg_FOUND OFF CACHE BOOL "pystencils source file generator found" )
+#[[
+Find-Module for pystencils-sfg.
 
-mark_as_advanced( PystencilsSfg_FOUND )
+# Setting the Python interpreter
 
-find_package( Python COMPONENTS Interpreter REQUIRED )
+If the cache entry PystencilsSfg_PYTHON_INTERPRETER is set, e.g. via the commandline
+(`-DPystencilsSfg_PYTHON_INTERPRETER=<...>`), its value be taken as the Python interpreter
+used to find and run pystencils-sfg.
+
+If the cache entry is unset, but the hint PystencilsSfg_PYTHON_PATH is set, its value will
+be used as the Python interpreter.
+
+If none of these is set, a Python interpreter will be selected using the `FindPython` module.
+
+#]]
+
+if(NOT DEFINED CACHE{PystencilsSfg_PYTHON_INTERPRETER})
+    #   The Python interpreter cache variable is not set externally, so...
+    if(DEFINED PystencilsSfg_PYTHON_PATH)
+        #   ... either initialize it from the hint variable ...
+        set( _sfg_cache_python_init ${PystencilsSfg_PYTHON_PATH} )
+    else()
+        #   ... or, if that is also unset, use the system Python
+        find_package( Python COMPONENTS Interpreter REQUIRED )
+        set( _sfg_cache_python_init ${Python_EXECUTABLE} )
+    endif()
+endif()
+
+set(PystencilsSfg_PYTHON_INTERPRETER ${_sfg_cache_python_init} CACHE PATH "Path to the Python executable used to run pystencils-sfg")
 
 #   Try to find pystencils-sfg in the python environment
 
-execute_process(COMMAND ${Python_EXECUTABLE} -m pystencilssfg version --no-newline
+execute_process(COMMAND ${PystencilsSfg_PYTHON_INTERPRETER} -m pystencilssfg version --no-newline
                 RESULT_VARIABLE _PystencilsSfgFindResult OUTPUT_VARIABLE PystencilsSfg_VERSION )
 
 if(${_PystencilsSfgFindResult} EQUAL 0)
     set( PystencilsSfg_FOUND ON )
+else()
+    set( PystencilsSfg_FOUND OFF )
 endif()
 
 if(DEFINED PystencilsSfg_FIND_REQUIRED)
@@ -21,8 +47,9 @@ endif()
 
 if(${PystencilsSfg_FOUND})
     message( STATUS "Found pystencils Source File Generator (Version ${PystencilsSfg_VERSION})")
+    message( STATUS "Using Python interpreter ${PystencilsSfg_PYTHON_INTERPRETER} for SFG generator scripts.")
     
-    execute_process(COMMAND ${Python_EXECUTABLE} -m pystencilssfg cmake modulepath --no-newline
+    execute_process(COMMAND ${PystencilsSfg_PYTHON_INTERPRETER} -m pystencilssfg cmake modulepath --no-newline
                     OUTPUT_VARIABLE _PystencilsSfg_CMAKE_MODULE_PATH)
 
     set( CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${_PystencilsSfg_CMAKE_MODULE_PATH})
diff --git a/cmake/PrepareSFG.cmake b/cmake/PrepareSFG.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..1f34e3c4006f8b2ab1883a5ccd38018f06b5d7c5
--- /dev/null
+++ b/cmake/PrepareSFG.cmake
@@ -0,0 +1,129 @@
+#   Maybe set up private virtual environment
+
+set( 
+    WALBERLA_CODEGEN_USE_PRIVATE_VENV ON
+    CACHE BOOL 
+    "Create a private virtual Python environment inside the build tree for code generation" 
+)
+
+if( WALBERLA_CODEGEN_USE_PRIVATE_VENV )
+    set(_codegen_venv_path ${CMAKE_CURRENT_BINARY_DIR}/codegen-venv)
+    set(_venv_python_exe ${_codegen_venv_path}/bin/python)
+
+    find_package( Python COMPONENTS Interpreter REQUIRED )
+
+    if(NOT _sfg_private_venv_done)
+        message( STATUS "Setting up Python virtual environment at ${_codegen_venv_path}" )
+
+        #   Create the venv and register its interpreter with pystencils-sfg
+        execute_process(
+            COMMAND ${Python_EXECUTABLE} -m venv ${_codegen_venv_path}
+        )
+
+        message( STATUS "Installing required Python packages..." )
+
+        execute_process(
+            COMMAND ${_venv_python_exe} -m pip install -r ${sfg_walberla_SOURCE_DIR}/cmake/venv-reqs.txt
+            OUTPUT_QUIET
+        )
+
+        execute_process(
+            COMMAND ${_venv_python_exe} -m pip install -e ${sfg_walberla_SOURCE_DIR}
+            OUTPUT_QUIET
+        )
+
+        set( _sfg_private_venv_done TRUE CACHE BOOL "" )
+        set( _wlb_codegen_python_init ${_venv_python_exe} )
+        mark_as_advanced(_sfg_private_venv_done)
+    endif()
+else()
+    #   Use the external Python environment, but check if all packages are installed
+    find_package( Python COMPONENTS Interpreter REQUIRED )
+
+    execute_process(
+        COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_python_env.py
+        RESULT_VARIABLE _package_check_status
+        OUTPUT_VARIABLE _package_check_output
+        ERROR_VARIABLE _package_check_error
+    )
+
+    message( STATUS ${_package_check_output} )
+
+    if(NOT (${_package_check_status} EQUAL 0) )
+        message( FATAL_ERROR ${_package_check_error} )
+    endif()
+
+    set( _wlb_codegen_python_init ${Python_EXECUTABLE} )
+endif()
+
+set(PystencilsSfg_PYTHON_PATH ${_wlb_codegen_python_init})
+set(
+    WALBERLA_CODEGEN_PYTHON
+    ${_wlb_codegen_python_init}
+    CACHE PATH
+    "Path to the Python interpreter used for code generation"
+)
+mark_as_advanced(WALBERLA_CODEGEN_PYTHON)
+
+#   Find pystencils-sfg
+
+find_package( PystencilsSfg REQUIRED )
+
+#   Project Configuration Module
+
+set( 
+    WALBERLA_CODEGEN_CONFIG_MODULE
+    ${CMAKE_BINARY_DIR}/CodegenConfig.py
+    CACHE
+    FILEPATH
+    "Path to waLBerla-wide codegen config module" 
+)
+mark_as_advanced( WALBERLA_CODEGEN_CONFIG_MODULE )
+
+configure_file(
+    ${CMAKE_CURRENT_SOURCE_DIR}/cmake/CodegenConfig.template.py
+    ${WALBERLA_CODEGEN_CONFIG_MODULE}
+)
+
+message( STATUS "Wrote project-wide code generator configuration to ${WALBERLA_CODEGEN_CONFIG_MODULE}" )
+
+#[[
+Run `pip install` in the code generation environment with the given arguments.
+#]]
+function(walberla_codegen_venv_install)
+    if(NOT WALBERLA_CODEGEN_USE_PRIVATE_VENV)
+        message( FATAL_ERROR "The private virtual environment for code generation is disabled" )
+    endif()
+
+    if(NOT _sfg_private_venv_done)
+        message( FATAL_ERROR "The private virtual environment for code generation was not initialized yet" )
+    endif()
+
+    execute_process(
+        COMMAND ${WALBERLA_CODEGEN_PYTHON} -m pip install ${ARGV}
+    )
+endfunction()
+
+#   Code Generation Functions
+
+#[[
+Register code generation scripts for a CMake target.
+
+Signature:
+
+```
+walberla_generate_sources( <target> 
+    SCRIPTS script1.py [script2.py ...]
+    [DEPENDS dependency1.py [dependency2.py...] ]
+    [FILE_EXTENSIONS <header-extension> <impl-extension>]
+    [OUTPUT_MODE <standalone|inline|header-only>]
+)
+```
+
+This is a wrapper around `pystencilssfg_generate_target_sources`
+without the `CONFIG_MODULE` parameter.
+See also https://pycodegen.pages.i10git.cs.fau.de/pystencils-sfg/usage/project_integration.html#add-generator-scripts
+#]]
+function(walberla_generate_sources TARGET)
+    pystencilssfg_generate_target_sources(${ARGV} CONFIG_MODULE $CACHE{WALBERLA_CODEGEN_CONFIG_MODULE})
+endfunction()
diff --git a/cmake/check_python_env.py b/cmake/check_python_env.py
new file mode 100644
index 0000000000000000000000000000000000000000..7d00009b56992b3b16fb1dc07f2854e30e9d49b3
--- /dev/null
+++ b/cmake/check_python_env.py
@@ -0,0 +1,29 @@
+import sys
+
+
+def error(msg: str):
+    msg = "Required Python packages could not be found:\n" + msg
+    print(msg, file=sys.stderr)
+    sys.exit(1)
+
+
+print("Checking Python environment - ", end="")
+
+try:
+    import pystencils
+except ImportError:
+    error("pystencils is not installed in the current Python environment.")
+
+try:
+    import pystencilssfg
+except ImportError:
+    error("pystencils-sfg is not installed in the current Python environment.")
+
+try:
+    import sfg_walberla
+except ImportError:
+    error("sfg_walberla is not installed in the current Python environment.")
+
+print("found required packages pystencils, pystencils-sfg, sfg-walberla", end="")
+
+sys.exit(0)
diff --git a/cmake/venv-reqs.txt b/cmake/venv-reqs.txt
new file mode 100644
index 0000000000000000000000000000000000000000..97f53bbb590d9fd5e0fede0c74ca92b59014b804
--- /dev/null
+++ b/cmake/venv-reqs.txt
@@ -0,0 +1,8 @@
+# pystencils 2.0 Development Branch
+git+https://i10git.cs.fau.de/pycodegen/pystencils.git@v2.0-dev
+
+# lbmpy: feature branch for pystencils-2.0 compatibility
+git+https://i10git.cs.fau.de/pycodegen/lbmpy.git@fhennig/pystencils2.0-compat
+
+# pystencils-sfg: (development branch with updated CMake modules and cpptypes)
+git+https://i10git.cs.fau.de/pycodegen/pystencils-sfg.git@fhennig/devel
diff --git a/noxfile.py b/noxfile.py
new file mode 100644
index 0000000000000000000000000000000000000000..0d4bf13187c7c6a37309469d5945c12abf8a4e5e
--- /dev/null
+++ b/noxfile.py
@@ -0,0 +1,12 @@
+import nox
+
+
+@nox.session
+def user_manual(session: nox.Session):
+    session.chdir("user_manual")
+    session.install("-r", "requirements.txt")
+
+    if "--clean" in session.posargs:
+        session.run("make", "clean", external=True)
+
+    session.run("make", "html", external=True)
diff --git a/src/sfg_walberla/__init__.py b/src/sfg_walberla/__init__.py
index 063b350bec25105d9167aa6b0a7476e448c6a574..50097ef8b19500c16e44a5e25622d4fbaf6889d9 100644
--- a/src/sfg_walberla/__init__.py
+++ b/src/sfg_walberla/__init__.py
@@ -1,6 +1,7 @@
 from .api import real_t, Vector3, GhostLayerFieldPtr, glfield, IBlockPtr
 from .postprocessing import CaptureToClass
 from .sweep import Sweep
+from .build_config import WalberlaBuildConfig
 
 __all__ = [
     "real_t",
@@ -10,6 +11,7 @@ __all__ = [
     "IBlockPtr",
     "CaptureToClass",
     "Sweep",
+    "WalberlaBuildConfig",
 ]
 
 from . import _version
diff --git a/src/sfg_walberla/api.py b/src/sfg_walberla/api.py
index ad2819acb6f725ea2a240f221671bc4deb7d69ef..2818b43f0ebe7bec7ef17859bc6857e7c40d8430 100644
--- a/src/sfg_walberla/api.py
+++ b/src/sfg_walberla/api.py
@@ -1,5 +1,7 @@
 from __future__ import annotations
 
+from typing import Callable
+
 from pystencils import Field
 from pystencils.types import (
     UserTypeSpec,
@@ -15,8 +17,9 @@ from pystencilssfg.lang import (
     SrcVector,
     Ref,
     ExprLike,
+    cpptype,
 )
-from pystencilssfg.ir import SfgHeaderInclude
+from pystencilssfg.lang.types import CppType
 
 
 real_t = PsCustomType("walberla::real_t")
@@ -24,39 +27,53 @@ cell_idx_t = PsCustomType("walberla::cell_idx_t")
 uint_t = PsCustomType("walberla::uint_t")
 
 
+class _CppClass(AugExpr):
+    _type: Callable[..., CppType]
+
+    def __init__(self, const: bool = False, ref: bool = False):
+        dtype = self._type(const=const, ref=ref)
+        super().__init__(dtype)
+
+
 class Vector2(SrcVector):
-    def __init__(self, val_type: UserTypeSpec):
-        self._value_type = create_type(val_type)
-        val_type_str = self._value_type.c_string()
-        super().__init__(PsCustomType(f"walberla::Vector2< {val_type_str} >"))
+    _template = cpptype("walberla::Vector2< {element_type} >", "core/math/Vector2.h")
+
+    def __init__(
+        self, element_type: UserTypeSpec, const: bool = False, ref: bool = False
+    ):
+        self._element_type = create_type(element_type)
+        dtype = self._template(element_type=element_type, const=const, ref=ref)
+        super().__init__(dtype)
 
     def extract_component(self, coordinate: int) -> AugExpr:
         if coordinate > 1:
             raise ValueError(f"Cannot extract component {coordinate} from Vector2")
 
-        return AugExpr(self._value_type).bind("{}[{}]", self, coordinate)
+        return AugExpr(self._element_type).bind("{}[{}]", self, coordinate)
 
 
 class Vector3(SrcVector):
-    def __init__(self, val_type: UserTypeSpec):
-        self._value_type = create_type(val_type)
-        val_type_str = self._value_type.c_string()
-        super().__init__(PsCustomType(f"walberla::Vector3< {val_type_str} >"))
+    _template = cpptype("walberla::Vector3< {element_type} >", "core/math/Vector3.h")
+
+    def __init__(
+        self, element_type: UserTypeSpec, const: bool = False, ref: bool = False
+    ):
+        self._element_type = create_type(element_type)
+        dtype = self._template(element_type=element_type, const=const, ref=ref)
+        super().__init__(dtype)
 
     def extract_component(self, coordinate: int) -> AugExpr:
         if coordinate > 2:
             raise ValueError(f"Cannot extract component {coordinate} from Vector3")
 
-        return AugExpr(self._value_type).bind("{}[{}]", self, coordinate)
+        return AugExpr(self._element_type).bind("{}[{}]", self, coordinate)
 
     def __getitem__(self, idx: int | ExprLike):
-        return AugExpr(self._value_type).bind("{}[{}]", self, idx)
+        return AugExpr(self._element_type).bind("{}[{}]", self, idx)
 
 
-class AABB(AugExpr):
-    def __init__(self):
-        dtype = PsCustomType("walberla::AABB")
-        super().__init__(dtype)
+class AABB(_CppClass):
+    _type = cpptype("walberla::AABB", "core/math/AABB.h")
 
     def min(self) -> Vector3:
         return Vector3(real_t).bind("{}.min()", self)
@@ -65,26 +82,16 @@ class AABB(AugExpr):
         return Vector3(real_t).bind("{}.max()", self)
 
 
-class CellInterval(AugExpr):
-    def __init__(self, const: bool = False, ref: bool = False):
-        dtype = PsCustomType("walberla::CellInterval", const=const)
-        if ref:
-            dtype = Ref(dtype)
-        super().__init__(dtype)
+class CellInterval(_CppClass):
+    _type = cpptype("walberla::CellInterval", "core/cell/CellInterval.h")
 
 
-class BlockDataID(AugExpr):
-    def __init__(self):
-        super().__init__(PsCustomType("walberla::BlockDataID"))
+class BlockDataID(_CppClass):
+    _type = cpptype("walberla::BlockDataID", "domain_decomposition/BlockDataID.h")
 
-    @property
-    def required_includes(self) -> set[SfgHeaderInclude]:
-        return {SfgHeaderInclude.parse("domain_decomposition/BlockDataID.h")}
 
-
-class IBlockPtr(AugExpr):
-    def __init__(self):
-        super().__init__(PsCustomType("walberla::IBlock *"))
+class IBlockPtr(_CppClass):
+    _type = cpptype("walberla::IBlock *", "domain_decomposition/IBlock.h")
 
     def getData(self, dtype: str | PsType, id: BlockDataID) -> AugExpr:
         return AugExpr.format("{}->template getData< {} >({})", self, dtype, id)
@@ -92,10 +99,6 @@ class IBlockPtr(AugExpr):
     def getAABB(self) -> AABB:
         return AABB().bind("{}->getAABB()", self)
 
-    @property
-    def required_includes(self) -> set[SfgHeaderInclude]:
-        return {SfgHeaderInclude.parse("domain_decomposition/IBlock.h")}
-
     def deref(self) -> AugExpr:
         return AugExpr.format("*{}", self)
 
@@ -208,6 +211,11 @@ class GenericWalberlaField(SrcField):
 
 
 class GhostLayerFieldPtr(GenericWalberlaField):
+    _template = cpptype(
+        "walberla::field::GhostLayerField< {element_type}, {fsize} >",
+        "field/GhostLayerField.h",
+    )
+
     @staticmethod
     def create(field: Field):
         if field.index_dimensions > 1:
@@ -226,19 +234,17 @@ class GhostLayerFieldPtr(GenericWalberlaField):
         fsize: int,
     ):
         element_type = create_type(element_type)
-        elmt_type_str = element_type.c_string()
-        field_type = PsCustomType(
-            f"walberla::field::GhostLayerField< {elmt_type_str}, {fsize} >"
-        )
+        field_type = self._template(element_type=element_type, fsize=fsize)
 
         super().__init__(element_type, field_type, ptr=True)
 
-    @property
-    def required_includes(self) -> set[SfgHeaderInclude]:
-        return {SfgHeaderInclude("field/GhostLayerField.h")}
-
 
 class GpuFieldPtr(GenericWalberlaField):
+    _template = cpptype(
+        "walberla::gpu::GpuField< {element_type} >",
+        "gpu/GpuField.h",
+    )
+
     @staticmethod
     def create(field: Field):
         if field.index_dimensions > 1:
@@ -257,15 +263,10 @@ class GpuFieldPtr(GenericWalberlaField):
         fsize: int,
     ):
         element_type = create_type(element_type)
-        elmt_type_str = element_type.c_string()
-        field_type = PsCustomType(f"walberla::gpu::GpuField< {elmt_type_str} >")
+        field_type = self._template(element_type=element_type)
 
         super().__init__(element_type, field_type, ptr=True)
 
-    @property
-    def required_includes(self) -> set[SfgHeaderInclude]:
-        return {SfgHeaderInclude("gpu/GpuField.h")}
-
 
 class GhostLayerFieldExtraction(IFieldExtraction):
     def __init__(
diff --git a/src/sfg_walberla/build_config.py b/src/sfg_walberla/build_config.py
new file mode 100644
index 0000000000000000000000000000000000000000..c47539d357be23d5c922ebd728e45061282a8eaf
--- /dev/null
+++ b/src/sfg_walberla/build_config.py
@@ -0,0 +1,74 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+from pystencils import CreateKernelConfig
+from pystencils.types.quick import Fp
+from pystencils.jit import no_jit
+
+from pystencilssfg import SfgContext
+from pystencilssfg.composer import SfgIComposer
+
+
+def cmake_parse_bool(var: str):
+    var = var.upper()
+    if var in ("ON", "1", "TRUE"):
+        return True
+    elif var in ("OFF", "0", "FALSE"):
+        return False
+    else:
+        raise ValueError(f"Could not parse cmake value `{var}` as boolean.")
+
+
+@dataclass
+class WalberlaBuildConfig:
+    """Represents a waLBerla build system configuration"""
+
+    c_compiler_id: str
+    """Value of `CMAKE_C_COMPILER_ID`."""
+
+    cxx_compiler_id: str
+    """Value of `CMAKE_CXX_COMPILER_ID`."""
+
+    use_double_precision: bool
+    """Value of `WALBERLA_DOUBLE_ACCURACY`"""
+
+    optimize_for_localhost: bool
+    """Value of `WALBERLA_OPTIMIZE_FOR_LOCALHOST`."""
+
+    mpi_enabled: bool
+    """Value of `WALBERLA_BUILD_WITH_MPI`."""
+
+    openmp_enabled: bool
+    """Value of `WALBERLA_BUILD_WITH_OPENMP`."""
+
+    cuda_enabled: bool
+    """Value of `WALBERLA_BUILD_WITH_CUDA`."""
+
+    hip_enabled: bool
+    """Value of `WALBERLA_BUILD_WITH_HIP`."""
+
+    likwid_enabled: bool
+    """Value of `WALBERLA_BUILD_WITH_LIKWID_MARKERS`"""
+
+    @staticmethod
+    def from_sfg(sfg: SfgContext | SfgIComposer) -> WalberlaBuildConfig:
+        if isinstance(sfg, SfgIComposer):
+            ctx = sfg.context
+        else:
+            ctx = sfg
+
+        if isinstance(ctx.project_info, WalberlaBuildConfig):
+            return ctx.project_info
+        else:
+            raise ValueError(
+                "The given SfgContext does not encapsulate a waLBerla build config object."
+            )
+
+    def get_pystencils_config(self) -> CreateKernelConfig:
+        dtype = Fp(64) if self.use_double_precision else Fp(32)
+
+        return CreateKernelConfig(
+            default_dtype=dtype,
+            jit=no_jit,
+        )
diff --git a/user_manual/.gitignore b/user_manual/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..4741371e5883899742b08cf256ca454fefa6267a
--- /dev/null
+++ b/user_manual/.gitignore
@@ -0,0 +1,3 @@
+_sphinx_build
+downloads
+zipped-examples
diff --git a/user_manual/CMakeLists.txt b/user_manual/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..59c36c295de0103a5eb5657faee2ff1dfadc1fb4
--- /dev/null
+++ b/user_manual/CMakeLists.txt
@@ -0,0 +1,17 @@
+cmake_minimum_required( VERSION 3.24 )
+project( walberla-codegen-examples )
+
+include(FetchContent)
+
+FetchContent_Declare(
+    walberla
+    GIT_REPOSITORY https://i10git.cs.fau.de/walberla/walberla.git
+)
+
+message( STATUS "Fetching waLBerla sources (this might take a while)..." )
+FetchContent_MakeAvailable(walberla)
+
+add_subdirectory(${CMAKE_SOURCE_DIR}/.. ${CMAKE_BINARY_DIR}/sfg-walberla)
+
+add_subdirectory( GeneratorScriptBasics )
+add_subdirectory( ForceDrivenChannel )
diff --git a/user_manual/CMakeSetup/CMakeLists.txt b/user_manual/CMakeSetup/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..36d2851483f0469106030983921772cb5a658dc9
--- /dev/null
+++ b/user_manual/CMakeSetup/CMakeLists.txt
@@ -0,0 +1,16 @@
+cmake_minimum_required( VERSION 3.24 )
+project( <your-project-name> )
+
+include(FetchContent)
+
+FetchContent_Declare(
+    walberla
+    GIT_REPOSITORY https://i10git.cs.fau.de/walberla/walberla.git
+)
+
+FetchContent_Declare(
+    sfg-walberla
+    GIT_REPOSITORY https://i10git.cs.fau.de/da15siwa/sfg-walberla.git
+)
+
+FetchContent_MakeAvailable(walberla sfg-walberla)
diff --git a/user_manual/CMakeSetup/CMakeSetup.md b/user_manual/CMakeSetup/CMakeSetup.md
new file mode 100644
index 0000000000000000000000000000000000000000..003caad5711019cd0dc3a55a6f45a5667c750a2c
--- /dev/null
+++ b/user_manual/CMakeSetup/CMakeSetup.md
@@ -0,0 +1,73 @@
+(EnvSetup)=
+# CMake Project Setup
+
+This chapter describes the necessary steps to set up a CMake project to use waLBerla
+with the nextcodegen features provided by sfg-walberla.
+
+## Prequesites
+
+You are going to need at least
+- a C++ compiler supporting at least C++17;
+- a Python installation of version >= 3.10;
+- a CMake installation of version >= 3.24;
+- an up-to-date installation of Git.
+
+Also, the following optional dependencies are highly recommended:
+- clang-format for prettier code generator output
+
+If you are starting from scratch, create a new, empty directory for your CMake project.
+Then, set up its `CMakeLists.txt` with the following minimal settings:
+
+:::{literalinclude} CMakeLists.txt
+:lines: 1-2
+:::
+
+On the other hand. if you already have a CMake project set up, you can just as well extend it.
+
+## Add sfg-walberla to your CMake Project
+
+There are many ways to include `waLBerla` and `sfg-walberla` into your project:
+you can clone them locally and use `add_subdirectory`, add them as submodules, et cetera.
+For getting started, though, the easiest way is to use [FetchContent][FetchContent] to dynamically
+pull their sources into your build tree.
+
+Place the following code in your `CMakeLists.txt`:
+
+::::{card} {download}`CMakeLists.txt`
+:::{literalinclude} CMakeLists.txt
+:lines: 4-
+:::
+::::
+
+Now, you can run `cmake` to configure your build system:
+
+```bash
+mkdir build
+cmake -S . -B build -DPython_EXECUTABLE=`pwd`/.venv/bin/python
+```
+
+If, near the end of the long configuration log, you see two messages like this:
+
+```
+-- Found pystencils Source File Generator (Version 0.1a4+25.g9d3e553)
+-- Using Python interpreter <your-project-dir>/build/_deps/sfg-walberla-build/codegen-venv/bin/python for SFG generator scripts.
+```
+
+Then your build system setup was successful!
+Now you can get down to business and populate your project with simulation applications.
+
+(adding_examples)=
+## Adding Examples from this Manual
+
+As you explore the examples in this book,
+you can try out each by downloading, extracting, and including them into your project.
+For example, to include the [force-driven poiseulle channel example](#ForceDrivenChannel),
+extract its code into the `ForceDrivenChannel` subdirectory and add
+
+```CMake
+add_subdirectory( ForceDrivenChannel )
+```
+
+to your root `CMakeLists.txt`.
+
+[FetchContent]: https://cmake.org/cmake/help/latest/module/FetchContent.html
diff --git a/user_manual/ForceDrivenChannel/CMakeLists.txt b/user_manual/ForceDrivenChannel/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4fbb89077b6d17f564cad91113e1f74cc0cd81b6
--- /dev/null
+++ b/user_manual/ForceDrivenChannel/CMakeLists.txt
@@ -0,0 +1,14 @@
+waLBerla_link_files_to_builddir( Channel.prm )
+
+add_executable( Ex_ForceDrivenChannel )
+target_sources( Ex_ForceDrivenChannel PRIVATE ForceDrivenChannel.cpp )
+
+pystencilssfg_generate_target_sources( Ex_ForceDrivenChannel
+    SCRIPTS LbmAlgorithms.py
+)
+
+target_link_libraries(
+    Ex_ForceDrivenChannel
+    PRIVATE 
+    core stencil blockforest geometry vtk sfg_walberla 
+)
diff --git a/user_manual/ForceDrivenChannel/Channel.prm b/user_manual/ForceDrivenChannel/Channel.prm
new file mode 100644
index 0000000000000000000000000000000000000000..36aa90daf3229784037bd6b99d89412df5a7f9f8
--- /dev/null
+++ b/user_manual/ForceDrivenChannel/Channel.prm
@@ -0,0 +1,23 @@
+DomainSetup
+{
+    blocks < 1, 1, 1 >;
+    cellsPerBlock < 4, 4, 32 >;
+    periodic < 1, 1, 0 >;
+}
+
+Parameters
+{
+    omega       1.0;
+    force       < 6.25e-5, 0, 0 >;
+    timesteps   5000;
+}
+
+Boundaries {
+    Border { direction T; walldistance -1; flag NoSlip;  }
+    Border { direction B; walldistance -1; flag NoSlip; }
+}
+
+Output
+{
+    vtkWriteFrequency   100;
+}
\ No newline at end of file
diff --git a/user_manual/ForceDrivenChannel/ForceDrivenChannel.cpp b/user_manual/ForceDrivenChannel/ForceDrivenChannel.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..52238a70a2ebcb079d0f61257174e7aef82102bf
--- /dev/null
+++ b/user_manual/ForceDrivenChannel/ForceDrivenChannel.cpp
@@ -0,0 +1,145 @@
+#include "blockforest/all.h"
+#include "blockforest/communication/UniformBufferedScheme.h"
+
+#include "core/all.h"
+
+#include "domain_decomposition/SharedSweep.h"
+
+#include "field/all.h"
+#include "field/communication/PackInfo.h"
+#include "field/communication/StencilRestrictedPackInfo.h"
+
+#include "geometry/InitBoundaryHandling.h"
+
+#include "stencil/all.h"
+
+#include "timeloop/all.h"
+
+#include "vtk/all.h"
+
+#include "gen/Ex_ForceDrivenChannel/LbmAlgorithms.hpp"
+
+namespace Ex_ForceDrivenChannel
+{
+    using namespace walberla;
+    using namespace blockforest;
+
+    using std::make_unique;
+    using std::shared_ptr;
+
+    using ScalarField_T = field::GhostLayerField<real_t, 1>;
+    using VectorField_T = field::GhostLayerField<real_t, 3>;
+
+    using LbStencil = stencil::D3Q19;
+    using PdfField_T = field::GhostLayerField<real_t, LbStencil::Q>;
+
+    using CommScheme = blockforest::communication::UniformBufferedScheme<LbStencil>;
+    using PdfsPackInfo = field::communication::StencilRestrictedPackInfo<PdfField_T, LbStencil>;
+
+    using FlagField_T = FlagField<uint8_t>;
+
+    void run(const shared_ptr<Config> &config)
+    {
+        auto blocks = createUniformBlockGridFromConfig(config);
+
+        Config::BlockHandle simParams = config->getBlock("Parameters");
+        const real_t omega{simParams.getParameter<real_t>("omega")};
+
+        const Vector3<real_t> force = simParams.getParameter<Vector3<real_t>>("force");
+
+        BlockDataID pdfsId = field::addToStorage<PdfField_T>(blocks, "pdfs", real_c(0.0), field::fzyx, 1);
+        BlockDataID rhoId = field::addToStorage<ScalarField_T>(blocks, "rho", real_c(1.0), field::fzyx, 0);
+        BlockDataID uId = field::addToStorage<VectorField_T>(blocks, "u", real_c(0.0), field::fzyx, 0);
+
+        gen::LbInit lbInit{pdfsId, rhoId, uId, force};
+
+        for (auto &block : *blocks)
+        {
+            lbInit(&block);
+        }
+
+        //  Stream-Collide
+
+        auto streamCollide = make_shared<gen::LbStreamCollide>(pdfsId, rhoId, uId, force, omega);
+
+        //  Communication Setup
+
+        CommScheme comm{blocks};
+        auto pdfsPackInfo = std::make_shared<PdfsPackInfo>(pdfsId);
+        comm.addPackInfo(pdfsPackInfo);
+
+        //  Boundary Conditions
+
+        const BlockDataID flagFieldId = field::addFlagFieldToStorage<FlagField_T>(blocks, "flagField");
+        const FlagUID fluidFlagUid{"Fluid"};
+        const FlagUID noSlipFlagUid{"NoSlip"};
+
+        auto boundariesConfig = config->getBlock("Boundaries");
+        if (boundariesConfig)
+        {
+            WALBERLA_LOG_INFO_ON_ROOT("Setting boundary conditions")
+            geometry::initBoundaryHandling<FlagField_T>(*blocks, flagFieldId, boundariesConfig);
+        }
+
+        geometry::setNonBoundaryCellsToDomain<FlagField_T>(*blocks, flagFieldId, fluidFlagUid);
+
+        auto flagFieldOutput = field::createVTKOutput<FlagField_T>(flagFieldId, *blocks, "flagField", 1, 0);
+        flagFieldOutput();
+
+        auto noSlip = make_unique<gen::NoSlip>(blocks, pdfsId);
+        noSlip->fillFromFlagField<FlagField_T>(*blocks, flagFieldId, noSlipFlagUid, fluidFlagUid);
+
+        auto boundarySweep = [&](IBlock *block)
+        {
+            (*noSlip)(block);
+        };
+
+        //  Timeloop
+        const uint_t numTimesteps{simParams.getParameter<uint_t>("timesteps")};
+        SweepTimeloop loop{blocks->getBlockStorage(), numTimesteps};
+
+        loop.add() << Sweep(makeSharedSweep(streamCollide));
+        loop.add() << Sweep(boundarySweep) << AfterFunction(comm);
+
+        RemainingTimeLogger logger{numTimesteps};
+        loop.addFuncAfterTimeStep(logger);
+
+        //  VTK Output
+
+        Config::BlockHandle outputParams = config->getBlock("Output");
+
+        const uint_t vtkWriteFrequency = outputParams.getParameter<uint_t>("vtkWriteFrequency", 0);
+        if (vtkWriteFrequency > 0)
+        {
+            auto vtkOutput = vtk::createVTKOutput_BlockData(*blocks, "vtk", vtkWriteFrequency, 0, false, "vtk_out",
+                                                            "simulation_step", false, true, true, false, 0);
+
+            auto densityWriter = make_shared<field::VTKWriter<ScalarField_T, float32>>(rhoId, "density");
+            vtkOutput->addCellDataWriter(densityWriter);
+
+            auto velWriter = make_shared<field::VTKWriter<VectorField_T, float32>>(uId, "velocity");
+            vtkOutput->addCellDataWriter(velWriter);
+
+            const cell_idx_t xCells = cell_idx_c(blocks->getNumberOfXCells());
+            const cell_idx_t yCells = cell_idx_c(blocks->getNumberOfYCells());
+            const cell_idx_t zCells = cell_idx_c(blocks->getNumberOfZCells());
+
+            loop.addFuncAfterTimeStep(vtk::writeFiles(vtkOutput), "VTK Output");
+        }
+
+        //  Run the Simulation
+
+        WALBERLA_LOG_INFO_ON_ROOT("Commencing simulation with " << numTimesteps << " timesteps")
+
+        loop.run();
+    }
+}
+
+int main(int argc, char **argv)
+{
+    walberla::Environment env{argc, argv};
+
+    Ex_ForceDrivenChannel::run(env.config());
+
+    return EXIT_SUCCESS;
+}
diff --git a/user_manual/ForceDrivenChannel/ForceDrivenChannel.md b/user_manual/ForceDrivenChannel/ForceDrivenChannel.md
new file mode 100644
index 0000000000000000000000000000000000000000..1cc8d1481dd7767d6e111bceebb60646890b77e0
--- /dev/null
+++ b/user_manual/ForceDrivenChannel/ForceDrivenChannel.md
@@ -0,0 +1,34 @@
+(ForceDrivenChannel)=
+# Force-Driven Poiseuille Channel
+
+This example aims to illustrate the basic code generation features of `sfg-walberla`
+by building a force-driven channel flow application.
+
+## Files
+
+{download}`ForceDrivenChannel.zip </zipped-examples/ForceDrivenChannel.zip>`.
+
+This example comprises the following files:
+
+ - `ForceDrivenChannel.cpp`: The main simulation application;
+ - `LbmAlgorithms.py`: The code generation script producing the lattice Boltzmann algorithms;
+ - `CMakeLists.txt`: The CMake build system configuration;
+ - `Channel.prm`: the parameter file describing the channel.
+
+
+## CMake Target Definition
+
+:::{card} `CMakeLists.txt`
+::::{literalinclude} CMakeLists.txt
+:language: CMake
+::::
+:::
+
+The CMake target setup for this example is quite straight-forward.
+We create a new executable called `Ex_PoiseuilleChannel`
+and add to it the single C++ source file `PoiseuilleChannel.cpp`.
+Then, we register our code generator script `LbmAlgorithms.py` via the
+[`pystencilssfg_generate_target_sources`][sfg_add_gen_scripts] CMake function.
+
+
+[sfg_add_gen_scripts]: https://pycodegen.pages.i10git.cs.fau.de/pystencils-sfg/usage/project_integration.html#add-generator-scripts "pystencils-sfg Documentation"
\ No newline at end of file
diff --git a/user_manual/ForceDrivenChannel/LbmAlgorithms.py b/user_manual/ForceDrivenChannel/LbmAlgorithms.py
new file mode 100644
index 0000000000000000000000000000000000000000..fe9b7c1c314090e88db71ee82afe01311e6983c4
--- /dev/null
+++ b/user_manual/ForceDrivenChannel/LbmAlgorithms.py
@@ -0,0 +1,66 @@
+import sympy as sp
+
+from pystencilssfg import SourceFileGenerator
+from sfg_walberla import Sweep
+from sfg_walberla.boundaries import SimpleHbbBoundary
+
+from pystencils import Target, fields
+import pystencils.codegen.config as cfg
+from lbmpy import (
+    LBStencil,
+    Stencil,
+    Method,
+    LBMConfig,
+    LBMOptimisation,
+    create_lb_method,
+    create_lb_update_rule,
+)
+
+from lbmpy.boundaries import NoSlip
+from lbmpy.macroscopic_value_kernels import macroscopic_values_setter
+
+stencil = LBStencil(Stencil.D3Q19)
+dim = stencil.D
+f, f_tmp, rho, u = fields(
+    f"f({stencil.Q}), f_tmp({stencil.Q}), rho(1), u({dim}): [{dim}D]", layout="fzyx"
+)
+omega = sp.Symbol("omega")
+force = sp.symbols(f"F_:{dim}")
+
+lbm_config = LBMConfig(
+    stencil=stencil,
+    method=Method.CENTRAL_MOMENT,
+    relaxation_rate=omega,
+    force=force,
+    compressible=True,
+    zero_centered=False,
+    output={"density": rho, "velocity": u},
+)
+
+lb_method = create_lb_method(lbm_config)
+
+with SourceFileGenerator() as sfg:
+    sfg.namespace("Ex_ForceDrivenChannel::gen")
+
+    sfg.include(f"stencil/{stencil.name}.h")
+    sfg.code(f"using LbStencil = walberla::stencil::{stencil.name};")
+
+    lbm_opt = LBMOptimisation(symbolic_field=f, symbolic_temporary_field=f_tmp)
+
+    gen_config = cfg.CreateKernelConfig(
+        target=Target.CPU, cpu_optim=cfg.CpuOptimConfig(openmp=True)
+    )
+
+    lb_update = create_lb_update_rule(lbm_config=lbm_config, lbm_optimisation=lbm_opt)
+    lb_update_sweep = Sweep("LbStreamCollide", lb_update, gen_config)
+    lb_update_sweep.swap_fields(f, f_tmp)
+    sfg.generate(lb_update_sweep)
+
+    lb_init = macroscopic_values_setter(
+        lb_update.method, density=rho, velocity=u, pdfs=f, set_pre_collision_pdfs=True
+    )
+    lb_init_sweep = Sweep("LbInit", lb_init, gen_config)
+    sfg.generate(lb_init_sweep)
+
+    #   No-Slip Wall
+    sfg.generate(SimpleHbbBoundary(NoSlip(), lb_method, f))
diff --git a/user_manual/FullyPeriodicAde/AdvectionDiffusionSweep.py b/user_manual/FullyPeriodicAde/AdvectionDiffusionSweep.py
new file mode 100644
index 0000000000000000000000000000000000000000..876da3b324faa49e13a18a589161ffe08dfb204a
--- /dev/null
+++ b/user_manual/FullyPeriodicAde/AdvectionDiffusionSweep.py
@@ -0,0 +1,8 @@
+from pystencilssfg import SourceFileGenerator
+
+from sfg_walberla import Sweep
+from sfg_walberla.symbolic import cell
+
+
+with SourceFileGenerator() as sfg:
+    pass
diff --git a/user_manual/FullyPeriodicAde/CMakeLists.txt b/user_manual/FullyPeriodicAde/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/user_manual/GeneratorScriptBasics/BasicCodegen.py b/user_manual/GeneratorScriptBasics/BasicCodegen.py
new file mode 100644
index 0000000000000000000000000000000000000000..86ff253100201ffa23ea4aa9b115dc758201d1d5
--- /dev/null
+++ b/user_manual/GeneratorScriptBasics/BasicCodegen.py
@@ -0,0 +1,6 @@
+from pystencilssfg import SourceFileGenerator
+
+with SourceFileGenerator() as sfg:
+    sfg.include("<cstdint>")
+    sfg.namespace("gen")
+    sfg.code("constexpr uint64_t MAGIC_NUMBER = 0xcafe;")
diff --git a/user_manual/GeneratorScriptBasics/BasicCodegenApp.cpp b/user_manual/GeneratorScriptBasics/BasicCodegenApp.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0cb852b61fda67ad65a4ba18612acf40f23f7882
--- /dev/null
+++ b/user_manual/GeneratorScriptBasics/BasicCodegenApp.cpp
@@ -0,0 +1,6 @@
+#include "gen/Ex_GeneratorScriptBasics/BasicCodegen.hpp"
+#include <iostream>
+
+int main(void) {
+    std::cout << gen::MAGIC_NUMBER << std::endl;
+}
diff --git a/user_manual/GeneratorScriptBasics/CMakeLists.txt b/user_manual/GeneratorScriptBasics/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..5804fe28101f15e23ead12756d362a476ff823c5
--- /dev/null
+++ b/user_manual/GeneratorScriptBasics/CMakeLists.txt
@@ -0,0 +1,6 @@
+add_executable( Ex_GeneratorScriptBasics )
+target_sources( Ex_GeneratorScriptBasics PRIVATE BasicCodegenApp.cpp )
+
+walberla_generate_sources( Ex_GeneratorScriptBasics 
+    SCRIPTS BasicCodegen.py
+)
\ No newline at end of file
diff --git a/user_manual/GeneratorScriptBasics/GeneratorScriptBasics.md b/user_manual/GeneratorScriptBasics/GeneratorScriptBasics.md
new file mode 100644
index 0000000000000000000000000000000000000000..f0577069ae5cf79de01ddeafaf49ca2dd9439178
--- /dev/null
+++ b/user_manual/GeneratorScriptBasics/GeneratorScriptBasics.md
@@ -0,0 +1,96 @@
+# Getting Started with Generator Scripts
+
+This chapter aims to give an introduction on working with *generator scripts*,
+which are the driving force of the next-codegen system in waLBerla.
+
+In the course of this guide, we will
+ - set up the basic structure of a generator script;
+ - register that script with CMake;
+ - and include the generated files into a simple *Hello World*-like application.
+
+This guide assumes that you already have a CMake project and Python environment correctly
+set up, as described in [](EnvSetup).
+
+:::{dropdown} Get The Files
+
+{download}`GeneratorScriptBasics.zip </zipped-examples/GeneratorScriptBasics.zip>`.
+
+This example comprises the following files:
+
+ - `GeneratorScriptBasics.cpp`: The demo simulation;
+ - `BasicCodegen.py`: A sample code generation script;
+ - `CMakeLists.txt`: The CMake build system configuration.
+
+:::
+
+## Our First Generator Script
+
+Create and open the file `BasicCodegen.py` and populate it with the following code:
+
+```{literalinclude} BasicCodegen.py
+:caption: BasicCodegen.py
+:lineno-start: 1
+```
+
+Let's take this apart.
+- In line 1, we import the `SourceFileGenerator`, which is the object responsible for running code generation
+  and interacting with the build system.
+  It is exposed by [pystencils-sfg][pystencils-sfg], a lower-level package that `sfg-walberla` relies on.
+- In line 3, we enter code generation mode by opening up a managed block controlled by the `SourceFileGenerator`.
+  It gives us the `sfg` object, which is our primary interaction point with the code generation engine.
+- In lines 4 to 6, we use the `sfg` object to do three things:
+    - Include a header file; this will cause the respective `#include` directive to be generated.
+    - Set the namespace; this will place all generated code into the `gen` namespace.
+    - Define a constant; this will print the definition verbatim into the output header file.
+
+To see the generator script in effect, we need to create a CMake target and a simple application we can include it into.
+Create both the `BasicCodegenApp.cpp` and `CMakeLists.txt` files, open up `CMakeLists.txt`, and add the following:
+
+:::{literalinclude} CMakeLists.txt
+:language: CMake
+:caption: CMakeLists.txt
+:lineno-start: 1
+:emphasize-lines: 4-6
+:::
+
+At first, we create a new executable target called `Ex_GeneratorScriptBasics`
+and add the `BasicCodegen.cpp` source file to it.
+To us code generators, however, the second part is more interesting:
+we register our generator script `BasicCodegen.py` at the target using
+`walberla_generate_sources`.
+This will cause the build system to execute the generator script during build,
+compile its generated sources,
+and make its generated headers available to us.
+
+To observe this, add the following to your `BasicCodegenApp.cpp`:
+
+```{literalinclude} BasicCodegenApp.cpp
+:caption: BasicCodegenApp.cpp
+:lineno-start: 1
+```
+
+Don't worry if your IDE tells you `BasicCodegen.hpp` does not exist yet;
+the file will be generated in a moment.
+Use CMake to build the application `Ex_GeneratorScriptBasics` and run it,
+and you should see it print `51966` to stdout.
+
+Let's take a look at what is happening during the build.
+Your CMake build output should contain a line somewhat like this:
+
+```
+Generating sfg_sources/gen/Ex_GeneratorScriptBasics/BasicCodegen.hpp, sfg_sources/gen/Ex_GeneratorScriptBasics/BasicCodegen.cpp
+```
+
+This indicates that the generator script was executed and produced exactly two files below the `sfg_sources` directory:
+- a header file `gen/Ex_GeneratorScriptBasics/BasicCodegen.hpp`, which is the one we included into our application, and
+- a source file `gen/Ex_GeneratorScriptBasics/BasicCodegen.cpp`, which at this time is still empty
+  because we haven't defined any kernels or functions yet.
+
+The `walberla_generate_sources` CMake command made sure that the `sfg_sources` directory was placed in your
+target's include path, so you can include the generated files.
+
+That covers the basics of setting up and running a code generator script;
+you're all set now to generate and run your first numerical kernels.
+
+[pystencils-sfg]: https://pycodegen.pages.i10git.cs.fau.de/pystencils-sfg/index.html
+[sfg_add_gen_scripts]: https://pycodegen.pages.i10git.cs.fau.de/pystencils-sfg/usage/project_integration.html#add-generator-scripts "pystencils-sfg Documentation"
diff --git a/user_manual/Makefile b/user_manual/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..d931dfac6dcefe86452b20b310c390c0ea648d86
--- /dev/null
+++ b/user_manual/Makefile
@@ -0,0 +1,39 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS    ?=
+SPHINXBUILD   ?= sphinx-build
+SOURCEDIR     = .
+BUILDDIR      = _sphinx_build
+
+ZIPPED_EXAMPLES := zipped-examples
+
+include examples.mk
+
+MKDIR := mkdir -p
+dir_guard = $(MKDIR) $(@D)
+
+# Put it first so that "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help html clean Makefile ZipExamples
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+html: Makefile ZipExamples
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+clean:
+	@echo "Removing generated downloadable files"
+	@rm -rf $(ZIPPED_EXAMPLES)
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+ZipExamples: $(foreach example, $(EXAMPLES), $(ZIPPED_EXAMPLES)/$(example).zip)
+
+$(ZIPPED_EXAMPLES)/%.zip: %/*
+	@$(dir_guard)
+	@echo Zipping $(<D)
+	@zip -r $@ $(<D)
diff --git a/user_manual/conf.py b/user_manual/conf.py
new file mode 100644
index 0000000000000000000000000000000000000000..cfa54f38b4a54d40be07e51cb183f088bdf36c32
--- /dev/null
+++ b/user_manual/conf.py
@@ -0,0 +1,38 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# For the full list of built-in configuration values, see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Project information -----------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
+
+project = 'waLBerla next-codegen'
+copyright = '2024, Frederik Hennig'
+author = 'Frederik Hennig'
+
+
+# -- General configuration ---------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
+
+extensions = [
+    "myst_parser",
+    "sphinx_design",
+    "sphinx_copybutton",
+]
+
+# templates_path = ['_templates']
+exclude_patterns = ["build"]
+
+myst_enable_extensions = [
+    "colon_fence",
+    "dollarmath",
+    "attrs_inline",
+    "attrs_block",
+]
+
+# -- Options for HTML output -------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
+
+html_theme = 'sphinx_book_theme'
+html_static_path = ['zipped-examples']
+html_title = "waLBerla next-codegen"
diff --git a/user_manual/examples.mk b/user_manual/examples.mk
new file mode 100644
index 0000000000000000000000000000000000000000..2d78f20423a18f80a6bc70282b541fdaee4ac96d
--- /dev/null
+++ b/user_manual/examples.mk
@@ -0,0 +1 @@
+EXAMPLES = GeneratorScriptBasics ForceDrivenChannel
diff --git a/user_manual/index.md b/user_manual/index.md
new file mode 100644
index 0000000000000000000000000000000000000000..473b4f1a05c50df6418adf4cad216475acf8e0dd
--- /dev/null
+++ b/user_manual/index.md
@@ -0,0 +1,45 @@
+# The Next Generation of waLBerla Code Generation
+
+Welcome to *The Next Generation of waLBerla Code Generation*.
+This book is aimed at teaching you how to use the next generation of code generators for waLBerla
+by walking through a set of example applications, each designed to highlight specific features.
+
+The next-gen code generators for waLBerla are based on bleeding-edge developments in
+[pystencils 2.0][pystencils_2_0] and [pystencils-sfg][pystencils-sfg],
+and are currently located in the separate [sfg-walberla][sfg-walberla] repository.
+This project is still unstable and immature, but growing steadily.
+
+Until the next-gen code generators are stabilized and merged with the waLBerla master,
+setting up a build system and development environment for working with them is slightly more
+complicated. Since you will need such an environment to follow along with the examples
+in this book, start by reading the chapter [](EnvSetup).
+Afterward, you are free to explore the remainder of the book.
+
+## Table of Contents
+
+:::{toctree}
+:caption: Fundamentals
+:maxdepth: 1
+
+CMakeSetup/CMakeSetup
+GeneratorScriptBasics/GeneratorScriptBasics
+:::
+
+:::{toctree}
+:caption: Basic LBM Simulations
+:maxdepth: 1
+
+ForceDrivenChannel/ForceDrivenChannel
+:::
+
+:::{toctree}
+:caption: Reference
+:maxdepth: 1
+
+Python Environment <reference/PythonEnvironment>
+:::
+
+
+[pystencils_2_0]: https://da15siwa.pages.i10git.cs.fau.de/dev-docs/pystencils-nbackend/ "pystencils 2.0 Documentation"
+[pystencils-sfg]: https://pycodegen.pages.i10git.cs.fau.de/pystencils-sfg/index.html "pystencils-sfg Documentation"
+[sfg-walberla]: https://i10git.cs.fau.de/da15siwa/sfg-walberla "SFG-waLBerla Repository"
diff --git a/user_manual/reference/PythonEnvironment.md b/user_manual/reference/PythonEnvironment.md
new file mode 100644
index 0000000000000000000000000000000000000000..614cf1fb3610fbb5e1d7e0ac58631f29e4a0a494
--- /dev/null
+++ b/user_manual/reference/PythonEnvironment.md
@@ -0,0 +1,38 @@
+# Managing the Code Generator's Python Environment
+
+On this page, you can find information on managing, customizing, and extending the Python environment
+used by the waLBerla code generation system.
+
+## Using the Default Virtual Environment
+
+By default, `sfg-walberla` creates a new Python virtual environment within the CMake build tree,
+and there installs all packages required for code generation.
+
+### Install Additional Packages
+
+For projects that require external dependencies, *sfg-walberla* exposes the CMake function
+`walberla_codegen_venv_install`, which can be used to install additional packages into the
+code generator virtual environment;
+for instance, the following invocation installs `pyyaml`:
+
+```CMake
+walberla_codegen_venv_install( pyyaml )
+```
+
+The arguments passed to `walberla_codegen_venv_install` are forwarded directly to `pip install`.
+You can therefore use any parameters that `pip` can interpret, for instance `-e` to perform an
+editable install, or `-r <requirements-file>` to install packages from a requirements file.
+
+## Using an External Virtual Environment
+
+To have even more control over your Python environment, you can configure sfg-walberla to
+forego creating a private virtual environment, and instead use the Python interpreter
+supplied from the outside.
+
+To explicitly specify a Python interpreter, you need to set the `WALBERLA_CODEGEN_USE_PRIVATE_VENV` cache
+variable to `FALSE`, and set `Python_EXECUTABLE` to point at your Python binary.
+For instance, at configuration time:
+
+```bash
+cmake -S . -B build -DWALBERLA_CODEGEN_USE_PRIVATE_VENV=FALSE -DPython_EXECUTABLE=<path-to-python>
+```
diff --git a/user_manual/requirements.txt b/user_manual/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..c353de95b9606e7e9e6fc2a65de09b763257fed3
--- /dev/null
+++ b/user_manual/requirements.txt
@@ -0,0 +1,5 @@
+sphinx
+sphinx-design
+myst-parser
+sphinx-copybutton
+sphinx-book-theme