about summary refs log tree commit diff
path: root/nixos/modules/services
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services')
-rw-r--r--nixos/modules/services/backup/syncoid.nix2
-rw-r--r--nixos/modules/services/databases/memcached.nix2
-rw-r--r--nixos/modules/services/databases/postgresql.nix43
-rw-r--r--nixos/modules/services/desktops/pipewire/pipewire.nix6
-rw-r--r--nixos/modules/services/desktops/pipewire/wireplumber.nix8
-rw-r--r--nixos/modules/services/development/nixseparatedebuginfod.nix4
-rw-r--r--nixos/modules/services/games/armagetronad.nix268
-rw-r--r--nixos/modules/services/hardware/nvidia-container-toolkit-cdi-generator/cdi-generate.nix57
-rw-r--r--nixos/modules/services/hardware/nvidia-container-toolkit-cdi-generator/default.nix8
-rw-r--r--nixos/modules/services/home-automation/ebusd.nix2
-rw-r--r--nixos/modules/services/home-automation/matter-server.nix125
-rw-r--r--nixos/modules/services/mail/listmonk.nix6
-rw-r--r--nixos/modules/services/misc/docker-registry.nix10
-rw-r--r--nixos/modules/services/misc/etebase-server.nix28
-rw-r--r--nixos/modules/services/misc/homepage-dashboard.nix224
-rw-r--r--nixos/modules/services/misc/paperless.nix1
-rw-r--r--nixos/modules/services/misc/tabby.nix203
-rw-r--r--nixos/modules/services/misc/transfer-sh.nix102
-rw-r--r--nixos/modules/services/monitoring/mackerel-agent.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/restic.nix13
-rw-r--r--nixos/modules/services/monitoring/scrutiny.nix149
-rw-r--r--nixos/modules/services/networking/bird-lg.nix4
-rw-r--r--nixos/modules/services/networking/searx.nix2
-rw-r--r--nixos/modules/services/networking/tailscale.nix9
-rw-r--r--nixos/modules/services/networking/unbound.nix25
-rw-r--r--nixos/modules/services/security/kanidm.nix32
-rw-r--r--nixos/modules/services/security/vaultwarden/default.nix10
-rw-r--r--nixos/modules/services/web-apps/miniflux.nix28
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix2
-rw-r--r--nixos/modules/services/web-apps/photoprism.nix1
-rw-r--r--nixos/modules/services/web-apps/vikunja.nix47
-rw-r--r--nixos/modules/services/x11/desktop-managers/plasma6.nix15
32 files changed, 1199 insertions, 239 deletions
diff --git a/nixos/modules/services/backup/syncoid.nix b/nixos/modules/services/backup/syncoid.nix
index 7b8d3b431309..4a04f0aa1622 100644
--- a/nixos/modules/services/backup/syncoid.nix
+++ b/nixos/modules/services/backup/syncoid.nix
@@ -134,7 +134,7 @@ in
     localSourceAllow = mkOption {
       type = types.listOf types.str;
       # Permissions snapshot and destroy are in case --no-sync-snap is not used
-      default = [ "bookmark" "hold" "send" "snapshot" "destroy" ];
+      default = [ "bookmark" "hold" "send" "snapshot" "destroy" "mount" ];
       description = lib.mdDoc ''
         Permissions granted for the {option}`services.syncoid.user` user
         for local source datasets. See
diff --git a/nixos/modules/services/databases/memcached.nix b/nixos/modules/services/databases/memcached.nix
index 542c80ab2e67..fd943c20091a 100644
--- a/nixos/modules/services/databases/memcached.nix
+++ b/nixos/modules/services/databases/memcached.nix
@@ -37,7 +37,7 @@ in
         description = lib.mdDoc "The port to bind to.";
       };
 
-      enableUnixSocket = mkEnableOption (lib.mdDoc "unix socket at /run/memcached/memcached.sock");
+      enableUnixSocket = mkEnableOption (lib.mdDoc "Unix Domain Socket at /run/memcached/memcached.sock instead of listening on an IP address and port. The `listen` and `port` options are ignored.");
 
       maxMemory = mkOption {
         type = types.ints.unsigned;
diff --git a/nixos/modules/services/databases/postgresql.nix b/nixos/modules/services/databases/postgresql.nix
index ed5915735730..c4e76c82ba5c 100644
--- a/nixos/modules/services/databases/postgresql.nix
+++ b/nixos/modules/services/databases/postgresql.nix
@@ -161,33 +161,6 @@ in
               '';
             };
 
-            ensurePermissions = mkOption {
-              type = types.attrsOf types.str;
-              default = {};
-              visible = false; # This option has been deprecated.
-              description = lib.mdDoc ''
-                This option is DEPRECATED and should not be used in nixpkgs anymore,
-                use `ensureDBOwnership` instead. It can also break with newer
-                versions of PostgreSQL (≥ 15).
-
-                Permissions to ensure for the user, specified as an attribute set.
-                The attribute names specify the database and tables to grant the permissions for.
-                The attribute values specify the permissions to grant. You may specify one or
-                multiple comma-separated SQL privileges here.
-
-                For more information on how to specify the target
-                and on which privileges exist, see the
-                [GRANT syntax](https://www.postgresql.org/docs/current/sql-grant.html).
-                The attributes are used as `GRANT ''${attrValue} ON ''${attrName}`.
-              '';
-              example = literalExpression ''
-                {
-                  "DATABASE \"nextcloud\"" = "ALL PRIVILEGES";
-                  "ALL TABLES IN SCHEMA public" = "ALL PRIVILEGES";
-                }
-              '';
-            };
-
             ensureDBOwnership = mkOption {
               type = types.bool;
               default = false;
@@ -460,16 +433,6 @@ in
         Offender: ${name} has not been found among databases.
       '';
     }) cfg.ensureUsers;
-    # `ensurePermissions` is now deprecated, let's avoid it.
-    warnings = lib.optional (any ({ ensurePermissions, ... }: ensurePermissions != {}) cfg.ensureUsers) "
-      `services.postgresql.ensureUsers.*.ensurePermissions` is used in your expressions,
-      this option is known to be broken with newer PostgreSQL versions,
-      consider migrating to `services.postgresql.ensureUsers.*.ensureDBOwnership` or
-      consult the release notes or manual for more migration guidelines.
-
-      This option will be removed in NixOS 24.05 unless it sees significant
-      maintenance improvements.
-    ";
 
     services.postgresql.settings =
       {
@@ -583,11 +546,6 @@ in
               concatMapStrings
               (user:
               let
-                  userPermissions = concatStringsSep "\n"
-                    (mapAttrsToList
-                      (database: permission: ''$PSQL -tAc 'GRANT ${permission} ON ${database} TO "${user.name}"' '')
-                      user.ensurePermissions
-                    );
                   dbOwnershipStmt = optionalString
                     user.ensureDBOwnership
                     ''$PSQL -tAc 'ALTER DATABASE "${user.name}" OWNER TO "${user.name}";' '';
@@ -599,7 +557,6 @@ in
                   userClauses = ''$PSQL -tAc 'ALTER ROLE "${user.name}" ${concatStringsSep " " clauseSqlStatements}' '';
                 in ''
                   $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='${user.name}'" | grep -q 1 || $PSQL -tAc 'CREATE USER "${user.name}"'
-                  ${userPermissions}
                   ${userClauses}
 
                   ${dbOwnershipStmt}
diff --git a/nixos/modules/services/desktops/pipewire/pipewire.nix b/nixos/modules/services/desktops/pipewire/pipewire.nix
index 8f3ad78d50ce..09448833620c 100644
--- a/nixos/modules/services/desktops/pipewire/pipewire.nix
+++ b/nixos/modules/services/desktops/pipewire/pipewire.nix
@@ -246,6 +246,9 @@ in {
         description = lib.mdDoc ''
           List of packages that provide PipeWire configuration, in the form of
           `share/pipewire/*/*.conf` files.
+
+          LV2 dependencies will be picked up from config packages automatically
+          via `passthru.requiredLv2Packages`.
         '';
       };
 
@@ -258,7 +261,8 @@ in {
           be made available to PipeWire for [filter chains][wiki-filter-chain].
 
           Config packages have their required LV2 plugins added automatically,
-          so they don't need to be specified here.
+          so they don't need to be specified here. Config packages need to set
+          `passthru.requiredLv2Packages` for this to work.
 
           [wiki-filter-chain]: https://docs.pipewire.org/page_module_filter_chain.html
         '';
diff --git a/nixos/modules/services/desktops/pipewire/wireplumber.nix b/nixos/modules/services/desktops/pipewire/wireplumber.nix
index 99aea8facb16..009d68bd4f28 100644
--- a/nixos/modules/services/desktops/pipewire/wireplumber.nix
+++ b/nixos/modules/services/desktops/pipewire/wireplumber.nix
@@ -30,6 +30,9 @@ in
         description = lib.mdDoc ''
           List of packages that provide WirePlumber configuration, in the form of
           `share/wireplumber/*/*.lua` files.
+
+          LV2 dependencies will be picked up from config packages automatically
+          via `passthru.requiredLv2Packages`.
         '';
       };
 
@@ -42,7 +45,8 @@ in
           be made available to WirePlumber for [filter chains][wiki-filter-chain].
 
           Config packages have their required LV2 plugins added automatically,
-          so they don't need to be specified here.
+          so they don't need to be specified here. Config packages need to set
+          `passthru.requiredLv2Packages` for this to work.
 
           [wiki-filter-chain]: https://docs.pipewire.org/page_module_filter_chain.html
         '';
@@ -108,7 +112,7 @@ in
                   )
                   config.environment.etc
               )) == 1;
-          message = "Using `environment.etc.\"wireplumber<...>\"` directly is no longer supported in 24.05. Use `services.wireplumber.configPackages` instead.";
+          message = "Using `environment.etc.\"wireplumber<...>\"` directly is no longer supported in 24.05. Use `services.pipewire.wireplumber.configPackages` instead.";
         }
       ];
 
diff --git a/nixos/modules/services/development/nixseparatedebuginfod.nix b/nixos/modules/services/development/nixseparatedebuginfod.nix
index daf85153d339..a2ec0d2c80e1 100644
--- a/nixos/modules/services/development/nixseparatedebuginfod.nix
+++ b/nixos/modules/services/development/nixseparatedebuginfod.nix
@@ -90,7 +90,9 @@ in
 
     users.groups.nixseparatedebuginfod = { };
 
-    nix.settings.extra-allowed-users = [ "nixseparatedebuginfod" ];
+    nix.settings = lib.optionalAttrs (lib.versionAtLeast config.nix.package.version "2.4") {
+      extra-allowed-users = [ "nixseparatedebuginfod" ];
+    };
 
     environment.variables.DEBUGINFOD_URLS = "http://${url}";
 
