diff --git a/algo/base/Prelude.h b/algo/base/Prelude.h
new file mode 100644
index 0000000000000000000000000000000000000000..e8d614071183796143de962be9db7778c95955ca
--- /dev/null
+++ b/algo/base/Prelude.h
@@ -0,0 +1,41 @@
+/* Copyright (C) 2023 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Felix Weiglhofer [committer] */
+#ifndef CBM_ALGO_BASE_PRELUDE_H
+#define CBM_ALGO_BASE_PRELUDE_H
+
+#include <cstdint>
+#include <gsl/span>
+
+namespace cbm::algo
+{
+
+  // typealias for Rust-like fixed size integer types
+  using i8  = std::int8_t;
+  using u8  = std::uint8_t;
+  using i16 = std::int16_t;
+  using u16 = std::uint16_t;
+  using i32 = std::int32_t;
+  using u32 = std::uint32_t;
+  using i64 = std::int64_t;
+  using u64 = std::uint64_t;
+  using f32 = float;
+  using f64 = double;
+
+#ifdef CBM_ALGO_REAL64
+  using real = f64;
+#else
+  using real = f32;
+#endif
+
+#if !XPU_IS_CUDA && !XPU_IS_HIP  // FIXME why doesn't this work with CUDA?
+  template<typename T>
+  using span = gsl::span<T>;
+
+  template<typename T>
+  using cspan = gsl::span<const T>;
+#endif
+
+}  // namespace cbm::algo
+
+#endif  // CBM_ALGO_BASE_PRELUDE_H
diff --git a/algo/base/RecoParams.h b/algo/base/RecoParams.h
new file mode 100644
index 0000000000000000000000000000000000000000..ed9a7e79f27f0108727865e931f3c6aa49d22d50
--- /dev/null
+++ b/algo/base/RecoParams.h
@@ -0,0 +1,101 @@
+/* Copyright (C) 2023 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Felix Weiglhofer [committer] */
+#ifndef CBM_ALGO_BASE_RECOPARAMS_H
+#define CBM_ALGO_BASE_RECOPARAMS_H
+
+#include <xpu/defines.h>
+
+#include "Prelude.h"
+#include "config/Property.h"
+
+namespace cbm::algo
+{
+
+  /**
+   * @brief RecoParams contains all parameters to configure reconstruction
+   */
+  struct RecoParams {
+    enum SortMode : u8
+    {
+      SortBlockSort        = 0,
+      SortCUBSegmentedSort = 1,
+    };
+    enum UnpackMode : u8
+    {
+      CPU,
+      XPU,
+    };
+
+    struct STS {
+      SortMode digiSortMode;
+      SortMode clusterSortMode;
+
+      UnpackMode unpackMode;
+      u8 findClustersMultiKernels;
+
+      f32 timeCutDigiAbs;
+      f32 timeCutDigiSig;
+      f32 timeCutClusterAbs;
+      f32 timeCutClusterSig;
+
+      struct MemoryLimits {
+        u64 maxDigisPerTS;
+        u64 maxDigisPerMS;
+        u64 maxDigisPerModule;
+        f64 clustersPerDigiTS;
+        f64 clustersPerDigiModule;
+        f64 hitsPerClusterTS;
+        f64 hitsPerClusterModule;
+
+        XPU_D u64 maxClustersPerTS() const { return maxDigisPerTS * clustersPerDigiTS; }
+        XPU_D u64 maxClustersPerModule() const { return maxDigisPerModule * clustersPerDigiModule; }
+        XPU_D u64 maxHitsPerClusterTS() const { return maxClustersPerTS() * hitsPerClusterTS; }
+        XPU_D u64 maxHitsPerClusterModule() const { return maxClustersPerModule() * hitsPerClusterModule; }
+
+        static constexpr auto Properties = std::make_tuple(
+          config::Property(&MemoryLimits::maxDigisPerTS, "maxDigisPerTS", "Maximal number of digis per time slice"),
+          config::Property(&MemoryLimits::maxDigisPerMS, "maxDigisPerMS", "Maximal number of digis per micro slice"),
+          config::Property(&MemoryLimits::maxDigisPerModule, "maxDigisPerModule", "Maximal number of digis per module"),
+          config::Property(&MemoryLimits::clustersPerDigiTS, "clustersPerDigiTS",
+                           "Number of clusters per digi in a time slice"),
+          config::Property(&MemoryLimits::clustersPerDigiModule, "clustersPerDigiModule",
+                           "Number of clusters per digi in a module"),
+          config::Property(&MemoryLimits::hitsPerClusterTS, "hitsPerClusterTS",
+                           "Number of hits per cluster in a time slice"),
+          config::Property(&MemoryLimits::hitsPerClusterModule, "hitsPerClusterModule",
+                           "Number of hits per cluster in a module"));
+      } memoryLimits;
+
+      static constexpr auto Properties = std::make_tuple(
+        config::Property(&STS::digiSortMode, "digiSortMode",
+                         "Digi sort mode (0 = block sort, 1 = cub segmented sort))"),
+        config::Property(&STS::clusterSortMode, "clusterSortMode", "Cluster sort mode"),
+
+        config::Property(&STS::unpackMode, "unpackMode", "Unpack mode (0 = legacy, 1 = XPU)"),
+        config::Property(&STS::findClustersMultiKernels, "findClustersMultiKernels",
+                         "Split cluster finding into multiple kernels"),
+
+        config::Property(&STS::timeCutDigiAbs, "timeCutDigiAbs",
+                         "Time delta for neighboring digis to be considered for the same cluster. [ns]"),
+        config::Property(
+          &STS::timeCutDigiSig, "timeCutDigiSig",
+          "Used if timeCutDigiAbs is negative. Time delta must be < 'value * sqrt2 * timeResolution'. [ns]"),
+        config::Property(&STS::timeCutClusterAbs, "timeCutClusterAbs",
+                         "Maximal time difference between two clusters in a hit [ns]."
+                         " Setting to a positive value will override timeCutClustersSig."),
+        config::Property(
+          &STS::timeCutClusterSig, "timeCutClusterSig",
+          "Time cut for clusters."
+          " Two clusters are considered it their time difference is below 'value * sqrt(terr1**2 + terr2*+2)'"),
+        config::Property(&STS::memoryLimits, "memoryLimits", "Memory limits for STS reco"));
+    };
+
+    STS sts;
+
+    static constexpr auto Properties = std::make_tuple(config::Property(&RecoParams::sts, "sts", "STS reco settings"));
+  };
+
+};  // namespace cbm::algo
+
+#endif  // CBM_ALGO_BASE_RECOPARAMS_H
diff --git a/algo/base/config/BaseTypes.h b/algo/base/config/BaseTypes.h
new file mode 100644
index 0000000000000000000000000000000000000000..b69620abcde029914f5515a190dcc3887c9b4c5c
--- /dev/null
+++ b/algo/base/config/BaseTypes.h
@@ -0,0 +1,88 @@
+/* Copyright (C) 2023 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Felix Weiglhofer [committer] */
+#ifndef CBM_ALGO_BASE_CONFIG_BASETYPES_H
+#define CBM_ALGO_BASE_CONFIG_BASETYPES_H
+
+#include <string>
+#include <tuple>
+#include <vector>
+
+#include "Prelude.h"
+
+namespace cbm::algo::config
+{
+
+  using BaseTypes = std::tuple<bool, u8, i8, u16, i16, u32, i32, u64, i64, f32, f64, std::string>;
+
+  template<typename T, typename Tuple>
+  struct has_type;
+
+  template<typename T, typename... Us>
+  struct has_type<T, std::tuple<Us...>> : std::disjunction<std::is_same<T, Us>...> {
+  };
+
+  template<typename T>
+  constexpr bool IsBaseType = has_type<T, BaseTypes>::value;
+
+  template<typename T>
+  constexpr bool IsEnum = std::is_enum_v<T>;
+
+  template<typename T, typename = std::enable_if_t<IsBaseType<T>>>
+  constexpr std::string_view BaseTypeToStr()
+  {
+    if constexpr (std::is_same_v<bool, T>) { return "bool"; }
+    else if constexpr (std::is_same_v<u8, T>) {
+      return "u8";
+    }
+    else if constexpr (std::is_same_v<i8, T>) {
+      return "i8";
+    }
+    else if constexpr (std::is_same_v<u16, T>) {
+      return "u16";
+    }
+    else if constexpr (std::is_same_v<i16, T>) {
+      return "i16";
+    }
+    else if constexpr (std::is_same_v<u32, T>) {
+      return "u32";
+    }
+    else if constexpr (std::is_same_v<i32, T>) {
+      return "i32";
+    }
+    else if constexpr (std::is_same_v<float, T>) {
+      return "float";
+    }
+    else if constexpr (std::is_same_v<std::string, T>) {
+      return "string";
+    }
+    else {
+      return "unknown";
+    }
+  }
+
+  template<typename>
+  struct is_std_vector : std::false_type {
+  };
+
+  template<typename T, typename A>
+  struct is_std_vector<std::vector<T, A>> : std::true_type {
+  };
+
+  template<typename T>
+  constexpr bool IsVector = is_std_vector<T>::value;
+
+  template<typename>
+  struct is_std_array : std::false_type {
+  };
+
+  template<typename T, std::size_t N>
+  struct is_std_array<std::array<T, N>> : std::true_type {
+  };
+
+  template<typename T>
+  constexpr bool IsArray = is_std_array<T>::value;
+
+}  // namespace cbm::algo::config
+
+#endif  // CBM_ALGO_BASE_CONFIG_BASETYPES_H
diff --git a/algo/base/config/Property.h b/algo/base/config/Property.h
new file mode 100644
index 0000000000000000000000000000000000000000..baddbd9755e22dff031fd8120c43d72f2c49374b
--- /dev/null
+++ b/algo/base/config/Property.h
@@ -0,0 +1,66 @@
+/* Copyright (C) 2023 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Felix Weiglhofer [committer] */
+#ifndef CBM_ALGO_BASE_CONFIG_PROPERTY_H
+#define CBM_ALGO_BASE_CONFIG_PROPERTY_H
+
+#include <optional>
+#include <string_view>
+#include <tuple>
+
+#include <yaml-cpp/emittermanip.h>
+
+#include "Prelude.h"
+
+namespace cbm::algo::config
+{
+
+  template<typename Class, typename T>
+  class Property {
+
+  private:
+    T Class::*fMember;
+    std::string_view fKey;
+    std::string_view fDescription;
+    std::optional<YAML::EMITTER_MANIP> fFormat;
+    std::optional<YAML::EMITTER_MANIP> fFormatEntries;
+
+  public:
+    using ClassType = Class;
+    using ValueType = T;
+
+    Property() = delete;
+
+    constexpr Property(T Class::*member, std::string_view key, std::string_view description = "",
+                       std::optional<YAML::EMITTER_MANIP> fmt = {}, std::optional<YAML::EMITTER_MANIP> fmtEntries = {})
+      : fMember(member)
+      , fKey(key)
+      , fDescription(description)
+      , fFormat(fmt)
+      , fFormatEntries(fmtEntries)
+    {
+    }
+
+    Property(const Property&) = delete;
+    Property& operator=(const Property&) = delete;
+
+    Property(Property&&) = default;
+    Property& operator=(Property&&) = default;
+
+    std::string_view Key() const { return fKey; }
+    std::string_view Description() const { return fDescription; }
+    std::optional<YAML::EMITTER_MANIP> Format() const { return fFormat; }
+    std::optional<YAML::EMITTER_MANIP> FormatEntries() const { return fFormatEntries; }
+
+    T& Get(Class& object) const { return object.*fMember; }
+    const T& Get(const Class& object) const { return object.*fMember; }
+
+    void Set(Class& object, const T& value) const { object.*fMember = value; }
+  };
+
+  template<typename Class, typename T>
+  Property(T Class::*member, std::string_view key, std::string_view description) -> Property<Class, T>;
+
+}  // namespace cbm::algo::config
+
+#endif  // CBM_ALGO_BASE_CONFIG_PROPERTY_H
diff --git a/algo/base/config/Yaml.h b/algo/base/config/Yaml.h
new file mode 100644
index 0000000000000000000000000000000000000000..0109c5b2344f0732a97c3b6d8a441545fabe5c0e
--- /dev/null
+++ b/algo/base/config/Yaml.h
@@ -0,0 +1,149 @@
+/* Copyright (C) 2023 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Felix Weiglhofer [committer] */
+#ifndef CBM_ALGO_BASE_CONFIG_DESERIALIZE_H
+#define CBM_ALGO_BASE_CONFIG_DESERIALIZE_H
+
+#include <fairlogger/Logger.h>
+
+#include <sstream>
+#include <string_view>
+
+#include <yaml-cpp/yaml.h>
+
+#include "BaseTypes.h"
+#include "Prelude.h"
+#include "Property.h"
+
+namespace CORE_INTERFACE_NS::config
+{
+
+  template<typename T, T... Values, typename Func>
+  constexpr void ForEach(std::integer_sequence<T, Values...>, Func&& func)
+  {
+    (func(std::integral_constant<T, Values> {}), ...);
+  }
+
+  template<typename T>
+  T Read(const YAML::Node& node)
+  {
+    using Type = std::remove_cv_t<std::remove_reference_t<T>>;
+
+    // TODO: error handling
+    if constexpr (IsBaseType<Type>) { return node.as<Type>(); }
+    else if constexpr (IsEnum<Type>) {
+      return static_cast<Type>(node.as<std::underlying_type_t<Type>>());
+    }
+    else if constexpr (IsVector<Type>) {
+      Type vector;
+      for (const auto& element : node) {
+        vector.push_back(Read<typename Type::value_type>(element));
+      }
+      return vector;
+    }
+    else if constexpr (IsArray<Type>) {
+      Type array {};
+      auto vector = Read<std::vector<typename Type::value_type>>(node);
+      if (vector.size() != array.size()) {
+        throw std::runtime_error(fmt::format("Array size mismatch: expected {}, got {}", array.size(), vector.size()));
+      }
+      std::copy(vector.begin(), vector.end(), array.begin());
+      return array;
+    }
+    else {
+      Type object;
+      constexpr auto nProperties = std::tuple_size<decltype(Type::Properties)>::value;
+      ForEach(std::make_integer_sequence<std::size_t, nProperties> {}, [&](auto index) {
+        auto& property   = std::get<index>(Type::Properties);
+        using ValueType  = std::remove_cv_t<std::remove_reference_t<decltype(property.Get(object))>>;
+        ValueType& value = property.Get(object);
+        value            = Read<ValueType>(node[std::string {property.Key()}]);
+      });
+      return object;
+    }
+  }
+
+  template<typename T>
+  std::string MakeDocString(int indent = 0)
+  {
+    using Type = std::remove_cv_t<std::remove_reference_t<T>>;
+
+    std::stringstream docString;
+
+    if constexpr (IsBaseType<Type>) { docString << BaseTypeToStr<Type>(); }
+    else if constexpr (IsVector<Type> || IsArray<Type>) {
+      using ChildType = typename Type::value_type;
+      if constexpr (IsBaseType<ChildType>) {
+        docString << std::string(indent, ' ') << "list of " << BaseTypeToStr<ChildType>() << std::endl;
+      }
+      else {
+        docString << std::string(indent, ' ') << "list of" << std::endl;
+        docString << MakeDocString<ChildType>(indent + 2);
+      }
+    }
+    else {
+      constexpr auto nProperties = std::tuple_size<decltype(Type::Properties)>::value;
+      ForEach(std::make_integer_sequence<std::size_t, nProperties> {}, [&](auto index) {
+        using ChildType = std::remove_cv_t<
+          std::remove_reference_t<decltype(std::get<index>(Type::Properties).Get(std::declval<Type>()))>>;
+        auto& property = std::get<index>(Type::Properties);
+        if constexpr (IsBaseType<ChildType>) {
+          docString << std::string(indent, ' ') << property.Key() << ": " << property.Description() << " ["
+                    << BaseTypeToStr<ChildType>() << "]" << std::endl;
+        }
+        else {
+          docString << std::string(indent, ' ') << property.Key() << ": " << property.Description() << std::endl;
+          docString << MakeDocString<ChildType>(indent + 2);
+        }
+      });
+    }
+
+    return docString.str();
+  }
+
+  class Dump {
+
+  public:
+    template<typename T>
+    std::string operator()(const T& object)
+    {
+      YAML::Emitter ss;
+      ss << YAML::BeginDoc;
+      DoDump(object, ss);
+      ss << YAML::EndDoc;
+      return ss.c_str();
+    }
+
+  private:
+    template<typename T>
+    void DoDump(const T& object, YAML::Emitter& ss, std::optional<YAML::EMITTER_MANIP> formatEntries = {})
+    {
+      if constexpr (IsBaseType<T>) { ss << object; }
+      else if constexpr (IsVector<T> || IsArray<T>) {
+        ss << YAML::BeginSeq;
+        for (const auto& element : object) {
+          if (formatEntries.has_value()) { ss << formatEntries.value(); }
+          DoDump(element, ss);
+        }
+        ss << YAML::EndSeq;
+      }
+      else {
+        constexpr auto nProperties = std::tuple_size<decltype(T::Properties)>::value;
+        ss << YAML::BeginMap;
+        ForEach(std::make_integer_sequence<std::size_t, nProperties> {}, [&](auto index) {
+          auto& property = std::get<index>(T::Properties);
+          auto& value    = property.Get(object);
+          auto format    = property.Format();
+          ss << YAML::Key << std::string {property.Key()};
+          if (format.has_value()) { ss << format.value(); }
+          ss << YAML::Value;
+          DoDump(value, ss, property.FormatEntries());
+        });
+        ss << YAML::EndMap;
+      }
+    }
+  };
+
+}  // namespace CORE_INTERFACE_NS::config
+
+#endif
diff --git a/algo/params/RecoParams.yaml b/algo/params/RecoParams.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b69e5db5ea563ddfef1646a202181e773d9aecbd
--- /dev/null
+++ b/algo/params/RecoParams.yaml
@@ -0,0 +1,24 @@
+---
+sts:
+  # 0: block_sort, 1: cub
+  digiSortMode: 0
+  clusterSortMode: 0
+  # 0: cpu, 1: xpu
+  unpackMode: 0
+  findClustersMultiKernels: 1
+  timeCutDigiAbs: -1
+  timeCutDigiSig: 3
+  timeCutClusterAbs: -1
+  timeCutClusterSig: 4
+
+  memoryLimits:
+    maxDigisPerTS: 80000000
+    # TODO: Find sensible value!
+    maxDigisPerMS:   100000
+    # TODO: Find sensible value
+    maxDigisPerModule: 4000000
+    clustersPerDigiTS: 1.0
+    clustersPerDigiModule: 1.0
+    hitsPerClusterTS: 1.0
+    hitsPerClusterModule: 1.0
+...