diff --git a/nixos/modules/services/games/armagetronad.nix b/nixos/modules/services/games/armagetronad.nix
new file mode 100644
index 000000000000..f79818e0e53b
--- /dev/null
+++ b/nixos/modules/services/games/armagetronad.nix
@@ -0,0 +1,268 @@
+{ config, lib, pkgs, ... }:
+let
+  inherit (lib) mkEnableOption mkIf mkOption mkMerge literalExpression;
+  inherit (lib) mapAttrsToList filterAttrs unique recursiveUpdate types;
+
+  mkValueStringArmagetron = with lib; v:
+    if isInt v then toString v
+    else if isFloat v then toString v
+    else if isString v then v
+    else if true == v then "1"
+    else if false == v then "0"
+    else if null == v then ""
+    else throw "unsupported type: ${builtins.typeOf v}: ${(lib.generators.toPretty {} v)}";
+
+  settingsFormat = pkgs.formats.keyValue {
+    mkKeyValue = lib.generators.mkKeyValueDefault
+      {
+        mkValueString = mkValueStringArmagetron;
+      } " ";
+    listsAsDuplicateKeys = true;
+  };
+
+  cfg = config.services.armagetronad;
+  enabledServers = lib.filterAttrs (n: v: v.enable) cfg.servers;
+  nameToId = serverName: "armagetronad-${serverName}";
+  getStateDirectory = serverName: "armagetronad/${serverName}";
+  getServerRoot = serverName: "/var/lib/${getStateDirectory serverName}";
+in
+{
+  options = {
+    services.armagetronad = {
+      servers = mkOption {
+        description = lib.mdDoc "Armagetron server definitions.";
+        default = { };
+        type = types.attrsOf (types.submodule {
+          options = {
+            enable = mkEnableOption (lib.mdDoc "armagetronad");
+
+            package = lib.mkPackageOptionMD pkgs "armagetronad-dedicated" {
+              example = ''
+                pkgs.armagetronad."0.2.9-sty+ct+ap".dedicated
+              '';
+              extraDescription = ''
+                Ensure that you use a derivation which contains the path `bin/armagetronad-dedicated`.
+              '';
+            };
+
+            host = mkOption {
+              type = types.str;
+              default = "0.0.0.0";
+              description = lib.mdDoc "Host to listen on. Used for SERVER_IP.";
+            };
+
+            port = mkOption {
+              type = types.port;
+              default = 4534;
+              description = lib.mdDoc "Port to listen on. Used for SERVER_PORT.";
+            };
+
+            dns = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = lib.mdDoc "DNS address to use for this server. Optional.";
+            };
+
+            openFirewall = mkOption {
+              type = types.bool;
+              default = true;
+              description = lib.mdDoc "Set to true to open the configured UDP port for Armagetron Advanced.";
+            };
+
+            name = mkOption {
+              type = types.str;
+              description = "The name of this server.";
+            };
+
+            settings = mkOption {
+              type = settingsFormat.type;
+              default = { };
+              description = lib.mdDoc ''
+                Armagetron Advanced server rules configuration. Refer to:
+                <https://wiki.armagetronad.org/index.php?title=Console_Commands>
+                or `armagetronad-dedicated --doc` for a list.
+
+                This attrset is used to populate `settings_custom.cfg`; see:
+                <https://wiki.armagetronad.org/index.php/Configuration_Files>
+              '';
+              example = literalExpression ''
+                {
+                  CYCLE_RUBBER = 40;
+                }
+              '';
+            };
+
+            roundSettings = mkOption {
+              type = settingsFormat.type;
+              default = { };
+              description = lib.mdDoc ''
+                Armagetron Advanced server per-round configuration. Refer to:
+                <https://wiki.armagetronad.org/index.php?title=Console_Commands>
+                or `armagetronad-dedicated --doc` for a list.
+
+                This attrset is used to populate `everytime.cfg`; see:
+                <https://wiki.armagetronad.org/index.php/Configuration_Files>
+              '';
+              example = literalExpression ''
+                {
+                  SAY = [
+                    "Hosted on NixOS"
+                    "https://nixos.org"
+                    "iD Tech High Rubber rul3z!! Happy New Year 2008!!1"
+                  ];
+                }
+              '';
+            };
+          };
+        });
+      };
+    };
+  };
+
+  config = mkIf (enabledServers != { }) {
+    systemd.tmpfiles.settings = mkMerge (mapAttrsToList
+      (serverName: serverCfg:
+        let
+          serverId = nameToId serverName;
+          serverRoot = getServerRoot serverName;
+          serverInfo = (
+            {
+              SERVER_IP = serverCfg.host;
+              SERVER_PORT = serverCfg.port;
+              SERVER_NAME = serverCfg.name;
+            } // (lib.optionalAttrs (serverCfg.dns != null) { SERVER_DNS = serverCfg.dns; })
+          );
+          customSettings = serverCfg.settings;
+          everytimeSettings = serverCfg.roundSettings;
+
+          serverInfoCfg = settingsFormat.generate "server_info.${serverName}.cfg" serverInfo;
+          customSettingsCfg = settingsFormat.generate "settings_custom.${serverName}.cfg" customSettings;
+          everytimeSettingsCfg = settingsFormat.generate "everytime.${serverName}.cfg" everytimeSettings;
+        in
+        {
+          "10-armagetronad-${serverId}" = {
+            "${serverRoot}/data" = {
+              d = {
+                group = serverId;
+                user = serverId;
+                mode = "0750";
+              };
+            };
+            "${serverRoot}/settings" = {
+              d = {
+                group = serverId;
+                user = serverId;
+                mode = "0750";
+              };
+            };
+            "${serverRoot}/var" = {
+              d = {
+                group = serverId;
+                user = serverId;
+                mode = "0750";
+              };
+            };
+            "${serverRoot}/resource" = {
+              d = {
+                group = serverId;
+                user = serverId;
+                mode = "0750";
+              };
+            };
+            "${serverRoot}/input" = {
+              "f+" = {
+                group = serverId;
+                user = serverId;
+                mode = "0640";
+              };
+            };
+            "${serverRoot}/settings/server_info.cfg" = {
+              "L+" = {
+                argument = "${serverInfoCfg}";
+              };
+            };
+            "${serverRoot}/settings/settings_custom.cfg" = {
+              "L+" = {
+                argument = "${customSettingsCfg}";
+              };
+            };
+            "${serverRoot}/settings/everytime.cfg" = {
+              "L+" = {
+                argument = "${everytimeSettingsCfg}";
+              };
+            };
+          };
+        }
+      )
+      enabledServers
+    );
+
+    systemd.services = mkMerge (mapAttrsToList
+      (serverName: serverCfg:
+        let
+          serverId = nameToId serverName;
+        in
+        {
+          "armagetronad-${serverName}" = {
+            description = "Armagetron Advanced Dedicated Server for ${serverName}";
+            wants = [ "basic.target" ];
+            after = [ "basic.target" "network.target" "multi-user.target" ];
+            wantedBy = [ "multi-user.target" ];
+            serviceConfig =
+              let
+                serverRoot = getServerRoot serverName;
+              in
+              {
+                Type = "simple";
+                StateDirectory = getStateDirectory serverName;
+                ExecStart = "${lib.getExe serverCfg.package} --daemon --input ${serverRoot}/input --userdatadir ${serverRoot}/data --userconfigdir ${serverRoot}/settings --vardir ${serverRoot}/var --autoresourcedir ${serverRoot}/resource";
+                Restart = "on-failure";
+                CapabilityBoundingSet = "";
+                LockPersonality = true;
+                NoNewPrivileges = true;
+                PrivateDevices = true;
+                PrivateTmp = true;
+                PrivateUsers = true;
+                ProtectClock = true;
+                ProtectControlGroups = true;
+                ProtectHome = true;
+                ProtectHostname = true;
+                ProtectKernelLogs = true;
+                ProtectKernelModules = true;
+                ProtectKernelTunables = true;
+                ProtectProc = "invisible";
+                ProtectSystem = "strict";
+                RestrictNamespaces = true;
+                RestrictSUIDSGID = true;
+                User = serverId;
+                Group = serverId;
+              };
+          };
+        })
+      enabledServers
+    );
+
+    networking.firewall.allowedUDPPorts =
+      unique (mapAttrsToList (serverName: serverCfg: serverCfg.port) (filterAttrs (serverName: serverCfg: serverCfg.openFirewall) enabledServers));
+
+    users.users = mkMerge (mapAttrsToList
+      (serverName: serverCfg:
+        {
+          ${nameToId serverName} = {
+            group = nameToId serverName;
+            description = "Armagetron Advanced dedicated user for server ${serverName}";
+            isSystemUser = true;
+          };
+        })
+      enabledServers
+    );
+
+    users.groups = mkMerge (mapAttrsToList
+      (serverName: serverCfg:
+        {
+          ${nameToId serverName} = { };
+        })
+      enabledServers
+    );
+  };
+}
diff --git a/nixos/modules/services/hardware/nvidia-container-toolkit-cdi-generator/cdi-generate.nix b/nixos/modules/services/hardware/nvidia-container-toolkit-cdi-generator/cdi-generate.nix
index a90d234f65c0..1aaa2d07b9bd 100644
--- a/nixos/modules/services/hardware/nvidia-container-toolkit-cdi-generator/cdi-generate.nix
+++ b/nixos/modules/services/hardware/nvidia-container-toolkit-cdi-generator/cdi-generate.nix
@@ -1,37 +1,58 @@
-{ config, lib, pkgs }: let
+{
+  addDriverRunpath,
+  glibc,
+  jq,
+  lib,
+  nvidia-container-toolkit,
+  nvidia-driver,
+  runtimeShell,
+  writeScriptBin,
+}:
+let
   mountOptions = { options = ["ro" "nosuid" "nodev" "bind"]; };
   mounts = [
-    { hostPath = "${lib.getBin config.hardware.nvidia.package}/bin/nvidia-cuda-mps-control";
+    # FIXME: Making /usr mounts optional
+    { hostPath = lib.getExe' nvidia-driver "nvidia-cuda-mps-control";
       containerPath = "/usr/bin/nvidia-cuda-mps-control"; }
-    { hostPath = "${lib.getBin config.hardware.nvidia.package}/bin/nvidia-cuda-mps-server";
+    { hostPath = lib.getExe' nvidia-driver "nvidia-cuda-mps-server";
       containerPath = "/usr/bin/nvidia-cuda-mps-server"; }
-    { hostPath = "${lib.getBin config.hardware.nvidia.package}/bin/nvidia-debugdump";
+    { hostPath = lib.getExe' nvidia-driver "nvidia-debugdump";
       containerPath = "/usr/bin/nvidia-debugdump"; }
-    { hostPath = "${lib.getBin config.hardware.nvidia.package}/bin/nvidia-powerd";
+    { hostPath = lib.getExe' nvidia-driver "nvidia-powerd";
       containerPath = "/usr/bin/nvidia-powerd"; }
-    { hostPath = "${lib.getBin config.hardware.nvidia.package}/bin/nvidia-smi";
+    { hostPath = lib.getExe' nvidia-driver "nvidia-smi";
       containerPath = "/usr/bin/nvidia-smi"; }
-    { hostPath = "${pkgs.nvidia-container-toolkit}/bin/nvidia-ctk";
+    { hostPath = lib.getExe' nvidia-container-toolkit "nvidia-ctk";
       containerPath = "/usr/bin/nvidia-ctk"; }
-    { hostPath = "${pkgs.glibc}/lib";
-      containerPath = "${pkgs.glibc}/lib"; }
-    { hostPath = "${pkgs.glibc}/lib64";
-      containerPath = "${pkgs.glibc}/lib64"; }
+    { hostPath = "${lib.getLib glibc}/lib";
+      containerPath = "${lib.getLib glibc}/lib"; }
+
+    # FIXME: use closureinfo
+    {
+      hostPath = addDriverRunpath.driverLink;
+      containerPath = addDriverRunpath.driverLink;
+    }
+    { hostPath = "${lib.getLib glibc}/lib";
+      containerPath = "${lib.getLib glibc}/lib"; }
+    { hostPath = "${lib.getLib glibc}/lib64";
+      containerPath = "${lib.getLib glibc}/lib64"; }
   ];
   jqAddMountExpression = ".containerEdits.mounts[.containerEdits.mounts | length] |= . +";
   mountsToJq = lib.concatMap
     (mount:
-      ["${pkgs.jq}/bin/jq '${jqAddMountExpression} ${builtins.toJSON (mount // mountOptions)}'"])
+      ["${lib.getExe jq} '${jqAddMountExpression} ${builtins.toJSON (mount // mountOptions)}'"])
     mounts;
-in ''
-#! ${pkgs.runtimeShell}
+in
+writeScriptBin "nvidia-cdi-generator"
+''
+#! ${runtimeShell}
 
 function cdiGenerate {
-  ${pkgs.nvidia-container-toolkit}/bin/nvidia-ctk cdi generate \
+  ${lib.getExe' nvidia-container-toolkit "nvidia-ctk"} cdi generate \
     --format json \
-    --ldconfig-path ${pkgs.glibc.bin}/bin/ldconfig \
-    --library-search-path ${config.hardware.nvidia.package}/lib \
-    --nvidia-ctk-path ${pkgs.nvidia-container-toolkit}/bin/nvidia-ctk
+    --ldconfig-path ${lib.getExe' glibc "ldconfig"} \
+    --library-search-path ${lib.getLib nvidia-driver}/lib \
+    --nvidia-ctk-path ${lib.getExe' nvidia-container-toolkit "nvidia-ctk"}
 }
 
 cdiGenerate | \
diff --git a/nixos/modules/services/hardware/nvidia-container-toolkit-cdi-generator/default.nix b/nixos/modules/services/hardware/nvidia-container-toolkit-cdi-generator/default.nix
index 3c96e9c41be5..b95bdf191fad 100644
--- a/nixos/modules/services/hardware/nvidia-container-toolkit-cdi-generator/default.nix
+++ b/nixos/modules/services/hardware/nvidia-container-toolkit-cdi-generator/default.nix
@@ -26,9 +26,11 @@
       serviceConfig = {
         RuntimeDirectory = "cdi";
         RemainAfterExit = true;
-        ExecStart = let
-          script = (pkgs.writeScriptBin "nvidia-cdi-generator"
-            (import ./cdi-generate.nix { inherit config lib pkgs; })); in (lib.getExe script);
+        ExecStart =
+          let
+            script = pkgs.callPackage ./cdi-generate.nix { nvidia-driver = config.hardware.nvidia.package; };
+          in
+          lib.getExe script;
         Type = "oneshot";
       };
     };
diff --git a/nixos/modules/services/home-automation/ebusd.nix b/nixos/modules/services/home-automation/ebusd.nix
index 519d116e0e55..f68a8bdb6bfa 100644
--- a/nixos/modules/services/home-automation/ebusd.nix
+++ b/nixos/modules/services/home-automation/ebusd.nix
@@ -15,12 +15,12 @@ let
     "--port=${toString cfg.port}"
     "--configpath=${cfg.configpath}"
     "--scanconfig=${cfg.scanconfig}"
+    "--log=all:${cfg.logs.all}"
     "--log=main:${cfg.logs.main}"
     "--log=network:${cfg.logs.network}"
     "--log=bus:${cfg.logs.bus}"
     "--log=update:${cfg.logs.update}"
     "--log=other:${cfg.logs.other}"
-    "--log=all:${cfg.logs.all}"
   ] ++ lib.optionals cfg.readonly [
     "--readonly"
   ] ++ lib.optionals cfg.mqtt.enable [
diff --git a/nixos/modules/services/home-automation/matter-server.nix b/nixos/modules/services/home-automation/matter-server.nix
new file mode 100644
index 000000000000..864ef9e20083
--- /dev/null
+++ b/nixos/modules/services/home-automation/matter-server.nix
@@ -0,0 +1,125 @@
+{ lib
+, pkgs
+, config
+, ...
+}:
+
+with lib;
+
+let
+  cfg = config.services.matter-server;
+  storageDir = "matter-server";
+  storagePath = "/var/lib/${storageDir}";
+  vendorId = "4939"; # home-assistant vendor ID
+in
+
+{
+  meta.maintainers = with lib.maintainers; [ leonm1 ];
+
+  options.services.matter-server = with types; {
+    enable = mkEnableOption (lib.mdDoc "Matter-server");
+
+    package = mkPackageOptionMD pkgs "python-matter-server" { };
+
+    port = mkOption {
+      type = types.port;
+      default = 5580;
+      description = "Port to expose the matter-server service on.";
+    };
+
+    logLevel = mkOption {
+      type = types.enum [ "critical" "error" "warning" "info" "debug" ];
+      default = "info";
+      description = "Verbosity of logs from the matter-server";
+    };
+
+    extraArgs = mkOption {
+      type = listOf str;
+      default = [];
+      description = ''
+        Extra arguments to pass to the matter-server executable.
+        See https://github.com/home-assistant-libs/python-matter-server?tab=readme-ov-file#running-the-development-server for options.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.matter-server = {
+      after = [ "network-online.target" ];
+      before = [ "home-assistant.service" ];
+      wants = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      description = "Matter Server";
+      environment.HOME = storagePath;
+      serviceConfig = {
+        ExecStart = (concatStringsSep " " [
+          "${cfg.package}/bin/matter-server"
+          "--port" (toString cfg.port)
+          "--vendorid" vendorId
+          "--storage-path" storagePath
+          "--log-level" "${cfg.logLevel}"
+          "${escapeShellArgs cfg.extraArgs}"
+        ]);
+        # Start with a clean root filesystem, and allowlist what the container
+        # is permitted to access.
+        TemporaryFileSystem = "/";
+        # Allowlist /nix/store (to allow the binary to find its dependencies)
+        # and dbus.
+        ReadOnlyPaths = "/nix/store /run/dbus";
+        # Let systemd manage `/var/lib/matter-server` for us inside the
+        # ephemeral TemporaryFileSystem.
+        StateDirectory = storageDir;
+        # `python-matter-server` writes to /data even when a storage-path is
+        # specified. This bind-mount points /data at the systemd-managed
+        # /var/lib/matter-server, so all files get dropped into the state
+        # directory.
+        BindPaths = "${storagePath}:/data";
+
+        # Hardening bits
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+        DevicePolicy = "closed";
+        DynamicUser = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+          "AF_NETLINK"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallFilter = concatStringsSep " " [
+          "~" # Blocklist
+          "@clock"
+          "@cpu-emulation"
+          "@debug"
+          "@module"
+          "@mount"
+          "@obsolete"
+          "@privileged"
+          "@raw-io"
+          "@reboot"
+          "@resources"
+          "@swap"
+        ];
+        UMask = "0077";
+      };
+    };
+  };
+}
+
diff --git a/nixos/modules/services/mail/listmonk.nix b/nixos/modules/services/mail/listmonk.nix
index 945eb436c1f2..d6399304cc10 100644
--- a/nixos/modules/services/mail/listmonk.nix
+++ b/nixos/modules/services/mail/listmonk.nix
@@ -187,7 +187,11 @@ in {
           # Indeed, it will try to create all the folders and realize one of them already exist.
           # Therefore, we have to create it ourselves.
           ''${pkgs.coreutils}/bin/mkdir -p "''${STATE_DIRECTORY}/listmonk/uploads"''
-          "${cfg.package}/bin/listmonk --config ${cfgFile} --idempotent --install --upgrade --yes"
+          # setup database if not already done
+          "${cfg.package}/bin/listmonk --config ${cfgFile} --idempotent --install --yes"
+          # apply db migrations (setup and migrations can not be done in one step
+          # with "--install --upgrade" listmonk ignores the upgrade)
+          "${cfg.package}/bin/listmonk --config ${cfgFile} --upgrade --yes"
           "${updateDatabaseConfigScript}/bin/update-database-config.sh"
         ];
         ExecStart = "${cfg.package}/bin/listmonk --config ${cfgFile}";
diff --git a/nixos/modules/services/misc/docker-registry.nix b/nixos/modules/services/misc/docker-registry.nix
index e8fbc05423d3..78d1d6339ed6 100644
--- a/nixos/modules/services/misc/docker-registry.nix
+++ b/nixos/modules/services/misc/docker-registry.nix
@@ -63,6 +63,12 @@ in {
       type = types.port;
     };
 
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc "Opens the port used by the firewall.";
+    };
+
     storagePath = mkOption {
       type = types.nullOr types.path;
       default = "/var/lib/docker-registry";
@@ -154,5 +160,9 @@ in {
         isSystemUser = true;
       };
     users.groups.docker-registry = {};
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
   };
 }
diff --git a/nixos/modules/services/misc/etebase-server.nix b/nixos/modules/services/misc/etebase-server.nix
index 045048a1a2e3..f5a5e8a780d4 100644
--- a/nixos/modules/services/misc/etebase-server.nix
+++ b/nixos/modules/services/misc/etebase-server.nix
@@ -5,9 +5,6 @@ with lib;
 let
   cfg = config.services.etebase-server;
 
-  pythonEnv = pkgs.python3.withPackages (ps: with ps;
-    [ etebase-server daphne ]);
-
   iniFmt = pkgs.formats.ini {};
 
   configIni = iniFmt.generate "etebase-server.ini" cfg.settings;
@@ -46,6 +43,13 @@ in
         '';
       };
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.python3.pkgs.etebase-server;
+        defaultText = literalExpression "pkgs.python3.pkgs.etebase-server";
+        description = lib.mdDoc "etebase-server package to use.";
+      };
+
       dataDir = mkOption {
         type = types.str;
         default = "/var/lib/etebase-server";
@@ -164,7 +168,7 @@ in
       (runCommand "etebase-server" {
         nativeBuildInputs = [ makeWrapper ];
       } ''
-        makeWrapper ${pythonEnv}/bin/etebase-server \
+        makeWrapper ${cfg.package}/bin/etebase-server \
           $out/bin/etebase-server \
           --chdir ${escapeShellArg cfg.dataDir} \
           --prefix ETEBASE_EASY_CONFIG_PATH : "${configIni}"
@@ -178,8 +182,8 @@ in
     systemd.services.etebase-server = {
       description = "An Etebase (EteSync 2.0) server";
       after = [ "network.target" "systemd-tmpfiles-setup.service" ];
+      path = [ cfg.package ];
       wantedBy = [ "multi-user.target" ];
-      path = [ pythonEnv ];
       serviceConfig = {
         User = cfg.user;
         Restart = "always";
@@ -187,24 +191,26 @@ in
       };
       environment = {
         ETEBASE_EASY_CONFIG_PATH = configIni;
+        PYTHONPATH = cfg.package.pythonPath;
       };
       preStart = ''
         # Auto-migrate on first run or if the package has changed
         versionFile="${cfg.dataDir}/src-version"
-        if [[ $(cat "$versionFile" 2>/dev/null) != ${pkgs.etebase-server} ]]; then
+        if [[ $(cat "$versionFile" 2>/dev/null) != ${cfg.package} ]]; then
           etebase-server migrate --no-input
           etebase-server collectstatic --no-input --clear
-          echo ${pkgs.etebase-server} > "$versionFile"
+          echo ${cfg.package} > "$versionFile"
         fi
       '';
       script =
         let
+          python = cfg.package.python;
           networking = if cfg.unixSocket != null
-          then "-u ${cfg.unixSocket}"
-          else "-b 0.0.0.0 -p ${toString cfg.port}";
+          then "--uds ${cfg.unixSocket}"
+          else "--host 0.0.0.0 --port ${toString cfg.port}";
         in ''
-          cd "${pythonEnv}/lib/etebase-server";
-          daphne ${networking} \
+          ${python.pkgs.uvicorn}/bin/uvicorn ${networking} \
+            --app-dir ${cfg.package}/${cfg.package.python.sitePackages} \
             etebase_server.asgi:application
         '';
     };
diff --git a/nixos/modules/services/misc/homepage-dashboard.nix b/nixos/modules/services/misc/homepage-dashboard.nix
index 07a09e2b6bbf..02f1378cb0d5 100644
--- a/nixos/modules/services/misc/homepage-dashboard.nix
+++ b/nixos/modules/services/misc/homepage-dashboard.nix
@@ -6,6 +6,8 @@
 
 let
   cfg = config.services.homepage-dashboard;
+  # Define the settings format used for this program
+  settingsFormat = pkgs.formats.yaml { };
 in
 {
   options = {
@@ -25,31 +27,217 @@ in
         default = 8082;
         description = lib.mdDoc "Port for Homepage to bind to.";
       };
+
+      environmentFile = lib.mkOption {
+        type = lib.types.str;
+        description = ''
+          The path to an environment file that contains environment variables to pass
+          to the homepage-dashboard service, for the purpose of passing secrets to
+          the service.
+
+          See the upstream documentation:
+
+          https://gethomepage.dev/latest/installation/docker/#using-environment-secrets
+        '';
+        default = "";
+      };
+
+      customCSS = lib.mkOption {
+        type = lib.types.lines;
+        description = lib.mdDoc ''
+          Custom CSS for styling Homepage.
+
+          See https://gethomepage.dev/latest/configs/custom-css-js/.
+        '';
+        default = "";
+      };
+
+      customJS = lib.mkOption {
+        type = lib.types.lines;
+        description = lib.mdDoc ''
+          Custom Javascript for Homepage.
+
+          See https://gethomepage.dev/latest/configs/custom-css-js/.
+        '';
+        default = "";
+      };
+
+      bookmarks = lib.mkOption {
+        inherit (settingsFormat) type;
+        description = lib.mdDoc ''
+          Homepage bookmarks configuration.
+
+          See https://gethomepage.dev/latest/configs/bookmarks/.
+        '';
+        # Defaults: https://github.com/gethomepage/homepage/blob/main/src/skeleton/bookmarks.yaml
+        example = [
+          {
+            Developer = [
+              { Github = [{ abbr = "GH"; href = "https://github.com/"; }]; }
+            ];
+          }
+          {
+            Entertainment = [
+              { YouTube = [{ abbr = "YT"; href = "https://youtube.com/"; }]; }
+            ];
+          }
+        ];
+        default = [ ];
+      };
+
+      services = lib.mkOption {
+        inherit (settingsFormat) type;
+        description = lib.mdDoc ''
+          Homepage services configuration.
+
+          See https://gethomepage.dev/latest/configs/services/.
+        '';
+        # Defaults: https://github.com/gethomepage/homepage/blob/main/src/skeleton/services.yaml
+        example = [
+          {
+            "My First Group" = [
+              {
+                "My First Service" = {
+                  href = "http://localhost/";
+                  description = "Homepage is awesome";
+                };
+              }
+            ];
+          }
+          {
+            "My Second Group" = [
+              {
+                "My Second Service" = {
+                  href = "http://localhost/";
+                  description = "Homepage is the best";
+                };
+              }
+            ];
+          }
+        ];
+        default = [ ];
+      };
+
+      widgets = lib.mkOption {
+        inherit (settingsFormat) type;
+        description = lib.mdDoc ''
+          Homepage widgets configuration.
+
+          See https://gethomepage.dev/latest/configs/service-widgets/.
+        '';
+        # Defaults: https://github.com/gethomepage/homepage/blob/main/src/skeleton/widgets.yaml
+        example = [
+          {
+            resources = {
+              cpu = true;
+              memory = true;
+              disk = "/";
+            };
+          }
+          {
+            search = {
+              provider = "duckduckgo";
+              target = "_blank";
+            };
+          }
+        ];
+        default = [ ];
+      };
+
+      kubernetes = lib.mkOption {
+        inherit (settingsFormat) type;
+        description = lib.mdDoc ''
+          Homepage kubernetes configuration.
+
+          See https://gethomepage.dev/latest/configs/kubernetes/.
+        '';
+        default = { };
+      };
+
+      docker = lib.mkOption {
+        inherit (settingsFormat) type;
+        description = lib.mdDoc ''
+          Homepage docker configuration.
+
+          See https://gethomepage.dev/latest/configs/docker/.
+        '';
+        default = { };
+      };
+
+      settings = lib.mkOption {
+        inherit (settingsFormat) type;
+        description = lib.mdDoc ''
+          Homepage settings.
+
+          See https://gethomepage.dev/latest/configs/settings/.
+        '';
+        # Defaults: https://github.com/gethomepage/homepage/blob/main/src/skeleton/settings.yaml
+        default = { };
+      };
     };
   };
 
-  config = lib.mkIf cfg.enable {
-    systemd.services.homepage-dashboard = {
-      description = "Homepage Dashboard";
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
+  config =
+    let
+      # If homepage-dashboard is enabled, but none of the configuration values have been updated,
+      # then default to "unmanaged" configuration which is manually updated in
+      # var/lib/homepage-dashboard. This is to maintain backwards compatibility, and should be
+      # deprecated in a future release.
+      managedConfig = !(
+        cfg.bookmarks == [ ] &&
+        cfg.customCSS == "" &&
+        cfg.customJS == "" &&
+        cfg.docker == { } &&
+        cfg.kubernetes == { } &&
+        cfg.services == [ ] &&
+        cfg.settings == { } &&
+        cfg.widgets == [ ]
+      );
+
+      configDir = if managedConfig then "/etc/homepage-dashboard" else "/var/lib/homepage-dashboard";
+
+      msg = "using unmanaged configuration for homepage-dashboard is deprecated and will be removed"
+        + " in 24.05. please see the NixOS documentation for `services.homepage-dashboard' and add"
+        + " your bookmarks, services, widgets, and other configuration using the options provided.";
+    in
+    lib.mkIf cfg.enable {
+      warnings = lib.optional (!managedConfig) msg;
 
-      environment = {
-        HOMEPAGE_CONFIG_DIR = "/var/lib/homepage-dashboard";
-        PORT = "${toString cfg.listenPort}";
+      environment.etc = lib.mkIf managedConfig {
+        "homepage-dashboard/custom.css".text = cfg.customCSS;
+        "homepage-dashboard/custom.js".text = cfg.customJS;
+
+        "homepage-dashboard/bookmarks.yaml".source = settingsFormat.generate "bookmarks.yaml" cfg.bookmarks;
+        "homepage-dashboard/docker.yaml".source = settingsFormat.generate "docker.yaml" cfg.docker;
+        "homepage-dashboard/kubernetes.yaml".source = settingsFormat.generate "kubernetes.yaml" cfg.kubernetes;
+        "homepage-dashboard/services.yaml".source = settingsFormat.generate "services.yaml" cfg.services;
+        "homepage-dashboard/settings.yaml".source = settingsFormat.generate "settings.yaml" cfg.settings;
+        "homepage-dashboard/widgets.yaml".source = settingsFormat.generate "widgets.yaml" cfg.widgets;
       };
 
-      serviceConfig = {
-        Type = "simple";
-        DynamicUser = true;
-        StateDirectory = "homepage-dashboard";
-        ExecStart = "${lib.getExe cfg.package}";
-        Restart = "on-failure";
+      systemd.services.homepage-dashboard = {
+        description = "Homepage Dashboard";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        environment = {
+          HOMEPAGE_CONFIG_DIR = configDir;
+          PORT = toString cfg.listenPort;
+          LOG_TARGETS = lib.mkIf managedConfig "stdout";
+        };
+
+        serviceConfig = {
+          Type = "simple";
+          DynamicUser = true;
+          EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
+          StateDirectory = lib.mkIf (!managedConfig) "homepage-dashboard";
+          ExecStart = lib.getExe cfg.package;
+          Restart = "on-failure";
+        };
       };
-    };
 
-    networking.firewall = lib.mkIf cfg.openFirewall {
-      allowedTCPPorts = [ cfg.listenPort ];
+      networking.firewall = lib.mkIf cfg.openFirewall {
+        allowedTCPPorts = [ cfg.listenPort ];
+      };
     };
-  };
 }
diff --git a/nixos/modules/services/misc/paperless.nix b/nixos/modules/services/misc/paperless.nix
index ab042e4b6ee2..9314c4f3848d 100644
--- a/nixos/modules/services/misc/paperless.nix
+++ b/nixos/modules/services/misc/paperless.nix
@@ -342,6 +342,7 @@ in
         User = cfg.user;
         Restart = "on-failure";
 
+        LimitNOFILE = 65536;
         # gunicorn needs setuid, liblapack needs mbind
         SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "@setuid mbind" ];
         # Needs to serve web page
diff --git a/nixos/modules/services/misc/tabby.nix b/nixos/modules/services/misc/tabby.nix
new file mode 100644
index 000000000000..a3072e5df75e
--- /dev/null
+++ b/nixos/modules/services/misc/tabby.nix
@@ -0,0 +1,203 @@
+{ config, lib, pkgs, ... }:
+let
+  inherit (lib) types;
+
+  cfg = config.services.tabby;
+  format = pkgs.formats.toml { };
+  tabbyPackage = cfg.package.override {
+    inherit (cfg) acceleration;
+  };
+in
+{
+  options = {
+    services.tabby = {
+      enable = lib.mkEnableOption (
+        lib.mdDoc "Self-hosted AI coding assistant using large language models"
+      );
+
+      package = lib.mkPackageOption pkgs "tabby" { };
+
+      port = lib.mkOption {
+        type = types.port;
+        default = 11029;
+        description = lib.mdDoc ''
+          Specifies the bind port on which the tabby server HTTP interface listens.
+        '';
+      };
+
+      model = lib.mkOption {
+        type = types.str;
+        default = "TabbyML/StarCoder-1B";
+        description = lib.mdDoc ''
+          Specify the model that tabby will use to generate completions.
+
+          This model will be downloaded automatically if it is not already present.
+
+          If you want to utilize an existing model that you've already
+          downloaded you'll need to move it into tabby's state directory which
+          lives in `/var/lib/tabby`. Because the tabby.service is configured to
+          use a DyanmicUser the service will need to have been started at least
+          once before you can move the locally existing model into
+          `/var/lib/tabby`. You can set the model to 'none' and tabby will
+          startup and fail to download a model, but will have created the
+          `/var/lib/tabby` directory. You can then copy over the model manually
+          into `/var/lib/tabby`, update the model option to the name you just
+          downloaded and copied over then `nixos-rebuild switch` to start using
+          it.
+
+          $ tabby download --model TabbyML/DeepseekCoder-6.7B
+          $ find ~/.tabby/ | tail -n1
+          /home/ghthor/.tabby/models/TabbyML/DeepseekCoder-6.7B/ggml/q8_0.v2.gguf
+          $ sudo rsync -r ~/.tabby/models/ /var/lib/tabby/models/
+          $ sudo chown -R tabby:tabby /var/lib/tabby/models/
+
+          See for Model Options:
+          > https://github.com/TabbyML/registry-tabby
+        '';
+      };
+
+      acceleration = lib.mkOption {
+        type = types.nullOr (types.enum [ "cpu" "rocm" "cuda" "metal" ]);
+        default = null;
+        example = "rocm";
+        description = lib.mdDoc ''
+          Specifies the device to use for hardware acceleration.
+
+          -   `cpu`: no acceleration just use the CPU
+          -  `rocm`: supported by modern AMD GPUs
+          -  `cuda`: supported by modern NVIDIA GPUs
+          - `metal`: supported on darwin aarch64 machines
+
+          Tabby will try and determine what type of acceleration that is
+          already enabled in your configuration when `acceleration = null`.
+
+          - nixpkgs.config.cudaSupport
+          - nixpkgs.config.rocmSupport
+          - if stdenv.isDarwin && stdenv.isAarch64
+
+          IFF multiple acceleration methods are found to be enabled or if you
+          haven't set either `cudaSupport or rocmSupport` you will have to
+          specify the device type manually here otherwise it will default to
+          the first from the list above or to cpu.
+        '';
+      };
+
+      settings = lib.mkOption {
+        inherit (format) type;
+        default = { };
+        description = lib.mdDoc ''
+          Tabby scheduler configuration
+
+          See for more details:
+          > https://tabby.tabbyml.com/docs/configuration/#repository-context-for-code-completion
+        '';
+        example = lib.literalExpression ''
+          settings = {
+            repositories = [
+              { name = "tabby"; git_url = "https://github.com/TabbyML/tabby.git"; }
+              { name = "CTranslate2"; git_url = "git@github.com:OpenNMT/CTranslate2.git"; }
+
+              # local directory is also supported, but limited by systemd DynamicUser=1
+              # adding local repositories will need to be done manually
+              { name = "repository_a"; git_url = "file:///var/lib/tabby/repository_a"; }
+            ];
+          };
+        '';
+      };
+
+      usageCollection = lib.mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Enable sending anonymous usage data.
+
+          See for more details:
+          > https://tabby.tabbyml.com/docs/configuration#usage-collection
+        '';
+      };
+
+      indexInterval = lib.mkOption {
+        type = types.str;
+        default = "5hours";
+        example = "5hours";
+        description = lib.mdDoc ''
+          Run tabby scheduler to generate the index database at this interval.
+          Updates by default every 5 hours. This value applies to
+          `OnUnitInactiveSec`
+
+          The format is described in
+          {manpage}`systemd.time(7)`.
+
+          To disable running `tabby scheduler --now` updates, set to `"never"`
+        '';
+      };
+    };
+  };
+
+  # TODO(ghthor): firewall config
+
+  config = lib.mkIf cfg.enable {
+    environment = {
+      etc."tabby/config.toml".source = format.generate "config.toml" cfg.settings;
+      systemPackages = [ tabbyPackage ];
+    };
+
+
+    systemd = let
+      serviceUser = {
+        WorkingDirectory = "/var/lib/tabby";
+        StateDirectory = [ "tabby" ];
+        ConfigurationDirectory = [ "tabby" ];
+        DynamicUser = true;
+        User = "tabby";
+        Group = "tabby";
+      };
+
+      serviceEnv = lib.mkMerge [
+        {
+          TABBY_ROOT = "%S/tabby";
+        }
+        (lib.mkIf (!cfg.usageCollection) {
+          TABBY_DISABLE_USAGE_COLLECTION = "1";
+        })
+      ];
+    in {
+      services.tabby = {
+        wantedBy = [ "multi-user.target" ];
+        description = "Self-hosted AI coding assistant using large language models";
+        after = [ "network.target" ];
+        environment = serviceEnv;
+        serviceConfig = lib.mkMerge [
+          serviceUser
+          {
+            ExecStart =
+              "${lib.getExe tabbyPackage} serve --model ${cfg.model} --port ${toString cfg.port} --device ${tabbyPackage.featureDevice}";
+          }
+        ];
+      };
+
+      services.tabby-scheduler = lib.mkIf (cfg.indexInterval != "never") {
+        wantedBy = [ "multi-user.target" ];
+        description = "Tabby repository indexing service";
+        after = [ "network.target" ];
+        environment = serviceEnv;
+        preStart = "cp -f /etc/tabby/config.toml \${TABBY_ROOT}/config.toml";
+        serviceConfig = lib.mkMerge [
+          serviceUser
+          {
+            # Type = "oneshot";
+            ExecStart = "${lib.getExe tabbyPackage} scheduler --now";
+          }
+        ];
+      };
+      timers.tabby-scheduler = lib.mkIf (cfg.indexInterval != "never") {
+        description = "Update timer for tabby-scheduler";
+        partOf = [ "tabby-scheduler.service" ];
+        wantedBy = [ "timers.target" ];
+        timerConfig.OnUnitInactiveSec = cfg.indexInterval;
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ ghthor ];
+}
diff --git a/nixos/modules/services/misc/transfer-sh.nix b/nixos/modules/services/misc/transfer-sh.nix
new file mode 100644
index 000000000000..899d9dfc3c10
--- /dev/null
+++ b/nixos/modules/services/misc/transfer-sh.nix
@@ -0,0 +1,102 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.transfer-sh;
+  inherit (lib)
+    mkDefault mkEnableOption mkPackageOption mkIf mkOption
+    types mapAttrs isBool getExe boolToString mdDoc optionalAttrs;
+in
+{
+  options.services.transfer-sh = {
+    enable = mkEnableOption (mdDoc "Easy and fast file sharing from the command-line");
+
+    package = mkPackageOption pkgs "transfer-sh" { };
+
+    settings = mkOption {
+      type = types.submodule { freeformType = with types; attrsOf (oneOf [ bool int str ]); };
+      default = { };
+      example = {
+        LISTENER = ":8080";
+        BASEDIR = "/var/lib/transfer.sh";
+        TLS_LISTENER_ONLY = false;
+      };
+      description = mdDoc ''
+        Additional configuration for transfer-sh, see
+        <https://github.com/dutchcoders/transfer.sh#usage-1>
+        for supported values.
+
+        For secrets use secretFile option instead.
+      '';
+    };
+
+    provider = mkOption {
+      type = types.enum [ "local" "s3" "storj" "gdrive" ];
+      default = "local";
+      description = mdDoc "Storage providers to use";
+    };
+
+    secretFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/run/secrets/transfer-sh.env";
+      description = mdDoc ''
+        Path to file containing environment variables.
+        Useful for passing down secrets.
+        Some variables that can be considered secrets are:
+         - AWS_ACCESS_KEY
+         - AWS_ACCESS_KEY
+         - TLS_PRIVATE_KEY
+         - HTTP_AUTH_HTPASSWD
+      '';
+    };
+  };
+
+  config =
+    let
+      localProvider = (cfg.provider == "local");
+      stateDirectory = "/var/lib/transfer.sh";
+    in
+    mkIf cfg.enable
+      {
+        services.transfer-sh.settings = {
+          LISTENER = mkDefault ":8080";
+        } // optionalAttrs localProvider {
+          BASEDIR = mkDefault stateDirectory;
+        };
+
+        systemd.services.transfer-sh = {
+          after = [ "network.target" ];
+          wantedBy = [ "multi-user.target" ];
+          environment = mapAttrs (_: v: if isBool v then boolToString v else toString v) cfg.settings;
+          serviceConfig = {
+            CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+            DevicePolicy = "closed";
+            DynamicUser = true;
+            ExecStart = "${getExe cfg.package} --provider ${cfg.provider}";
+            LockPersonality = true;
+            MemoryDenyWriteExecute = true;
+            PrivateDevices = true;
+            PrivateUsers = true;
+            ProtectClock = true;
+            ProtectControlGroups = true;
+            ProtectHostname = true;
+            ProtectKernelLogs = true;
+            ProtectKernelModules = true;
+            ProtectKernelTunables = true;
+            ProtectProc = "invisible";
+            RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+            RestrictNamespaces = true;
+            RestrictRealtime = true;
+            SystemCallArchitectures = [ "native" ];
+            SystemCallFilter = [ "@system-service" ];
+            StateDirectory = baseNameOf stateDirectory;
+          } // optionalAttrs (cfg.secretFile != null) {
+            EnvironmentFile = cfg.secretFile;
+          } // optionalAttrs localProvider {
+            ReadWritePaths = cfg.settings.BASEDIR;
+          };
+        };
+      };
+
+  meta.maintainers = with lib.maintainers; [ ocfox ];
+}
diff --git a/nixos/modules/services/monitoring/mackerel-agent.nix b/nixos/modules/services/monitoring/mackerel-agent.nix
index 5915634ed26f..d1e84c0359dc 100644
--- a/nixos/modules/services/monitoring/mackerel-agent.nix
+++ b/nixos/modules/services/monitoring/mackerel-agent.nix
@@ -81,7 +81,7 @@ in {
       include = mkDefault "/etc/mackerel-agent/conf.d/*.conf";
     };
 
-    # upstream service file in https://git.io/JUt4Q
+    # upstream service file in https://github.com/mackerelio/mackerel-agent/blob/master/packaging/rpm/src/mackerel-agent.service
     systemd.services.mackerel-agent = {
       description = "mackerel.io agent";
       wants = [ "network-online.target" ];
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/restic.nix b/nixos/modules/services/monitoring/prometheus/exporters/restic.nix
index 5b32c93a666d..977bd42e9812 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/restic.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/restic.nix
@@ -93,12 +93,14 @@ in
   };
 
   serviceOpts = {
+    script = ''
+      export RESTIC_PASSWORD_FILE=$CREDENTIALS_DIRECTORY/RESTIC_PASSWORD_FILE
+      ${pkgs.prometheus-restic-exporter}/bin/restic-exporter.py \
+        ${concatStringsSep " \\\n  " cfg.extraFlags}
+    '';
     serviceConfig = {
-      ExecStart = ''
-        ${pkgs.prometheus-restic-exporter}/bin/restic-exporter.py \
-          ${concatStringsSep " \\\n  " cfg.extraFlags}
-      '';
       EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
+      LoadCredential = [ "RESTIC_PASSWORD_FILE:${cfg.passwordFile}" ];
     };
     environment =
       let
@@ -108,8 +110,7 @@ in
         toRcloneVal = v: if lib.isBool v then lib.boolToString v else v;
       in
       {
-        RESTIC_REPO_URL = cfg.repository;
-        RESTIC_REPO_PASSWORD_FILE = cfg.passwordFile;
+        RESTIC_REPOSITORY = cfg.repository;
         LISTEN_ADDRESS = cfg.listenAddress;
         LISTEN_PORT = toString cfg.port;
         REFRESH_INTERVAL = toString cfg.refreshInterval;
diff --git a/nixos/modules/services/monitoring/scrutiny.nix b/nixos/modules/services/monitoring/scrutiny.nix
index 454668a9a128..aef924ef840c 100644
--- a/nixos/modules/services/monitoring/scrutiny.nix
+++ b/nixos/modules/services/monitoring/scrutiny.nix
@@ -1,5 +1,11 @@
 { config, lib, pkgs, ... }:
 let
+  inherit (lib) maintainers;
+  inherit (lib.meta) getExe;
+  inherit (lib.modules) mkIf;
+  inherit (lib.options) literalExpression mkEnableOption mkOption mkPackageOption;
+  inherit (lib.types) bool enum nullOr port str submodule;
+
   cfg = config.services.scrutiny;
   # Define the settings format used for this program
   settingsFormat = pkgs.formats.yaml { };
@@ -7,20 +13,16 @@ in
 {
   options = {
     services.scrutiny = {
-      enable = lib.mkEnableOption "Enables the scrutiny web application.";
+      enable = mkEnableOption "Scrutiny, a web application for drive monitoring";
 
-      package = lib.mkPackageOptionMD pkgs "scrutiny" { };
+      package = mkPackageOption pkgs "scrutiny" { };
 
-      openFirewall = lib.mkOption {
-        type = lib.types.bool;
-        default = false;
-        description = "Open the default ports in the firewall for Scrutiny.";
-      };
+      openFirewall = mkEnableOption "opening the default ports in the firewall for Scrutiny";
 
-      influxdb.enable = lib.mkOption {
-        type = lib.types.bool;
+      influxdb.enable = mkOption {
+        type = bool;
         default = true;
-        description = lib.mdDoc ''
+        description = ''
           Enables InfluxDB on the host system using the `services.influxdb2` NixOS module
           with default options.
 
@@ -29,127 +31,124 @@ in
         '';
       };
 
-      settings = lib.mkOption {
-        description = lib.mdDoc ''
+      settings = mkOption {
+        description = ''
           Scrutiny settings to be rendered into the configuration file.
 
           See https://github.com/AnalogJ/scrutiny/blob/master/example.scrutiny.yaml.
         '';
         default = { };
-        type = lib.types.submodule {
+        type = submodule {
           freeformType = settingsFormat.type;
 
-          options.web.listen.port = lib.mkOption {
-            type = lib.types.port;
+          options.web.listen.port = mkOption {
+            type = port;
             default = 8080;
-            description = lib.mdDoc "Port for web application to listen on.";
+            description = "Port for web application to listen on.";
           };
 
-          options.web.listen.host = lib.mkOption {
-            type = lib.types.str;
+          options.web.listen.host = mkOption {
+            type = str;
             default = "0.0.0.0";
-            description = lib.mdDoc "Interface address for web application to bind to.";
+            description = "Interface address for web application to bind to.";
           };
 
-          options.web.listen.basepath = lib.mkOption {
-            type = lib.types.str;
+          options.web.listen.basepath = mkOption {
+            type = str;
             default = "";
             example = "/scrutiny";
-            description = lib.mdDoc ''
+            description = ''
               If Scrutiny will be behind a path prefixed reverse proxy, you can override this
               value to serve Scrutiny on a subpath.
             '';
           };
 
-          options.log.level = lib.mkOption {
-            type = lib.types.enum [ "INFO" "DEBUG" ];
+          options.log.level = mkOption {
+            type = enum [ "INFO" "DEBUG" ];
             default = "INFO";
-            description = lib.mdDoc "Log level for Scrutiny.";
+            description = "Log level for Scrutiny.";
           };
 
-          options.web.influxdb.scheme = lib.mkOption {
-            type = lib.types.str;
+          options.web.influxdb.scheme = mkOption {
+            type = str;
             default = "http";
-            description = lib.mdDoc "URL scheme to use when connecting to InfluxDB.";
+            description = "URL scheme to use when connecting to InfluxDB.";
           };
 
-          options.web.influxdb.host = lib.mkOption {
-            type = lib.types.str;
+          options.web.influxdb.host = mkOption {
+            type = str;
             default = "0.0.0.0";
-            description = lib.mdDoc "IP or hostname of the InfluxDB instance.";
+            description = "IP or hostname of the InfluxDB instance.";
           };
 
-          options.web.influxdb.port = lib.mkOption {
-            type = lib.types.port;
+          options.web.influxdb.port = mkOption {
+            type = port;
             default = 8086;
-            description = lib.mdDoc "The port of the InfluxDB instance.";
+            description = "The port of the InfluxDB instance.";
           };
 
-          options.web.influxdb.tls.insecure_skip_verify = lib.mkOption {
-            type = lib.types.bool;
-            default = false;
-            description = lib.mdDoc "Skip TLS verification when connecting to InfluxDB.";
-          };
+          options.web.influxdb.tls.insecure_skip_verify = mkEnableOption "skipping TLS verification when connecting to InfluxDB";
 
-          options.web.influxdb.token = lib.mkOption {
-            type = lib.types.nullOr lib.types.str;
+          options.web.influxdb.token = mkOption {
+            type = nullOr str;
             default = null;
-            description = lib.mdDoc "Authentication token for connecting to InfluxDB.";
+            description = "Authentication token for connecting to InfluxDB.";
           };
 
-          options.web.influxdb.org = lib.mkOption {
-            type = lib.types.nullOr lib.types.str;
+          options.web.influxdb.org = mkOption {
+            type = nullOr str;
             default = null;
-            description = lib.mdDoc "InfluxDB organisation under which to store data.";
+            description = "InfluxDB organisation under which to store data.";
           };
 
-          options.web.influxdb.bucket = lib.mkOption {
-            type = lib.types.nullOr lib.types.str;
+          options.web.influxdb.bucket = mkOption {
+            type = nullOr str;
             default = null;
-            description = lib.mdDoc "InfluxDB bucket in which to store data.";
+            description = "InfluxDB bucket in which to store data.";
           };
         };
       };
 
       collector = {
-        enable = lib.mkEnableOption "Enables the scrutiny metrics collector.";
+        enable = mkEnableOption "the Scrutiny metrics collector";
 
-        package = lib.mkPackageOptionMD pkgs "scrutiny-collector" { };
+        package = mkPackageOption pkgs "scrutiny-collector" { };
 
-        schedule = lib.mkOption {
-          type = lib.types.str;
+        schedule = mkOption {
+          type = str;
           default = "*:0/15";
-          description = lib.mdDoc ''
+          description = ''
             How often to run the collector in systemd calendar format.
           '';
         };
 
-        settings = lib.mkOption {
-          description = lib.mdDoc ''
+        settings = mkOption {
+          description = ''
             Collector settings to be rendered into the collector configuration file.
 
             See https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml.
           '';
           default = { };
-          type = lib.types.submodule {
+          type = submodule {
             freeformType = settingsFormat.type;
 
-            options.host.id = lib.mkOption {
-              type = lib.types.nullOr lib.types.str;
+            options.host.id = mkOption {
+              type = nullOr str;
               default = null;
-              description = lib.mdDoc "Host ID for identifying/labelling groups of disks";
+              description = "Host ID for identifying/labelling groups of disks";
             };
 
-            options.api.endpoint = lib.mkOption {
-              type = lib.types.str;
-              default = "http://localhost:8080";
-              description = lib.mdDoc "Scrutiny app API endpoint for sending metrics to.";
+            options.api.endpoint = mkOption {
+              type = str;
+              default = "http://localhost:${toString cfg.settings.web.listen.port}";
+              defaultText = literalExpression ''"http://localhost:''${config.services.scrutiny.settings.web.listen.port}"'';
+              description = "Scrutiny app API endpoint for sending metrics to.";
             };
 
-            options.log.level = lib.mkOption {
-              type = lib.types.enum [ "INFO" "DEBUG" ];
+            options.log.level = mkOption {
+              type = enum [ "INFO" "DEBUG" ];
               default = "INFO";
-              description = lib.mdDoc "Log level for Scrutiny collector.";
+              description = "Log level for Scrutiny collector.";
             };
           };
         };
@@ -157,14 +156,14 @@ in
     };
   };
 
-  config = lib.mkIf (cfg.enable || cfg.collector.enable) {
+  config = mkIf (cfg.enable || cfg.collector.enable) {
     services.influxdb2.enable = cfg.influxdb.enable;
 
-    networking.firewall = lib.mkIf cfg.openFirewall {
+    networking.firewall = mkIf cfg.openFirewall {
       allowedTCPPorts = [ cfg.settings.web.listen.port ];
     };
 
-    services.smartd = lib.mkIf cfg.collector.enable {
+    services.smartd = mkIf cfg.collector.enable {
       enable = true;
       extraOptions = [
         "-A /var/log/smartd/"
@@ -174,7 +173,7 @@ in
 
     systemd = {
       services = {
-        scrutiny = lib.mkIf cfg.enable {
+        scrutiny = mkIf cfg.enable {
           description = "Hard Drive S.M.A.R.T Monitoring, Historical Trends & Real World Failure Thresholds";
           wantedBy = [ "multi-user.target" ];
           after = [ "network.target" ];
@@ -185,14 +184,14 @@ in
           };
           serviceConfig = {
             DynamicUser = true;
-            ExecStart = "${lib.getExe cfg.package} start --config ${settingsFormat.generate "scrutiny.yaml" cfg.settings}";
+            ExecStart = "${getExe cfg.package} start --config ${settingsFormat.generate "scrutiny.yaml" cfg.settings}";
             Restart = "always";
             StateDirectory = "scrutiny";
             StateDirectoryMode = "0750";
           };
         };
 
-        scrutiny-collector = lib.mkIf cfg.collector.enable {
+        scrutiny-collector = mkIf cfg.collector.enable {
           description = "Scrutiny Collector Service";
           environment = {
             COLLECTOR_VERSION = "1";
@@ -200,12 +199,12 @@ in
           };
           serviceConfig = {
             Type = "oneshot";
-            ExecStart = "${lib.getExe cfg.collector.package} run --config ${settingsFormat.generate "scrutiny-collector.yaml" cfg.collector.settings}";
+            ExecStart = "${getExe cfg.collector.package} run --config ${settingsFormat.generate "scrutiny-collector.yaml" cfg.collector.settings}";
           };
         };
       };
 
-      timers = lib.mkIf cfg.collector.enable {
+      timers = mkIf cfg.collector.enable {
         scrutiny-collector = {
           timerConfig = {
             OnCalendar = cfg.collector.schedule;
@@ -217,5 +216,5 @@ in
     };
   };
 
-  meta.maintainers = [ lib.maintainers.jnsgruk ];
+  meta.maintainers = [ maintainers.jnsgruk ];
 }
diff --git a/nixos/modules/services/networking/bird-lg.nix b/nixos/modules/services/networking/bird-lg.nix
index be9f4101e6ab..1c59f7a6ae7c 100644
--- a/nixos/modules/services/networking/bird-lg.nix
+++ b/nixos/modules/services/networking/bird-lg.nix
@@ -194,8 +194,8 @@ in
         allowedIPs = mkOption {
           type = types.listOf types.str;
           default = [ ];
-          example = [ "192.168.25.52" "192.168.25.53" ];
-          description = lib.mdDoc "List of IPs to allow (default all allowed).";
+          example = [ "192.168.25.52" "192.168.25.53" "192.168.0.0/24" ];
+          description = lib.mdDoc "List of IPs or networks to allow (default all allowed).";
         };
 
         birdSocket = mkOption {
diff --git a/nixos/modules/services/networking/searx.nix b/nixos/modules/services/networking/searx.nix
index 938d585e3179..5bbf875f0d57 100644
--- a/nixos/modules/services/networking/searx.nix
+++ b/nixos/modules/services/networking/searx.nix
@@ -213,7 +213,7 @@ in
       serviceConfig = {
         User  = "searx";
         Group = "searx";
-        ExecStart = "${cfg.package}/bin/searx-run";
+        ExecStart = lib.getExe cfg.package;
       } // optionalAttrs (cfg.environmentFile != null)
         { EnvironmentFile = builtins.toPath cfg.environmentFile; };
       environment = {
diff --git a/nixos/modules/services/networking/tailscale.nix b/nixos/modules/services/networking/tailscale.nix
index f11fe57d6ce5..972299a4697a 100644
--- a/nixos/modules/services/networking/tailscale.nix
+++ b/nixos/modules/services/networking/tailscale.nix
@@ -66,6 +66,13 @@ in {
       default = [];
       example = ["--ssh"];
     };
+
+    extraDaemonFlags = mkOption {
+      description = lib.mdDoc "Extra flags to pass to {command}`tailscaled`.";
+      type = types.listOf types.str;
+      default = [];
+      example = ["--no-logs-no-support"];
+    };
   };
 
   config = mkIf cfg.enable {
@@ -80,7 +87,7 @@ in {
       ] ++ lib.optional config.networking.resolvconf.enable config.networking.resolvconf.package;
       serviceConfig.Environment = [
         "PORT=${toString cfg.port}"
-        ''"FLAGS=--tun ${lib.escapeShellArg cfg.interfaceName}"''
+        ''"FLAGS=--tun ${lib.escapeShellArg cfg.interfaceName} ${lib.concatStringsSep " " cfg.extraDaemonFlags}"''
       ] ++ (lib.optionals (cfg.permitCertUid != null) [
         "TS_PERMIT_CERT_UID=${cfg.permitCertUid}"
       ]);
diff --git a/nixos/modules/services/networking/unbound.nix b/nixos/modules/services/networking/unbound.nix
index 616b32f11797..8438e472e11e 100644
--- a/nixos/modules/services/networking/unbound.nix
+++ b/nixos/modules/services/networking/unbound.nix
@@ -24,12 +24,24 @@ let
   confNoServer = concatStringsSep "\n" ((mapAttrsToList (toConf "") (builtins.removeAttrs cfg.settings [ "server" ])) ++ [""]);
   confServer = concatStringsSep "\n" (mapAttrsToList (toConf "  ") (builtins.removeAttrs cfg.settings.server [ "define-tag" ]));
 
-  confFile = pkgs.writeText "unbound.conf" ''
+  confFileUnchecked = pkgs.writeText "unbound.conf" ''
     server:
     ${optionalString (cfg.settings.server.define-tag != "") (toOption "  " "define-tag" cfg.settings.server.define-tag)}
     ${confServer}
     ${confNoServer}
   '';
+  confFile = if cfg.checkconf then pkgs.runCommandLocal "unbound-checkconf" { } ''
+    cp ${confFileUnchecked} unbound.conf
+
+    # fake stateDir which is not accesible in the sandbox
+    mkdir -p $PWD/state
+    sed -i unbound.conf \
+      -e '/auto-trust-anchor-file/d' \
+      -e "s|${cfg.stateDir}|$PWD/state|"
+    ${cfg.package}/bin/unbound-checkconf unbound.conf
+
+    cp ${confFileUnchecked} $out
+  '' else confFileUnchecked;
 
   rootTrustAnchorFile = "${cfg.stateDir}/root.key";
 
@@ -62,6 +74,17 @@ in {
         description = lib.mdDoc "Directory holding all state for unbound to run.";
       };
 
+      checkconf = mkOption {
+        type = types.bool;
+        default = !cfg.settings ? include;
+        defaultText = "!config.services.unbound.settings ? include";
+        description = lib.mdDoc ''
+          Wether to check the resulting config file with unbound checkconf for syntax errors.
+
+          If settings.include is used, then this options is disabled, as the import can likely not be resolved at build time.
+        '';
+      };
+
       resolveLocalQueries = mkOption {
         type = types.bool;
         default = true;
diff --git a/nixos/modules/services/security/kanidm.nix b/nixos/modules/services/security/kanidm.nix
index c659d93b4087..9d074c3027d0 100644
--- a/nixos/modules/services/security/kanidm.nix
+++ b/nixos/modules/services/security/kanidm.nix
@@ -132,6 +132,28 @@ in
             default = "WriteReplica";
             type = lib.types.enum [ "WriteReplica" "WriteReplicaNoUI" "ReadOnlyReplica" ];
           };
+          online_backup = {
+            path = lib.mkOption {
+              description = lib.mdDoc "Path to the output directory for backups.";
+              type = lib.types.path;
+              default = "/var/lib/kanidm/backups";
+            };
+            schedule = lib.mkOption {
+              description = lib.mdDoc "The schedule for backups in cron format.";
+              type = lib.types.str;
+              default = "00 22 * * *";
+            };
+            versions = lib.mkOption {
+              description = lib.mdDoc ''
+                Number of backups to keep.
+
+                The default is set to `0`, in order to disable backups by default.
+              '';
+              type = lib.types.ints.unsigned;
+              default = 0;
+              example = 7;
+            };
+          };
         };
       };
       default = { };
@@ -233,6 +255,14 @@ in
 
     environment.systemPackages = lib.mkIf cfg.enableClient [ cfg.package ];
 
+    systemd.tmpfiles.settings."10-kanidm" = {
+      ${cfg.serverSettings.online_backup.path}.d = {
+        mode = "0700";
+        user = "kanidm";
+        group = "kanidm";
+      };
+    };
+
     systemd.services.kanidm = lib.mkIf cfg.enableServer {
       description = "kanidm identity management daemon";
       wantedBy = [ "multi-user.target" ];
@@ -253,6 +283,8 @@ in
           BindPaths = [
             # To create the socket
             "/run/kanidmd:/run/kanidmd"
+            # To store backups
+            cfg.serverSettings.online_backup.path
           ];
 
           AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
diff --git a/nixos/modules/services/security/vaultwarden/default.nix b/nixos/modules/services/security/vaultwarden/default.nix
index 470db735bf64..60d8015d0cee 100644
--- a/nixos/modules/services/security/vaultwarden/default.nix
+++ b/nixos/modules/services/security/vaultwarden/default.nix
@@ -180,7 +180,6 @@ in {
     users.groups.vaultwarden = { };
 
     systemd.services.vaultwarden = {
-      aliases = [ "bitwarden_rs.service" ];
       after = [ "network.target" ];
       path = with pkgs; [ openssl ];
       serviceConfig = {
@@ -202,7 +201,6 @@ in {
     };
 
     systemd.services.backup-vaultwarden = mkIf (cfg.backupDir != null) {
-      aliases = [ "backup-bitwarden_rs.service" ];
       description = "Backup vaultwarden";
       environment = {
         DATA_FOLDER = "/var/lib/bitwarden_rs";
@@ -222,7 +220,6 @@ in {
     };
 
     systemd.timers.backup-vaultwarden = mkIf (cfg.backupDir != null) {
-      aliases = [ "backup-bitwarden_rs.timer" ];
       description = "Backup vaultwarden on time";
       timerConfig = {
         OnCalendar = mkDefault "23:00";
@@ -240,6 +237,9 @@ in {
     };
   };
 
-  # uses attributes of the linked package
-  meta.buildDocsInSandbox = false;
+  meta = {
+    # uses attributes of the linked package
+    buildDocsInSandbox = false;
+    maintainers = with lib.maintainers; [ dotlambda SuperSandro2000 ];
+  };
 }
diff --git a/nixos/modules/services/web-apps/miniflux.nix b/nixos/modules/services/web-apps/miniflux.nix
index 1a5b7d0c24e9..16b6fb0d655d 100644
--- a/nixos/modules/services/web-apps/miniflux.nix
+++ b/nixos/modules/services/web-apps/miniflux.nix
@@ -16,10 +16,20 @@ in
 {
   options = {
     services.miniflux = {
-      enable = mkEnableOption (lib.mdDoc "miniflux and creates a local postgres database for it");
+      enable = mkEnableOption (lib.mdDoc "miniflux");
 
       package = mkPackageOption pkgs "miniflux" { };
 
+      createDatabaseLocally = lib.mkOption {
+        type = lib.types.bool;
+        default = true;
+        description = ''
+          Whether a PostgreSQL database should be automatically created and
+          configured on the local host. If set to `false`, you need provision a
+          database yourself and make sure to create the hstore extension in it.
+        '';
+      };
+
       config = mkOption {
         type = with types; attrsOf (oneOf [ str int ]);
         example = literalExpression ''
@@ -38,7 +48,7 @@ in
         '';
       };
 
-      adminCredentialsFile = mkOption  {
+      adminCredentialsFile = mkOption {
         type = types.path;
         description = lib.mdDoc ''
           File containing the ADMIN_USERNAME and
@@ -51,14 +61,14 @@ in
   };
 
   config = mkIf cfg.enable {
-    services.miniflux.config =  {
+    services.miniflux.config = {
       LISTEN_ADDR = mkDefault defaultAddress;
-      DATABASE_URL = "user=miniflux host=/run/postgresql dbname=miniflux";
+      DATABASE_URL = lib.mkIf cfg.createDatabaseLocally "user=miniflux host=/run/postgresql dbname=miniflux";
       RUN_MIGRATIONS = 1;
       CREATE_ADMIN = 1;
     };
 
-    services.postgresql = {
+    services.postgresql = lib.mkIf cfg.createDatabaseLocally {
       enable = true;
       ensureUsers = [ {
         name = "miniflux";
@@ -67,7 +77,7 @@ in
       ensureDatabases = [ "miniflux" ];
     };
 
-    systemd.services.miniflux-dbsetup = {
+    systemd.services.miniflux-dbsetup = lib.mkIf cfg.createDatabaseLocally {
       description = "Miniflux database setup";
       requires = [ "postgresql.service" ];
       after = [ "network.target" "postgresql.service" ];
@@ -81,8 +91,9 @@ in
     systemd.services.miniflux = {
       description = "Miniflux service";
       wantedBy = [ "multi-user.target" ];
-      requires = [ "miniflux-dbsetup.service" ];
-      after = [ "network.target" "postgresql.service" "miniflux-dbsetup.service" ];
+      requires = lib.optional cfg.createDatabaseLocally "miniflux-dbsetup.service";
+      after = [ "network.target" ]
+        ++ lib.optionals cfg.createDatabaseLocally [ "postgresql.service" "miniflux-dbsetup.service" ];
 
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/miniflux";
@@ -129,6 +140,7 @@ in
         include "${pkgs.apparmorRulesFromClosure { name = "miniflux"; } cfg.package}"
         r ${cfg.package}/bin/miniflux,
         r @{sys}/kernel/mm/transparent_hugepage/hpage_pmd_size,
+        rw /run/miniflux/**,
       }
     '';
   };
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index 08f90dcf59d8..5cda4a00a9de 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -45,7 +45,7 @@ let
     };
   };
 
-  webroot = pkgs.runCommand
+  webroot = pkgs.runCommandLocal
     "${cfg.package.name or "nextcloud"}-with-apps"
     { }
     ''
diff --git a/nixos/modules/services/web-apps/photoprism.nix b/nixos/modules/services/web-apps/photoprism.nix
index ccf995fccf3e..39eb7c65c635 100644
--- a/nixos/modules/services/web-apps/photoprism.nix
+++ b/nixos/modules/services/web-apps/photoprism.nix
@@ -104,6 +104,7 @@ in
         StateDirectory = "photoprism";
         WorkingDirectory = "/var/lib/photoprism";
         RuntimeDirectory = "photoprism";
+        ReadWritePaths = [ cfg.originalsPath cfg.importPath cfg.storagePath ];
 
         LoadCredential = lib.optionalString (cfg.passwordFile != null)
           "PHOTOPRISM_ADMIN_PASSWORD:${cfg.passwordFile}";
diff --git a/nixos/modules/services/web-apps/vikunja.nix b/nixos/modules/services/web-apps/vikunja.nix
index b893f2c1f33c..efa9c676d9a5 100644
--- a/nixos/modules/services/web-apps/vikunja.nix
+++ b/nixos/modules/services/web-apps/vikunja.nix
@@ -9,10 +9,13 @@ let
   useMysql = cfg.database.type == "mysql";
   usePostgresql = cfg.database.type == "postgres";
 in {
+  imports = [
+    (mkRemovedOptionModule [ "services" "vikunja" "setupNginx" ] "services.vikunja no longer supports the automatic set up of a nginx virtual host. Set up your own webserver config with a proxy pass to the vikunja service.")
+  ];
+
   options.services.vikunja = with lib; {
     enable = mkEnableOption (lib.mdDoc "vikunja service");
-    package-api = mkPackageOption pkgs "vikunja-api" { };
-    package-frontend = mkPackageOption pkgs "vikunja-frontend" { };
+    package = mkPackageOption pkgs "vikunja" { };
     environmentFiles = mkOption {
       type = types.listOf types.path;
       default = [ ];
@@ -21,25 +24,10 @@ in {
         For example passwords should be set in one of these files.
       '';
     };
-    setupNginx = mkOption {
-      type = types.bool;
-      default = config.services.nginx.enable;
-      defaultText = literalExpression "config.services.nginx.enable";
-      description = lib.mdDoc ''
-        Whether to setup NGINX.
-        Further nginx configuration can be done by changing
-        {option}`services.nginx.virtualHosts.<frontendHostname>`.
-        This does not enable TLS or ACME by default. To enable this, set the
-        {option}`services.nginx.virtualHosts.<frontendHostname>.enableACME` to
-        `true` and if appropriate do the same for
-        {option}`services.nginx.virtualHosts.<frontendHostname>.forceSSL`.
-      '';
-    };
     frontendScheme = mkOption {
       type = types.enum [ "http" "https" ];
       description = lib.mdDoc ''
         Whether the site is available via http or https.
-        This does not configure https or ACME in nginx!
       '';
     };
     frontendHostname = mkOption {
@@ -104,42 +92,27 @@ in {
       };
     };
 
-    systemd.services.vikunja-api = {
-      description = "vikunja-api";
+    systemd.services.vikunja = {
+      description = "vikunja";
       after = [ "network.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
       wantedBy = [ "multi-user.target" ];
-      path = [ cfg.package-api ];
+      path = [ cfg.package ];
       restartTriggers = [ configFile ];
 
       serviceConfig = {
         Type = "simple";
         DynamicUser = true;
         StateDirectory = "vikunja";
-        ExecStart = "${cfg.package-api}/bin/vikunja";
+        ExecStart = "${cfg.package}/bin/vikunja";
         Restart = "always";
         EnvironmentFile = cfg.environmentFiles;
       };
     };
 
-    services.nginx.virtualHosts."${cfg.frontendHostname}" = mkIf cfg.setupNginx {
-      locations = {
-        "/" = {
-          root = cfg.package-frontend;
-          tryFiles = "try_files $uri $uri/ /";
-        };
-        "~* ^/(api|dav|\\.well-known)/" = {
-          proxyPass = "http://localhost:${toString cfg.port}";
-          extraConfig = ''
-            client_max_body_size 20M;
-          '';
-        };
-      };
-    };
-
     environment.etc."vikunja/config.yaml".source = configFile;
 
     environment.systemPackages = [
-      cfg.package-api # for admin `vikunja` CLI
+      cfg.package # for admin `vikunja` CLI
     ];
   };
 }
diff --git a/nixos/modules/services/x11/desktop-managers/plasma6.nix b/nixos/modules/services/x11/desktop-managers/plasma6.nix
index bc246b1af278..1237261e0af7 100644
--- a/nixos/modules/services/x11/desktop-managers/plasma6.nix
+++ b/nixos/modules/services/x11/desktop-managers/plasma6.nix
@@ -10,6 +10,11 @@
 
   inherit (pkgs) kdePackages;
   inherit (lib) literalExpression mkDefault mkIf mkOption mkPackageOptionMD types;
+
+  activationScript = ''
+    # will be rebuilt automatically
+    rm -fv $HOME/.cache/ksycoca*
+  '';
 in {
   options = {
     services.xserver.desktopManager.plasma6 = {
@@ -127,6 +132,7 @@ in {
 
         spectacle
         systemsettings
+        kcmutils
 
         # Gear
         baloo
@@ -272,5 +278,14 @@ in {
     };
 
     programs.kdeconnect.package = kdePackages.kdeconnect-kde;
+
+    # FIXME: ugly hack. See #292632 for details.
+    system.userActivationScripts.rebuildSycoca = activationScript;
+    systemd.user.services.nixos-rebuild-sycoca = {
+      description = "Rebuild KDE system configuration cache";
+      wantedBy = [ "graphical-session-pre.target" ];
+      serviceConfig.Type = "oneshot";
+      script = activationScript;
+    };
   };
 }