summary refs log tree commit diff
path: root/nixos/modules/services/misc
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services/misc')
-rw-r--r--nixos/modules/services/misc/airsonic.nix2
-rw-r--r--nixos/modules/services/misc/apache-kafka.nix23
-rw-r--r--nixos/modules/services/misc/autofs.nix1
-rw-r--r--nixos/modules/services/misc/autorandr.nix6
-rw-r--r--nixos/modules/services/misc/bazarr.nix1
-rw-r--r--nixos/modules/services/misc/beanstalkd.nix10
-rw-r--r--nixos/modules/services/misc/bees.nix72
-rw-r--r--nixos/modules/services/misc/calibre-server.nix49
-rw-r--r--nixos/modules/services/misc/cfdyndns.nix22
-rw-r--r--nixos/modules/services/misc/cgminer.nix8
-rw-r--r--nixos/modules/services/misc/clipcat.nix31
-rw-r--r--nixos/modules/services/misc/defaultUnicornConfig.rb69
-rw-r--r--nixos/modules/services/misc/dendrite.nix181
-rw-r--r--nixos/modules/services/misc/dictd.nix2
-rw-r--r--nixos/modules/services/misc/disnix.nix13
-rw-r--r--nixos/modules/services/misc/docker-registry.nix2
-rw-r--r--nixos/modules/services/misc/domoticz.nix51
-rw-r--r--nixos/modules/services/misc/duckling.nix39
-rw-r--r--nixos/modules/services/misc/dysnomia.nix88
-rw-r--r--nixos/modules/services/misc/etcd.nix2
-rw-r--r--nixos/modules/services/misc/etebase-server.nix226
-rw-r--r--nixos/modules/services/misc/etesync-dav.nix92
-rw-r--r--nixos/modules/services/misc/exhibitor.nix2
-rw-r--r--nixos/modules/services/misc/felix.nix2
-rw-r--r--nixos/modules/services/misc/fstrim.nix4
-rw-r--r--nixos/modules/services/misc/gammu-smsd.nix2
-rw-r--r--nixos/modules/services/misc/geoip-updater.nix306
-rw-r--r--nixos/modules/services/misc/geoipupdate.nix187
-rw-r--r--nixos/modules/services/misc/gitea.nix97
-rw-r--r--nixos/modules/services/misc/gitit.nix1
-rw-r--r--nixos/modules/services/misc/gitlab.nix776
-rw-r--r--nixos/modules/services/misc/gitlab.xml61
-rw-r--r--nixos/modules/services/misc/gitolite.nix2
-rw-r--r--nixos/modules/services/misc/gitweb.nix2
-rw-r--r--nixos/modules/services/misc/gogs.nix8
-rw-r--r--nixos/modules/services/misc/gollum.nix2
-rw-r--r--nixos/modules/services/misc/gpsd.nix2
-rw-r--r--nixos/modules/services/misc/home-assistant.nix147
-rw-r--r--nixos/modules/services/misc/ihaskell.nix1
-rw-r--r--nixos/modules/services/misc/jellyfin.nix60
-rw-r--r--nixos/modules/services/misc/klipper.nix117
-rw-r--r--nixos/modules/services/misc/leaps.nix2
-rw-r--r--nixos/modules/services/misc/lifecycled.nix164
-rw-r--r--nixos/modules/services/misc/mame.nix4
-rw-r--r--nixos/modules/services/misc/matrix-appservice-discord.nix8
-rw-r--r--nixos/modules/services/misc/matrix-appservice-irc.nix229
-rw-r--r--nixos/modules/services/misc/matrix-synapse.nix77
-rw-r--r--nixos/modules/services/misc/matrix-synapse.xml11
-rw-r--r--nixos/modules/services/misc/mautrix-telegram.nix23
-rw-r--r--nixos/modules/services/misc/mediatomb.nix237
-rw-r--r--nixos/modules/services/misc/mwlib.nix6
-rw-r--r--nixos/modules/services/misc/n8n.nix78
-rw-r--r--nixos/modules/services/misc/nix-daemon.nix26
-rw-r--r--nixos/modules/services/misc/nix-gc.nix53
-rw-r--r--nixos/modules/services/misc/nzbhydra2.nix78
-rw-r--r--nixos/modules/services/misc/octoprint.nix5
-rw-r--r--nixos/modules/services/misc/ombi.nix81
-rw-r--r--nixos/modules/services/misc/packagekit.nix91
-rw-r--r--nixos/modules/services/misc/paperless.nix2
-rw-r--r--nixos/modules/services/misc/pinnwand.nix69
-rw-r--r--nixos/modules/services/misc/plikd.nix82
-rw-r--r--nixos/modules/services/misc/podgrab.nix50
-rw-r--r--nixos/modules/services/misc/pykms.nix13
-rw-r--r--nixos/modules/services/misc/redmine.nix108
-rw-r--r--nixos/modules/services/misc/rippled.nix1
-rw-r--r--nixos/modules/services/misc/safeeyes.nix4
-rw-r--r--nixos/modules/services/misc/sdrplay.nix35
-rw-r--r--nixos/modules/services/misc/siproxd.nix18
-rw-r--r--nixos/modules/services/misc/snapper.nix12
-rw-r--r--nixos/modules/services/misc/sourcehut/builds.nix234
-rw-r--r--nixos/modules/services/misc/sourcehut/default.nix198
-rw-r--r--nixos/modules/services/misc/sourcehut/dispatch.nix125
-rw-r--r--nixos/modules/services/misc/sourcehut/git.nix214
-rw-r--r--nixos/modules/services/misc/sourcehut/hg.nix173
-rw-r--r--nixos/modules/services/misc/sourcehut/hub.nix118
-rw-r--r--nixos/modules/services/misc/sourcehut/lists.nix185
-rw-r--r--nixos/modules/services/misc/sourcehut/man.nix122
-rw-r--r--nixos/modules/services/misc/sourcehut/meta.nix211
-rw-r--r--nixos/modules/services/misc/sourcehut/paste.nix133
-rw-r--r--nixos/modules/services/misc/sourcehut/service.nix66
-rw-r--r--nixos/modules/services/misc/sourcehut/sourcehut.xml115
-rw-r--r--nixos/modules/services/misc/sourcehut/todo.nix161
-rw-r--r--nixos/modules/services/misc/ssm-agent.nix38
-rw-r--r--nixos/modules/services/misc/sssd.nix4
-rw-r--r--nixos/modules/services/misc/subsonic.nix4
-rw-r--r--nixos/modules/services/misc/svnserve.nix3
-rw-r--r--nixos/modules/services/misc/synergy.nix27
-rw-r--r--nixos/modules/services/misc/weechat.nix1
-rw-r--r--nixos/modules/services/misc/zigbee2mqtt.nix97
-rw-r--r--nixos/modules/services/misc/zookeeper.nix5
90 files changed, 5561 insertions, 1009 deletions
diff --git a/nixos/modules/services/misc/airsonic.nix b/nixos/modules/services/misc/airsonic.nix
index 5cc2ff7f4bd..a572f1f6d6f 100644
--- a/nixos/modules/services/misc/airsonic.nix
+++ b/nixos/modules/services/misc/airsonic.nix
@@ -118,7 +118,7 @@ in {
       '';
       serviceConfig = {
         ExecStart = ''
-          ${pkgs.jre}/bin/java -Xmx${toString cfg.maxMemory}m \
+          ${pkgs.jre8}/bin/java -Xmx${toString cfg.maxMemory}m \
           -Dairsonic.home=${cfg.home} \
           -Dserver.address=${cfg.listenAddress} \
           -Dserver.port=${toString cfg.port} \
diff --git a/nixos/modules/services/misc/apache-kafka.nix b/nixos/modules/services/misc/apache-kafka.nix
index f3a650a260f..69dfadfe54e 100644
--- a/nixos/modules/services/misc/apache-kafka.nix
+++ b/nixos/modules/services/misc/apache-kafka.nix
@@ -90,19 +90,7 @@ in {
 
     jvmOptions = mkOption {
       description = "Extra command line options for the JVM running Kafka.";
-      default = [
-        "-server"
-        "-Xmx1G"
-        "-Xms1G"
-        "-XX:+UseCompressedOops"
-        "-XX:+UseParNewGC"
-        "-XX:+UseConcMarkSweepGC"
-        "-XX:+CMSClassUnloadingEnabled"
-        "-XX:+CMSScavengeBeforeRemark"
-        "-XX:+DisableExplicitGC"
-        "-Djava.awt.headless=true"
-        "-Djava.net.preferIPv4Stack=true"
-      ];
+      default = [];
       type = types.listOf types.str;
       example = [
         "-Djava.net.preferIPv4Stack=true"
@@ -118,6 +106,13 @@ in {
       type = types.package;
     };
 
+    jre = mkOption {
+      description = "The JRE with which to run Kafka";
+      default = cfg.package.passthru.jre;
+      defaultText = "pkgs.apacheKafka.passthru.jre";
+      type = types.package;
+    };
+
   };
 
   config = mkIf cfg.enable {
@@ -138,7 +133,7 @@ in {
       after = [ "network.target" ];
       serviceConfig = {
         ExecStart = ''
-          ${pkgs.jre}/bin/java \
+          ${cfg.jre}/bin/java \
             -cp "${cfg.package}/libs/*" \
             -Dlog4j.configuration=file:${logConfig} \
             ${toString cfg.jvmOptions} \
diff --git a/nixos/modules/services/misc/autofs.nix b/nixos/modules/services/misc/autofs.nix
index 5e7c1e66828..541f0d2db19 100644
--- a/nixos/modules/services/misc/autofs.nix
+++ b/nixos/modules/services/misc/autofs.nix
@@ -52,6 +52,7 @@ in
       };
 
       timeout = mkOption {
+        type = types.int;
         default = 600;
         description = "Set the global minimum timeout, in seconds, until directories are unmounted";
       };
diff --git a/nixos/modules/services/misc/autorandr.nix b/nixos/modules/services/misc/autorandr.nix
index cf7fb5f78d3..95cee5046e8 100644
--- a/nixos/modules/services/misc/autorandr.nix
+++ b/nixos/modules/services/misc/autorandr.nix
@@ -37,9 +37,9 @@ in {
       description = "Autorandr execution hook";
       after = [ "sleep.target" ];
 
+      startLimitIntervalSec = 5;
+      startLimitBurst = 1;
       serviceConfig = {
-        StartLimitInterval = 5;
-        StartLimitBurst = 1;
         ExecStart = "${pkgs.autorandr}/bin/autorandr --batch --change --default ${cfg.defaultTarget}";
         Type = "oneshot";
         RemainAfterExit = false;
@@ -48,5 +48,5 @@ in {
 
   };
 
-  meta.maintainers = with maintainers; [ gnidorah ];
+  meta.maintainers = with maintainers; [ ];
 }
diff --git a/nixos/modules/services/misc/bazarr.nix b/nixos/modules/services/misc/bazarr.nix
index d3fd5b08cc8..99343a146a7 100644
--- a/nixos/modules/services/misc/bazarr.nix
+++ b/nixos/modules/services/misc/bazarr.nix
@@ -64,6 +64,7 @@ in
 
     users.users = mkIf (cfg.user == "bazarr") {
       bazarr = {
+        isSystemUser = true;
         group = cfg.group;
         home = "/var/lib/${config.systemd.services.bazarr.serviceConfig.StateDirectory}";
       };
diff --git a/nixos/modules/services/misc/beanstalkd.nix b/nixos/modules/services/misc/beanstalkd.nix
index bcd133c9741..1c674a5b23b 100644
--- a/nixos/modules/services/misc/beanstalkd.nix
+++ b/nixos/modules/services/misc/beanstalkd.nix
@@ -28,6 +28,12 @@ in
           example = "0.0.0.0";
         };
       };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to open ports in the firewall for the server.";
+      };
     };
   };
 
@@ -35,6 +41,10 @@ in
 
   config = mkIf cfg.enable {
 
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.listen.port ];
+    };
+
     environment.systemPackages = [ pkg ];
 
     systemd.services.beanstalkd = {
diff --git a/nixos/modules/services/misc/bees.nix b/nixos/modules/services/misc/bees.nix
index b0ed2d5c286..6b8cae84642 100644
--- a/nixos/modules/services/misc/bees.nix
+++ b/nixos/modules/services/misc/bees.nix
@@ -57,7 +57,7 @@ let
     };
     options.extraOptions = mkOption {
       type = listOf str;
-      default = [];
+      default = [ ];
       description = ''
         Extra command-line options passed to the daemon. See upstream bees documentation.
       '';
@@ -67,7 +67,8 @@ let
     };
   };
 
-in {
+in
+{
 
   options.services.beesd = {
     filesystems = mkOption {
@@ -87,37 +88,42 @@ in {
     };
   };
   config = {
-    systemd.services = mapAttrs' (name: fs: nameValuePair "beesd@${name}" {
-      description = "Block-level BTRFS deduplication for %i";
-      after = [ "sysinit.target" ];
+    systemd.services = mapAttrs'
+      (name: fs: nameValuePair "beesd@${name}" {
+        description = "Block-level BTRFS deduplication for %i";
+        after = [ "sysinit.target" ];
 
-      serviceConfig = let
-        configOpts = [
-          fs.spec
-          "verbosity=${toString fs.verbosity}"
-          "idxSizeMB=${toString fs.hashTableSizeMB}"
-          "workDir=${fs.workDir}"
-        ];
-        configOptsStr = escapeShellArgs configOpts;
-      in {
-        # Values from https://github.com/Zygo/bees/blob/v0.6.1/scripts/beesd%40.service.in
-        ExecStart = "${pkgs.bees}/bin/bees-service-wrapper run ${configOptsStr} -- --no-timestamps ${escapeShellArgs fs.extraOptions}";
-        ExecStopPost = "${pkgs.bees}/bin/bees-service-wrapper cleanup ${configOptsStr}";
-        CPUAccounting = true;
-        CPUWeight = 12;
-        IOSchedulingClass = "idle";
-        IOSchedulingPriority = 7;
-        IOWeight = 10;
-        KillMode = "control-group";
-        KillSignal = "SIGTERM";
-        MemoryAccounting = true;
-        Nice = 19;
-        Restart = "on-abnormal";
-        StartupCPUWeight = 25;
-        StartupIOWeight = 25;
-        SyslogIdentifier = "bees"; # would otherwise be "bees-service-wrapper"
-      };
-      wantedBy = ["multi-user.target"];
-    }) cfg.filesystems;
+        serviceConfig =
+          let
+            configOpts = [
+              fs.spec
+              "verbosity=${toString fs.verbosity}"
+              "idxSizeMB=${toString fs.hashTableSizeMB}"
+              "workDir=${fs.workDir}"
+            ];
+            configOptsStr = escapeShellArgs configOpts;
+          in
+          {
+            # Values from https://github.com/Zygo/bees/blob/v0.6.5/scripts/beesd@.service.in
+            ExecStart = "${pkgs.bees}/bin/bees-service-wrapper run ${configOptsStr} -- --no-timestamps ${escapeShellArgs fs.extraOptions}";
+            ExecStopPost = "${pkgs.bees}/bin/bees-service-wrapper cleanup ${configOptsStr}";
+            CPUAccounting = true;
+            CPUSchedulingPolicy = "batch";
+            CPUWeight = 12;
+            IOSchedulingClass = "idle";
+            IOSchedulingPriority = 7;
+            IOWeight = 10;
+            KillMode = "control-group";
+            KillSignal = "SIGTERM";
+            MemoryAccounting = true;
+            Nice = 19;
+            Restart = "on-abnormal";
+            StartupCPUWeight = 25;
+            StartupIOWeight = 25;
+            SyslogIdentifier = "beesd"; # would otherwise be "bees-service-wrapper"
+          };
+        wantedBy = [ "multi-user.target" ];
+      })
+      cfg.filesystems;
   };
 }
diff --git a/nixos/modules/services/misc/calibre-server.nix b/nixos/modules/services/misc/calibre-server.nix
index 84c04f403d3..2467d34b524 100644
--- a/nixos/modules/services/misc/calibre-server.nix
+++ b/nixos/modules/services/misc/calibre-server.nix
@@ -9,24 +9,42 @@ let
 in
 
 {
+  imports = [
+    (mkChangedOptionModule [ "services" "calibre-server" "libraryDir" ] [ "services" "calibre-server" "libraries" ]
+      (config:
+        let libraryDir = getAttrFromPath [ "services" "calibre-server" "libraryDir" ] config;
+        in [ libraryDir ]
+      )
+    )
+  ];
 
   ###### interface
 
   options = {
-
     services.calibre-server = {
 
       enable = mkEnableOption "calibre-server";
 
-      libraryDir = mkOption {
+      libraries = mkOption {
         description = ''
-          The directory where the Calibre library to serve is.
-          '';
-          type = types.path;
+          The directories of the libraries to serve. They must be readable for the user under which the server runs.
+        '';
+        type = types.listOf types.path;
       };
 
-    };
+      user = mkOption {
+        description = "The user under which calibre-server runs.";
+        type = types.str;
+        default = "calibre-server";
+      };
+
+      group = mkOption {
+        description = "The group under which calibre-server runs.";
+        type = types.str;
+        default = "calibre-server";
+      };
 
+    };
   };
 
 
@@ -34,29 +52,34 @@ in
 
   config = mkIf cfg.enable {
 
-    systemd.services.calibre-server =
-      {
+    systemd.services.calibre-server = {
         description = "Calibre Server";
         after = [ "network.target" ];
         wantedBy = [ "multi-user.target" ];
         serviceConfig = {
-          User = "calibre-server";
+          User = cfg.user;
           Restart = "always";
-          ExecStart = "${pkgs.calibre}/bin/calibre-server ${cfg.libraryDir}";
+          ExecStart = "${pkgs.calibre}/bin/calibre-server ${lib.concatStringsSep " " cfg.libraries}";
         };
 
       };
 
     environment.systemPackages = [ pkgs.calibre ];
 
-    users.users.calibre-server = {
+    users.users = optionalAttrs (cfg.user == "calibre-server") {
+      calibre-server = {
+        home = "/var/lib/calibre-server";
+        createHome = true;
         uid = config.ids.uids.calibre-server;
-        group = "calibre-server";
+        group = cfg.group;
       };
+    };
 
-    users.groups.calibre-server = {
+    users.groups = optionalAttrs (cfg.group == "calibre-server") {
+      calibre-server = {
         gid = config.ids.gids.calibre-server;
       };
+    };
 
   };
 
diff --git a/nixos/modules/services/misc/cfdyndns.nix b/nixos/modules/services/misc/cfdyndns.nix
index dcf41602273..15af1f50da1 100644
--- a/nixos/modules/services/misc/cfdyndns.nix
+++ b/nixos/modules/services/misc/cfdyndns.nix
@@ -6,6 +6,12 @@ let
   cfg = config.services.cfdyndns;
 in
 {
+  imports = [
+    (mkRemovedOptionModule
+      [ "services" "cfdyndns" "apikey" ]
+      "Use services.cfdyndns.apikeyFile instead.")
+  ];
+
   options = {
     services.cfdyndns = {
       enable = mkEnableOption "Cloudflare Dynamic DNS Client";
@@ -17,10 +23,12 @@ in
         '';
       };
 
-      apikey = mkOption {
-        type = types.str;
+      apikeyFile = mkOption {
+        default = null;
+        type = types.nullOr types.str;
         description = ''
-          The API Key to use to authenticate to CloudFlare.
+          The path to a file containing the API Key
+          used to authenticate with CloudFlare.
         '';
       };
 
@@ -45,13 +53,17 @@ in
         Type = "simple";
         User = config.ids.uids.cfdyndns;
         Group = config.ids.gids.cfdyndns;
-        ExecStart = "/bin/sh -c '${pkgs.cfdyndns}/bin/cfdyndns'";
       };
       environment = {
         CLOUDFLARE_EMAIL="${cfg.email}";
-        CLOUDFLARE_APIKEY="${cfg.apikey}";
         CLOUDFLARE_RECORDS="${concatStringsSep "," cfg.records}";
       };
+      script = ''
+        ${optionalString (cfg.apikeyFile != null) ''
+          export CLOUDFLARE_APIKEY="$(cat ${escapeShellArg cfg.apikeyFile})"
+        ''}
+        ${pkgs.cfdyndns}/bin/cfdyndns
+      '';
     };
 
     users.users = {
diff --git a/nixos/modules/services/misc/cgminer.nix b/nixos/modules/services/misc/cgminer.nix
index 7635c2a0f4e..662570f9451 100644
--- a/nixos/modules/services/misc/cgminer.nix
+++ b/nixos/modules/services/misc/cgminer.nix
@@ -41,12 +41,14 @@ in
       };
 
       user = mkOption {
+        type = types.str;
         default = "cgminer";
         description = "User account under which cgminer runs";
       };
 
       pools = mkOption {
         default = [];  # Run benchmark
+        type = types.listOf (types.attrsOf types.str);
         description = "List of pools where to mine";
         example = [{
           url = "http://p2pool.org:9332";
@@ -57,6 +59,7 @@ in
 
       hardware = mkOption {
         default = []; # Run without options
+        type = types.listOf (types.attrsOf (types.either types.str types.int));
         description= "List of config options for every GPU";
         example = [
         {
@@ -83,6 +86,7 @@ in
 
       config = mkOption {
         default = {};
+        type = (types.either types.bool types.int);
         description = "Additional config";
         example = {
           auto-fan = true;
@@ -120,18 +124,18 @@ in
       wantedBy = [ "multi-user.target" ];
 
       environment = {
-        LD_LIBRARY_PATH = ''/run/opengl-driver/lib:/run/opengl-driver-32/lib'';
+        LD_LIBRARY_PATH = "/run/opengl-driver/lib:/run/opengl-driver-32/lib";
         DISPLAY = ":${toString config.services.xserver.display}";
         GPU_MAX_ALLOC_PERCENT = "100";
         GPU_USE_SYNC_OBJECTS = "1";
       };
 
+      startLimitIntervalSec = 60;  # 1 min
       serviceConfig = {
         ExecStart = "${pkgs.cgminer}/bin/cgminer --syslog --text-only --config ${cgminerConfig}";
         User = cfg.user;
         RestartSec = "30s";
         Restart = "always";
-        StartLimitInterval = "1m";
       };
     };
 
diff --git a/nixos/modules/services/misc/clipcat.nix b/nixos/modules/services/misc/clipcat.nix
new file mode 100644
index 00000000000..128bb9a89d6
--- /dev/null
+++ b/nixos/modules/services/misc/clipcat.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.clipcat;
+in {
+
+  options.services.clipcat= {
+    enable = mkEnableOption "Clipcat clipboard daemon";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.clipcat;
+      defaultText = "pkgs.clipcat";
+      description = "clipcat derivation to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.clipcat = {
+      enable      = true;
+      description = "clipcat daemon";
+      wantedBy = [ "graphical-session.target" ];
+      after    = [ "graphical-session.target" ];
+      serviceConfig.ExecStart = "${cfg.package}/bin/clipcatd --no-daemon";
+    };
+
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/misc/defaultUnicornConfig.rb b/nixos/modules/services/misc/defaultUnicornConfig.rb
deleted file mode 100644
index 0b58c59c7a5..00000000000
--- a/nixos/modules/services/misc/defaultUnicornConfig.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-worker_processes 3
-
-listen ENV["UNICORN_PATH"] + "/tmp/sockets/gitlab.socket", :backlog => 1024
-listen "/run/gitlab/gitlab.socket", :backlog => 1024
-
-working_directory ENV["GITLAB_PATH"]
-
-pid ENV["UNICORN_PATH"] + "/tmp/pids/unicorn.pid"
-
-timeout 60
-
-# combine Ruby 2.0.0dev or REE with "preload_app true" for memory savings
-# http://rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
-preload_app true
-GC.respond_to?(:copy_on_write_friendly=) and
-  GC.copy_on_write_friendly = true
-
-check_client_connection false
-
-before_fork do |server, worker|
-  # the following is highly recommended for Rails + "preload_app true"
-  # as there's no need for the master process to hold a connection
-  defined?(ActiveRecord::Base) and
-    ActiveRecord::Base.connection.disconnect!
-
-  # The following is only recommended for memory/DB-constrained
-  # installations.  It is not needed if your system can house
-  # twice as many worker_processes as you have configured.
-  #
-  # This allows a new master process to incrementally
-  # phase out the old master process with SIGTTOU to avoid a
-  # thundering herd (especially in the "preload_app false" case)
-  # when doing a transparent upgrade.  The last worker spawned
-  # will then kill off the old master process with a SIGQUIT.
-  old_pid = "#{server.config[:pid]}.oldbin"
-  if old_pid != server.pid
-    begin
-      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
-      Process.kill(sig, File.read(old_pid).to_i)
-    rescue Errno::ENOENT, Errno::ESRCH
-    end
-  end
-
-  # Throttle the master from forking too quickly by sleeping.  Due
-  # to the implementation of standard Unix signal handlers, this
-  # helps (but does not completely) prevent identical, repeated signals
-  # from being lost when the receiving process is busy.
-  # sleep 1
-end
-
-after_fork do |server, worker|
-  # per-process listener ports for debugging/admin/migrations
-  # addr = "127.0.0.1:#{9293 + worker.nr}"
-  # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true)
-
-  # the following is *required* for Rails + "preload_app true",
-  defined?(ActiveRecord::Base) and
-    ActiveRecord::Base.establish_connection
-
-  # reset prometheus client, this will cause any opened metrics files to be closed
-  defined?(::Prometheus::Client.reinitialize_on_pid_change) &&
-    Prometheus::Client.reinitialize_on_pid_change
-
-  # if preload_app is true, then you may also want to check and
-  # restart any other shared sockets/descriptors such as Memcached,
-  # and Redis.  TokyoCabinet file handles are safe to reuse
-  # between any number of forked children (assuming your kernel
-  # correctly implements pread()/pwrite() system calls)
-end
diff --git a/nixos/modules/services/misc/dendrite.nix b/nixos/modules/services/misc/dendrite.nix
new file mode 100644
index 00000000000..c967fc3a362
--- /dev/null
+++ b/nixos/modules/services/misc/dendrite.nix
@@ -0,0 +1,181 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.dendrite;
+  settingsFormat = pkgs.formats.yaml { };
+  configurationYaml = settingsFormat.generate "dendrite.yaml" cfg.settings;
+  workingDir = "/var/lib/dendrite";
+in
+{
+  options.services.dendrite = {
+    enable = lib.mkEnableOption "matrix.org dendrite";
+    httpPort = lib.mkOption {
+      type = lib.types.nullOr lib.types.port;
+      default = 8008;
+      description = ''
+        The port to listen for HTTP requests on.
+      '';
+    };
+    httpsPort = lib.mkOption {
+      type = lib.types.nullOr lib.types.port;
+      default = null;
+      description = ''
+        The port to listen for HTTPS requests on.
+      '';
+    };
+    tlsCert = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      example = "/var/lib/dendrite/server.cert";
+      default = null;
+      description = ''
+        The path to the TLS certificate.
+
+        <programlisting>
+          nix-shell -p dendrite --command "generate-keys --tls-cert server.crt --tls-key server.key"
+        </programlisting>
+      '';
+    };
+    tlsKey = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      example = "/var/lib/dendrite/server.key";
+      default = null;
+      description = ''
+        The path to the TLS key.
+
+        <programlisting>
+          nix-shell -p dendrite --command "generate-keys --tls-cert server.crt --tls-key server.key"
+        </programlisting>
+      '';
+    };
+    environmentFile = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      example = "/var/lib/dendrite/registration_secret";
+      default = null;
+      description = ''
+        Environment file as defined in <citerefentry>
+        <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+        </citerefentry>.
+        Secrets may be passed to the service without adding them to the world-readable
+        Nix store, by specifying placeholder variables as the option value in Nix and
+        setting these variables accordingly in the environment file. Currently only used
+        for the registration secret to allow secure registration when
+        client_api.registration_disabled is true.
+
+        <programlisting>
+          # snippet of dendrite-related config
+          services.dendrite.settings.client_api.registration_shared_secret = "$REGISTRATION_SHARED_SECRET";
+        </programlisting>
+
+        <programlisting>
+          # content of the environment file
+          REGISTRATION_SHARED_SECRET=verysecretpassword
+        </programlisting>
+
+        Note that this file needs to be available on the host on which
+        <literal>dendrite</literal> is running.
+      '';
+    };
+    settings = lib.mkOption {
+      type = lib.types.submodule {
+        freeformType = settingsFormat.type;
+        options.global = {
+          server_name = lib.mkOption {
+            type = lib.types.str;
+            example = "example.com";
+            description = ''
+              The domain name of the server, with optional explicit port.
+              This is used by remote servers to connect to this server.
+              This is also the last part of your UserID.
+            '';
+          };
+          private_key = lib.mkOption {
+            type = lib.types.path;
+            example = "${workingDir}/matrix_key.pem";
+            description = ''
+              The path to the signing private key file, used to sign
+              requests and events.
+
+              <programlisting>
+                nix-shell -p dendrite --command "generate-keys --private-key matrix_key.pem"
+              </programlisting>
+            '';
+          };
+          trusted_third_party_id_servers = lib.mkOption {
+            type = lib.types.listOf lib.types.str;
+            example = [ "matrix.org" ];
+            default = [ "matrix.org" "vector.im" ];
+            description = ''
+              Lists of domains that the server will trust as identity
+              servers to verify third party identifiers such as phone
+              numbers and email addresses
+            '';
+          };
+        };
+        options.client_api = {
+          registration_disabled = lib.mkOption {
+            type = lib.types.bool;
+            default = true;
+            description = ''
+              Whether to disable user registration to the server
+              without the shared secret.
+            '';
+          };
+        };
+      };
+      default = { };
+      description = ''
+        Configuration for dendrite, see:
+        <link xlink:href="https://github.com/matrix-org/dendrite/blob/master/dendrite-config.yaml"/>
+        for available options with which to populate settings.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [{
+      assertion = cfg.httpsPort != null -> (cfg.tlsCert != null && cfg.tlsKey != null);
+      message = ''
+        If Dendrite is configured to use https, tlsCert and tlsKey must be provided.
+
+        nix-shell -p dendrite --command "generate-keys --tls-cert server.crt --tls-key server.key"
+      '';
+    }];
+
+    systemd.services.dendrite = {
+      description = "Dendrite Matrix homeserver";
+      after = [
+        "network.target"
+      ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "simple";
+        DynamicUser = true;
+        StateDirectory = "dendrite";
+        WorkingDirectory = workingDir;
+        RuntimeDirectory = "dendrite";
+        RuntimeDirectoryMode = "0700";
+        EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
+        ExecStartPre =
+          if (cfg.environmentFile != null) then ''
+            ${pkgs.envsubst}/bin/envsubst \
+              -i ${configurationYaml} \
+              -o /run/dendrite/dendrite.yaml
+          '' else ''
+            ${pkgs.coreutils}/bin/cp ${configurationYaml} /run/dendrite/dendrite.yaml
+          '';
+        ExecStart = lib.strings.concatStringsSep " " ([
+          "${pkgs.dendrite}/bin/dendrite-monolith-server"
+          "--config /run/dendrite/dendrite.yaml"
+        ] ++ lib.optionals (cfg.httpPort != null) [
+          "--http-bind-address :${builtins.toString cfg.httpPort}"
+        ] ++ lib.optionals (cfg.httpsPort != null) [
+          "--https-bind-address :${builtins.toString cfg.httpsPort}"
+          "--tls-cert ${cfg.tlsCert}"
+          "--tls-key ${cfg.tlsKey}"
+        ]);
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        Restart = "on-failure";
+      };
+    };
+  };
+  meta.maintainers = lib.teams.matrix.members;
+}
diff --git a/nixos/modules/services/misc/dictd.nix b/nixos/modules/services/misc/dictd.nix
index d175854d2d1..6e796a3a1fc 100644
--- a/nixos/modules/services/misc/dictd.nix
+++ b/nixos/modules/services/misc/dictd.nix
@@ -27,7 +27,7 @@ in
         default = with pkgs.dictdDBs; [ wiktionary wordnet ];
         defaultText = "with pkgs.dictdDBs; [ wiktionary wordnet ]";
         example = literalExample "[ pkgs.dictdDBs.nld2eng ]";
-        description = ''List of databases to make available.'';
+        description = "List of databases to make available.";
       };
 
     };
diff --git a/nixos/modules/services/misc/disnix.nix b/nixos/modules/services/misc/disnix.nix
index 69386cdbb38..24a259bb4d2 100644
--- a/nixos/modules/services/misc/disnix.nix
+++ b/nixos/modules/services/misc/disnix.nix
@@ -34,6 +34,14 @@ in
         defaultText = "pkgs.disnix";
       };
 
+      enableProfilePath = mkEnableOption "exposing the Disnix profiles in the system's PATH";
+
+      profiles = mkOption {
+        type = types.listOf types.str;
+        default = [ "default" ];
+        example = [ "default" ];
+        description = "Names of the Disnix profiles to expose in the system's PATH";
+      };
     };
 
   };
@@ -44,6 +52,8 @@ in
     dysnomia.enable = true;
 
     environment.systemPackages = [ pkgs.disnix ] ++ optional cfg.useWebServiceInterface pkgs.DisnixWebService;
+    environment.variables.PATH = lib.optionals cfg.enableProfilePath (map (profileName: "/nix/var/nix/profiles/disnix/${profileName}/bin" ) cfg.profiles);
+    environment.variables.DISNIX_REMOTE_CLIENT = lib.optionalString (cfg.enableMultiUser) "disnix-client";
 
     services.dbus.enable = true;
     services.dbus.packages = [ pkgs.disnix ];
@@ -68,7 +78,8 @@ in
           ++ optional config.services.postgresql.enable "postgresql.service"
           ++ optional config.services.tomcat.enable "tomcat.service"
           ++ optional config.services.svnserve.enable "svnserve.service"
-          ++ optional config.services.mongodb.enable "mongodb.service";
+          ++ optional config.services.mongodb.enable "mongodb.service"
+          ++ optional config.services.influxdb.enable "influxdb.service";
 
         restartIfChanged = false;
 
diff --git a/nixos/modules/services/misc/docker-registry.nix b/nixos/modules/services/misc/docker-registry.nix
index 1c2e2cc5359..e212f581c28 100644
--- a/nixos/modules/services/misc/docker-registry.nix
+++ b/nixos/modules/services/misc/docker-registry.nix
@@ -58,7 +58,7 @@ in {
     port = mkOption {
       description = "Docker registry port to bind to.";
       default = 5000;
-      type = types.int;
+      type = types.port;
     };
 
     storagePath = mkOption {
diff --git a/nixos/modules/services/misc/domoticz.nix b/nixos/modules/services/misc/domoticz.nix
new file mode 100644
index 00000000000..b1353d48404
--- /dev/null
+++ b/nixos/modules/services/misc/domoticz.nix
@@ -0,0 +1,51 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.domoticz;
+  pkgDesc = "Domoticz home automation";
+
+in {
+
+  options = {
+
+    services.domoticz = {
+      enable = mkEnableOption pkgDesc;
+
+      bind = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = "IP address to bind to.";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 8080;
+        description = "Port to bind to for HTTP, set to 0 to disable HTTP.";
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services."domoticz" = {
+      description = pkgDesc;
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "domoticz";
+        Restart = "always";
+        ExecStart = ''
+          ${pkgs.domoticz}/bin/domoticz -noupdates -www ${toString cfg.port} -wwwbind ${cfg.bind} -sslwww 0 -userdata /var/lib/domoticz -approot ${pkgs.domoticz}/share/domoticz/ -pidfile /var/run/domoticz.pid
+        '';
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/duckling.nix b/nixos/modules/services/misc/duckling.nix
new file mode 100644
index 00000000000..77d2a92380b
--- /dev/null
+++ b/nixos/modules/services/misc/duckling.nix
@@ -0,0 +1,39 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.duckling;
+in {
+  options = {
+    services.duckling = {
+      enable = mkEnableOption "duckling";
+
+      port = mkOption {
+        type = types.port;
+        default = 8080;
+        description = ''
+          Port on which duckling will run.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.duckling = {
+      description = "Duckling server service";
+      wantedBy    = [ "multi-user.target" ];
+      after       = [ "network.target" ];
+
+      environment = {
+        PORT = builtins.toString cfg.port;
+      };
+
+      serviceConfig = {
+        ExecStart = "${pkgs.haskellPackages.duckling}/bin/duckling-example-exe --no-access-log --no-error-log";
+        Restart = "always";
+        DynamicUser = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/dysnomia.nix b/nixos/modules/services/misc/dysnomia.nix
index 4b52963500d..333ba651cde 100644
--- a/nixos/modules/services/misc/dysnomia.nix
+++ b/nixos/modules/services/misc/dysnomia.nix
@@ -66,6 +66,19 @@ let
       ) (builtins.attrNames cfg.components)}
     '';
   };
+
+  dysnomiaFlags = {
+    enableApacheWebApplication = config.services.httpd.enable;
+    enableAxis2WebService = config.services.tomcat.axis2.enable;
+    enableDockerContainer = config.virtualisation.docker.enable;
+    enableEjabberdDump = config.services.ejabberd.enable;
+    enableMySQLDatabase = config.services.mysql.enable;
+    enablePostgreSQLDatabase = config.services.postgresql.enable;
+    enableTomcatWebApplication = config.services.tomcat.enable;
+    enableMongoDatabase = config.services.mongodb.enable;
+    enableSubversionRepository = config.services.svnserve.enable;
+    enableInfluxDatabase = config.services.influxdb.enable;
+  };
 in
 {
   options = {
@@ -117,6 +130,12 @@ in
         description = "A list of paths containing additional modules that are added to the search folders";
         default = [];
       };
+
+      enableLegacyModules = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to enable Dysnomia legacy process and wrapper modules";
+      };
     };
   };
 
@@ -142,34 +161,48 @@ in
 
     environment.systemPackages = [ cfg.package ];
 
-    dysnomia.package = pkgs.dysnomia.override (origArgs: {
-      enableApacheWebApplication = config.services.httpd.enable;
-      enableAxis2WebService = config.services.tomcat.axis2.enable;
-      enableEjabberdDump = config.services.ejabberd.enable;
-      enableMySQLDatabase = config.services.mysql.enable;
-      enablePostgreSQLDatabase = config.services.postgresql.enable;
-      enableSubversionRepository = config.services.svnserve.enable;
-      enableTomcatWebApplication = config.services.tomcat.enable;
-      enableMongoDatabase = config.services.mongodb.enable;
-      enableInfluxDatabase = config.services.influxdb.enable;
+    dysnomia.package = pkgs.dysnomia.override (origArgs: dysnomiaFlags // lib.optionalAttrs (cfg.enableLegacyModules) {
+      enableLegacy = builtins.trace ''
+        WARNING: Dysnomia has been configured to use the legacy 'process' and 'wrapper'
+        modules for compatibility reasons! If you rely on these modules, consider
+        migrating to better alternatives.
+
+        More information: https://raw.githubusercontent.com/svanderburg/dysnomia/f65a9a84827bcc4024d6b16527098b33b02e4054/README-legacy.md
+
+        If you have migrated already or don't rely on these Dysnomia modules, you can
+        disable legacy mode with the following NixOS configuration option:
+
+        dysnomia.enableLegacyModules = false;
+
+        In a future version of Dysnomia (and NixOS) the legacy option will go away!
+      '' true;
     });
 
     dysnomia.properties = {
       hostname = config.networking.hostName;
       inherit (config.nixpkgs.localSystem) system;
 
-      supportedTypes = (import "${pkgs.stdenv.mkDerivation {
-        name = "supportedtypes";
-        buildCommand = ''
-          ( echo -n "[ "
-            cd ${cfg.package}/libexec/dysnomia
-            for i in *
-            do
-                echo -n "\"$i\" "
-            done
-            echo -n " ]") > $out
-        '';
-      }}");
+      supportedTypes = [
+        "echo"
+        "fileset"
+        "process"
+        "wrapper"
+
+        # These are not base modules, but they are still enabled because they work with technology that are always enabled in NixOS
+        "systemd-unit"
+        "sysvinit-script"
+        "nixos-configuration"
+      ]
+      ++ optional (dysnomiaFlags.enableApacheWebApplication) "apache-webapplication"
+      ++ optional (dysnomiaFlags.enableAxis2WebService) "axis2-webservice"
+      ++ optional (dysnomiaFlags.enableDockerContainer) "docker-container"
+      ++ optional (dysnomiaFlags.enableEjabberdDump) "ejabberd-dump"
+      ++ optional (dysnomiaFlags.enableInfluxDatabase) "influx-database"
+      ++ optional (dysnomiaFlags.enableMySQLDatabase) "mysql-database"
+      ++ optional (dysnomiaFlags.enablePostgreSQLDatabase) "postgresql-database"
+      ++ optional (dysnomiaFlags.enableTomcatWebApplication) "tomcat-webapplication"
+      ++ optional (dysnomiaFlags.enableMongoDatabase) "mongo-database"
+      ++ optional (dysnomiaFlags.enableSubversionRepository) "subversion-repository";
     };
 
     dysnomia.containers = lib.recursiveUpdate ({
@@ -185,9 +218,9 @@ in
     }; }
     // lib.optionalAttrs (config.services.mysql.enable) { mysql-database = {
         mysqlPort = config.services.mysql.port;
+        mysqlSocket = "/run/mysqld/mysqld.sock";
       } // lib.optionalAttrs cfg.enableAuthentication {
         mysqlUsername = "root";
-        mysqlPassword = builtins.readFile (config.services.mysql.rootPassword);
       };
     }
     // lib.optionalAttrs (config.services.postgresql.enable) { postgresql-database = {
@@ -199,10 +232,19 @@ in
       tomcatPort = 8080;
     }; }
     // lib.optionalAttrs (config.services.mongodb.enable) { mongo-database = {}; }
+    // lib.optionalAttrs (config.services.influxdb.enable) {
+      influx-database = {
+        influxdbUsername = config.services.influxdb.user;
+        influxdbDataDir = "${config.services.influxdb.dataDir}/data";
+        influxdbMetaDir = "${config.services.influxdb.dataDir}/meta";
+      };
+    }
     // lib.optionalAttrs (config.services.svnserve.enable) { subversion-repository = {
       svnBaseDir = config.services.svnserve.svnBaseDir;
     }; }) cfg.extraContainerProperties;
 
+    boot.extraSystemdUnitPaths = [ "/etc/systemd-mutable/system" ];
+
     system.activationScripts.dysnomia = ''
       mkdir -p /etc/systemd-mutable/system
       if [ ! -f /etc/systemd-mutable/system/dysnomia.target ]
diff --git a/nixos/modules/services/misc/etcd.nix b/nixos/modules/services/misc/etcd.nix
index 32360d43768..eb266f043eb 100644
--- a/nixos/modules/services/misc/etcd.nix
+++ b/nixos/modules/services/misc/etcd.nix
@@ -184,7 +184,7 @@ in {
       };
     };
 
-    environment.systemPackages = [ pkgs.etcdctl ];
+    environment.systemPackages = [ pkgs.etcd ];
 
     users.users.etcd = {
       uid = config.ids.uids.etcd;
diff --git a/nixos/modules/services/misc/etebase-server.nix b/nixos/modules/services/misc/etebase-server.nix
new file mode 100644
index 00000000000..b6bd6e9fd37
--- /dev/null
+++ b/nixos/modules/services/misc/etebase-server.nix
@@ -0,0 +1,226 @@
+{ config, pkgs, lib, ... }:
+
+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;
+
+  defaultUser = "etebase-server";
+in
+{
+  imports = [
+    (mkRemovedOptionModule
+      [ "services" "etebase-server" "customIni" ]
+      "Set the option `services.etebase-server.settings' instead.")
+    (mkRemovedOptionModule
+      [ "services" "etebase-server" "database" ]
+      "Set the option `services.etebase-server.settings.database' instead.")
+    (mkRenamedOptionModule
+      [ "services" "etebase-server" "secretFile" ]
+      [ "services" "etebase-server" "settings" "secret_file" ])
+    (mkRenamedOptionModule
+      [ "services" "etebase-server" "host" ]
+      [ "services" "etebase-server" "settings" "allowed_hosts" "allowed_host1" ])
+  ];
+
+  options = {
+    services.etebase-server = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        example = true;
+        description = ''
+          Whether to enable the Etebase server.
+
+          Once enabled you need to create an admin user by invoking the
+          shell command <literal>etebase-server createsuperuser</literal> with
+          the user specified by the <literal>user</literal> option or a superuser.
+          Then you can login and create accounts on your-etebase-server.com/admin
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/etebase-server";
+        description = "Directory to store the Etebase server data.";
+      };
+
+      port = mkOption {
+        type = with types; nullOr port;
+        default = 8001;
+        description = "Port to listen on.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to open ports in the firewall for the server.
+        '';
+      };
+
+      unixSocket = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "The path to the socket to bind to.";
+        example = "/run/etebase-server/etebase-server.sock";
+      };
+
+      settings = mkOption {
+        type = lib.types.submodule {
+          freeformType = iniFmt.type;
+
+          options = {
+            global = {
+              debug = mkOption {
+                type = types.bool;
+                default = false;
+                description = ''
+                  Whether to set django's DEBUG flag.
+                '';
+              };
+              secret_file = mkOption {
+                type = with types; nullOr str;
+                default = null;
+                description = ''
+                  The path to a file containing the secret
+                  used as django's SECRET_KEY.
+                '';
+              };
+              static_root = mkOption {
+                type = types.str;
+                default = "${cfg.dataDir}/static";
+                defaultText = "\${config.services.etebase-server.dataDir}/static";
+                description = "The directory for static files.";
+              };
+              media_root = mkOption {
+                type = types.str;
+                default = "${cfg.dataDir}/media";
+                defaultText = "\${config.services.etebase-server.dataDir}/media";
+                description = "The media directory.";
+              };
+            };
+            allowed_hosts = {
+              allowed_host1 = mkOption {
+                type = types.str;
+                default = "0.0.0.0";
+                example = "localhost";
+                description = ''
+                  The main host that is allowed access.
+                '';
+              };
+            };
+            database = {
+              engine = mkOption {
+                type = types.enum [ "django.db.backends.sqlite3" "django.db.backends.postgresql" ];
+                default = "django.db.backends.sqlite3";
+                description = "The database engine to use.";
+              };
+              name = mkOption {
+                type = types.str;
+                default = "${cfg.dataDir}/db.sqlite3";
+                defaultText = "\${config.services.etebase-server.dataDir}/db.sqlite3";
+                description = "The database name.";
+              };
+            };
+          };
+        };
+        default = {};
+        description = ''
+          Configuration for <package>etebase-server</package>. Refer to
+          <link xlink:href="https://github.com/etesync/server/blob/master/etebase-server.ini.example" />
+          and <link xlink:href="https://github.com/etesync/server/wiki" />
+          for details on supported values.
+        '';
+        example = {
+          global = {
+            debug = true;
+            media_root = "/path/to/media";
+          };
+          allowed_hosts = {
+            allowed_host2 = "localhost";
+          };
+        };
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = defaultUser;
+        description = "User under which Etebase server runs.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = with pkgs; [
+      (runCommand "etebase-server" {
+        buildInputs = [ makeWrapper ];
+      } ''
+        makeWrapper ${pythonEnv}/bin/etebase-server \
+          $out/bin/etebase-server \
+          --run "cd ${cfg.dataDir}" \
+          --prefix ETEBASE_EASY_CONFIG_PATH : "${configIni}"
+      '')
+    ];
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
+    ];
+
+    systemd.services.etebase-server = {
+      description = "An Etebase (EteSync 2.0) server";
+      after = [ "network.target" "systemd-tmpfiles-setup.service" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        User = cfg.user;
+        Restart = "always";
+        WorkingDirectory = cfg.dataDir;
+      };
+      environment = {
+        PYTHONPATH = "${pythonEnv}/${pkgs.python3.sitePackages}";
+        ETEBASE_EASY_CONFIG_PATH = configIni;
+      };
+      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
+          ${pythonEnv}/bin/etebase-server migrate --no-input
+          ${pythonEnv}/bin/etebase-server collectstatic --no-input --clear
+          echo ${pkgs.etebase-server} > "$versionFile"
+        fi
+      '';
+      script =
+        let
+          networking = if cfg.unixSocket != null
+          then "-u ${cfg.unixSocket}"
+          else "-b 0.0.0.0 -p ${toString cfg.port}";
+        in ''
+          cd "${pythonEnv}/lib/etebase-server";
+          ${pythonEnv}/bin/daphne ${networking} \
+            etebase_server.asgi:application
+        '';
+    };
+
+    users = optionalAttrs (cfg.user == defaultUser) {
+      users.${defaultUser} = {
+        isSystemUser = true;
+        group = defaultUser;
+        home = cfg.dataDir;
+      };
+
+      groups.${defaultUser} = {};
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/etesync-dav.nix b/nixos/modules/services/misc/etesync-dav.nix
new file mode 100644
index 00000000000..9d7cfda371b
--- /dev/null
+++ b/nixos/modules/services/misc/etesync-dav.nix
@@ -0,0 +1,92 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.etesync-dav;
+in
+  {
+    options.services.etesync-dav = {
+      enable = mkEnableOption "etesync-dav";
+
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "The server host address.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 37358;
+        description = "The server host port.";
+      };
+
+      apiUrl = mkOption {
+        type = types.str;
+        default = "https://api.etesync.com/";
+        description = "The url to the etesync API.";
+      };
+
+      openFirewall = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to open the firewall for the specified port.";
+      };
+
+      sslCertificate = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/etesync.crt";
+        description = ''
+          Path to server SSL certificate. It will be copied into
+          etesync-dav's data directory.
+        '';
+      };
+
+      sslCertificateKey = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/etesync.key";
+        description = ''
+          Path to server SSL certificate key.  It will be copied into
+          etesync-dav's data directory.
+        '';
+      };
+    };
+
+    config = mkIf cfg.enable {
+      networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
+
+      systemd.services.etesync-dav = {
+        description = "etesync-dav - A CalDAV and CardDAV adapter for EteSync";
+        after = [ "network-online.target" ];
+        wantedBy = [ "multi-user.target" ];
+        path = [ pkgs.etesync-dav ];
+        environment = {
+          ETESYNC_LISTEN_ADDRESS = cfg.host;
+          ETESYNC_LISTEN_PORT = toString cfg.port;
+          ETESYNC_URL = cfg.apiUrl;
+          ETESYNC_DATA_DIR = "/var/lib/etesync-dav";
+        };
+
+        serviceConfig = {
+          Type = "simple";
+          DynamicUser = true;
+          StateDirectory = "etesync-dav";
+          ExecStart = "${pkgs.etesync-dav}/bin/etesync-dav";
+          ExecStartPre = mkIf (cfg.sslCertificate != null || cfg.sslCertificateKey != null) (
+            pkgs.writers.writeBash "etesync-dav-copy-keys" ''
+              ${optionalString (cfg.sslCertificate != null) ''
+                cp ${toString cfg.sslCertificate} $STATE_DIRECTORY/etesync.crt
+              ''}
+              ${optionalString (cfg.sslCertificateKey != null) ''
+                cp ${toString cfg.sslCertificateKey} $STATE_DIRECTORY/etesync.key
+              ''}
+            ''
+          );
+          Restart = "on-failure";
+          RestartSec = "30min 1s";
+        };
+      };
+    };
+  }
diff --git a/nixos/modules/services/misc/exhibitor.nix b/nixos/modules/services/misc/exhibitor.nix
index f8c79f892da..28c98edf47a 100644
--- a/nixos/modules/services/misc/exhibitor.nix
+++ b/nixos/modules/services/misc/exhibitor.nix
@@ -185,7 +185,7 @@ in
       };
       zkExtraCfg = mkOption {
         type = types.str;
-        default = ''initLimit=5&syncLimit=2&tickTime=2000'';
+        default = "initLimit=5&syncLimit=2&tickTime=2000";
         description = ''
           Extra options to pass into Zookeeper
         '';
diff --git a/nixos/modules/services/misc/felix.nix b/nixos/modules/services/misc/felix.nix
index 21740c8c0b7..8d438bb9eb1 100644
--- a/nixos/modules/services/misc/felix.nix
+++ b/nixos/modules/services/misc/felix.nix
@@ -27,11 +27,13 @@ in
       };
 
       user = mkOption {
+        type = types.str;
         default = "osgi";
         description = "User account under which Apache Felix runs.";
       };
 
       group = mkOption {
+        type = types.str;
         default = "osgi";
         description = "Group account under which Apache Felix runs.";
       };
diff --git a/nixos/modules/services/misc/fstrim.nix b/nixos/modules/services/misc/fstrim.nix
index b8841a7fe74..a9fc04b46f0 100644
--- a/nixos/modules/services/misc/fstrim.nix
+++ b/nixos/modules/services/misc/fstrim.nix
@@ -31,7 +31,7 @@ in {
 
   config = mkIf cfg.enable {
 
-    systemd.packages = [ pkgs.utillinux ];
+    systemd.packages = [ pkgs.util-linux ];
 
     systemd.timers.fstrim = {
       timerConfig = {
@@ -42,5 +42,5 @@ in {
 
   };
 
-  meta.maintainers = with maintainers; [ gnidorah ];
+  meta.maintainers = with maintainers; [ ];
 }
diff --git a/nixos/modules/services/misc/gammu-smsd.nix b/nixos/modules/services/misc/gammu-smsd.nix
index 3057d7fd1a0..552725f1384 100644
--- a/nixos/modules/services/misc/gammu-smsd.nix
+++ b/nixos/modules/services/misc/gammu-smsd.nix
@@ -172,7 +172,7 @@ in {
           };
 
           database = mkOption {
-            type = types.str;
+            type = types.nullOr types.str;
             default = null;
             description = "Database name to store sms data";
           };
diff --git a/nixos/modules/services/misc/geoip-updater.nix b/nixos/modules/services/misc/geoip-updater.nix
deleted file mode 100644
index baf0a8d73d1..00000000000
--- a/nixos/modules/services/misc/geoip-updater.nix
+++ /dev/null
@@ -1,306 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.geoip-updater;
-
-  dbBaseUrl = "https://geolite.maxmind.com/download/geoip/database";
-
-  randomizedTimerDelaySec = "3600";
-
-  # Use writeScriptBin instead of writeScript, so that argv[0] (logged to the
-  # journal) doesn't include the long nix store path hash. (Prefixing the
-  # ExecStart= command with '@' doesn't work because we start a shell (new
-  # process) that creates a new argv[0].)
-  geoip-updater = pkgs.writeScriptBin "geoip-updater" ''
-    #!${pkgs.runtimeShell}
-    skipExisting=0
-    debug()
-    {
-        echo "<7>$@"
-    }
-    info()
-    {
-        echo "<6>$@"
-    }
-    error()
-    {
-        echo "<3>$@"
-    }
-    die()
-    {
-        error "$@"
-        exit 1
-    }
-    waitNetworkOnline()
-    {
-        ret=1
-        for i in $(seq 6); do
-            curl_out=$("${pkgs.curl.bin}/bin/curl" \
-                --silent --fail --show-error --max-time 60 "${dbBaseUrl}" 2>&1)
-            if [ $? -eq 0 ]; then
-                debug "Server is reachable (try $i)"
-                ret=0
-                break
-            else
-                debug "Server is unreachable (try $i): $curl_out"
-                sleep 10
-            fi
-        done
-        return $ret
-    }
-    dbFnameTmp()
-    {
-        dburl=$1
-        echo "${cfg.databaseDir}/.$(basename "$dburl")"
-    }
-    dbFnameTmpDecompressed()
-    {
-        dburl=$1
-        echo "${cfg.databaseDir}/.$(basename "$dburl")" | sed 's/\.\(gz\|xz\)$//'
-    }
-    dbFname()
-    {
-        dburl=$1
-        echo "${cfg.databaseDir}/$(basename "$dburl")" | sed 's/\.\(gz\|xz\)$//'
-    }
-    downloadDb()
-    {
-        dburl=$1
-        curl_out=$("${pkgs.curl.bin}/bin/curl" \
-            --silent --fail --show-error --max-time 900 -L -o "$(dbFnameTmp "$dburl")" "$dburl" 2>&1)
-        if [ $? -ne 0 ]; then
-            error "Failed to download $dburl: $curl_out"
-            return 1
-        fi
-    }
-    decompressDb()
-    {
-        fn=$(dbFnameTmp "$1")
-        ret=0
-        case "$fn" in
-            *.gz)
-                cmd_out=$("${pkgs.gzip}/bin/gzip" --decompress --force "$fn" 2>&1)
-                ;;
-            *.xz)
-                cmd_out=$("${pkgs.xz.bin}/bin/xz" --decompress --force "$fn" 2>&1)
-                ;;
-            *)
-                cmd_out=$(echo "File \"$fn\" is neither a .gz nor .xz file")
-                false
-                ;;
-        esac
-        if [ $? -ne 0 ]; then
-            error "$cmd_out"
-            ret=1
-        fi
-    }
-    atomicRename()
-    {
-        dburl=$1
-        mv "$(dbFnameTmpDecompressed "$dburl")" "$(dbFname "$dburl")"
-    }
-    removeIfNotInConfig()
-    {
-        # Arg 1 is the full path of an installed DB.
-        # If the corresponding database is not specified in the NixOS config we
-        # remove it.
-        db=$1
-        for cdb in ${lib.concatStringsSep " " cfg.databases}; do
-            confDb=$(echo "$cdb" | sed 's/\.\(gz\|xz\)$//')
-            if [ "$(basename "$db")" = "$(basename "$confDb")" ]; then
-                return 0
-            fi
-        done
-        rm "$db"
-        if [ $? -eq 0 ]; then
-            debug "Removed $(basename "$db") (not listed in services.geoip-updater.databases)"
-        else
-            error "Failed to remove $db"
-        fi
-    }
-    removeUnspecifiedDbs()
-    {
-        for f in "${cfg.databaseDir}/"*; do
-            test -f "$f" || continue
-            case "$f" in
-                *.dat|*.mmdb|*.csv)
-                    removeIfNotInConfig "$f"
-                    ;;
-                *)
-                    debug "Not removing \"$f\" (unknown file extension)"
-                    ;;
-            esac
-        done
-    }
-    downloadAndInstall()
-    {
-        dburl=$1
-        if [ "$skipExisting" -eq 1 -a -f "$(dbFname "$dburl")" ]; then
-            debug "Skipping existing file: $(dbFname "$dburl")"
-            return 0
-        fi
-        downloadDb "$dburl" || return 1
-        decompressDb "$dburl" || return 1
-        atomicRename "$dburl" || return 1
-        info "Updated $(basename "$(dbFname "$dburl")")"
-    }
-    for arg in "$@"; do
-        case "$arg" in
-            --skip-existing)
-                skipExisting=1
-                info "Option --skip-existing is set: not updating existing databases"
-                ;;
-            *)
-                error "Unknown argument: $arg";;
-        esac
-    done
-    waitNetworkOnline || die "Network is down (${dbBaseUrl} is unreachable)"
-    test -d "${cfg.databaseDir}" || die "Database directory (${cfg.databaseDir}) doesn't exist"
-    debug "Starting update of GeoIP databases in ${cfg.databaseDir}"
-    all_ret=0
-    for db in ${lib.concatStringsSep " \\\n        " cfg.databases}; do
-        downloadAndInstall "${dbBaseUrl}/$db" || all_ret=1
-    done
-    removeUnspecifiedDbs || all_ret=1
-    if [ $all_ret -eq 0 ]; then
-        info "Completed GeoIP database update in ${cfg.databaseDir}"
-    else
-        error "Completed GeoIP database update in ${cfg.databaseDir}, with error(s)"
-    fi
-    # Hack to work around systemd journal race:
-    # https://github.com/systemd/systemd/issues/2913
-    sleep 2
-    exit $all_ret
-  '';
-
-in
-
-{
-  options = {
-    services.geoip-updater = {
-      enable = mkOption {
-        default = false;
-        type = types.bool;
-        description = ''
-          Whether to enable periodic downloading of GeoIP databases from
-          maxmind.com. You might want to enable this if you, for instance, use
-          ntopng or Wireshark.
-        '';
-      };
-
-      interval = mkOption {
-        type = types.str;
-        default = "weekly";
-        description = ''
-          Update the GeoIP databases at this time / interval.
-          The format is described in
-          <citerefentry><refentrytitle>systemd.time</refentrytitle>
-          <manvolnum>7</manvolnum></citerefentry>.
-          To prevent load spikes on maxmind.com, the timer interval is
-          randomized by an additional delay of ${randomizedTimerDelaySec}
-          seconds. Setting a shorter interval than this is not recommended.
-        '';
-      };
-
-      databaseDir = mkOption {
-        type = types.path;
-        default = "/var/lib/geoip-databases";
-        description = ''
-          Directory that will contain GeoIP databases.
-        '';
-      };
-
-      databases = mkOption {
-        type = types.listOf types.str;
-        default = [
-          "GeoLiteCountry/GeoIP.dat.gz"
-          "GeoIPv6.dat.gz"
-          "GeoLiteCity.dat.xz"
-          "GeoLiteCityv6-beta/GeoLiteCityv6.dat.gz"
-          "asnum/GeoIPASNum.dat.gz"
-          "asnum/GeoIPASNumv6.dat.gz"
-          "GeoLite2-Country.mmdb.gz"
-          "GeoLite2-City.mmdb.gz"
-        ];
-        description = ''
-          Which GeoIP databases to update. The full URL is ${dbBaseUrl}/ +
-          <literal>the_database</literal>.
-        '';
-      };
-
-    };
-
-  };
-
-  config = mkIf cfg.enable {
-
-    assertions = [
-      { assertion = (builtins.filter
-          (x: builtins.match ".*\\.(gz|xz)$" x == null) cfg.databases) == [];
-        message = ''
-          services.geoip-updater.databases supports only .gz and .xz databases.
-
-          Current value:
-          ${toString cfg.databases}
-
-          Offending element(s):
-          ${toString (builtins.filter (x: builtins.match ".*\\.(gz|xz)$" x == null) cfg.databases)};
-        '';
-      }
-    ];
-
-    users.users.geoip = {
-      group = "root";
-      description = "GeoIP database updater";
-      uid = config.ids.uids.geoip;
-    };
-
-    systemd.timers.geoip-updater =
-      { description = "GeoIP Updater Timer";
-        partOf = [ "geoip-updater.service" ];
-        wantedBy = [ "timers.target" ];
-        timerConfig.OnCalendar = cfg.interval;
-        timerConfig.Persistent = "true";
-        timerConfig.RandomizedDelaySec = randomizedTimerDelaySec;
-      };
-
-    systemd.services.geoip-updater = {
-      description = "GeoIP Updater";
-      after = [ "network-online.target" "nss-lookup.target" ];
-      wants = [ "network-online.target" ];
-      preStart = ''
-        mkdir -p "${cfg.databaseDir}"
-        chmod 755 "${cfg.databaseDir}"
-        chown geoip:root "${cfg.databaseDir}"
-      '';
-      serviceConfig = {
-        ExecStart = "${geoip-updater}/bin/geoip-updater";
-        User = "geoip";
-        PermissionsStartOnly = true;
-      };
-    };
-
-    systemd.services.geoip-updater-setup = {
-      description = "GeoIP Updater Setup";
-      after = [ "network-online.target" "nss-lookup.target" ];
-      wants = [ "network-online.target" ];
-      wantedBy = [ "multi-user.target" ];
-      conflicts = [ "geoip-updater.service" ];
-      preStart = ''
-        mkdir -p "${cfg.databaseDir}"
-        chmod 755 "${cfg.databaseDir}"
-        chown geoip:root "${cfg.databaseDir}"
-      '';
-      serviceConfig = {
-        ExecStart = "${geoip-updater}/bin/geoip-updater --skip-existing";
-        User = "geoip";
-        PermissionsStartOnly = true;
-        # So it won't be (needlessly) restarted:
-        RemainAfterExit = true;
-      };
-    };
-
-  };
-}
diff --git a/nixos/modules/services/misc/geoipupdate.nix b/nixos/modules/services/misc/geoipupdate.nix
new file mode 100644
index 00000000000..3211d4d88e4
--- /dev/null
+++ b/nixos/modules/services/misc/geoipupdate.nix
@@ -0,0 +1,187 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.geoipupdate;
+in
+{
+  imports = [
+    (lib.mkRemovedOptionModule [ "services" "geoip-updater" ] "services.geoip-updater has been removed, use services.geoipupdate instead.")
+  ];
+
+  options = {
+    services.geoipupdate = {
+      enable = lib.mkEnableOption ''
+        periodic downloading of GeoIP databases using
+        <productname>geoipupdate</productname>.
+      '';
+
+      interval = lib.mkOption {
+        type = lib.types.str;
+        default = "weekly";
+        description = ''
+          Update the GeoIP databases at this time / interval.
+          The format is described in
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>.
+        '';
+      };
+
+      settings = lib.mkOption {
+        description = ''
+          <productname>geoipupdate</productname> configuration
+          options. See
+          <link xlink:href="https://github.com/maxmind/geoipupdate/blob/main/doc/GeoIP.conf.md" />
+          for a full list of available options.
+        '';
+        type = lib.types.submodule {
+          freeformType =
+            with lib.types;
+            let
+              type = oneOf [str int bool];
+            in
+              attrsOf (either type (listOf type));
+
+          options = {
+
+            AccountID = lib.mkOption {
+              type = lib.types.int;
+              description = ''
+                Your MaxMind account ID.
+              '';
+            };
+
+            EditionIDs = lib.mkOption {
+              type = with lib.types; listOf (either str int);
+              example = [
+                "GeoLite2-ASN"
+                "GeoLite2-City"
+                "GeoLite2-Country"
+              ];
+              description = ''
+                List of database edition IDs. This includes new string
+                IDs like <literal>GeoIP2-City</literal> and old
+                numeric IDs like <literal>106</literal>.
+              '';
+            };
+
+            LicenseKey = lib.mkOption {
+              type = lib.types.path;
+              description = ''
+                A file containing the <productname>MaxMind</productname>
+                license key.
+              '';
+            };
+
+            DatabaseDirectory = lib.mkOption {
+              type = lib.types.path;
+              default = "/var/lib/GeoIP";
+              example = "/run/GeoIP";
+              description = ''
+                The directory to store the database files in. The
+                directory will be automatically created, the owner
+                changed to <literal>geoip</literal> and permissions
+                set to world readable. This applies if the directory
+                already exists as well, so don't use a directory with
+                sensitive contents.
+              '';
+            };
+
+          };
+        };
+      };
+    };
+
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    services.geoipupdate.settings = {
+      LockFile = "/run/geoipupdate/.lock";
+    };
+
+    systemd.services.geoipupdate-create-db-dir = {
+      serviceConfig.Type = "oneshot";
+      script = ''
+        mkdir -p ${cfg.settings.DatabaseDirectory}
+        chmod 0755 ${cfg.settings.DatabaseDirectory}
+      '';
+    };
+
+    systemd.services.geoipupdate = {
+      description = "GeoIP Updater";
+      requires = [ "geoipupdate-create-db-dir.service" ];
+      after = [
+        "geoipupdate-create-db-dir.service"
+        "network-online.target"
+        "nss-lookup.target"
+      ];
+      wants = [ "network-online.target" ];
+      startAt = cfg.interval;
+      serviceConfig = {
+        ExecStartPre =
+          let
+            geoipupdateKeyValue = lib.generators.toKeyValue {
+              mkKeyValue = lib.flip lib.generators.mkKeyValueDefault " " rec {
+                mkValueString = v: with builtins;
+                  if isInt           v then toString v
+                  else if isString   v then v
+                  else if true  ==   v then "1"
+                  else if false ==   v then "0"
+                  else if isList     v then lib.concatMapStringsSep " " mkValueString v
+                  else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
+              };
+            };
+
+            geoipupdateConf = pkgs.writeText "geoipupdate.conf" (geoipupdateKeyValue cfg.settings);
+
+            script = ''
+              chown geoip "${cfg.settings.DatabaseDirectory}"
+
+              cp ${geoipupdateConf} /run/geoipupdate/GeoIP.conf
+              ${pkgs.replace-secret}/bin/replace-secret '${cfg.settings.LicenseKey}' \
+                                                        '${cfg.settings.LicenseKey}' \
+                                                        /run/geoipupdate/GeoIP.conf
+            '';
+          in
+            "+${pkgs.writeShellScript "start-pre-full-privileges" script}";
+        ExecStart = "${pkgs.geoipupdate}/bin/geoipupdate -f /run/geoipupdate/GeoIP.conf";
+        User = "geoip";
+        DynamicUser = true;
+        ReadWritePaths = cfg.settings.DatabaseDirectory;
+        RuntimeDirectory = "geoipupdate";
+        RuntimeDirectoryMode = 0700;
+        CapabilityBoundingSet = "";
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
+        SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+        RestrictNamespaces = true;
+        MemoryDenyWriteExecute = true;
+        LockPersonality = true;
+        SystemCallArchitectures = "native";
+      };
+    };
+
+    systemd.timers.geoipupdate-initial-run = {
+      wantedBy = [ "timers.target" ];
+      unitConfig.ConditionPathExists = "!${cfg.settings.DatabaseDirectory}";
+      timerConfig = {
+        Unit = "geoipupdate.service";
+        OnActiveSec = 0;
+      };
+    };
+  };
+
+  meta.maintainers = [ lib.maintainers.talyz ];
+}
diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix
index af80e99746b..b6c1ca3e61a 100644
--- a/nixos/modules/services/misc/gitea.nix
+++ b/nixos/modules/services/misc/gitea.nix
@@ -82,7 +82,7 @@ in
         };
 
         port = mkOption {
-          type = types.int;
+          type = types.port;
           default = (if !usePostgresql then 3306 else pg.port);
           description = "Database host port.";
         };
@@ -349,7 +349,7 @@ in
         {
           DOMAIN = cfg.domain;
           STATIC_ROOT_PATH = cfg.staticRootPath;
-          LFS_JWT_SECRET = "#jwtsecret#";
+          LFS_JWT_SECRET = "#lfsjwtsecret#";
           ROOT_URL = cfg.rootUrl;
         }
         (mkIf cfg.enableUnixSocket {
@@ -381,6 +381,7 @@ in
 
       security = {
         SECRET_KEY = "#secretkey#";
+        INTERNAL_TOKEN = "#internaltoken#";
         INSTALL_LOCK = true;
       };
 
@@ -396,6 +397,10 @@ in
       mailer = mkIf (cfg.mailerPasswordFile != null) {
         PASSWD = "#mailerpass#";
       };
+
+      oauth2 = {
+        JWT_SECRET = "#oauth2jwtsecret#";
+      };
     };
 
     services.postgresql = optionalAttrs (usePostgresql && cfg.database.createDatabase) {
@@ -453,39 +458,68 @@ in
       description = "gitea";
       after = [ "network.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
       wantedBy = [ "multi-user.target" ];
-      path = [ gitea pkgs.gitAndTools.git ];
-
+      path = [ gitea pkgs.git ];
+
+      # In older versions the secret naming for JWT was kind of confusing.
+      # The file jwt_secret hold the value for LFS_JWT_SECRET and JWT_SECRET
+      # wasn't persistant at all.
+      # To fix that, there is now the file oauth2_jwt_secret containing the
+      # values for JWT_SECRET and the file jwt_secret gets renamed to
+      # lfs_jwt_secret.
+      # We have to consider this to stay compatible with older installations.
       preStart = let
         runConfig = "${cfg.stateDir}/custom/conf/app.ini";
         secretKey = "${cfg.stateDir}/custom/conf/secret_key";
-        jwtSecret = "${cfg.stateDir}/custom/conf/jwt_secret";
+        oauth2JwtSecret = "${cfg.stateDir}/custom/conf/oauth2_jwt_secret";
+        oldLfsJwtSecret = "${cfg.stateDir}/custom/conf/jwt_secret"; # old file for LFS_JWT_SECRET
+        lfsJwtSecret = "${cfg.stateDir}/custom/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET
+        internalToken = "${cfg.stateDir}/custom/conf/internal_token";
       in ''
         # copy custom configuration and generate a random secret key if needed
         ${optionalString (cfg.useWizard == false) ''
-          cp -f ${configFile} ${runConfig}
-
-          if [ ! -e ${secretKey} ]; then
-              ${gitea}/bin/gitea generate secret SECRET_KEY > ${secretKey}
-          fi
-
-          if [ ! -e ${jwtSecret} ]; then
-              ${gitea}/bin/gitea generate secret LFS_JWT_SECRET > ${jwtSecret}
-          fi
-
-          KEY="$(head -n1 ${secretKey})"
-          DBPASS="$(head -n1 ${cfg.database.passwordFile})"
-          JWTSECRET="$(head -n1 ${jwtSecret})"
-          ${if (cfg.mailerPasswordFile == null) then ''
-            MAILERPASSWORD="#mailerpass#"
-          '' else ''
-            MAILERPASSWORD="$(head -n1 ${cfg.mailerPasswordFile} || :)"
-          ''}
-          sed -e "s,#secretkey#,$KEY,g" \
-              -e "s,#dbpass#,$DBPASS,g" \
-              -e "s,#jwtsecret#,$JWTSECRET,g" \
-              -e "s,#mailerpass#,$MAILERPASSWORD,g" \
-              -i ${runConfig}
-          chmod 640 ${runConfig} ${secretKey} ${jwtSecret}
+          function gitea_setup {
+            cp -f ${configFile} ${runConfig}
+
+            if [ ! -e ${secretKey} ]; then
+                ${gitea}/bin/gitea generate secret SECRET_KEY > ${secretKey}
+            fi
+
+            # Migrate LFS_JWT_SECRET filename
+            if [[ -e ${oldLfsJwtSecret} && ! -e ${lfsJwtSecret} ]]; then
+                mv ${oldLfsJwtSecret} ${lfsJwtSecret}
+            fi
+
+            if [ ! -e ${oauth2JwtSecret} ]; then
+                ${gitea}/bin/gitea generate secret JWT_SECRET > ${oauth2JwtSecret}
+            fi
+
+            if [ ! -e ${lfsJwtSecret} ]; then
+                ${gitea}/bin/gitea generate secret LFS_JWT_SECRET > ${lfsJwtSecret}
+            fi
+
+            if [ ! -e ${internalToken} ]; then
+                ${gitea}/bin/gitea generate secret INTERNAL_TOKEN > ${internalToken}
+            fi
+
+            SECRETKEY="$(head -n1 ${secretKey})"
+            DBPASS="$(head -n1 ${cfg.database.passwordFile})"
+            OAUTH2JWTSECRET="$(head -n1 ${oauth2JwtSecret})"
+            LFSJWTSECRET="$(head -n1 ${lfsJwtSecret})"
+            INTERNALTOKEN="$(head -n1 ${internalToken})"
+            ${if (cfg.mailerPasswordFile == null) then ''
+              MAILERPASSWORD="#mailerpass#"
+            '' else ''
+              MAILERPASSWORD="$(head -n1 ${cfg.mailerPasswordFile} || :)"
+            ''}
+            sed -e "s,#secretkey#,$SECRETKEY,g" \
+                -e "s,#dbpass#,$DBPASS,g" \
+                -e "s,#oauth2jwtsecret#,$OAUTH2JWTSECRET,g" \
+                -e "s,#lfsjwtsecret#,$LFSJWTSECRET,g" \
+                -e "s,#internaltoken#,$INTERNALTOKEN,g" \
+                -e "s,#mailerpass#,$MAILERPASSWORD,g" \
+                -i ${runConfig}
+          }
+          (umask 027; gitea_setup)
         ''}
 
         # update all hooks' binary paths
@@ -565,8 +599,7 @@ in
     users.groups.gitea = {};
 
     warnings =
-      optional (cfg.database.password != "") ''
-        config.services.gitea.database.password will be stored as plaintext in the Nix store. Use database.passwordFile instead.'' ++
+      optional (cfg.database.password != "") "config.services.gitea.database.password will be stored as plaintext in the Nix store. Use database.passwordFile instead." ++
       optional (cfg.extraConfig != null) ''
         services.gitea.`extraConfig` is deprecated, please use services.gitea.`settings`.
       '';
@@ -605,5 +638,5 @@ in
       timerConfig.OnCalendar = cfg.dump.interval;
     };
   };
-  meta.maintainers = with lib.maintainers; [ srhb ];
+  meta.maintainers = with lib.maintainers; [ srhb ma27 ];
 }
diff --git a/nixos/modules/services/misc/gitit.nix b/nixos/modules/services/misc/gitit.nix
index 1ec030549f9..f09565283f3 100644
--- a/nixos/modules/services/misc/gitit.nix
+++ b/nixos/modules/services/misc/gitit.nix
@@ -42,6 +42,7 @@ let
       };
 
       extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
         default = self: [];
         example = literalExample ''
           haskellPackages: [
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix
index 425f35f37cb..1514cc0665d 100644
--- a/nixos/modules/services/misc/gitlab.nix
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -10,7 +10,7 @@ let
   postgresqlPackage = if config.services.postgresql.enable then
                         config.services.postgresql.package
                       else
-                        pkgs.postgresql;
+                        pkgs.postgresql_12;
 
   gitlabSocket = "${cfg.statePath}/tmp/sockets/gitlab.socket";
   gitalySocket = "${cfg.statePath}/tmp/sockets/gitaly.socket";
@@ -43,9 +43,16 @@ let
 
     [gitlab-shell]
     dir = "${cfg.packages.gitlab-shell}"
+
+    [hooks]
+    custom_hooks_dir = "${cfg.statePath}/custom_hooks"
+
+    [gitlab]
     secret_file = "${cfg.statePath}/gitlab_shell_secret"
-    gitlab_url = "http+unix://${pathUrlQuote gitlabSocket}"
-    http_settings = { self_signed_cert = false }
+    url = "http+unix://${pathUrlQuote gitlabSocket}"
+
+    [gitlab.http-settings]
+    self_signed_cert = false
 
     ${concatStringsSep "\n" (attrValues (mapAttrs (k: v: ''
     [[storage]]
@@ -61,7 +68,6 @@ let
     repos_path = "${cfg.statePath}/repositories";
     secret_file = "${cfg.statePath}/gitlab_shell_secret";
     log_file = "${cfg.statePath}/log/gitlab-shell.log";
-    custom_hooks_dir = "${cfg.statePath}/custom_hooks";
     redis = {
       bin = "${pkgs.redis}/bin/redis-cli";
       host = "127.0.0.1";
@@ -73,6 +79,11 @@ let
 
   redisConfig.production.url = cfg.redisUrl;
 
+  pagesArgs = [
+    "-pages-domain" gitlabConfig.production.pages.host
+    "-pages-root" "${gitlabConfig.production.shared.path}/pages"
+  ] ++ cfg.pagesExtraArgs;
+
   gitlabConfig = {
     # These are the default settings from config/gitlab.example.yml
     production = flip recursiveUpdate cfg.extraConfig {
@@ -105,7 +116,12 @@ let
       omniauth.enabled = false;
       shared.path = "${cfg.statePath}/shared";
       gitaly.client_path = "${cfg.packages.gitaly}/bin";
-      backup.path = "${cfg.backupPath}";
+      backup = {
+        path = cfg.backup.path;
+        keep_time = cfg.backup.keepTime;
+      } // (optionalAttrs (cfg.backup.uploadOptions != {}) {
+        upload = cfg.backup.uploadOptions;
+      });
       gitlab_shell = {
         path = "${cfg.packages.gitlab-shell}";
         hooks_path = "${cfg.statePath}/shell/hooks";
@@ -114,6 +130,7 @@ let
         receive_pack = true;
       };
       workhorse.secret_file = "${cfg.statePath}/.gitlab_workhorse_secret";
+      gitlab_kas.secret_file = "${cfg.statePath}/.gitlab_kas_secret";
       git.bin_path = "git";
       monitoring = {
         ip_whitelist = [ "127.0.0.0/8" "::1/128" ];
@@ -123,14 +140,22 @@ let
           port = 3807;
         };
       };
+      registry = lib.optionalAttrs cfg.registry.enable {
+        enabled = true;
+        host = cfg.registry.externalAddress;
+        port = cfg.registry.externalPort;
+        key = cfg.registry.keyFile;
+        api_url = "http://${config.services.dockerRegistry.listenAddress}:${toString config.services.dockerRegistry.port}/";
+        issuer = "gitlab-issuer";
+      };
       extra = {};
       uploads.storage_path = cfg.statePath;
     };
   };
 
-  gitlabEnv = {
+  gitlabEnv = cfg.packages.gitlab.gitlabEnv // {
     HOME = "${cfg.statePath}/home";
-    UNICORN_PATH = "${cfg.statePath}/";
+    PUMA_PATH = "${cfg.statePath}/";
     GITLAB_PATH = "${cfg.packages.gitlab}/share/gitlab/";
     SCHEMA = "${cfg.statePath}/db/structure.sql";
     GITLAB_UPLOADS_PATH = "${cfg.statePath}/uploads";
@@ -138,7 +163,8 @@ let
     GITLAB_REDIS_CONFIG_FILE = pkgs.writeText "redis.yml" (builtins.toJSON redisConfig);
     prometheus_multiproc_dir = "/run/gitlab";
     RAILS_ENV = "production";
-  };
+    MALLOC_ARENA_MAX = "2";
+  } // cfg.extraEnv;
 
   gitlab-rake = pkgs.stdenv.mkDerivation {
     name = "gitlab-rake";
@@ -184,6 +210,7 @@ let
         domain: "${cfg.smtp.domain}",
         ${optionalString (cfg.smtp.authentication != null) "authentication: :${cfg.smtp.authentication},"}
         enable_starttls_auto: ${boolToString cfg.smtp.enableStartTLSAuto},
+        tls: ${boolToString cfg.smtp.tls},
         ca_file: "/etc/ssl/certs/ca-certificates.crt",
         openssl_verify_mode: '${cfg.smtp.opensslVerifyMode}'
       }
@@ -194,6 +221,7 @@ in {
 
   imports = [
     (mkRenamedOptionModule [ "services" "gitlab" "stateDir" ] [ "services" "gitlab" "statePath" ])
+    (mkRenamedOptionModule [ "services" "gitlab" "backupPath" ] [ "services" "gitlab" "backup" "path" ])
     (mkRemovedOptionModule [ "services" "gitlab" "satelliteDir" ] "")
   ];
 
@@ -236,11 +264,18 @@ in {
         description = "Reference to the gitaly package";
       };
 
+      packages.pages = mkOption {
+        type = types.package;
+        default = pkgs.gitlab-pages;
+        defaultText = "pkgs.gitlab-pages";
+        description = "Reference to the gitlab-pages package";
+      };
+
       statePath = mkOption {
         type = types.str;
         default = "/var/gitlab/state";
         description = ''
-          Gitlab state directory. Configuration, repositories and
+          GitLab state directory. Configuration, repositories and
           logs, among other things, are stored here.
 
           The directory will be created automatically if it doesn't
@@ -250,17 +285,116 @@ in {
         '';
       };
 
-      backupPath = mkOption {
+      extraEnv = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        description = ''
+          Additional environment variables for the GitLab environment.
+        '';
+      };
+
+      backup.startAt = mkOption {
+        type = with types; either str (listOf str);
+        default = [];
+        example = "03:00";
+        description = ''
+          The time(s) to run automatic backup of GitLab
+          state. Specified in systemd's time format; see
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>.
+        '';
+      };
+
+      backup.path = mkOption {
         type = types.str;
         default = cfg.statePath + "/backup";
-        description = "Gitlab path for backups.";
+        description = "GitLab path for backups.";
+      };
+
+      backup.keepTime = mkOption {
+        type = types.int;
+        default = 0;
+        example = 48;
+        apply = x: x * 60 * 60;
+        description = ''
+          How long to keep the backups around, in
+          hours. <literal>0</literal> means <quote>keep
+          forever</quote>.
+        '';
+      };
+
+      backup.skip = mkOption {
+        type = with types;
+          let value = enum [
+                "db"
+                "uploads"
+                "builds"
+                "artifacts"
+                "lfs"
+                "registry"
+                "pages"
+                "repositories"
+                "tar"
+              ];
+          in
+            either value (listOf value);
+        default = [];
+        example = [ "artifacts" "lfs" ];
+        apply = x: if isString x then x else concatStringsSep "," x;
+        description = ''
+          Directories to exclude from the backup. The example excludes
+          CI artifacts and LFS objects from the backups. The
+          <literal>tar</literal> option skips the creation of a tar
+          file.
+
+          Refer to <link xlink:href="https://docs.gitlab.com/ee/raketasks/backup_restore.html#excluding-specific-directories-from-the-backup"/>
+          for more information.
+        '';
+      };
+
+      backup.uploadOptions = mkOption {
+        type = types.attrs;
+        default = {};
+        example = literalExample ''
+          {
+            # Fog storage connection settings, see http://fog.io/storage/
+            connection = {
+              provider = "AWS";
+              region = "eu-north-1";
+              aws_access_key_id = "AKIAXXXXXXXXXXXXXXXX";
+              aws_secret_access_key = { _secret = config.deployment.keys.aws_access_key.path; };
+            };
+
+            # The remote 'directory' to store your backups in.
+            # For S3, this would be the bucket name.
+            remote_directory = "my-gitlab-backups";
+
+            # Use multipart uploads when file size reaches 100MB, see
+            # http://docs.aws.amazon.com/AmazonS3/latest/dev/uploadobjusingmpu.html
+            multipart_chunk_size = 104857600;
+
+            # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
+            encryption = "AES256";
+
+            # Specifies Amazon S3 storage class to use for backups, this is optional
+            storage_class = "STANDARD";
+          };
+        '';
+        description = ''
+          GitLab automatic upload specification. Tells GitLab to
+          upload the backup to a remote location when done.
+
+          Attributes specified here are added under
+          <literal>production -> backup -> upload</literal> in
+          <filename>config/gitlab.yml</filename>.
+        '';
       };
 
       databaseHost = mkOption {
         type = types.str;
         default = "";
         description = ''
-          Gitlab database hostname. An empty string means <quote>use
+          GitLab database hostname. An empty string means <quote>use
           local unix socket connection</quote>.
         '';
       };
@@ -269,7 +403,7 @@ in {
         type = with types; nullOr path;
         default = null;
         description = ''
-          File containing the Gitlab database user password.
+          File containing the GitLab database user password.
 
           This should be a string, not a nix path, since nix paths are
           copied into the world-readable nix store.
@@ -290,13 +424,13 @@ in {
       databaseName = mkOption {
         type = types.str;
         default = "gitlab";
-        description = "Gitlab database name.";
+        description = "GitLab database name.";
       };
 
       databaseUsername = mkOption {
         type = types.str;
         default = "gitlab";
-        description = "Gitlab database user.";
+        description = "GitLab database user.";
       };
 
       databasePool = mkOption {
@@ -340,14 +474,14 @@ in {
       host = mkOption {
         type = types.str;
         default = config.networking.hostName;
-        description = "Gitlab host name. Used e.g. for copy-paste URLs.";
+        description = "GitLab host name. Used e.g. for copy-paste URLs.";
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 8080;
         description = ''
-          Gitlab server port for copy-paste URLs, e.g. 80 or 443 if you're
+          GitLab server port for copy-paste URLs, e.g. 80 or 443 if you're
           service over https.
         '';
       };
@@ -390,6 +524,58 @@ in {
         '';
       };
 
+      registry = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Enable GitLab container registry.";
+        };
+        host = mkOption {
+          type = types.str;
+          default = config.services.gitlab.host;
+          description = "GitLab container registry host name.";
+        };
+        port = mkOption {
+          type = types.int;
+          default = 4567;
+          description = "GitLab container registry port.";
+        };
+        certFile = mkOption {
+          type = types.path;
+          default = null;
+          description = "Path to GitLab container registry certificate.";
+        };
+        keyFile = mkOption {
+          type = types.path;
+          default = null;
+          description = "Path to GitLab container registry certificate-key.";
+        };
+        defaultForProjects = mkOption {
+          type = types.bool;
+          default = cfg.registry.enable;
+          description = "If GitLab container registry should be enabled by default for projects.";
+        };
+        issuer = mkOption {
+          type = types.str;
+          default = "gitlab-issuer";
+          description = "GitLab container registry issuer.";
+        };
+        serviceName = mkOption {
+          type = types.str;
+          default = "container_registry";
+          description = "GitLab container registry service name.";
+        };
+        externalAddress = mkOption {
+          type = types.str;
+          default = "";
+          description = "External address used to access registry from the internet";
+        };
+        externalPort = mkOption {
+          type = types.int;
+          description = "External port used to access registry from the internet";
+        };
+      };
+
       smtp = {
         enable = mkOption {
           type = types.bool;
@@ -400,26 +586,26 @@ in {
         address = mkOption {
           type = types.str;
           default = "localhost";
-          description = "Address of the SMTP server for Gitlab.";
+          description = "Address of the SMTP server for GitLab.";
         };
 
         port = mkOption {
           type = types.int;
-          default = 465;
-          description = "Port of the SMTP server for Gitlab.";
+          default = 25;
+          description = "Port of the SMTP server for GitLab.";
         };
 
         username = mkOption {
           type = with types; nullOr str;
           default = null;
-          description = "Username of the SMTP server for Gitlab.";
+          description = "Username of the SMTP server for GitLab.";
         };
 
         passwordFile = mkOption {
           type = types.nullOr types.path;
           default = null;
           description = ''
-            File containing the password of the SMTP server for Gitlab.
+            File containing the password of the SMTP server for GitLab.
 
             This should be a string, not a nix path, since nix paths
             are copied into the world-readable nix store.
@@ -435,7 +621,7 @@ in {
         authentication = mkOption {
           type = with types; nullOr str;
           default = null;
-          description = "Authentitcation type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
+          description = "Authentication type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
         };
 
         enableStartTLSAuto = mkOption {
@@ -444,6 +630,12 @@ in {
           description = "Whether to try to use StartTLS.";
         };
 
+        tls = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Whether to use TLS wrapper-mode.";
+        };
+
         opensslVerifyMode = mkOption {
           type = types.str;
           default = "peer";
@@ -451,6 +643,12 @@ in {
         };
       };
 
+      pagesExtraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [ "-listen-proxy" "127.0.0.1:8090" ];
+        description = "Arguments to pass to the gitlab-pages daemon";
+      };
+
       secrets.secretFile = mkOption {
         type = with types; nullOr path;
         default = null;
@@ -459,7 +657,7 @@ in {
           the DB. If you change or lose this key you will be unable to
           access variables stored in database.
 
-          Make sure the secret is at least 30 characters and all random,
+          Make sure the secret is at least 32 characters and all random,
           no regular words or you'll be exposed to dictionary attacks.
 
           This should be a string, not a nix path, since nix paths are
@@ -475,7 +673,7 @@ in {
           the DB. If you change or lose this key you will be unable to
           access variables stored in database.
 
-          Make sure the secret is at least 30 characters and all random,
+          Make sure the secret is at least 32 characters and all random,
           no regular words or you'll be exposed to dictionary attacks.
 
           This should be a string, not a nix path, since nix paths are
@@ -491,7 +689,7 @@ in {
           tokens. If you change or lose this key, users which have 2FA
           enabled for login won't be able to login anymore.
 
-          Make sure the secret is at least 30 characters and all random,
+          Make sure the secret is at least 32 characters and all random,
           no regular words or you'll be exposed to dictionary attacks.
 
           This should be a string, not a nix path, since nix paths are
@@ -523,6 +721,105 @@ in {
         description = "Extra configuration to merge into shell-config.yml";
       };
 
+      puma.workers = mkOption {
+        type = types.int;
+        default = 2;
+        apply = x: builtins.toString x;
+        description = ''
+          The number of worker processes Puma should spawn. This
+          controls the amount of parallel Ruby code can be
+          executed. GitLab recommends <quote>Number of CPU cores -
+          1</quote>, but at least two.
+
+          <note>
+            <para>
+              Each worker consumes quite a bit of memory, so
+              be careful when increasing this.
+            </para>
+          </note>
+        '';
+      };
+
+      puma.threadsMin = mkOption {
+        type = types.int;
+        default = 0;
+        apply = x: builtins.toString x;
+        description = ''
+          The minimum number of threads Puma should use per
+          worker.
+
+          <note>
+            <para>
+              Each thread consumes memory and contributes to Global VM
+              Lock contention, so be careful when increasing this.
+            </para>
+          </note>
+        '';
+      };
+
+      puma.threadsMax = mkOption {
+        type = types.int;
+        default = 4;
+        apply = x: builtins.toString x;
+        description = ''
+          The maximum number of threads Puma should use per
+          worker. This limits how many threads Puma will automatically
+          spawn in response to requests. In contrast to workers,
+          threads will never be able to run Ruby code in parallel, but
+          give higher IO parallelism.
+
+          <note>
+            <para>
+              Each thread consumes memory and contributes to Global VM
+              Lock contention, so be careful when increasing this.
+            </para>
+          </note>
+        '';
+      };
+
+      sidekiq.memoryKiller.enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether the Sidekiq MemoryKiller should be turned
+          on. MemoryKiller kills Sidekiq when its memory consumption
+          exceeds a certain limit.
+
+          See <link xlink:href="https://docs.gitlab.com/ee/administration/operations/sidekiq_memory_killer.html"/>
+          for details.
+        '';
+      };
+
+      sidekiq.memoryKiller.maxMemory = mkOption {
+        type = types.int;
+        default = 2000;
+        apply = x: builtins.toString (x * 1024);
+        description = ''
+          The maximum amount of memory, in MiB, a Sidekiq worker is
+          allowed to consume before being killed.
+        '';
+      };
+
+      sidekiq.memoryKiller.graceTime = mkOption {
+        type = types.int;
+        default = 900;
+        apply = x: builtins.toString x;
+        description = ''
+          The time MemoryKiller waits after noticing excessive memory
+          consumption before killing Sidekiq.
+        '';
+      };
+
+      sidekiq.memoryKiller.shutdownWait = mkOption {
+        type = types.int;
+        default = 30;
+        apply = x: builtins.toString x;
+        description = ''
+          The time allowed for all jobs to finish before Sidekiq is
+          killed forcefully.
+        '';
+      };
+
       extraConfig = mkOption {
         type = types.attrs;
         default = {};
@@ -612,10 +909,19 @@ in {
         assertion = cfg.secrets.jwsFile != null;
         message = "services.gitlab.secrets.jwsFile must be set!";
       }
+      {
+        assertion = versionAtLeast postgresqlPackage.version "12.0.0";
+        message = "PostgreSQL >=12 is required to run GitLab 14. Follow the instructions in the manual section for upgrading PostgreSQL here: https://nixos.org/manual/nixos/stable/index.html#module-services-postgres-upgrading";
+      }
     ];
 
     environment.systemPackages = [ pkgs.git gitlab-rake gitlab-rails cfg.packages.gitlab-shell ];
 
+    systemd.targets.gitlab = {
+      description = "Common target for all GitLab services.";
+      wantedBy = [ "multi-user.target" ];
+    };
+
     # Redis is required for the sidekiq queue runner.
     services.redis.enable = mkDefault true;
 
@@ -630,35 +936,83 @@ in {
     # here.
     systemd.services.gitlab-postgresql = let pgsql = config.services.postgresql; in mkIf databaseActuallyCreateLocally {
       after = [ "postgresql.service" ];
-      wantedBy = [ "multi-user.target" ];
-      path = [ pgsql.package ];
+      bindsTo = [ "postgresql.service" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      path = [
+        pgsql.package
+        pkgs.util-linux
+      ];
       script = ''
         set -eu
 
-        PSQL="${pkgs.utillinux}/bin/runuser -u ${pgsql.superUser} -- psql --port=${toString pgsql.port}"
+        PSQL() {
+            psql --port=${toString pgsql.port} "$@"
+        }
 
-        $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${cfg.databaseName}'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "${cfg.databaseName}" OWNER "${cfg.databaseUsername}"'
-        current_owner=$($PSQL -tAc "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_catalog.pg_database WHERE datname = '${cfg.databaseName}'")
+        PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${cfg.databaseName}'" | grep -q 1 || PSQL -tAc 'CREATE DATABASE "${cfg.databaseName}" OWNER "${cfg.databaseUsername}"'
+        current_owner=$(PSQL -tAc "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_catalog.pg_database WHERE datname = '${cfg.databaseName}'")
         if [[ "$current_owner" != "${cfg.databaseUsername}" ]]; then
-            $PSQL -tAc 'ALTER DATABASE "${cfg.databaseName}" OWNER TO "${cfg.databaseUsername}"'
+            PSQL -tAc 'ALTER DATABASE "${cfg.databaseName}" OWNER TO "${cfg.databaseUsername}"'
             if [[ -e "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}" ]]; then
                 echo "Reassigning ownership of database ${cfg.databaseName} to user ${cfg.databaseUsername} failed on last boot. Failing..."
                 exit 1
             fi
             touch "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}"
-            $PSQL "${cfg.databaseName}" -tAc "REASSIGN OWNED BY \"$current_owner\" TO \"${cfg.databaseUsername}\""
+            PSQL "${cfg.databaseName}" -tAc "REASSIGN OWNED BY \"$current_owner\" TO \"${cfg.databaseUsername}\""
             rm "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}"
         fi
-        $PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm"
+        PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm"
+        PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS btree_gist;"
       '';
 
       serviceConfig = {
+        User = pgsql.superUser;
         Type = "oneshot";
+        RemainAfterExit = true;
+      };
+    };
+
+    systemd.services.gitlab-registry-cert = optionalAttrs cfg.registry.enable {
+      path = with pkgs; [ openssl ];
+
+      script = ''
+        mkdir -p $(dirname ${cfg.registry.keyFile})
+        mkdir -p $(dirname ${cfg.registry.certFile})
+        openssl req -nodes -newkey rsa:4096 -keyout ${cfg.registry.keyFile} -out /tmp/registry-auth.csr -subj "/CN=${cfg.registry.issuer}"
+        openssl x509 -in /tmp/registry-auth.csr -out ${cfg.registry.certFile} -req -signkey ${cfg.registry.keyFile} -days 3650
+        chown ${cfg.user}:${cfg.group} $(dirname ${cfg.registry.keyFile})
+        chown ${cfg.user}:${cfg.group} $(dirname ${cfg.registry.certFile})
+        chown ${cfg.user}:${cfg.group} ${cfg.registry.keyFile}
+        chown ${cfg.user}:${cfg.group} ${cfg.registry.certFile}
+      '';
+
+      serviceConfig = {
+        ConditionPathExists = "!${cfg.registry.certFile}";
+      };
+    };
+
+    # Ensure Docker Registry launches after the certificate generation job
+    systemd.services.docker-registry = optionalAttrs cfg.registry.enable {
+      wants = [ "gitlab-registry-cert.service" ];
+    };
+
+    # Enable Docker Registry, if GitLab-Container Registry is enabled
+    services.dockerRegistry = optionalAttrs cfg.registry.enable {
+      enable = true;
+      enableDelete = true; # This must be true, otherwise GitLab won't manage it correctly
+      extraConfig = {
+        auth.token = {
+          realm = "http${if cfg.https == true then "s" else ""}://${cfg.host}/jwt/auth";
+          service = cfg.registry.serviceName;
+          issuer = cfg.registry.issuer;
+          rootcertbundle = cfg.registry.certFile;
+        };
       };
     };
 
     # Use postfix to send out mails.
-    services.postfix.enable = mkDefault true;
+    services.postfix.enable = mkDefault (cfg.smtp.enable && cfg.smtp.address == "localhost");
 
     users.users.${cfg.user} =
       { group = cfg.group;
@@ -673,11 +1027,10 @@ in {
       "d /run/gitlab 0755 ${cfg.user} ${cfg.group} -"
       "d ${gitlabEnv.HOME} 0750 ${cfg.user} ${cfg.group} -"
       "z ${gitlabEnv.HOME}/.ssh/authorized_keys 0600 ${cfg.user} ${cfg.group} -"
-      "d ${cfg.backupPath} 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.backup.path} 0750 ${cfg.user} ${cfg.group} -"
       "d ${cfg.statePath} 0750 ${cfg.user} ${cfg.group} -"
       "d ${cfg.statePath}/builds 0750 ${cfg.user} ${cfg.group} -"
       "d ${cfg.statePath}/config 0750 ${cfg.user} ${cfg.group} -"
-      "d ${cfg.statePath}/config/initializers 0750 ${cfg.user} ${cfg.group} -"
       "d ${cfg.statePath}/db 0750 ${cfg.user} ${cfg.group} -"
       "d ${cfg.statePath}/log 0750 ${cfg.user} ${cfg.group} -"
       "d ${cfg.statePath}/repositories 2770 ${cfg.user} ${cfg.group} -"
@@ -700,17 +1053,163 @@ in {
       "L+ /run/gitlab/uploads - - - - ${cfg.statePath}/uploads"
 
       "L+ /run/gitlab/shell-config.yml - - - - ${pkgs.writeText "config.yml" (builtins.toJSON gitlabShellConfig)}"
-
-      "L+ ${cfg.statePath}/config/unicorn.rb - - - - ${./defaultUnicornConfig.rb}"
     ];
 
+
+    systemd.services.gitlab-config = {
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      path = with pkgs; [
+        jq
+        openssl
+        replace-secret
+        git
+      ];
+      serviceConfig = {
+        Type = "oneshot";
+        User = cfg.user;
+        Group = cfg.group;
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+        WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
+        RemainAfterExit = true;
+
+        ExecStartPre = let
+          preStartFullPrivileges = ''
+            shopt -s dotglob nullglob
+            set -eu
+
+            chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/*
+            if [[ -n "$(ls -A '${cfg.statePath}'/config/)" ]]; then
+              chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/config/*
+            fi
+          '';
+        in "+${pkgs.writeShellScript "gitlab-pre-start-full-privileges" preStartFullPrivileges}";
+
+        ExecStart = pkgs.writeShellScript "gitlab-config" ''
+          set -eu
+
+          umask u=rwx,g=rx,o=
+
+          cp -f ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION
+          rm -rf ${cfg.statePath}/db/*
+          rm -f ${cfg.statePath}/lib
+          find '${cfg.statePath}/config/' -maxdepth 1 -mindepth 1 -type d -execdir rm -rf {} \;
+          cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/config.dist/* ${cfg.statePath}/config
+          cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/db/* ${cfg.statePath}/db
+          ln -sf ${extraGitlabRb} ${cfg.statePath}/config/initializers/extra-gitlab.rb
+
+          ${cfg.packages.gitlab-shell}/bin/install
+
+          ${optionalString cfg.smtp.enable ''
+              install -m u=rw ${smtpSettings} ${cfg.statePath}/config/initializers/smtp_settings.rb
+              ${optionalString (cfg.smtp.passwordFile != null) ''
+                  replace-secret '@smtpPassword@' '${cfg.smtp.passwordFile}' '${cfg.statePath}/config/initializers/smtp_settings.rb'
+              ''}
+          ''}
+
+          (
+            umask u=rwx,g=,o=
+
+            openssl rand -hex 32 > ${cfg.statePath}/gitlab_shell_secret
+
+            rm -f '${cfg.statePath}/config/database.yml'
+
+            ${if cfg.databasePasswordFile != null then ''
+                export db_password="$(<'${cfg.databasePasswordFile}')"
+
+                if [[ -z "$db_password" ]]; then
+                  >&2 echo "Database password was an empty string!"
+                  exit 1
+                fi
+
+                jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \
+                   '.production.password = $ENV.db_password' \
+                   >'${cfg.statePath}/config/database.yml'
+              ''
+              else ''
+                jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \
+                   >'${cfg.statePath}/config/database.yml'
+              ''
+            }
+
+            ${utils.genJqSecretsReplacementSnippet
+                gitlabConfig
+                "${cfg.statePath}/config/gitlab.yml"
+            }
+
+            rm -f '${cfg.statePath}/config/secrets.yml'
+
+            export secret="$(<'${cfg.secrets.secretFile}')"
+            export db="$(<'${cfg.secrets.dbFile}')"
+            export otp="$(<'${cfg.secrets.otpFile}')"
+            export jws="$(<'${cfg.secrets.jwsFile}')"
+            jq -n '{production: {secret_key_base: $ENV.secret,
+                    otp_key_base: $ENV.otp,
+                    db_key_base: $ENV.db,
+                    openid_connect_signing_key: $ENV.jws}}' \
+               > '${cfg.statePath}/config/secrets.yml'
+          )
+
+          # We remove potentially broken links to old gitlab-shell versions
+          rm -Rf ${cfg.statePath}/repositories/**/*.git/hooks
+
+          git config --global core.autocrlf "input"
+        '';
+      };
+    };
+
+    systemd.services.gitlab-db-config = {
+      after = [ "gitlab-config.service" "gitlab-postgresql.service" "postgresql.service" ];
+      bindsTo = [
+        "gitlab-config.service"
+      ] ++ optional (cfg.databaseHost == "") "postgresql.service"
+        ++ optional databaseActuallyCreateLocally "gitlab-postgresql.service";
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        User = cfg.user;
+        Group = cfg.group;
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+        WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
+        RemainAfterExit = true;
+
+        ExecStart = pkgs.writeShellScript "gitlab-db-config" ''
+          set -eu
+          umask u=rwx,g=rx,o=
+
+          initial_root_password="$(<'${cfg.initialRootPasswordFile}')"
+          ${gitlab-rake}/bin/gitlab-rake gitlab:db:configure GITLAB_ROOT_PASSWORD="$initial_root_password" \
+                                                             GITLAB_ROOT_EMAIL='${cfg.initialRootEmail}' > /dev/null
+        '';
+      };
+    };
+
     systemd.services.gitlab-sidekiq = {
-      after = [ "network.target" "redis.service" "gitlab.service" ];
-      wantedBy = [ "multi-user.target" ];
-      environment = gitlabEnv;
+      after = [
+        "network.target"
+        "redis.service"
+        "postgresql.service"
+        "gitlab-config.service"
+        "gitlab-db-config.service"
+      ];
+      bindsTo = [
+        "redis.service"
+        "gitlab-config.service"
+        "gitlab-db-config.service"
+      ] ++ optional (cfg.databaseHost == "") "postgresql.service";
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      environment = gitlabEnv // (optionalAttrs cfg.sidekiq.memoryKiller.enable {
+        SIDEKIQ_MEMORY_KILLER_MAX_RSS = cfg.sidekiq.memoryKiller.maxMemory;
+        SIDEKIQ_MEMORY_KILLER_GRACE_TIME = cfg.sidekiq.memoryKiller.graceTime;
+        SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT = cfg.sidekiq.memoryKiller.shutdownWait;
+      });
       path = with pkgs; [
         postgresqlPackage
-        gitAndTools.git
+        git
         ruby
         openssh
         nodejs
@@ -719,25 +1218,29 @@ in {
         # Needed for GitLab project imports
         gnutar
         gzip
+
+        procps # Sidekiq MemoryKiller
       ];
       serviceConfig = {
         Type = "simple";
         User = cfg.user;
         Group = cfg.group;
         TimeoutSec = "infinity";
-        Restart = "on-failure";
+        Restart = "always";
         WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
         ExecStart="${cfg.packages.gitlab.rubyEnv}/bin/sidekiq -C \"${cfg.packages.gitlab}/share/gitlab/config/sidekiq_queues.yml\" -e production";
       };
     };
 
     systemd.services.gitaly = {
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "gitlab-config.service" ];
+      bindsTo = [ "gitlab-config.service" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
       path = with pkgs; [
         openssh
         procps  # See https://gitlab.com/gitlab-org/gitaly/issues/1562
-        gitAndTools.git
+        git
         cfg.packages.gitaly.rubyEnv
         cfg.packages.gitaly.rubyEnv.wrappedRuby
         gzip
@@ -754,12 +1257,35 @@ in {
       };
     };
 
+    systemd.services.gitlab-pages = mkIf (gitlabConfig.production.pages.enabled or false) {
+      description = "GitLab static pages daemon";
+      after = [ "network.target" "gitlab-config.service" ];
+      bindsTo = [ "gitlab-config.service" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+
+      path = [ pkgs.unzip ];
+
+      serviceConfig = {
+        Type = "simple";
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+
+        User = cfg.user;
+        Group = cfg.group;
+
+        ExecStart = "${cfg.packages.pages}/bin/gitlab-pages ${escapeShellArgs pagesArgs}";
+        WorkingDirectory = gitlabEnv.HOME;
+      };
+    };
+
     systemd.services.gitlab-workhorse = {
       after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
       path = with pkgs; [
         exiftool
-        gitAndTools.git
+        git
         gnutar
         gzip
         openssh
@@ -783,14 +1309,44 @@ in {
       };
     };
 
+    systemd.services.gitlab-mailroom = mkIf (gitlabConfig.production.incoming_email.enabled or false) {
+      description = "GitLab incoming mail daemon";
+      after = [ "network.target" "redis.service" "gitlab-config.service" ];
+      bindsTo = [ "gitlab-config.service" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      environment = gitlabEnv;
+      serviceConfig = {
+        Type = "simple";
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${cfg.packages.gitlab.rubyEnv}/bin/bundle exec mail_room -c ${cfg.statePath}/config/mail_room.yml";
+        WorkingDirectory = gitlabEnv.HOME;
+      };
+    };
+
     systemd.services.gitlab = {
-      after = [ "gitlab-workhorse.service" "gitaly.service" "network.target" "gitlab-postgresql.service" "redis.service" ];
-      requires = [ "gitlab-sidekiq.service" ];
-      wantedBy = [ "multi-user.target" ];
+      after = [
+        "gitlab-workhorse.service"
+        "network.target"
+        "redis.service"
+        "gitlab-config.service"
+        "gitlab-db-config.service"
+      ];
+      bindsTo = [
+        "redis.service"
+        "gitlab-config.service"
+        "gitlab-db-config.service"
+      ] ++ optional (cfg.databaseHost == "") "postgresql.service";
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
       environment = gitlabEnv;
       path = with pkgs; [
         postgresqlPackage
-        gitAndTools.git
+        git
         openssh
         nodejs
         procps
@@ -804,100 +1360,34 @@ in {
         TimeoutSec = "infinity";
         Restart = "on-failure";
         WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
-        ExecStartPre = let
-          preStartFullPrivileges = ''
-            shopt -s dotglob nullglob
-            set -eu
-
-            chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/*
-            chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/config/*
-          '';
-          preStart = ''
-            set -eu
-
-            cp -f ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION
-            rm -rf ${cfg.statePath}/db/*
-            rm -rf ${cfg.statePath}/config/initializers/*
-            rm -f ${cfg.statePath}/lib
-            cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/config.dist/* ${cfg.statePath}/config
-            cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/db/* ${cfg.statePath}/db
-            ln -sf ${extraGitlabRb} ${cfg.statePath}/config/initializers/extra-gitlab.rb
-
-            ${cfg.packages.gitlab-shell}/bin/install
-
-            ${optionalString cfg.smtp.enable ''
-              install -m u=rw ${smtpSettings} ${cfg.statePath}/config/initializers/smtp_settings.rb
-              ${optionalString (cfg.smtp.passwordFile != null) ''
-                smtp_password=$(<'${cfg.smtp.passwordFile}')
-                ${pkgs.replace}/bin/replace-literal -e '@smtpPassword@' "$smtp_password" '${cfg.statePath}/config/initializers/smtp_settings.rb'
-              ''}
-            ''}
-
-            (
-              umask u=rwx,g=,o=
-
-              ${pkgs.openssl}/bin/openssl rand -hex 32 > ${cfg.statePath}/gitlab_shell_secret
-
-              if [[ -h '${cfg.statePath}/config/database.yml' ]]; then
-                rm '${cfg.statePath}/config/database.yml'
-              fi
-
-              ${if cfg.databasePasswordFile != null then ''
-                  export db_password="$(<'${cfg.databasePasswordFile}')"
-
-                  if [[ -z "$db_password" ]]; then
-                    >&2 echo "Database password was an empty string!"
-                    exit 1
-                  fi
-
-                  ${pkgs.jq}/bin/jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \
-                                    '.production.password = $ENV.db_password' \
-                                    >'${cfg.statePath}/config/database.yml'
-                ''
-                else ''
-                  ${pkgs.jq}/bin/jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \
-                                    >'${cfg.statePath}/config/database.yml'
-                ''
-              }
-
-              ${utils.genJqSecretsReplacementSnippet
-                  gitlabConfig
-                  "${cfg.statePath}/config/gitlab.yml"
-              }
-
-              if [[ -h '${cfg.statePath}/config/secrets.yml' ]]; then
-                rm '${cfg.statePath}/config/secrets.yml'
-              fi
-
-              export secret="$(<'${cfg.secrets.secretFile}')"
-              export db="$(<'${cfg.secrets.dbFile}')"
-              export otp="$(<'${cfg.secrets.otpFile}')"
-              export jws="$(<'${cfg.secrets.jwsFile}')"
-              ${pkgs.jq}/bin/jq -n '{production: {secret_key_base: $ENV.secret,
-                                                  otp_key_base: $ENV.otp,
-                                                  db_key_base: $ENV.db,
-                                                  openid_connect_signing_key: $ENV.jws}}' \
-                                > '${cfg.statePath}/config/secrets.yml'
-            )
-
-            initial_root_password="$(<'${cfg.initialRootPasswordFile}')"
-            ${gitlab-rake}/bin/gitlab-rake gitlab:db:configure GITLAB_ROOT_PASSWORD="$initial_root_password" \
-                                                               GITLAB_ROOT_EMAIL='${cfg.initialRootEmail}' > /dev/null
-
-            # We remove potentially broken links to old gitlab-shell versions
-            rm -Rf ${cfg.statePath}/repositories/**/*.git/hooks
-
-            ${pkgs.git}/bin/git config --global core.autocrlf "input"
-          '';
-        in [
-          "+${pkgs.writeShellScript "gitlab-pre-start-full-privileges" preStartFullPrivileges}"
-          "${pkgs.writeShellScript "gitlab-pre-start" preStart}"
+        ExecStart = concatStringsSep " " [
+          "${cfg.packages.gitlab.rubyEnv}/bin/puma"
+          "-e production"
+          "-C ${cfg.statePath}/config/puma.rb"
+          "-w ${cfg.puma.workers}"
+          "-t ${cfg.puma.threadsMin}:${cfg.puma.threadsMax}"
         ];
-        ExecStart = "${cfg.packages.gitlab.rubyEnv}/bin/unicorn -c ${cfg.statePath}/config/unicorn.rb -E production";
       };
 
     };
 
+    systemd.services.gitlab-backup = {
+      after = [ "gitlab.service" ];
+      bindsTo = [ "gitlab.service" ];
+      startAt = cfg.backup.startAt;
+      environment = {
+        RAILS_ENV = "production";
+        CRON = "1";
+      } // optionalAttrs (stringLength cfg.backup.skip > 0) {
+        SKIP = cfg.backup.skip;
+      };
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${gitlab-rake}/bin/gitlab-rake gitlab:backup:create";
+      };
+    };
+
   };
 
   meta.doc = ./gitlab.xml;
diff --git a/nixos/modules/services/misc/gitlab.xml b/nixos/modules/services/misc/gitlab.xml
index b6171a9a194..40424c5039a 100644
--- a/nixos/modules/services/misc/gitlab.xml
+++ b/nixos/modules/services/misc/gitlab.xml
@@ -3,15 +3,15 @@
          xmlns:xi="http://www.w3.org/2001/XInclude"
          version="5.0"
          xml:id="module-services-gitlab">
- <title>Gitlab</title>
+ <title>GitLab</title>
  <para>
-  Gitlab is a feature-rich git hosting service.
+  GitLab is a feature-rich git hosting service.
  </para>
  <section xml:id="module-services-gitlab-prerequisites">
   <title>Prerequisites</title>
 
   <para>
-   The gitlab service exposes only an Unix socket at
+   The <literal>gitlab</literal> service exposes only an Unix socket at
    <literal>/run/gitlab/gitlab-workhorse.socket</literal>. You need to
    configure a webserver to proxy HTTP requests to the socket.
   </para>
@@ -39,7 +39,7 @@
   <title>Configuring</title>
 
   <para>
-   Gitlab depends on both PostgreSQL and Redis and will automatically enable
+   GitLab depends on both PostgreSQL and Redis and will automatically enable
    both services. In the case of PostgreSQL, a database and a role will be
    created.
   </para>
@@ -85,19 +85,25 @@ services.gitlab = {
   </para>
 
   <para>
-   If you're setting up a new Gitlab instance, generate new
+   If you're setting up a new GitLab instance, generate new
    secrets. You for instance use <literal>tr -dc A-Za-z0-9 &lt;
    /dev/urandom | head -c 128 &gt; /var/keys/gitlab/db</literal> to
    generate a new db secret. Make sure the files can be read by, and
    only by, the user specified by <link
-   linkend="opt-services.gitlab.user">services.gitlab.user</link>. Gitlab
+   linkend="opt-services.gitlab.user">services.gitlab.user</link>. GitLab
    encrypts sensitive data stored in the database. If you're restoring
-   an existing Gitlab instance, you must specify the secrets secret
-   from <literal>config/secrets.yml</literal> located in your Gitlab
+   an existing GitLab instance, you must specify the secrets secret
+   from <literal>config/secrets.yml</literal> located in your GitLab
    state folder.
   </para>
 
   <para>
+    When <literal>incoming_mail.enabled</literal> is set to <literal>true</literal>
+    in <link linkend="opt-services.gitlab.extraConfig">extraConfig</link> an additional
+    service called <literal>gitlab-mailroom</literal> is enabled for fetching incoming mail.
+  </para>
+
+  <para>
    Refer to <xref linkend="ch-options" /> for all available configuration
    options for the
    <link linkend="opt-services.gitlab.enable">services.gitlab</link> module.
@@ -106,21 +112,40 @@ services.gitlab = {
  <section xml:id="module-services-gitlab-maintenance">
   <title>Maintenance</title>
 
-  <para>
-   You can run Gitlab's rake tasks with <literal>gitlab-rake</literal> which
-   will be available on the system when gitlab is enabled. You will have to run
-   the command as the user that you configured to run gitlab with.
-  </para>
+  <section xml:id="module-services-gitlab-maintenance-backups">
+   <title>Backups</title>
+   <para>
+     Backups can be configured with the options in <link
+     linkend="opt-services.gitlab.backup.keepTime">services.gitlab.backup</link>. Use
+     the <link
+     linkend="opt-services.gitlab.backup.startAt">services.gitlab.backup.startAt</link>
+     option to configure regular backups.
+   </para>
 
-  <para>
-   For example, to backup a Gitlab instance:
+   <para>
+     To run a manual backup, start the <literal>gitlab-backup</literal> service:
 <screen>
-<prompt>$ </prompt>sudo -u git -H gitlab-rake gitlab:backup:create
+<prompt>$ </prompt>systemctl start gitlab-backup.service
 </screen>
-   A list of all availabe rake tasks can be obtained by running:
+   </para>
+  </section>
+
+  <section xml:id="module-services-gitlab-maintenance-rake">
+   <title>Rake tasks</title>
+
+   <para>
+    You can run GitLab's rake tasks with <literal>gitlab-rake</literal>
+    which will be available on the system when GitLab is enabled. You
+    will have to run the command as the user that you configured to run
+    GitLab with.
+   </para>
+
+   <para>
+    A list of all availabe rake tasks can be obtained by running:
 <screen>
 <prompt>$ </prompt>sudo -u git -H gitlab-rake -T
 </screen>
-  </para>
+   </para>
+  </section>
  </section>
 </chapter>
diff --git a/nixos/modules/services/misc/gitolite.nix b/nixos/modules/services/misc/gitolite.nix
index 59cbdac319c..190ea9212d2 100644
--- a/nixos/modules/services/misc/gitolite.nix
+++ b/nixos/modules/services/misc/gitolite.nix
@@ -227,6 +227,6 @@ in
     };
 
     environment.systemPackages = [ pkgs.gitolite pkgs.git ]
-        ++ optional cfg.enableGitAnnex pkgs.gitAndTools.git-annex;
+        ++ optional cfg.enableGitAnnex pkgs.git-annex;
   });
 }
diff --git a/nixos/modules/services/misc/gitweb.nix b/nixos/modules/services/misc/gitweb.nix
index ca21366b779..13396bf2eb0 100644
--- a/nixos/modules/services/misc/gitweb.nix
+++ b/nixos/modules/services/misc/gitweb.nix
@@ -54,6 +54,6 @@ in
 
   };
 
-  meta.maintainers = with maintainers; [ gnidorah ];
+  meta.maintainers = with maintainers; [ ];
 
 }
diff --git a/nixos/modules/services/misc/gogs.nix b/nixos/modules/services/misc/gogs.nix
index c5070aaa356..d7233f10c7c 100644
--- a/nixos/modules/services/misc/gogs.nix
+++ b/nixos/modules/services/misc/gogs.nix
@@ -25,7 +25,6 @@ let
     HTTP_ADDR = ${cfg.httpAddress}
     HTTP_PORT = ${toString cfg.httpPort}
     ROOT_URL = ${cfg.rootUrl}
-    STATIC_ROOT_PATH = ${cfg.staticRootPath}
 
     [session]
     COOKIE_NAME = session
@@ -179,13 +178,6 @@ in
         '';
       };
 
-      staticRootPath = mkOption {
-        type = types.str;
-        default = "${pkgs.gogs.data}";
-        example = "/var/lib/gogs/data";
-        description = "Upper level of template and static files path.";
-      };
-
       extraConfig = mkOption {
         type = types.str;
         default = "";
diff --git a/nixos/modules/services/misc/gollum.nix b/nixos/modules/services/misc/gollum.nix
index 0c9c7548305..4053afa69be 100644
--- a/nixos/modules/services/misc/gollum.nix
+++ b/nixos/modules/services/misc/gollum.nix
@@ -115,4 +115,6 @@ in
       };
     };
   };
+
+  meta.maintainers = with lib.maintainers; [ erictapen ];
 }
diff --git a/nixos/modules/services/misc/gpsd.nix b/nixos/modules/services/misc/gpsd.nix
index f954249942a..fafea10daba 100644
--- a/nixos/modules/services/misc/gpsd.nix
+++ b/nixos/modules/services/misc/gpsd.nix
@@ -62,7 +62,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 2947;
         description = ''
           The port where to listen for TCP connections.
diff --git a/nixos/modules/services/misc/home-assistant.nix b/nixos/modules/services/misc/home-assistant.nix
index 0477254e7c1..dcd825bba43 100644
--- a/nixos/modules/services/misc/home-assistant.nix
+++ b/nixos/modules/services/misc/home-assistant.nix
@@ -63,10 +63,12 @@ let
   };
 
 in {
-  meta.maintainers = with maintainers; [ dotlambda ];
+  meta.maintainers = teams.home-assistant.members;
 
   options.services.home-assistant = {
-    enable = mkEnableOption "Home Assistant";
+    # Running home-assistant on NixOS is considered an installation method that is unsupported by the upstream project.
+    # https://github.com/home-assistant/architecture/blob/master/adr/0012-define-supported-installation-method.md#decision
+    enable = mkEnableOption "Home Assistant. Please note that this installation method is unsupported upstream";
 
     configDir = mkOption {
       default = "/var/lib/hass";
@@ -183,8 +185,14 @@ in {
     };
 
     package = mkOption {
-      default = pkgs.home-assistant;
-      defaultText = "pkgs.home-assistant";
+      default = pkgs.home-assistant.overrideAttrs (oldAttrs: {
+        doInstallCheck = false;
+      });
+      defaultText = literalExample ''
+        pkgs.home-assistant.overrideAttrs (oldAttrs: {
+          doInstallCheck = false;
+        })
+      '';
       type = types.package;
       example = literalExample ''
         pkgs.home-assistant.override {
@@ -192,10 +200,11 @@ in {
         }
       '';
       description = ''
-        Home Assistant package to use.
+        Home Assistant package to use. By default the tests are disabled, as they take a considerable amout of time to complete.
         Override <literal>extraPackages</literal> or <literal>extraComponents</literal> in order to add additional dependencies.
         If you specify <option>config</option> and do not set <option>autoExtraComponents</option>
         to <literal>false</literal>, overriding <literal>extraComponents</literal> will have no effect.
+        Avoid <literal>home-assistant.overridePythonAttrs</literal> if you use <literal>autoExtraComponents</literal>.
       '';
     };
 
@@ -238,18 +247,135 @@ in {
         rm -f "${cfg.configDir}/ui-lovelace.yaml"
         ln -s ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
       '');
-      serviceConfig = {
-        ExecStart = "${package}/bin/hass --config '${cfg.configDir}'";
+      serviceConfig = let
+        # List of capabilities to equip home-assistant with, depending on configured components
+        capabilities = [
+          # Empty string first, so we will never accidentally have an empty capability bounding set
+          # https://github.com/NixOS/nixpkgs/issues/120617#issuecomment-830685115
+          ""
+        ] ++ (unique (optionals (useComponent "bluetooth_tracker" || useComponent "bluetooth_le_tracker") [
+          # Required for interaction with hci devices and bluetooth sockets
+          # https://www.home-assistant.io/integrations/bluetooth_le_tracker/#rootless-setup-on-core-installs
+          "CAP_NET_ADMIN"
+          "CAP_NET_RAW"
+        ] ++ lib.optionals (useComponent "emulated_hue") [
+          # Alexa looks for the service on port 80
+          # https://www.home-assistant.io/integrations/emulated_hue
+          "CAP_NET_BIND_SERVICE"
+        ] ++ lib.optionals (useComponent "nmap_tracker") [
+          # https://www.home-assistant.io/integrations/nmap_tracker#linux-capabilities
+          "CAP_NET_ADMIN"
+          "CAP_NET_BIND_SERVICE"
+          "CAP_NET_RAW"
+        ]));
+        componentsUsingBluetooth = [
+          # Components that require the AF_BLUETOOTH address family
+          "bluetooth_tracker"
+          "bluetooth_le_tracker"
+        ];
+        componentsUsingSerialDevices = [
+          # Components that require access to serial devices (/dev/tty*)
+          # List generated from home-assistant documentation:
+          #   git clone https://github.com/home-assistant/home-assistant.io/
+          #   cd source/_integrations
+          #   rg "/dev/tty" -l | cut -d'/' -f3 | cut -d'.' -f1 | sort
+          # And then extended by references found in the source code, these
+          # mostly the ones using config flows already.
+          "acer_projector"
+          "alarmdecoder"
+          "arduino"
+          "blackbird"
+          "dsmr"
+          "edl21"
+          "elkm1"
+          "elv"
+          "enocean"
+          "firmata"
+          "flexit"
+          "gpsd"
+          "insteon"
+          "kwb"
+          "lacrosse"
+          "mhz19"
+          "modbus"
+          "modem_callerid"
+          "mysensors"
+          "nad"
+          "numato"
+          "rflink"
+          "rfxtrx"
+          "scsgate"
+          "serial"
+          "serial_pm"
+          "sms"
+          "upb"
+          "velbus"
+          "w800rf32"
+          "xbee"
+          "zha"
+          "zwave"
+        ];
+      in {
+        ExecStart = "${package}/bin/hass --runner --config '${cfg.configDir}'";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         User = "hass";
         Group = "hass";
         Restart = "on-failure";
-        ProtectSystem = "strict";
-        ReadWritePaths = "${cfg.configDir}";
+        RestartForceExitStatus = "100";
+        SuccessExitStatus = "100";
         KillSignal = "SIGINT";
+
+        # Hardening
+        AmbientCapabilities = capabilities;
+        CapabilityBoundingSet = capabilities;
+        DeviceAllow = (optionals (any useComponent componentsUsingSerialDevices) [
+          "char-ttyACM rw"
+          "char-ttyAMA rw"
+          "char-ttyUSB rw"
+        ]);
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
         PrivateTmp = true;
+        PrivateUsers = false; # prevents gaining capabilities in the host namespace
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProcSubset = "all";
+        ProtectSystem = "strict";
         RemoveIPC = true;
-        AmbientCapabilities = "cap_net_raw,cap_net_admin+eip";
+        ReadWritePaths = let
+          # Allow rw access to explicitly configured paths
+          cfgPath = [ "config" "homeassistant" "allowlist_external_dirs" ];
+          value = attrByPath cfgPath [] cfg;
+          allowPaths = if isList value then value else singleton value;
+        in [ "${cfg.configDir}" ] ++ allowPaths;
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+          "AF_NETLINK"
+          "AF_UNIX"
+        ] ++ optionals (any useComponent componentsUsingBluetooth) [
+          "AF_BLUETOOTH"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SupplementaryGroups = optionals (any useComponent componentsUsingSerialDevices) [
+          "dialout"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+        ];
+        UMask = "0077";
       };
       path = [
         "/run/wrappers" # needed for ping
@@ -267,7 +393,6 @@ in {
       home = cfg.configDir;
       createHome = true;
       group = "hass";
-      extraGroups = [ "dialout" ];
       uid = config.ids.uids.hass;
     };
 
diff --git a/nixos/modules/services/misc/ihaskell.nix b/nixos/modules/services/misc/ihaskell.nix
index 684a242d738..c7332b87803 100644
--- a/nixos/modules/services/misc/ihaskell.nix
+++ b/nixos/modules/services/misc/ihaskell.nix
@@ -21,6 +21,7 @@ in
       };
 
       extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
         default = self: [];
         example = literalExample ''
           haskellPackages: [
diff --git a/nixos/modules/services/misc/jellyfin.nix b/nixos/modules/services/misc/jellyfin.nix
index 0493dadea94..6d64acc0291 100644
--- a/nixos/modules/services/misc/jellyfin.nix
+++ b/nixos/modules/services/misc/jellyfin.nix
@@ -18,6 +18,7 @@ in
 
       package = mkOption {
         type = types.package;
+        default = pkgs.jellyfin;
         example = literalExample "pkgs.jellyfin";
         description = ''
           Jellyfin package to use.
@@ -29,6 +30,16 @@ in
         default = "jellyfin";
         description = "Group under which jellyfin runs.";
       };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open the default ports in the firewall for the media server. The
+          HTTP/HTTPS ports can be changed in the Web UI, so this option should
+          only be used if they are unchanged.
+        '';
+      };
     };
   };
 
@@ -45,14 +56,47 @@ in
         CacheDirectory = "jellyfin";
         ExecStart = "${cfg.package}/bin/jellyfin --datadir '/var/lib/${StateDirectory}' --cachedir '/var/cache/${CacheDirectory}'";
         Restart = "on-failure";
+
+        # Security options:
+
+        NoNewPrivileges = true;
+
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+
+        LockPersonality = true;
+
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+
+        RemoveIPC = true;
+
+        RestrictNamespaces = true;
+        # AF_NETLINK needed because Jellyfin monitors the network connection
+        RestrictAddressFamilies = [ "AF_NETLINK" "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = [
+          "@system-service"
+          "~@cpu-emulation" "~@debug" "~@keyring" "~@memlock" "~@obsolete" "~@privileged" "~@setuid"
+        ];
       };
     };
 
-    services.jellyfin.package = mkDefault (
-      if versionAtLeast config.system.stateVersion "20.09" then pkgs.jellyfin
-        else pkgs.jellyfin_10_5
-    );
-
     users.users = mkIf (cfg.user == "jellyfin") {
       jellyfin = {
         group = cfg.group;
@@ -64,6 +108,12 @@ in
       jellyfin = {};
     };
 
+    networking.firewall = mkIf cfg.openFirewall {
+      # from https://jellyfin.org/docs/general/networking/index.html
+      allowedTCPPorts = [ 8096 8920 ];
+      allowedUDPPorts = [ 1900 7359 ];
+    };
+
   };
 
   meta.maintainers = with lib.maintainers; [ minijackson ];
diff --git a/nixos/modules/services/misc/klipper.nix b/nixos/modules/services/misc/klipper.nix
new file mode 100644
index 00000000000..909408225e0
--- /dev/null
+++ b/nixos/modules/services/misc/klipper.nix
@@ -0,0 +1,117 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.klipper;
+  format = pkgs.formats.ini {
+    # https://github.com/NixOS/nixpkgs/pull/121613#issuecomment-885241996
+    listToValue = l:
+      if builtins.length l == 1 then generators.mkValueStringDefault {} (head l)
+      else lib.concatMapStrings (s: "\n  ${generators.mkValueStringDefault {} s}") l;
+    mkKeyValue = generators.mkKeyValueDefault {} ":";
+  };
+in
+{
+  ##### interface
+  options = {
+    services.klipper = {
+      enable = mkEnableOption "Klipper, the 3D printer firmware";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.klipper;
+        description = "The Klipper package.";
+      };
+
+      inputTTY = mkOption {
+        type = types.path;
+        default = "/run/klipper/tty";
+        description = "Path of the virtual printer symlink to create.";
+      };
+
+      apiSocket = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/run/klipper/api";
+        description = "Path of the API socket to create.";
+      };
+
+      octoprintIntegration = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Allows Octoprint to control Klipper.";
+      };
+
+      user = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          User account under which Klipper runs.
+
+          If null is specified (default), a temporary user will be created by systemd.
+        '';
+      };
+
+      group = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Group account under which Klipper runs.
+
+          If null is specified (default), a temporary user will be created by systemd.
+        '';
+      };
+
+      settings = mkOption {
+        type = format.type;
+        default = { };
+        description = ''
+          Configuration for Klipper. See the <link xlink:href="https://www.klipper3d.org/Overview.html#configuration-and-tuning-guides">documentation</link>
+          for supported values.
+        '';
+      };
+    };
+  };
+
+  ##### implementation
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.octoprintIntegration -> config.services.octoprint.enable;
+        message = "Option klipper.octoprintIntegration requires Octoprint to be enabled on this system. Please enable services.octoprint to use it.";
+      }
+      {
+        assertion = cfg.user != null -> cfg.group != null;
+        message = "Option klipper.group is not set when a user is specified.";
+      }
+    ];
+
+    environment.etc."klipper.cfg".source = format.generate "klipper.cfg" cfg.settings;
+
+    services.klipper = mkIf cfg.octoprintIntegration {
+      user = config.services.octoprint.user;
+      group = config.services.octoprint.group;
+    };
+
+    systemd.services.klipper = let
+      klippyArgs = "--input-tty=${cfg.inputTTY}"
+        + optionalString (cfg.apiSocket != null) " --api-server=${cfg.apiSocket}";
+    in {
+      description = "Klipper 3D Printer Firmware";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/lib/klipper/klippy.py ${klippyArgs} /etc/klipper.cfg";
+        RuntimeDirectory = "klipper";
+        SupplementaryGroups = [ "dialout" ];
+        WorkingDirectory = "${cfg.package}/lib";
+      } // (if cfg.user != null then {
+        Group = cfg.group;
+        User = cfg.user;
+      } else {
+        DynamicUser = true;
+        User = "klipper";
+      });
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/leaps.nix b/nixos/modules/services/misc/leaps.nix
index ef89d3e64d0..f797218522c 100644
--- a/nixos/modules/services/misc/leaps.nix
+++ b/nixos/modules/services/misc/leaps.nix
@@ -11,7 +11,7 @@ in
     services.leaps = {
       enable = mkEnableOption "leaps";
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 8080;
         description = "A port where leaps listens for incoming http requests";
       };
diff --git a/nixos/modules/services/misc/lifecycled.nix b/nixos/modules/services/misc/lifecycled.nix
new file mode 100644
index 00000000000..1c8942998d6
--- /dev/null
+++ b/nixos/modules/services/misc/lifecycled.nix
@@ -0,0 +1,164 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.lifecycled;
+
+  # TODO: Add the ability to extend this with an rfc 42-like interface.
+  # In the meantime, one can modify the environment (as
+  # long as it's not overriding anything from here) with
+  # systemd.services.lifecycled.serviceConfig.Environment
+  configFile = pkgs.writeText "lifecycled" ''
+    LIFECYCLED_HANDLER=${cfg.handler}
+    ${lib.optionalString (cfg.cloudwatchGroup != null) "LIFECYCLED_CLOUDWATCH_GROUP=${cfg.cloudwatchGroup}"}
+    ${lib.optionalString (cfg.cloudwatchStream != null) "LIFECYCLED_CLOUDWATCH_STREAM=${cfg.cloudwatchStream}"}
+    ${lib.optionalString cfg.debug "LIFECYCLED_DEBUG=${lib.boolToString cfg.debug}"}
+    ${lib.optionalString (cfg.instanceId != null) "LIFECYCLED_INSTANCE_ID=${cfg.instanceId}"}
+    ${lib.optionalString cfg.json "LIFECYCLED_JSON=${lib.boolToString cfg.json}"}
+    ${lib.optionalString cfg.noSpot "LIFECYCLED_NO_SPOT=${lib.boolToString cfg.noSpot}"}
+    ${lib.optionalString (cfg.snsTopic != null) "LIFECYCLED_SNS_TOPIC=${cfg.snsTopic}"}
+    ${lib.optionalString (cfg.awsRegion != null) "AWS_REGION=${cfg.awsRegion}"}
+  '';
+in
+{
+  meta.maintainers = with maintainers; [ cole-h grahamc ];
+
+  options = {
+    services.lifecycled = {
+      enable = mkEnableOption "lifecycled";
+
+      queueCleaner = {
+        enable = mkEnableOption "lifecycled-queue-cleaner";
+
+        frequency = mkOption {
+          type = types.str;
+          default = "hourly";
+          description = ''
+            How often to trigger the queue cleaner.
+
+            NOTE: This string should be a valid value for a systemd
+            timer's <literal>OnCalendar</literal> configuration. See
+            <citerefentry><refentrytitle>systemd.timer</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+            for more information.
+          '';
+        };
+
+        parallel = mkOption {
+          type = types.ints.unsigned;
+          default = 20;
+          description = ''
+            The number of parallel deletes to run.
+          '';
+        };
+      };
+
+      instanceId = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          The instance ID to listen for events for.
+        '';
+      };
+
+      snsTopic = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          The SNS topic that receives events.
+        '';
+      };
+
+      noSpot = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Disable the spot termination listener.
+        '';
+      };
+
+      handler = mkOption {
+        type = types.path;
+        description = ''
+          The script to invoke to handle events.
+        '';
+      };
+
+      json = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable JSON logging.
+        '';
+      };
+
+      cloudwatchGroup = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Write logs to a specific Cloudwatch Logs group.
+        '';
+      };
+
+      cloudwatchStream = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Write logs to a specific Cloudwatch Logs stream. Defaults to the instance ID.
+        '';
+      };
+
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable debugging information.
+        '';
+      };
+
+      # XXX: Can be removed if / when
+      # https://github.com/buildkite/lifecycled/pull/91 is merged.
+      awsRegion = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          The region used for accessing AWS services.
+        '';
+      };
+    };
+  };
+
+  ### Implementation ###
+
+  config = mkMerge [
+    (mkIf cfg.enable {
+      environment.etc."lifecycled".source = configFile;
+
+      systemd.packages = [ pkgs.lifecycled ];
+      systemd.services.lifecycled = {
+        wantedBy = [ "network-online.target" ];
+        restartTriggers = [ configFile ];
+      };
+    })
+
+    (mkIf cfg.queueCleaner.enable {
+      systemd.services.lifecycled-queue-cleaner = {
+        description = "Lifecycle Daemon Queue Cleaner";
+        environment = optionalAttrs (cfg.awsRegion != null) { AWS_REGION = cfg.awsRegion; };
+        serviceConfig = {
+          Type = "oneshot";
+          ExecStart = "${pkgs.lifecycled}/bin/lifecycled-queue-cleaner -parallel ${toString cfg.queueCleaner.parallel}";
+        };
+      };
+
+      systemd.timers.lifecycled-queue-cleaner = {
+        description = "Lifecycle Daemon Queue Cleaner Timer";
+        wantedBy = [ "timers.target" ];
+        after = [ "network-online.target" ];
+        timerConfig = {
+          Unit = "lifecycled-queue-cleaner.service";
+          OnCalendar = "${cfg.queueCleaner.frequency}";
+        };
+      };
+    })
+  ];
+}
diff --git a/nixos/modules/services/misc/mame.nix b/nixos/modules/services/misc/mame.nix
index c5d5e9e4837..4b9a04be7c2 100644
--- a/nixos/modules/services/misc/mame.nix
+++ b/nixos/modules/services/misc/mame.nix
@@ -53,7 +53,7 @@ in
       description = "MAME TUN/TAP Ethernet interface";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      path = [ pkgs.iproute ];
+      path = [ pkgs.iproute2 ];
       serviceConfig = {
         Type = "oneshot";
         RemainAfterExit = true;
@@ -63,5 +63,5 @@ in
     };
   };
 
-  meta.maintainers = with lib.maintainers; [ gnidorah ];
+  meta.maintainers = with lib.maintainers; [ ];
 }
diff --git a/nixos/modules/services/misc/matrix-appservice-discord.nix b/nixos/modules/services/misc/matrix-appservice-discord.nix
index 49c41ff637a..71d1227f4ff 100644
--- a/nixos/modules/services/misc/matrix-appservice-discord.nix
+++ b/nixos/modules/services/misc/matrix-appservice-discord.nix
@@ -5,7 +5,7 @@ with lib;
 let
   dataDir = "/var/lib/matrix-appservice-discord";
   registrationFile = "${dataDir}/discord-registration.yaml";
-  appDir = "${pkgs.matrix-appservice-discord}/lib/node_modules/matrix-appservice-discord";
+  appDir = "${pkgs.matrix-appservice-discord}/${pkgs.matrix-appservice-discord.passthru.nodeAppDir}";
   cfg = config.services.matrix-appservice-discord;
   # TODO: switch to configGen.json once RFC42 is implemented
   settingsFile = pkgs.writeText "matrix-appservice-discord-settings.json" (builtins.toJSON cfg.settings);
@@ -22,12 +22,6 @@ in {
         default = {
           database = {
             filename = "${dataDir}/discord.db";
-
-            # TODO: remove those old config keys once the following issues are solved:
-            # * https://github.com/Half-Shot/matrix-appservice-discord/issues/490
-            # * https://github.com/Half-Shot/matrix-appservice-discord/issues/498
-            userStorePath = "${dataDir}/user-store.db";
-            roomStorePath = "${dataDir}/room-store.db";
           };
 
           # empty values necessary for registration file generation
diff --git a/nixos/modules/services/misc/matrix-appservice-irc.nix b/nixos/modules/services/misc/matrix-appservice-irc.nix
new file mode 100644
index 00000000000..a0a5973d30f
--- /dev/null
+++ b/nixos/modules/services/misc/matrix-appservice-irc.nix
@@ -0,0 +1,229 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.matrix-appservice-irc;
+
+  pkg = pkgs.matrix-appservice-irc;
+  bin = "${pkg}/bin/matrix-appservice-irc";
+
+  jsonType = (pkgs.formats.json {}).type;
+
+  configFile = pkgs.runCommandNoCC "matrix-appservice-irc.yml" {
+    # Because this program will be run at build time, we need `nativeBuildInputs`
+    nativeBuildInputs = [ (pkgs.python3.withPackages (ps: [ ps.pyyaml ps.jsonschema ])) ];
+    preferLocalBuild = true;
+
+    config = builtins.toJSON cfg.settings;
+    passAsFile = [ "config" ];
+  } ''
+    # The schema is given as yaml, we need to convert it to json
+    python -c 'import json; import yaml; import sys; json.dump(yaml.safe_load(sys.stdin), sys.stdout)' \
+      < ${pkg}/lib/node_modules/matrix-appservice-irc/config.schema.yml \
+      > config.schema.json
+    python -m jsonschema config.schema.json -i $configPath
+    cp "$configPath" "$out"
+  '';
+  registrationFile = "/var/lib/matrix-appservice-irc/registration.yml";
+in {
+  options.services.matrix-appservice-irc = with types; {
+    enable = mkEnableOption "the Matrix/IRC bridge";
+
+    port = mkOption {
+      type = port;
+      description = "The port to listen on";
+      default = 8009;
+    };
+
+    needBindingCap = mkOption {
+      type = bool;
+      description = "Whether the daemon needs to bind to ports below 1024 (e.g. for the ident service)";
+      default = false;
+    };
+
+    passwordEncryptionKeyLength = mkOption {
+      type = ints.unsigned;
+      description = "Length of the key to encrypt IRC passwords with";
+      default = 4096;
+      example = 8192;
+    };
+
+    registrationUrl = mkOption {
+      type = str;
+      description = ''
+        The URL where the application service is listening for homeserver requests,
+        from the Matrix homeserver perspective.
+      '';
+      example = "http://localhost:8009";
+    };
+
+    localpart = mkOption {
+      type = str;
+      description = "The user_id localpart to assign to the appservice";
+      default = "appservice-irc";
+    };
+
+    settings = mkOption {
+      description = ''
+        Configuration for the appservice, see
+        <link xlink:href="https://github.com/matrix-org/matrix-appservice-irc/blob/${pkgs.matrix-appservice-irc.version}/config.sample.yaml"/>
+        for supported values
+      '';
+      default = {};
+      type = submodule {
+        freeformType = jsonType;
+
+        options = {
+          homeserver = mkOption {
+            description = "Homeserver configuration";
+            default = {};
+            type = submodule {
+              freeformType = jsonType;
+
+              options = {
+                url = mkOption {
+                  type = str;
+                  description = "The URL to the home server for client-server API calls";
+                };
+
+                domain = mkOption {
+                  type = str;
+                  description = ''
+                    The 'domain' part for user IDs on this home server. Usually
+                    (but not always) is the "domain name" part of the homeserver URL.
+                  '';
+                };
+              };
+            };
+          };
+
+          database = mkOption {
+            default = {};
+            description = "Configuration for the database";
+            type = submodule {
+              freeformType = jsonType;
+
+              options = {
+                engine = mkOption {
+                  type = str;
+                  description = "Which database engine to use";
+                  default = "nedb";
+                  example = "postgres";
+                };
+
+                connectionString = mkOption {
+                  type = str;
+                  description = "The database connection string";
+                  default = "nedb://var/lib/matrix-appservice-irc/data";
+                  example = "postgres://username:password@host:port/databasename";
+                };
+              };
+            };
+          };
+
+          ircService = mkOption {
+            default = {};
+            description = "IRC bridge configuration";
+            type = submodule {
+              freeformType = jsonType;
+
+              options = {
+                passwordEncryptionKeyPath = mkOption {
+                  type = str;
+                  description = ''
+                    Location of the key with which IRC passwords are encrypted
+                    for storage. Will be generated on first run if not present.
+                  '';
+                  default = "/var/lib/matrix-appservice-irc/passkey.pem";
+                };
+
+                servers = mkOption {
+                  type = submodule { freeformType = jsonType; };
+                  description = "IRC servers to connect to";
+                };
+              };
+            };
+          };
+        };
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+    systemd.services.matrix-appservice-irc = {
+      description = "Matrix-IRC bridge";
+      before = [ "matrix-synapse.service" ]; # So the registration can be used by Synapse
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        umask 077
+        # Generate key for crypting passwords
+        if ! [ -f "${cfg.settings.ircService.passwordEncryptionKeyPath}" ]; then
+          ${pkgs.openssl}/bin/openssl genpkey \
+              -out "${cfg.settings.ircService.passwordEncryptionKeyPath}" \
+              -outform PEM \
+              -algorithm RSA \
+              -pkeyopt "rsa_keygen_bits:${toString cfg.passwordEncryptionKeyLength}"
+        fi
+        # Generate registration file
+        if ! [ -f "${registrationFile}" ]; then
+          # The easy case: the file has not been generated yet
+          ${bin} --generate-registration --file ${registrationFile} --config ${configFile} --url ${cfg.registrationUrl} --localpart ${cfg.localpart}
+        else
+          # The tricky case: we already have a generation file. Because the NixOS configuration might have changed, we need to
+          # regenerate it. But this would give the service a new random ID and tokens, so we need to back up and restore them.
+          # 1. Backup
+          id=$(grep "^id:.*$" ${registrationFile})
+          hs_token=$(grep "^hs_token:.*$" ${registrationFile})
+          as_token=$(grep "^as_token:.*$" ${registrationFile})
+          # 2. Regenerate
+          ${bin} --generate-registration --file ${registrationFile} --config ${configFile} --url ${cfg.registrationUrl} --localpart ${cfg.localpart}
+          # 3. Restore
+          sed -i "s/^id:.*$/$id/g" ${registrationFile}
+          sed -i "s/^hs_token:.*$/$hs_token/g" ${registrationFile}
+          sed -i "s/^as_token:.*$/$as_token/g" ${registrationFile}
+        fi
+        # Allow synapse access to the registration
+        if ${getBin pkgs.glibc}/bin/getent group matrix-synapse > /dev/null; then
+          chgrp matrix-synapse ${registrationFile}
+          chmod g+r ${registrationFile}
+        fi
+      '';
+
+      serviceConfig = rec {
+        Type = "simple";
+        ExecStart = "${bin} --config ${configFile} --file ${registrationFile} --port ${toString cfg.port}";
+
+        ProtectHome = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        StateDirectory = "matrix-appservice-irc";
+        StateDirectoryMode = "755";
+
+        User = "matrix-appservice-irc";
+        Group = "matrix-appservice-irc";
+
+        CapabilityBoundingSet = [ "CAP_CHOWN" ] ++ optional (cfg.needBindingCap) "CAP_NET_BIND_SERVICE";
+        AmbientCapabilities = CapabilityBoundingSet;
+        NoNewPrivileges = true;
+
+        LockPersonality = true;
+        RestrictRealtime = true;
+        PrivateMounts = true;
+        SystemCallFilter = "~@aio @clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @raw-io @setuid @swap";
+        SystemCallArchitectures = "native";
+        # AF_UNIX is required to connect to a postgres socket.
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
+      };
+    };
+
+    users.groups.matrix-appservice-irc = {};
+    users.users.matrix-appservice-irc = {
+      description = "Service user for the Matrix-IRC bridge";
+      group = "matrix-appservice-irc";
+      isSystemUser = true;
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/matrix-synapse.nix b/nixos/modules/services/misc/matrix-synapse.nix
index 3eb1073387f..3c734a94819 100644
--- a/nixos/modules/services/misc/matrix-synapse.nix
+++ b/nixos/modules/services/misc/matrix-synapse.nix
@@ -86,7 +86,9 @@ account_threepid_delegates:
   ${optionalString (cfg.account_threepid_delegates.email != null) "email: ${cfg.account_threepid_delegates.email}"}
   ${optionalString (cfg.account_threepid_delegates.msisdn != null) "msisdn: ${cfg.account_threepid_delegates.msisdn}"}
 
-room_invite_state_types: ${builtins.toJSON cfg.room_invite_state_types}
+room_prejoin_state:
+  disable_default_event_types: ${boolToString cfg.room_prejoin_state.disable_default_event_types}
+  additional_event_types: ${builtins.toJSON cfg.room_prejoin_state.additional_event_types}
 ${optionalString (cfg.macaroon_secret_key != null) ''
   macaroon_secret_key: "${cfg.macaroon_secret_key}"
 ''}
@@ -131,11 +133,23 @@ in {
       plugins = mkOption {
         type = types.listOf types.package;
         default = [ ];
-        defaultText = "with config.services.matrix-synapse.package.plugins [ matrix-synapse-ldap3 matrix-synapse-pam ]";
+        example = literalExample ''
+          with config.services.matrix-synapse.package.plugins; [
+            matrix-synapse-ldap3
+            matrix-synapse-pam
+          ];
+        '';
         description = ''
           List of additional Matrix plugins to make available.
         '';
       };
+      withJemalloc = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to preload jemalloc to reduce memory fragmentation and overall usage.
+        '';
+      };
       no_tls = mkOption {
         type = types.bool;
         default = false;
@@ -224,7 +238,7 @@ in {
         type = types.listOf (types.submodule {
           options = {
             port = mkOption {
-              type = types.int;
+              type = types.port;
               example = 8448;
               description = ''
                 The port to listen for HTTP(S) requests on.
@@ -499,8 +513,7 @@ in {
       report_stats = mkOption {
         type = types.bool;
         default = false;
-        description = ''
-        '';
+        description = "";
       };
       servers = mkOption {
         type = types.attrsOf (types.attrsOf types.str);
@@ -573,11 +586,28 @@ in {
           Delegate SMS sending to this local process (https://localhost:8090)
         '';
       };
-      room_invite_state_types = mkOption {
+      room_prejoin_state.additional_event_types = mkOption {
+        default = [];
         type = types.listOf types.str;
-        default = ["m.room.join_rules" "m.room.canonical_alias" "m.room.avatar" "m.room.name"];
         description = ''
-          A list of event types that will be included in the room_invite_state
+          Additional events to share with users who received an invite.
+        '';
+      };
+      room_prejoin_state.disable_default_event_types = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to disable the default state-event types for users invited to a room.
+          These are:
+
+          <itemizedlist>
+          <listitem><para>m.room.join_rules</para></listitem>
+          <listitem><para>m.room.canonical_alias</para></listitem>
+          <listitem><para>m.room.avatar</para></listitem>
+          <listitem><para>m.room.encryption</para></listitem>
+          <listitem><para>m.room.name</para></listitem>
+          <listitem><para>m.room.create</para></listitem>
+          </itemizedlist>
         '';
       };
       macaroon_secret_key = mkOption {
@@ -676,12 +706,12 @@ in {
     ];
 
     users.users.matrix-synapse = {
-        group = "matrix-synapse";
-        home = cfg.dataDir;
-        createHome = true;
-        shell = "${pkgs.bash}/bin/bash";
-        uid = config.ids.uids.matrix-synapse;
-      };
+      group = "matrix-synapse";
+      home = cfg.dataDir;
+      createHome = true;
+      shell = "${pkgs.bash}/bin/bash";
+      uid = config.ids.uids.matrix-synapse;
+    };
 
     users.groups.matrix-synapse = {
       gid = config.ids.gids.matrix-synapse;
@@ -697,19 +727,28 @@ in {
           --keys-directory ${cfg.dataDir} \
           --generate-keys
       '';
-      environment.PYTHONPATH = makeSearchPathOutput "lib" cfg.package.python.sitePackages [ pluginsEnv ];
+      environment = {
+        PYTHONPATH = makeSearchPathOutput "lib" cfg.package.python.sitePackages [ pluginsEnv ];
+      } // optionalAttrs (cfg.withJemalloc) {
+        LD_PRELOAD = "${pkgs.jemalloc}/lib/libjemalloc.so";
+      };
       serviceConfig = {
         Type = "notify";
         User = "matrix-synapse";
         Group = "matrix-synapse";
         WorkingDirectory = cfg.dataDir;
+        ExecStartPre = [ ("+" + (pkgs.writeShellScript "matrix-synapse-fix-permissions" ''
+          chown matrix-synapse:matrix-synapse ${cfg.dataDir}/homeserver.signing.key
+          chmod 0600 ${cfg.dataDir}/homeserver.signing.key
+        '')) ];
         ExecStart = ''
           ${cfg.package}/bin/homeserver \
             ${ concatMapStringsSep "\n  " (x: "--config-path ${x} \\") ([ configFile ] ++ cfg.extraConfigFiles) }
             --keys-directory ${cfg.dataDir}
         '';
-        ExecReload = "${pkgs.utillinux}/bin/kill -HUP $MAINPID";
+        ExecReload = "${pkgs.util-linux}/bin/kill -HUP $MAINPID";
         Restart = "on-failure";
+        UMask = "0077";
       };
     };
   };
@@ -724,6 +763,12 @@ in {
       <nixpkgs/nixos/tests/matrix-synapse.nix>
     '')
     (mkRemovedOptionModule [ "services" "matrix-synapse" "web_client" ] "")
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "room_invite_state_types" ] ''
+      You may add additional event types via
+      `services.matrix-synapse.room_prejoin_state.additional_event_types` and
+      disable the default events via
+      `services.matrix-synapse.room_prejoin_state.disable_default_event_types`.
+    '')
   ];
 
   meta.doc = ./matrix-synapse.xml;
diff --git a/nixos/modules/services/misc/matrix-synapse.xml b/nixos/modules/services/misc/matrix-synapse.xml
index fbfa838b168..41a56df0f2b 100644
--- a/nixos/modules/services/misc/matrix-synapse.xml
+++ b/nixos/modules/services/misc/matrix-synapse.xml
@@ -33,11 +33,11 @@
    <link xlink:href="https://github.com/matrix-org/synapse#synapse-installation">
    installation instructions of Synapse </link>.
 <programlisting>
-{ pkgs, ... }:
+{ pkgs, lib, ... }:
 let
   fqdn =
     let
-      join = hostName: domain: hostName + optionalString (domain != null) ".${domain}";
+      join = hostName: domain: hostName + lib.optionalString (domain != null) ".${domain}";
     in join config.networking.hostName config.networking.domain;
 in {
   networking = {
@@ -69,6 +69,9 @@ in {
       # i.e. to delegate from the host being accessible as ${config.networking.domain}
       # to another host actually running the Matrix homeserver.
       "${config.networking.domain}" = {
+        <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
+        <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
+
         <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.extraConfig">locations."= /.well-known/matrix/server".extraConfig</link> =
           let
             # use 443 instead of the default 8448 port to unite
@@ -129,7 +132,7 @@ in {
       }
     ];
   };
-};
+}
 </programlisting>
   </para>
 
@@ -203,7 +206,7 @@ Success!
     <link linkend="opt-services.nginx.virtualHosts._name_.root">root</link> = pkgs.element-web.override {
       conf = {
         default_server_config."m.homeserver" = {
-          "base_url" = "${config.networking.domain}";
+          "base_url" = "https://${fqdn}";
           "server_name" = "${fqdn}";
         };
       };
diff --git a/nixos/modules/services/misc/mautrix-telegram.nix b/nixos/modules/services/misc/mautrix-telegram.nix
index c5e8a5b85ec..0ae5797fea0 100644
--- a/nixos/modules/services/misc/mautrix-telegram.nix
+++ b/nixos/modules/services/misc/mautrix-telegram.nix
@@ -6,8 +6,9 @@ let
   dataDir = "/var/lib/mautrix-telegram";
   registrationFile = "${dataDir}/telegram-registration.yaml";
   cfg = config.services.mautrix-telegram;
-  # TODO: switch to configGen.json once RFC42 is implemented
-  settingsFile = pkgs.writeText "mautrix-telegram-settings.json" (builtins.toJSON cfg.settings);
+  settingsFormat = pkgs.formats.json {};
+  settingsFileUnsubstituted = settingsFormat.generate "mautrix-telegram-config-unsubstituted.json" cfg.settings;
+  settingsFile = "${dataDir}/config.json";
 
 in {
   options = {
@@ -15,12 +16,12 @@ in {
       enable = mkEnableOption "Mautrix-Telegram, a Matrix-Telegram hybrid puppeting/relaybot bridge";
 
       settings = mkOption rec {
-        # TODO: switch to types.config.json as prescribed by RFC42 once it's implemented
-        type = types.attrs;
         apply = recursiveUpdate default;
+        inherit (settingsFormat) type;
         default = {
           appservice = rec {
             database = "sqlite:///${dataDir}/mautrix-telegram.db";
+            database_opts = {};
             hostname = "0.0.0.0";
             port = 8080;
             address = "http://localhost:${toString port}";
@@ -29,6 +30,8 @@ in {
           bridge = {
             permissions."*" = "relaybot";
             relaybot.whitelist = [ ];
+            double_puppet_server_map = {};
+            login_shared_secret_map = {};
           };
 
           logging = {
@@ -121,6 +124,16 @@ in {
       after = [ "network-online.target" ] ++ cfg.serviceDependencies;
 
       preStart = ''
+        # Not all secrets can be passed as environment variable (yet)
+        # https://github.com/tulir/mautrix-telegram/issues/584
+        [ -f ${settingsFile} ] && rm -f ${settingsFile}
+        old_umask=$(umask)
+        umask 0277
+        ${pkgs.envsubst}/bin/envsubst \
+          -o ${settingsFile} \
+          -i ${settingsFileUnsubstituted}
+        umask $old_umask
+
         # generate the appservice's registration file if absent
         if [ ! -f '${registrationFile}' ]; then
           ${pkgs.mautrix-telegram}/bin/mautrix-telegram \
@@ -156,6 +169,8 @@ in {
             --config='${settingsFile}'
         '';
       };
+
+      restartTriggers = [ settingsFileUnsubstituted ];
     };
   };
 
diff --git a/nixos/modules/services/misc/mediatomb.nix b/nixos/modules/services/misc/mediatomb.nix
index 529f584a201..a19b73889ce 100644
--- a/nixos/modules/services/misc/mediatomb.nix
+++ b/nixos/modules/services/misc/mediatomb.nix
@@ -6,37 +6,97 @@ let
 
   gid = config.ids.gids.mediatomb;
   cfg = config.services.mediatomb;
+  name = cfg.package.pname;
+  pkg = cfg.package;
+  optionYesNo = option: if option then "yes" else "no";
+  # configuration on media directory
+  mediaDirectory = {
+    options = {
+      path = mkOption {
+        type = types.str;
+        description = ''
+          Absolute directory path to the media directory to index.
+        '';
+      };
+      recursive = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether the indexation must take place recursively or not.";
+      };
+      hidden-files = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to index the hidden files or not.";
+      };
+    };
+  };
+  toMediaDirectory = d: "<directory location=\"${d.path}\" mode=\"inotify\" recursive=\"${optionYesNo d.recursive}\" hidden-files=\"${optionYesNo d.hidden-files}\" />\n";
 
-  mtConf = pkgs.writeText "config.xml" ''
-  <?xml version="1.0" encoding="UTF-8"?>
-  <config version="2" xmlns="http://mediatomb.cc/config/2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://mediatomb.cc/config/2 http://mediatomb.cc/config/2.xsd">
+  transcodingConfig = if cfg.transcoding then with pkgs; ''
+    <transcoding enabled="yes">
+      <mimetype-profile-mappings>
+        <transcode mimetype="video/x-flv" using="vlcmpeg" />
+        <transcode mimetype="application/ogg" using="vlcmpeg" />
+        <transcode mimetype="audio/ogg" using="ogg2mp3" />
+        <transcode mimetype="audio/x-flac" using="oggflac2raw"/>
+      </mimetype-profile-mappings>
+      <profiles>
+        <profile name="ogg2mp3" enabled="no" type="external">
+          <mimetype>audio/mpeg</mimetype>
+          <accept-url>no</accept-url>
+          <first-resource>yes</first-resource>
+          <accept-ogg-theora>no</accept-ogg-theora>
+          <agent command="${ffmpeg}/bin/ffmpeg" arguments="-y -i %in -f mp3 %out" />
+          <buffer size="1048576" chunk-size="131072" fill-size="262144" />
+        </profile>
+        <profile name="vlcmpeg" enabled="no" type="external">
+          <mimetype>video/mpeg</mimetype>
+          <accept-url>yes</accept-url>
+          <first-resource>yes</first-resource>
+          <accept-ogg-theora>yes</accept-ogg-theora>
+          <agent command="${libsForQt5.vlc}/bin/vlc"
+            arguments="-I dummy %in --sout #transcode{venc=ffmpeg,vcodec=mp2v,vb=4096,fps=25,aenc=ffmpeg,acodec=mpga,ab=192,samplerate=44100,channels=2}:standard{access=file,mux=ps,dst=%out} vlc:quit" />
+          <buffer size="14400000" chunk-size="512000" fill-size="120000" />
+        </profile>
+      </profiles>
+    </transcoding>
+'' else ''
+    <transcoding enabled="no">
+    </transcoding>
+'';
+
+  configText = optionalString (! cfg.customCfg) ''
+<?xml version="1.0" encoding="UTF-8"?>
+<config version="2" xmlns="http://mediatomb.cc/config/2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://mediatomb.cc/config/2 http://mediatomb.cc/config/2.xsd">
     <server>
       <ui enabled="yes" show-tooltips="yes">
         <accounts enabled="no" session-timeout="30">
-          <account user="mediatomb" password="mediatomb"/>
+          <account user="${name}" password="${name}"/>
         </accounts>
       </ui>
       <name>${cfg.serverName}</name>
       <udn>uuid:${cfg.uuid}</udn>
       <home>${cfg.dataDir}</home>
-      <webroot>${pkgs.mediatomb}/share/mediatomb/web</webroot>
+      <interface>${cfg.interface}</interface>
+      <webroot>${pkg}/share/${name}/web</webroot>
+      <pc-directory upnp-hide="${optionYesNo cfg.pcDirectoryHide}"/>
       <storage>
         <sqlite3 enabled="yes">
-          <database-file>mediatomb.db</database-file>
+          <database-file>${name}.db</database-file>
         </sqlite3>
       </storage>
-      <protocolInfo extend="${if cfg.ps3Support then "yes" else "no"}"/>
-      ${if cfg.dsmSupport then ''
+      <protocolInfo extend="${optionYesNo cfg.ps3Support}"/>
+      ${optionalString cfg.dsmSupport ''
       <custom-http-headers>
         <add header="X-User-Agent: redsonic"/>
       </custom-http-headers>
 
       <manufacturerURL>redsonic.com</manufacturerURL>
       <modelNumber>105</modelNumber>
-      '' else ""}
-      ${if cfg.tg100Support then ''
+      ''}
+        ${optionalString cfg.tg100Support ''
       <upnp-string-limit>101</upnp-string-limit>
-      '' else ""}
+      ''}
       <extended-runtime-options>
         <mark-played-items enabled="yes" suppress-cds-updates="yes">
           <string mode="prepend">*</string>
@@ -47,11 +107,14 @@ let
       </extended-runtime-options>
     </server>
     <import hidden-files="no">
+      <autoscan use-inotify="auto">
+      ${concatMapStrings toMediaDirectory cfg.mediaDirectories}
+      </autoscan>
       <scripting script-charset="UTF-8">
-        <common-script>${pkgs.mediatomb}/share/mediatomb/js/common.js</common-script>
-        <playlist-script>${pkgs.mediatomb}/share/mediatomb/js/playlists.js</playlist-script>
+        <common-script>${pkg}/share/${name}/js/common.js</common-script>
+        <playlist-script>${pkg}/share/${name}/js/playlists.js</playlist-script>
         <virtual-layout type="builtin">
-          <import-script>${pkgs.mediatomb}/share/mediatomb/js/import.js</import-script>
+          <import-script>${pkg}/share/${name}/js/import.js</import-script>
         </virtual-layout>
       </scripting>
       <mappings>
@@ -75,12 +138,12 @@ let
           <map from="flv" to="video/x-flv"/>
           <map from="mkv" to="video/x-matroska"/>
           <map from="mka" to="audio/x-matroska"/>
-          ${if cfg.ps3Support then ''
+          ${optionalString cfg.ps3Support ''
           <map from="avi" to="video/divx"/>
-          '' else ""}
-          ${if cfg.dsmSupport then ''
+          ''}
+          ${optionalString cfg.dsmSupport ''
           <map from="avi" to="video/avi"/>
-          '' else ""}
+          ''}
         </extension-mimetype>
         <mimetype-upnpclass>
           <map from="audio/*" to="object.item.audioItem.musicTrack"/>
@@ -108,46 +171,27 @@ let
       </mappings>
       <online-content>
         <YouTube enabled="no" refresh="28800" update-at-start="no" purge-after="604800" racy-content="exclude" format="mp4" hd="no">
-          <favorites user="mediatomb"/>
+          <favorites user="${name}"/>
           <standardfeed feed="most_viewed" time-range="today"/>
-          <playlists user="mediatomb"/>
-          <uploads user="mediatomb"/>
+          <playlists user="${name}"/>
+          <uploads user="${name}"/>
           <standardfeed feed="recently_featured" time-range="today"/>
         </YouTube>
       </online-content>
     </import>
-    <transcoding enabled="${if cfg.transcoding then "yes" else "no"}">
-      <mimetype-profile-mappings>
-        <transcode mimetype="video/x-flv" using="vlcmpeg"/>
-        <transcode mimetype="application/ogg" using="vlcmpeg"/>
-        <transcode mimetype="application/ogg" using="oggflac2raw"/>
-        <transcode mimetype="audio/x-flac" using="oggflac2raw"/>
-      </mimetype-profile-mappings>
-      <profiles>
-        <profile name="oggflac2raw" enabled="no" type="external">
-          <mimetype>audio/L16</mimetype>
-          <accept-url>no</accept-url>
-          <first-resource>yes</first-resource>
-          <accept-ogg-theora>no</accept-ogg-theora>
-          <agent command="ogg123" arguments="-d raw -o byteorder:big -f %out %in"/>
-          <buffer size="1048576" chunk-size="131072" fill-size="262144"/>
-        </profile>
-        <profile name="vlcmpeg" enabled="no" type="external">
-          <mimetype>video/mpeg</mimetype>
-          <accept-url>yes</accept-url>
-          <first-resource>yes</first-resource>
-          <accept-ogg-theora>yes</accept-ogg-theora>
-          <agent command="vlc" arguments="-I dummy %in --sout #transcode{venc=ffmpeg,vcodec=mp2v,vb=4096,fps=25,aenc=ffmpeg,acodec=mpga,ab=192,samplerate=44100,channels=2}:standard{access=file,mux=ps,dst=%out} vlc:quit"/>
-          <buffer size="14400000" chunk-size="512000" fill-size="120000"/>
-        </profile>
-      </profiles>
-    </transcoding>
+    ${transcodingConfig}
   </config>
-  '';
+'';
+  defaultFirewallRules = {
+    # udp 1900 port needs to be opened for SSDP (not configurable within
+    # mediatomb/gerbera) cf.
+    # http://docs.gerbera.io/en/latest/run.html?highlight=udp%20port#network-setup
+    allowedUDPPorts = [ 1900 cfg.port ];
+    allowedTCPPorts = [ cfg.port ];
+  };
 
 in {
 
-
   ###### interface
 
   options = {
@@ -158,18 +202,27 @@ in {
         type = types.bool;
         default = false;
         description = ''
-          Whether to enable the mediatomb DLNA server.
+          Whether to enable the Gerbera/Mediatomb DLNA server.
         '';
       };
 
       serverName = mkOption {
         type = types.str;
-        default = "mediatomb";
+        default = "Gerbera (Mediatomb)";
         description = ''
           How to identify the server on the network.
         '';
       };
 
+      package = mkOption {
+        type = types.package;
+        example = literalExample "pkgs.mediatomb";
+        default = pkgs.gerbera;
+        description = ''
+          Underlying package to be used with the module (default: pkgs.gerbera).
+        '';
+      };
+
       ps3Support = mkOption {
         type = types.bool;
         default = false;
@@ -206,23 +259,34 @@ in {
 
       dataDir = mkOption {
         type = types.path;
-        default = "/var/lib/mediatomb";
+        default = "/var/lib/${name}";
         description = ''
-          The directory where mediatomb stores its state, data, etc.
+          The directory where Gerbera/Mediatomb stores its state, data, etc.
+        '';
+      };
+
+      pcDirectoryHide = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to list the top-level directory or not (from upnp client standpoint).
         '';
       };
 
       user = mkOption {
+        type = types.str;
         default = "mediatomb";
-        description = "User account under which mediatomb runs.";
+        description = "User account under which ${name} runs.";
       };
 
       group = mkOption {
+        type = types.str;
         default = "mediatomb";
-        description = "Group account under which mediatomb runs.";
+        description = "Group account under which ${name} runs.";
       };
 
       port = mkOption {
+        type = types.int;
         default = 49152;
         description = ''
           The network port to listen on.
@@ -230,40 +294,76 @@ in {
       };
 
       interface = mkOption {
+        type = types.str;
         default = "";
         description = ''
           A specific interface to bind to.
         '';
       };
 
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If false (the default), this is up to the user to declare the firewall rules.
+          If true, this opens port 1900 (tcp and udp) and the port specified by
+          <option>sercvices.mediatomb.port</option>.
+
+          If the option <option>services.mediatomb.interface</option> is set,
+          the firewall rules opened are dedicated to that interface. Otherwise,
+          those rules are opened globally.
+        '';
+      };
+
       uuid = mkOption {
+        type = types.str;
         default = "fdfc8a4e-a3ad-4c1d-b43d-a2eedb03a687";
         description = ''
           A unique (on your network) to identify the server by.
         '';
       };
 
+      mediaDirectories = mkOption {
+        type = with types; listOf (submodule mediaDirectory);
+        default = {};
+        description = ''
+          Declare media directories to index.
+        '';
+        example = [
+          { path = "/data/pictures"; recursive = false; hidden-files = false; }
+          { path = "/data/audio"; recursive = true; hidden-files = false; }
+        ];
+      };
+
       customCfg = mkOption {
         type = types.bool;
         default = false;
         description = ''
-          Allow mediatomb to create and use its own config file inside ${cfg.dataDir}.
+          Allow ${name} to create and use its own config file inside the <literal>dataDir</literal> as
+          configured by <option>services.mediatomb.dataDir</option>.
+          Deactivated by default, the service then runs with the configuration generated from this module.
+          Otherwise, when enabled, no service configuration is generated. Gerbera/Mediatomb then starts using
+          config.xml within the configured <literal>dataDir</literal>. It's up to the user to make a correct
+          configuration file.
         '';
       };
+
     };
   };
 
 
   ###### implementation
 
-  config = mkIf cfg.enable {
+  config = let binaryCommand = "${pkg}/bin/${name}";
+               interfaceFlag = optionalString ( cfg.interface != "") "--interface ${cfg.interface}";
+               configFlag = optionalString (! cfg.customCfg) "--config ${pkgs.writeText "config.xml" configText}";
+    in mkIf cfg.enable {
     systemd.services.mediatomb = {
-      description = "MediaTomb media Server";
+      description = "${cfg.serverName} media Server";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      path = [ pkgs.mediatomb ];
-      serviceConfig.ExecStart = "${pkgs.mediatomb}/bin/mediatomb -p ${toString cfg.port} ${if cfg.interface!="" then "-e ${cfg.interface}" else ""} ${if cfg.customCfg then "" else "-c ${mtConf}"} -m ${cfg.dataDir}";
-      serviceConfig.User = "${cfg.user}";
+      serviceConfig.ExecStart = "${binaryCommand} --port ${toString cfg.port} ${interfaceFlag} ${configFlag} --home ${cfg.dataDir}";
+      serviceConfig.User = cfg.user;
     };
 
     users.groups = optionalAttrs (cfg.group == "mediatomb") {
@@ -274,15 +374,18 @@ in {
       mediatomb = {
         isSystemUser = true;
         group = cfg.group;
-        home = "${cfg.dataDir}";
+        home = cfg.dataDir;
         createHome = true;
-        description = "Mediatomb DLNA Server User";
+        description = "${name} DLNA Server User";
       };
     };
 
-    networking.firewall = {
-      allowedUDPPorts = [ 1900 cfg.port ];
-      allowedTCPPorts = [ cfg.port ];
-    };
+    # Open firewall only if users enable it
+    networking.firewall = mkMerge [
+      (mkIf (cfg.openFirewall && cfg.interface != "") {
+        interfaces."${cfg.interface}" = defaultFirewallRules;
+      })
+      (mkIf (cfg.openFirewall && cfg.interface == "") defaultFirewallRules)
+    ];
   };
 }
diff --git a/nixos/modules/services/misc/mwlib.nix b/nixos/modules/services/misc/mwlib.nix
index 6b41b552a86..8dd17c06c0b 100644
--- a/nixos/modules/services/misc/mwlib.nix
+++ b/nixos/modules/services/misc/mwlib.nix
@@ -34,7 +34,7 @@ in
 
       port = mkOption {
         default = 8899;
-        type = types.int;
+        type = types.port;
         description = "Specify port to listen on.";
       }; # nserve.port
 
@@ -68,7 +68,7 @@ in
 
       port = mkOption {
         default = 14311;
-        type = types.int;
+        type = types.port;
         description = "Specify port to listen on.";
       }; # qserve.port
 
@@ -137,7 +137,7 @@ in
 
             port = mkOption {
               default = 8898;
-              type = types.int;
+              type = types.port;
               description = "Port to listen to when serving files from cache.";
             }; # nslave.http.port
 
diff --git a/nixos/modules/services/misc/n8n.nix b/nixos/modules/services/misc/n8n.nix
new file mode 100644
index 00000000000..516d0f70ef0
--- /dev/null
+++ b/nixos/modules/services/misc/n8n.nix
@@ -0,0 +1,78 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.n8n;
+  format = pkgs.formats.json {};
+  configFile = format.generate "n8n.json" cfg.settings;
+in
+{
+  options.services.n8n = {
+
+    enable = mkEnableOption "n8n server";
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Open ports in the firewall for the n8n web interface.";
+    };
+
+    settings = mkOption {
+      type = format.type;
+      default = {};
+      description = ''
+        Configuration for n8n, see <link xlink:href="https://docs.n8n.io/reference/configuration.html"/>
+        for supported values.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    services.n8n.settings = {
+      # We use this to open the firewall, so we need to know about the default at eval time
+      port = lib.mkDefault 5678;
+    };
+
+    systemd.services.n8n = {
+      description = "N8N service";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        # This folder must be writeable as the application is storing
+        # its data in it, so the StateDirectory is a good choice
+        N8N_USER_FOLDER = "/var/lib/n8n";
+        N8N_CONFIG_FILES = "${configFile}";
+      };
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${pkgs.n8n}/bin/n8n";
+        Restart = "on-failure";
+        StateDirectory = "n8n";
+
+        # Basic Hardening
+        NoNewPrivileges = "yes";
+        PrivateTmp = "yes";
+        PrivateDevices = "yes";
+        DevicePolicy = "closed";
+        DynamicUser = "true";
+        ProtectSystem = "strict";
+        ProtectHome = "read-only";
+        ProtectControlGroups = "yes";
+        ProtectKernelModules = "yes";
+        ProtectKernelTunables = "yes";
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
+        RestrictNamespaces = "yes";
+        RestrictRealtime = "yes";
+        RestrictSUIDSGID = "yes";
+        MemoryDenyWriteExecute = "yes";
+        LockPersonality = "yes";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.settings.port ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix
index 924a007efc6..133e96da0ec 100644
--- a/nixos/modules/services/misc/nix-daemon.nix
+++ b/nixos/modules/services/misc/nix-daemon.nix
@@ -21,6 +21,7 @@ let
          calls in `libstore/build.cc', don't add any supplementary group
          here except "nixbld".  */
       uid = builtins.add config.ids.uids.nixbld nr;
+      isSystemUser = true;
       group = "nixbld";
       extraGroups = [ "nixbld" ];
     };
@@ -45,7 +46,7 @@ let
         trusted-substituters = ${toString cfg.trustedBinaryCaches}
         trusted-public-keys = ${toString cfg.binaryCachePublicKeys}
         auto-optimise-store = ${boolToString cfg.autoOptimiseStore}
-        require-sigs = ${if cfg.requireSignedBinaryCaches then "true" else "false"}
+        require-sigs = ${boolToString cfg.requireSignedBinaryCaches}
         trusted-users = ${toString cfg.trustedUsers}
         allowed-users = ${toString cfg.allowedUsers}
         ${optionalString (!cfg.distributedBuilds) ''
@@ -500,13 +501,6 @@ in
 
   config = {
 
-    assertions = [
-      {
-        assertion = config.nix.distributedBuilds || config.nix.buildMachines == [];
-        message = "You must set `nix.distributedBuilds = true` to use nix.buildMachines";
-      }
-    ];
-
     nix.binaryCachePublicKeys = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ];
     nix.binaryCaches = [ "https://cache.nixos.org/" ];
 
@@ -546,7 +540,7 @@ in
     systemd.sockets.nix-daemon.wantedBy = [ "sockets.target" ];
 
     systemd.services.nix-daemon =
-      { path = [ nix pkgs.utillinux config.programs.ssh.package ]
+      { path = [ nix pkgs.util-linux config.programs.ssh.package ]
           ++ optionals cfg.distributedBuilds [ pkgs.gzip ];
 
         environment = cfg.envVars
@@ -594,16 +588,10 @@ in
 
     nix.systemFeatures = mkDefault (
       [ "nixos-test" "benchmark" "big-parallel" "kvm" ] ++
-      optionals (pkgs.stdenv.isx86_64 && pkgs.hostPlatform.platform ? gcc.arch) (
-        # a x86_64 builder can run code for `platform.gcc.arch` and minor architectures:
-        [ "gccarch-${pkgs.hostPlatform.platform.gcc.arch}" ] ++ {
-          sandybridge    = [ "gccarch-westmere" ];
-          ivybridge      = [ "gccarch-westmere" "gccarch-sandybridge" ];
-          haswell        = [ "gccarch-westmere" "gccarch-sandybridge" "gccarch-ivybridge" ];
-          broadwell      = [ "gccarch-westmere" "gccarch-sandybridge" "gccarch-ivybridge" "gccarch-haswell" ];
-          skylake        = [ "gccarch-westmere" "gccarch-sandybridge" "gccarch-ivybridge" "gccarch-haswell" "gccarch-broadwell" ];
-          skylake-avx512 = [ "gccarch-westmere" "gccarch-sandybridge" "gccarch-ivybridge" "gccarch-haswell" "gccarch-broadwell" "gccarch-skylake" ];
-        }.${pkgs.hostPlatform.platform.gcc.arch} or []
+      optionals (pkgs.hostPlatform ? gcc.arch) (
+        # a builder can run code for `gcc.arch` and inferior architectures
+        [ "gccarch-${pkgs.hostPlatform.gcc.arch}" ] ++
+        map (x: "gccarch-${x}") lib.systems.architectures.inferiors.${pkgs.hostPlatform.gcc.arch}
       )
     );
 
diff --git a/nixos/modules/services/misc/nix-gc.nix b/nixos/modules/services/misc/nix-gc.nix
index 12bed05757a..a7a6a3b5964 100644
--- a/nixos/modules/services/misc/nix-gc.nix
+++ b/nixos/modules/services/misc/nix-gc.nix
@@ -21,13 +21,45 @@ in
       };
 
       dates = mkOption {
+        type = types.str;
         default = "03:15";
+        example = "weekly";
+        description = ''
+          How often or when garbage collection is performed. For most desktop and server systems
+          a sufficient garbage collection is once a week.
+
+          The format is described in
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>.
+        '';
+      };
+
+      randomizedDelaySec = mkOption {
+        default = "0";
         type = types.str;
+        example = "45min";
         description = ''
-          Specification (in the format described by
+          Add a randomized delay before each automatic upgrade.
+          The delay will be chosen between zero and this value.
+          This value must be a time span in the format specified by
           <citerefentry><refentrytitle>systemd.time</refentrytitle>
-          <manvolnum>7</manvolnum></citerefentry>) of the time at
-          which the garbage collector will run.
+          <manvolnum>7</manvolnum></citerefentry>
+        '';
+      };
+
+      persistent = mkOption {
+        default = true;
+        type = types.bool;
+        example = false;
+        description = ''
+          Takes a boolean argument. If true, the time when the service
+          unit was last triggered is stored on disk. When the timer is
+          activated, the service unit is triggered immediately if it
+          would have been triggered at least once during the time when
+          the timer was inactive. Such triggering is nonetheless
+          subject to the delay imposed by RandomizedDelaySec=. This is
+          useful to catch up on missed runs of the service when the
+          system was powered down.
         '';
       };
 
@@ -50,11 +82,18 @@ in
 
   config = {
 
-    systemd.services.nix-gc =
-      { description = "Nix Garbage Collector";
-        script = "exec ${config.nix.package.out}/bin/nix-collect-garbage ${cfg.options}";
-        startAt = optional cfg.automatic cfg.dates;
+    systemd.services.nix-gc = {
+      description = "Nix Garbage Collector";
+      script = "exec ${config.nix.package.out}/bin/nix-collect-garbage ${cfg.options}";
+      startAt = optional cfg.automatic cfg.dates;
+    };
+
+    systemd.timers.nix-gc = lib.mkIf cfg.automatic {
+      timerConfig = {
+        RandomizedDelaySec = cfg.randomizedDelaySec;
+        Persistent = cfg.persistent;
       };
+    };
 
   };
 
diff --git a/nixos/modules/services/misc/nzbhydra2.nix b/nixos/modules/services/misc/nzbhydra2.nix
new file mode 100644
index 00000000000..c396b4b8f6e
--- /dev/null
+++ b/nixos/modules/services/misc/nzbhydra2.nix
@@ -0,0 +1,78 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let cfg = config.services.nzbhydra2;
+
+in {
+  options = {
+    services.nzbhydra2 = {
+      enable = mkEnableOption "NZBHydra2";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/nzbhydra2";
+        description = "The directory where NZBHydra2 stores its data files.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description =
+          "Open ports in the firewall for the NZBHydra2 web interface.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.nzbhydra2;
+        defaultText = "pkgs.nzbhydra2";
+        description = "NZBHydra2 package to use.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules =
+      [ "d '${cfg.dataDir}' 0700 nzbhydra2 nzbhydra2 - -" ];
+
+    systemd.services.nzbhydra2 = {
+      description = "NZBHydra2";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = "nzbhydra2";
+        Group = "nzbhydra2";
+        ExecStart =
+          "${cfg.package}/bin/nzbhydra2 --nobrowser --datafolder '${cfg.dataDir}'";
+        Restart = "on-failure";
+        # Hardening
+        NoNewPrivileges = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        DevicePolicy = "closed";
+        ProtectSystem = "strict";
+        ReadWritePaths = cfg.dataDir;
+        ProtectHome = "read-only";
+        ProtectControlGroups = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        RestrictAddressFamilies ="AF_UNIX AF_INET AF_INET6 AF_NETLINK";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        LockPersonality = true;
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall { allowedTCPPorts = [ 5076 ]; };
+
+    users.users.nzbhydra2 = {
+      group = "nzbhydra2";
+      isSystemUser = true;
+    };
+
+    users.groups.nzbhydra2 = {};
+  };
+}
diff --git a/nixos/modules/services/misc/octoprint.nix b/nixos/modules/services/misc/octoprint.nix
index e2fbd3b401c..c926d889b37 100644
--- a/nixos/modules/services/misc/octoprint.nix
+++ b/nixos/modules/services/misc/octoprint.nix
@@ -40,7 +40,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 5000;
         description = ''
           Port to bind OctoPrint to.
@@ -66,9 +66,10 @@ in
       };
 
       plugins = mkOption {
+        type = types.functionTo (types.listOf types.package);
         default = plugins: [];
         defaultText = "plugins: []";
-        example = literalExample "plugins: with plugins; [ m33-fio stlviewer ]";
+        example = literalExample "plugins: with plugins; [ themeify stlviewer ]";
         description = "Additional plugins to be used. Available plugins are passed through the plugins input.";
       };
 
diff --git a/nixos/modules/services/misc/ombi.nix b/nixos/modules/services/misc/ombi.nix
new file mode 100644
index 00000000000..b5882168e51
--- /dev/null
+++ b/nixos/modules/services/misc/ombi.nix
@@ -0,0 +1,81 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let cfg = config.services.ombi;
+
+in {
+  options = {
+    services.ombi = {
+      enable = mkEnableOption ''
+        Ombi.
+        Optionally see <link xlink:href="https://docs.ombi.app/info/reverse-proxy"/>
+        on how to set up a reverse proxy
+      '';
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/ombi";
+        description = "The directory where Ombi stores its data files.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 5000;
+        description = "The port for the Ombi web interface.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the Ombi web interface.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "ombi";
+        description = "User account under which Ombi runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "ombi";
+        description = "Group under which Ombi runs.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0700 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.ombi = {
+      description = "Ombi";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.ombi}/bin/Ombi --storage '${cfg.dataDir}' --host 'http://*:${toString cfg.port}'";
+        Restart = "on-failure";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+
+    users.users = mkIf (cfg.user == "ombi") {
+      ombi = {
+        isSystemUser = true;
+        group = cfg.group;
+        home = cfg.dataDir;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "ombi") { ombi = { }; };
+  };
+}
diff --git a/nixos/modules/services/misc/packagekit.nix b/nixos/modules/services/misc/packagekit.nix
index 325c4e84e0d..93bd206bd98 100644
--- a/nixos/modules/services/misc/packagekit.nix
+++ b/nixos/modules/services/misc/packagekit.nix
@@ -1,55 +1,60 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
-
   cfg = config.services.packagekit;
 
-  packagekitConf = ''
-    [Daemon]
-    DefaultBackend=${cfg.backend}
-    KeepCache=false
-  '';
+  inherit (lib)
+    mkEnableOption mkOption mkIf mkRemovedOptionModule types
+    listToAttrs recursiveUpdate;
 
-  vendorConf = ''
-    [PackagesNotFound]
-    DefaultUrl=https://github.com/NixOS/nixpkgs
-    CodecUrl=https://github.com/NixOS/nixpkgs
-    HardwareUrl=https://github.com/NixOS/nixpkgs
-    FontUrl=https://github.com/NixOS/nixpkgs
-    MimeUrl=https://github.com/NixOS/nixpkgs
-  '';
+  iniFmt = pkgs.formats.ini { };
 
-in
+  confFiles = [
+    (iniFmt.generate "PackageKit.conf" (recursiveUpdate
+      {
+        Daemon = {
+          DefaultBackend = "test_nop";
+          KeepCache = false;
+        };
+      }
+      cfg.settings))
 
+    (iniFmt.generate "Vendor.conf" (recursiveUpdate
+      {
+        PackagesNotFound = rec {
+          DefaultUrl = "https://github.com/NixOS/nixpkgs";
+          CodecUrl = DefaultUrl;
+          HardwareUrl = DefaultUrl;
+          FontUrl = DefaultUrl;
+          MimeUrl = DefaultUrl;
+        };
+      }
+      cfg.vendorSettings))
+  ];
+
+in
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "packagekit" "backend" ] "The only backend that doesn't blow up is `test_nop`.")
+  ];
 
-  options = {
+  options.services.packagekit = {
+    enable = mkEnableOption ''
+      PackageKit provides a cross-platform D-Bus abstraction layer for
+      installing software. Software utilizing PackageKit can install
+      software regardless of the package manager.
+    '';
 
-    services.packagekit = {
-      enable = mkEnableOption
-        ''
-          PackageKit provides a cross-platform D-Bus abstraction layer for
-          installing software. Software utilizing PackageKit can install
-          software regardless of the package manager.
-        '';
+    settings = mkOption {
+      type = iniFmt.type;
+      default = { };
+      description = "Additional settings passed straight through to PackageKit.conf";
+    };
 
-      # TODO: integrate with PolicyKit if the nix backend matures to the point
-      # where it will require elevated permissions
-      backend = mkOption {
-        type = types.enum [ "test_nop" ];
-        default = "test_nop";
-        description = ''
-          PackageKit supports multiple different backends and <literal>auto</literal> which
-          should do the right thing.
-          </para>
-          <para>
-          On NixOS however, we do not have a backend compatible with nix 2.0
-          (refer to <link xlink:href="https://github.com/NixOS/nix/issues/233">this issue</link> so we have to force
-          it to <literal>test_nop</literal> for now.
-        '';
-      };
+    vendorSettings = mkOption {
+      type = iniFmt.type;
+      default = { };
+      description = "Additional settings passed straight through to Vendor.conf";
     };
   };
 
@@ -59,7 +64,9 @@ in
 
     systemd.packages = with pkgs; [ packagekit ];
 
-    environment.etc."PackageKit/PackageKit.conf".text = packagekitConf;
-    environment.etc."PackageKit/Vendor.conf".text = vendorConf;
+    environment.etc = listToAttrs (map
+      (e:
+        lib.nameValuePair "PackageKit/${e.name}" { source = e; })
+      confFiles);
   };
 }
diff --git a/nixos/modules/services/misc/paperless.nix b/nixos/modules/services/misc/paperless.nix
index bfaf760fb83..43730b80eb2 100644
--- a/nixos/modules/services/misc/paperless.nix
+++ b/nixos/modules/services/misc/paperless.nix
@@ -67,7 +67,7 @@ in
     };
 
     port = mkOption {
-      type = types.int;
+      type = types.port;
       default = 28981;
       description = "Server port to listen on.";
     };
diff --git a/nixos/modules/services/misc/pinnwand.nix b/nixos/modules/services/misc/pinnwand.nix
index aa1ee5cfaa7..cbc796c9a7c 100644
--- a/nixos/modules/services/misc/pinnwand.nix
+++ b/nixos/modules/services/misc/pinnwand.nix
@@ -24,55 +24,80 @@ in
         Your <filename>pinnwand.toml</filename> as a Nix attribute set. Look up
         possible options in the <link xlink:href="https://github.com/supakeen/pinnwand/blob/master/pinnwand.toml-example">pinnwand.toml-example</link>.
       '';
-      default = {
-        # https://github.com/supakeen/pinnwand/blob/master/pinnwand.toml-example
-        database_uri = "sqlite:///var/lib/pinnwand/pinnwand.db";
-        preferred_lexeres = [];
-        paste_size = 262144;
-        paste_help = ''
-          <p>Welcome to pinnwand, this site is a pastebin. It allows you to share code with others. If you write code in the text area below and press the paste button you will be given a link you can share with others so they can view your code as well.</p><p>People with the link can view your pasted code, only you can remove your paste and it expires automatically. Note that anyone could guess the URI to your paste so don't rely on it being private.</p>
-        '';
-        footer = ''
-          View <a href="//github.com/supakeen/pinnwand" target="_BLANK">source code</a>, the <a href="/removal">removal</a> or <a href="/expiry">expiry</a> stories, or read the <a href="/about">about</a> page.
-        '';
-      };
+      default = {};
     };
   };
 
   config = mkIf cfg.enable {
-    systemd.services.pinnwand = {
-      description = "Pinnwannd HTTP Server";
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
+    services.pinnwand.settings = {
+      database_uri = mkDefault "sqlite:////var/lib/pinnwand/pinnwand.db";
+      paste_size = mkDefault 262144;
+      paste_help = mkDefault ''
+        <p>Welcome to pinnwand, this site is a pastebin. It allows you to share code with others. If you write code in the text area below and press the paste button you will be given a link you can share with others so they can view your code as well.</p><p>People with the link can view your pasted code, only you can remove your paste and it expires automatically. Note that anyone could guess the URI to your paste so don't rely on it being private.</p>
+      '';
+      footer = mkDefault ''
+        View <a href="//github.com/supakeen/pinnwand" target="_BLANK">source code</a>, the <a href="/removal">removal</a> or <a href="/expiry">expiry</a> stories, or read the <a href="/about">about</a> page.
+      '';
+    };
+
+    systemd.services = let
+      hardeningOptions = {
+        User = "pinnwand";
+        DynamicUser = true;
 
-      unitConfig.Documentation = "https://pinnwand.readthedocs.io/en/latest/";
-      serviceConfig = {
-        ExecStart = "${pkgs.pinnwand}/bin/pinnwand --configuration-path ${configFile} http --port ${toString(cfg.port)}";
         StateDirectory = "pinnwand";
         StateDirectoryMode = "0700";
 
         AmbientCapabilities = [];
         CapabilityBoundingSet = "";
         DevicePolicy = "closed";
-        DynamicUser = true;
         LockPersonality = true;
         MemoryDenyWriteExecute = true;
         PrivateDevices = true;
         PrivateUsers = true;
+        ProcSubset = "pid";
         ProtectClock = true;
         ProtectControlGroups = true;
-        ProtectKernelLogs = true;
         ProtectHome = true;
         ProtectHostname = true;
+        ProtectKernelLogs = true;
         ProtectKernelModules = true;
         ProtectKernelTunables = true;
-        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        ProtectProc = "invisible";
+        RestrictAddressFamilies = [
+          "AF_UNIX"
+          "AF_INET"
+          "AF_INET6"
+        ];
         RestrictNamespaces = true;
         RestrictRealtime = true;
         SystemCallArchitectures = "native";
         SystemCallFilter = "@system-service";
         UMask = "0077";
       };
+
+      command = "${pkgs.pinnwand}/bin/pinnwand --configuration-path ${configFile}";
+    in {
+      pinnwand = {
+        description = "Pinnwannd HTTP Server";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        unitConfig.Documentation = "https://pinnwand.readthedocs.io/en/latest/";
+
+        serviceConfig = {
+          ExecStart = "${command} http --port ${toString(cfg.port)}";
+        } // hardeningOptions;
+      };
+
+      pinnwand-reaper = {
+        description = "Pinnwand Reaper";
+        startAt = "daily";
+
+        serviceConfig = {
+          ExecStart = "${command} -vvvv reap";  # verbosity increased to show number of deleted pastes
+        } // hardeningOptions;
+      };
     };
   };
 }
diff --git a/nixos/modules/services/misc/plikd.nix b/nixos/modules/services/misc/plikd.nix
new file mode 100644
index 00000000000..a62dbef1d2a
--- /dev/null
+++ b/nixos/modules/services/misc/plikd.nix
@@ -0,0 +1,82 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.plikd;
+
+  format = pkgs.formats.toml {};
+  plikdCfg = format.generate "plikd.cfg" cfg.settings;
+in
+{
+  options = {
+    services.plikd = {
+      enable = mkEnableOption "the plikd server";
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the plikd.";
+      };
+
+      settings = mkOption {
+        type = format.type;
+        default = {};
+        description = ''
+          Configuration for plikd, see <link xlink:href="https://github.com/root-gg/plik/blob/master/server/plikd.cfg"/>
+          for supported values.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.plikd.settings = mapAttrs (name: mkDefault) {
+      ListenPort = 8080;
+      ListenAddress = "localhost";
+      DataBackend = "file";
+      DataBackendConfig = {
+         Directory = "/var/lib/plikd";
+      };
+      MetadataBackendConfig = {
+        Driver = "sqlite3";
+        ConnectionString = "/var/lib/plikd/plik.db";
+      };
+    };
+
+    systemd.services.plikd = {
+      description = "Plikd file sharing server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${pkgs.plikd}/bin/plikd --config ${plikdCfg}";
+        Restart = "on-failure";
+        StateDirectory = "plikd";
+        LogsDirectory = "plikd";
+        DynamicUser = true;
+
+        # Basic hardening
+        NoNewPrivileges = "yes";
+        PrivateTmp = "yes";
+        PrivateDevices = "yes";
+        DevicePolicy = "closed";
+        ProtectSystem = "strict";
+        ProtectHome = "read-only";
+        ProtectControlGroups = "yes";
+        ProtectKernelModules = "yes";
+        ProtectKernelTunables = "yes";
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
+        RestrictNamespaces = "yes";
+        RestrictRealtime = "yes";
+        RestrictSUIDSGID = "yes";
+        MemoryDenyWriteExecute = "yes";
+        LockPersonality = "yes";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.settings.ListenPort ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/podgrab.nix b/nixos/modules/services/misc/podgrab.nix
new file mode 100644
index 00000000000..7077408b794
--- /dev/null
+++ b/nixos/modules/services/misc/podgrab.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.podgrab;
+in
+{
+  options.services.podgrab = with lib; {
+    enable = mkEnableOption "Podgrab, a self-hosted podcast manager";
+
+    passwordFile = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "/run/secrets/password.env";
+      description = ''
+        The path to a file containing the PASSWORD environment variable
+        definition for Podgrab's authentification.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 8080;
+      example = 4242;
+      description = "The port on which Podgrab will listen for incoming HTTP traffic.";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.podgrab = {
+      description = "Podgrab podcast manager";
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        CONFIG = "/var/lib/podgrab/config";
+        DATA = "/var/lib/podgrab/data";
+        GIN_MODE = "release";
+        PORT = toString cfg.port;
+      };
+      serviceConfig = {
+        DynamicUser = true;
+        EnvironmentFile = lib.optional (cfg.passwordFile != null) [
+          cfg.passwordFile
+        ];
+        ExecStart = "${pkgs.podgrab}/bin/podgrab";
+        WorkingDirectory = "${pkgs.podgrab}/share";
+        StateDirectory = [ "podgrab/config" "podgrab/data" ];
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ ambroisie ];
+}
diff --git a/nixos/modules/services/misc/pykms.nix b/nixos/modules/services/misc/pykms.nix
index d6aeae48ccb..2f752bcc7ed 100644
--- a/nixos/modules/services/misc/pykms.nix
+++ b/nixos/modules/services/misc/pykms.nix
@@ -1,12 +1,12 @@
 { config, lib, pkgs, ... }:
 
 with lib;
-
 let
   cfg = config.services.pykms;
   libDir = "/var/lib/pykms";
 
-in {
+in
+{
   meta.maintainers = with lib.maintainers; [ peterhoeg ];
 
   imports = [
@@ -46,14 +46,14 @@ in {
       };
 
       logLevel = mkOption {
-        type = types.enum [ "CRITICAL" "ERROR" "WARNING" "INFO" "DEBUG" "MINI" ];
+        type = types.enum [ "CRITICAL" "ERROR" "WARNING" "INFO" "DEBUG" "MININFO" ];
         default = "INFO";
         description = "How much to log";
       };
 
       extraArgs = mkOption {
         type = types.listOf types.str;
-        default = [];
+        default = [ ];
         description = "Additional arguments";
       };
     };
@@ -74,8 +74,9 @@ in {
         ExecStartPre = "${getBin pykms}/libexec/create_pykms_db.sh ${libDir}/clients.db";
         ExecStart = lib.concatStringsSep " " ([
           "${getBin pykms}/bin/server"
-          "--logfile STDOUT"
-          "--loglevel ${cfg.logLevel}"
+          "--logfile=STDOUT"
+          "--loglevel=${cfg.logLevel}"
+          "--sqlite=${libDir}/clients.db"
         ] ++ cfg.extraArgs ++ [
           cfg.listenAddress
           (toString cfg.port)
diff --git a/nixos/modules/services/misc/redmine.nix b/nixos/modules/services/misc/redmine.nix
index 0e71cf92569..66c8e558fb0 100644
--- a/nixos/modules/services/misc/redmine.nix
+++ b/nixos/modules/services/misc/redmine.nix
@@ -1,12 +1,12 @@
 { config, lib, pkgs, ... }:
 
 let
-  inherit (lib) mkDefault mkEnableOption mkIf mkOption types;
+  inherit (lib) mkBefore mkDefault mkEnableOption mkIf mkOption mkRemovedOptionModule types;
   inherit (lib) concatStringsSep literalExample mapAttrsToList;
-  inherit (lib) optional optionalAttrs optionalString singleton versionAtLeast;
+  inherit (lib) optional optionalAttrs optionalString;
 
   cfg = config.services.redmine;
-
+  format = pkgs.formats.yaml {};
   bundle = "${cfg.package}/share/redmine/bin/bundle";
 
   databaseYml = pkgs.writeText "database.yml" ''
@@ -20,31 +20,15 @@ let
       ${optionalString (cfg.database.type == "mysql2" && cfg.database.socket != null) "socket: ${cfg.database.socket}"}
   '';
 
-  configurationYml = pkgs.writeText "configuration.yml" ''
-    default:
-      scm_subversion_command: ${pkgs.subversion}/bin/svn
-      scm_mercurial_command: ${pkgs.mercurial}/bin/hg
-      scm_git_command: ${pkgs.gitAndTools.git}/bin/git
-      scm_cvs_command: ${pkgs.cvs}/bin/cvs
-      scm_bazaar_command: ${pkgs.breezy}/bin/bzr
-      scm_darcs_command: ${pkgs.darcs}/bin/darcs
-
-    ${cfg.extraConfig}
-  '';
-
-  additionalEnvironment = pkgs.writeText "additional_environment.rb" ''
-    config.logger = Logger.new("${cfg.stateDir}/log/production.log", 14, 1048576)
-    config.logger.level = Logger::INFO
-
-    ${cfg.extraEnv}
-  '';
+  configurationYml = format.generate "configuration.yml" cfg.settings;
+  additionalEnvironment = pkgs.writeText "additional_environment.rb" cfg.extraEnv;
 
   unpackTheme = unpack "theme";
   unpackPlugin = unpack "plugin";
   unpack = id: (name: source:
     pkgs.stdenv.mkDerivation {
       name = "redmine-${id}-${name}";
-      buildInputs = [ pkgs.unzip ];
+      nativeBuildInputs = [ pkgs.unzip ];
       buildCommand = ''
         mkdir -p $out
         cd $out
@@ -56,8 +40,13 @@ let
   pgsqlLocal = cfg.database.createLocally && cfg.database.type == "postgresql";
 
 in
-
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "redmine" "extraConfig" ] "Use services.redmine.settings instead.")
+    (mkRemovedOptionModule [ "services" "redmine" "database" "password" ] "Use services.redmine.database.passwordFile instead.")
+  ];
+
+  # interface
   options = {
     services.redmine = {
       enable = mkEnableOption "Redmine";
@@ -82,7 +71,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 3000;
         description = "Port on which Redmine is ran.";
       };
@@ -93,21 +82,24 @@ in
         description = "The state directory, logs and plugins are stored here.";
       };
 
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
+      settings = mkOption {
+        type = format.type;
+        default = {};
         description = ''
-          Extra configuration in configuration.yml.
-
-          See <link xlink:href="https://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration"/>
+          Redmine configuration (<filename>configuration.yml</filename>). Refer to
+          <link xlink:href="https://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration"/>
           for details.
         '';
         example = literalExample ''
-          email_delivery:
-            delivery_method: smtp
-            smtp_settings:
-              address: mail.example.com
-              port: 25
+          {
+            email_delivery = {
+              delivery_method = "smtp";
+              smtp_settings = {
+                address = "mail.example.com";
+                port = 25;
+              };
+            };
+          }
         '';
       };
 
@@ -186,16 +178,6 @@ in
           description = "Database user.";
         };
 
-        password = mkOption {
-          type = types.str;
-          default = "";
-          description = ''
-            The password corresponding to <option>database.user</option>.
-            Warning: this is stored in cleartext in the Nix store!
-            Use <option>database.passwordFile</option> instead.
-          '';
-        };
-
         passwordFile = mkOption {
           type = types.nullOr types.path;
           default = null;
@@ -226,11 +208,12 @@ in
     };
   };
 
+  # implementation
   config = mkIf cfg.enable {
 
     assertions = [
-      { assertion = cfg.database.passwordFile != null || cfg.database.password != "" || cfg.database.socket != null;
-        message = "one of services.redmine.database.socket, services.redmine.database.passwordFile, or services.redmine.database.password must be set";
+      { assertion = cfg.database.passwordFile != null || cfg.database.socket != null;
+        message = "one of services.redmine.database.socket or services.redmine.database.passwordFile must be set";
       }
       { assertion = cfg.database.createLocally -> cfg.database.user == cfg.user;
         message = "services.redmine.database.user must be set to ${cfg.user} if services.redmine.database.createLocally is set true";
@@ -243,6 +226,22 @@ in
       }
     ];
 
+    services.redmine.settings = {
+      production = {
+        scm_subversion_command = "${pkgs.subversion}/bin/svn";
+        scm_mercurial_command = "${pkgs.mercurial}/bin/hg";
+        scm_git_command = "${pkgs.git}/bin/git";
+        scm_cvs_command = "${pkgs.cvs}/bin/cvs";
+        scm_bazaar_command = "${pkgs.breezy}/bin/bzr";
+        scm_darcs_command = "${pkgs.darcs}/bin/darcs";
+      };
+    };
+
+    services.redmine.extraEnv = mkBefore ''
+      config.logger = Logger.new("${cfg.stateDir}/log/production.log", 14, 1048576)
+      config.logger.level = Logger::INFO
+    '';
+
     services.mysql = mkIf mysqlLocal {
       enable = true;
       package = mkDefault pkgs.mariadb;
@@ -300,7 +299,7 @@ in
         breezy
         cvs
         darcs
-        gitAndTools.git
+        git
         mercurial
         subversion
       ];
@@ -338,7 +337,7 @@ in
 
 
         # handle database.passwordFile & permissions
-        DBPASS=$(head -n1 ${cfg.database.passwordFile})
+        DBPASS=${optionalString (cfg.database.passwordFile != null) "$(head -n1 ${cfg.database.passwordFile})"}
         cp -f ${databaseYml} "${cfg.stateDir}/config/database.yml"
         sed -e "s,#dbpass#,$DBPASS,g" -i "${cfg.stateDir}/config/database.yml"
         chmod 440 "${cfg.stateDir}/config/database.yml"
@@ -379,17 +378,6 @@ in
       redmine.gid = config.ids.gids.redmine;
     };
 
-    warnings = optional (cfg.database.password != "")
-      ''config.services.redmine.database.password will be stored as plaintext
-      in the Nix store. Use database.passwordFile instead.'';
-
-    # Create database passwordFile default when password is configured.
-    services.redmine.database.passwordFile =
-      (mkDefault (toString (pkgs.writeTextFile {
-        name = "redmine-database-password";
-        text = cfg.database.password;
-      })));
-
   };
 
 }
diff --git a/nixos/modules/services/misc/rippled.nix b/nixos/modules/services/misc/rippled.nix
index ef34e3a779f..2fce3b9dc94 100644
--- a/nixos/modules/services/misc/rippled.nix
+++ b/nixos/modules/services/misc/rippled.nix
@@ -389,6 +389,7 @@ in
 
       extraConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra lines to be added verbatim to the rippled.cfg configuration file.
         '';
diff --git a/nixos/modules/services/misc/safeeyes.nix b/nixos/modules/services/misc/safeeyes.nix
index 6ecb0d13187..1e748195e41 100644
--- a/nixos/modules/services/misc/safeeyes.nix
+++ b/nixos/modules/services/misc/safeeyes.nix
@@ -32,14 +32,14 @@ in
       wantedBy = [ "graphical-session.target" ];
       partOf   = [ "graphical-session.target" ];
 
+      startLimitIntervalSec = 350;
+      startLimitBurst = 10;
       serviceConfig = {
         ExecStart = ''
           ${pkgs.safeeyes}/bin/safeeyes
         '';
         Restart = "on-failure";
         RestartSec = 3;
-        StartLimitInterval = 350;
-        StartLimitBurst = 10;
       };
     };
 
diff --git a/nixos/modules/services/misc/sdrplay.nix b/nixos/modules/services/misc/sdrplay.nix
new file mode 100644
index 00000000000..2801108f082
--- /dev/null
+++ b/nixos/modules/services/misc/sdrplay.nix
@@ -0,0 +1,35 @@
+{ config, lib, pkgs, ... }:
+with lib;
+{
+  options.services.sdrplayApi = {
+    enable = mkOption {
+      default = false;
+      example = true;
+      description = ''
+        Whether to enable the SDRplay API service and udev rules.
+
+        <note><para>
+          To enable integration with SoapySDR and GUI applications like gqrx create an overlay containing
+          <literal>soapysdr-with-plugins = super.soapysdr.override { extraPackages = [ super.soapysdrplay ]; };</literal>
+        </para></note>
+      '';
+      type = lib.types.bool;
+    };
+  };
+
+  config = mkIf config.services.sdrplayApi.enable {
+    systemd.services.sdrplayApi = {
+      description = "SDRplay API Service";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.sdrplay}/bin/sdrplay_apiService";
+        DynamicUser = true;
+        Restart = "on-failure";
+        RestartSec = "1s";
+      };
+    };
+    services.udev.packages = [ pkgs.sdrplay ];
+
+  };
+}
diff --git a/nixos/modules/services/misc/siproxd.nix b/nixos/modules/services/misc/siproxd.nix
index 0e87fc461d3..20fe0793b84 100644
--- a/nixos/modules/services/misc/siproxd.nix
+++ b/nixos/modules/services/misc/siproxd.nix
@@ -39,7 +39,7 @@ in
         default = false;
         description = ''
           Whether to enable the Siproxd SIP
-	  proxy/masquerading daemon.
+          proxy/masquerading daemon.
         '';
       };
 
@@ -57,29 +57,29 @@ in
 
       hostsAllowReg = mkOption {
         type = types.listOf types.str;
-	default = [ ];
+        default = [ ];
         example = [ "192.168.1.0/24" "192.168.2.0/24" ];
-	description = ''
+        description = ''
           Acess control list for incoming SIP registrations.
         '';
       };
 
       hostsAllowSip = mkOption {
         type = types.listOf types.str;
-	default = [ ];
+        default = [ ];
         example = [ "123.45.0.0/16" "123.46.0.0/16" ];
-	description = ''
+        description = ''
           Acess control list for incoming SIP traffic.
         '';
       };
 
       hostsDenySip = mkOption {
         type = types.listOf types.str;
-	default = [ ];
+        default = [ ];
         example = [ "10.0.0.0/8" "11.0.0.0/8" ];
-	description = ''
+        description = ''
           Acess control list for denying incoming
-	   SIP registrations and traffic.
+          SIP registrations and traffic.
         '';
       };
 
@@ -87,7 +87,7 @@ in
         type = types.int;
         default = 5060;
         description = ''
-	  Port to listen for incoming SIP messages.
+          Port to listen for incoming SIP messages.
         '';
       };
 
diff --git a/nixos/modules/services/misc/snapper.nix b/nixos/modules/services/misc/snapper.nix
index 6f3aaa973a0..a821b9b6bf6 100644
--- a/nixos/modules/services/misc/snapper.nix
+++ b/nixos/modules/services/misc/snapper.nix
@@ -48,6 +48,8 @@ in
           subvolume = "/home";
           extraConfig = ''
             ALLOW_USERS="alice"
+            TIMELINE_CREATE=yes
+            TIMELINE_CLEANUP=yes
           '';
         };
       };
@@ -121,6 +123,16 @@ in
 
     services.dbus.packages = [ pkgs.snapper ];
 
+    systemd.services.snapperd = {
+      description = "DBus interface for snapper";
+      inherit documentation;
+      serviceConfig = {
+        Type = "dbus";
+        BusName = "org.opensuse.Snapper";
+        ExecStart = "${pkgs.snapper}/bin/snapperd";
+      };
+    };
+
     systemd.services.snapper-timeline = {
       description = "Timeline of Snapper Snapshots";
       inherit documentation;
diff --git a/nixos/modules/services/misc/sourcehut/builds.nix b/nixos/modules/services/misc/sourcehut/builds.nix
new file mode 100644
index 00000000000..a17a1010dbf
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/builds.nix
@@ -0,0 +1,234 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  scfg = cfg.builds;
+  rcfg = config.services.redis;
+  iniKey = "builds.sr.ht";
+
+  drv = pkgs.sourcehut.buildsrht;
+in
+{
+  options.services.sourcehut.builds = {
+    user = mkOption {
+      type = types.str;
+      default = "buildsrht";
+      description = ''
+        User for builds.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5002;
+      description = ''
+        Port on which the "builds" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "builds.sr.ht";
+      description = ''
+        PostgreSQL database name for builds.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/buildsrht";
+      description = ''
+        State path for builds.sr.ht.
+      '';
+    };
+
+    enableWorker = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Run workers for builds.sr.ht.
+      '';
+    };
+
+    images = mkOption {
+      type = types.attrsOf (types.attrsOf (types.attrsOf types.package));
+      default = { };
+      example = lib.literalExample ''(let
+          # Pinning unstable to allow usage with flakes and limit rebuilds.
+          pkgs_unstable = builtins.fetchGit {
+              url = "https://github.com/NixOS/nixpkgs";
+              rev = "ff96a0fa5635770390b184ae74debea75c3fd534";
+              ref = "nixos-unstable";
+          };
+          image_from_nixpkgs = pkgs_unstable: (import ("${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") {
+            pkgs = (import pkgs_unstable {});
+          });
+        in
+        {
+          nixos.unstable.x86_64 = image_from_nixpkgs pkgs_unstable;
+        }
+      )'';
+      description = ''
+        Images for builds.sr.ht. Each package should be distro.release.arch and point to a /nix/store/package/root.img.qcow2.
+      '';
+    };
+
+  };
+
+  config = with scfg; let
+    image_dirs = lib.lists.flatten (
+      lib.attrsets.mapAttrsToList
+        (distro: revs:
+          lib.attrsets.mapAttrsToList
+            (rev: archs:
+              lib.attrsets.mapAttrsToList
+                (arch: image:
+                  pkgs.runCommandNoCC "buildsrht-images" { } ''
+                    mkdir -p $out/${distro}/${rev}/${arch}
+                    ln -s ${image}/*.qcow2 $out/${distro}/${rev}/${arch}/root.img.qcow2
+                  '')
+                archs)
+            revs)
+        scfg.images);
+    image_dir_pre = pkgs.symlinkJoin {
+      name = "builds.sr.ht-worker-images-pre";
+      paths = image_dirs ++ [
+        "${pkgs.sourcehut.buildsrht}/lib/images"
+      ];
+    };
+    image_dir = pkgs.runCommandNoCC "builds.sr.ht-worker-images" { } ''
+      mkdir -p $out/images
+      cp -Lr ${image_dir_pre}/* $out/images
+    '';
+  in
+  lib.mkIf (cfg.enable && elem "builds" cfg.services) {
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          extraGroups = lib.optionals cfg.builds.enableWorker [ "docker" ];
+          description = "builds.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0755 ${user} ${user} -"
+      ] ++ (lib.optionals cfg.builds.enableWorker
+        [ "d ${statePath}/logs 0775 ${user} ${user} - -" ]
+      );
+
+      services = {
+        buildsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey
+          {
+            after = [ "postgresql.service" "network.target" ];
+            requires = [ "postgresql.service" ];
+            wantedBy = [ "multi-user.target" ];
+
+            description = "builds.sr.ht website service";
+
+            serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+
+            # Hack to bypass this hack: https://git.sr.ht/~sircmpwn/core.sr.ht/tree/master/item/srht-update-profiles#L6
+          } // { preStart = " "; };
+
+        buildsrht-worker = {
+          enable = scfg.enableWorker;
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+          partOf = [ "buildsrht.service" ];
+          description = "builds.sr.ht worker service";
+          path = [ pkgs.openssh pkgs.docker ];
+          preStart = let qemuPackage = pkgs.qemu_kvm;
+          in ''
+            if [[ "$(docker images -q qemu:latest 2> /dev/null)" == "" || "$(cat ${statePath}/docker-image-qemu 2> /dev/null || true)" != "${qemuPackage.version}" ]]; then
+              # Create and import qemu:latest image for docker
+              ${
+                pkgs.dockerTools.streamLayeredImage {
+                  name = "qemu";
+                  tag = "latest";
+                  contents = [ qemuPackage ];
+                }
+              } | docker load
+              # Mark down current package version
+              printf "%s" "${qemuPackage.version}" > ${statePath}/docker-image-qemu
+            fi
+          '';
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Group = "nginx";
+            Restart = "always";
+          };
+          serviceConfig.ExecStart = "${pkgs.sourcehut.buildsrht}/bin/builds.sr.ht-worker";
+        };
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL builds.sr.ht is being served at (protocol://domain)
+      "builds.sr.ht".origin = mkDefault "http://builds.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "builds.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "builds.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "builds.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "builds.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # builds.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "builds.sr.ht".oauth-client-id = mkDefault null;
+      "builds.sr.ht".oauth-client-secret = mkDefault null;
+      # The redis connection used for the celery worker
+      "builds.sr.ht".redis = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/3";
+      # The shell used for ssh
+      "builds.sr.ht".shell = mkDefault "runner-shell";
+      # Register the builds.sr.ht dispatcher
+      "git.sr.ht::dispatch".${builtins.unsafeDiscardStringContext "${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys"} = mkDefault "${user}:${user}";
+
+      # Location for build logs, images, and control command
+    } // lib.attrsets.optionalAttrs scfg.enableWorker {
+      # Default worker stores logs that are accessible via this address:port
+      "builds.sr.ht::worker".name = mkDefault "127.0.0.1:5020";
+      "builds.sr.ht::worker".buildlogs = mkDefault "${scfg.statePath}/logs";
+      "builds.sr.ht::worker".images = mkDefault "${image_dir}/images";
+      "builds.sr.ht::worker".controlcmd = mkDefault "${image_dir}/images/control";
+      "builds.sr.ht::worker".timeout = mkDefault "3m";
+    };
+
+    services.nginx.virtualHosts."logs.${cfg.originBase}" =
+      if scfg.enableWorker then {
+        listen = with builtins; let address = split ":" cfg.settings."builds.sr.ht::worker".name;
+        in [{ addr = elemAt address 0; port = lib.toInt (elemAt address 2); }];
+        locations."/logs".root = "${scfg.statePath}";
+      } else { };
+
+    services.nginx.virtualHosts."builds.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.buildsrht}/${pkgs.sourcehut.python.sitePackages}/buildsrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/default.nix b/nixos/modules/services/misc/sourcehut/default.nix
new file mode 100644
index 00000000000..9c812d6b043
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/default.nix
@@ -0,0 +1,198 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  cfgIni = cfg.settings;
+  settingsFormat = pkgs.formats.ini { };
+
+  # Specialized python containing all the modules
+  python = pkgs.sourcehut.python.withPackages (ps: with ps; [
+    gunicorn
+    # Sourcehut services
+    srht
+    buildsrht
+    dispatchsrht
+    gitsrht
+    hgsrht
+    hubsrht
+    listssrht
+    mansrht
+    metasrht
+    pastesrht
+    todosrht
+  ]);
+in
+{
+  imports =
+    [
+      ./git.nix
+      ./hg.nix
+      ./hub.nix
+      ./todo.nix
+      ./man.nix
+      ./meta.nix
+      ./paste.nix
+      ./builds.nix
+      ./lists.nix
+      ./dispatch.nix
+      (mkRemovedOptionModule [ "services" "sourcehut" "nginx" "enable" ] ''
+        The sourcehut module supports `nginx` as a local reverse-proxy by default and doesn't
+        support other reverse-proxies officially.
+
+        However it's possible to use an alternative reverse-proxy by
+
+          * disabling nginx
+          * adjusting the relevant settings for server addresses and ports directly
+
+        Further details about this can be found in the `Sourcehut`-section of the NixOS-manual.
+      '')
+    ];
+
+  options.services.sourcehut = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable sourcehut - git hosting, continuous integration, mailing list, ticket tracking,
+        task dispatching, wiki and account management services
+      '';
+    };
+
+    services = mkOption {
+      type = types.nonEmptyListOf (types.enum [ "builds" "dispatch" "git" "hub" "hg" "lists" "man" "meta" "paste" "todo" ]);
+      default = [ "man" "meta" "paste" ];
+      example = [ "builds" "dispatch" "git" "hub" "hg" "lists" "man" "meta" "paste" "todo" ];
+      description = ''
+        Services to enable on the sourcehut network.
+      '';
+    };
+
+    originBase = mkOption {
+      type = types.str;
+      default = with config.networking; hostName + lib.optionalString (domain != null) ".${domain}";
+      description = ''
+        Host name used by reverse-proxy and for default settings. Will host services at git."''${originBase}". For example: git.sr.ht
+      '';
+    };
+
+    address = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      description = ''
+        Address to bind to.
+      '';
+    };
+
+    python = mkOption {
+      internal = true;
+      type = types.package;
+      default = python;
+      description = ''
+        The python package to use. It should contain references to the *srht modules and also
+        gunicorn.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "/var/lib/sourcehut";
+      description = ''
+        Root state path for the sourcehut network. If left as the default value
+        this directory will automatically be created before the sourcehut server
+        starts, otherwise the sysadmin is responsible for ensuring the
+        directory exists with appropriate ownership and permissions.
+      '';
+    };
+
+    settings = mkOption {
+      type = lib.types.submodule {
+        freeformType = settingsFormat.type;
+      };
+      default = { };
+      description = ''
+        The configuration for the sourcehut network.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions =
+      [
+        {
+          assertion = with cfgIni.webhooks; private-key != null && stringLength private-key == 44;
+          message = "The webhook's private key must be defined and of a 44 byte length.";
+        }
+
+        {
+          assertion = hasAttrByPath [ "meta.sr.ht" "origin" ] cfgIni && cfgIni."meta.sr.ht".origin != null;
+          message = "meta.sr.ht's origin must be defined.";
+        }
+      ];
+
+    virtualisation.docker.enable = true;
+    environment.etc."sr.ht/config.ini".source =
+      settingsFormat.generate "sourcehut-config.ini" (mapAttrsRecursive
+        (
+          path: v: if v == null then "" else v
+        )
+        cfg.settings);
+
+    environment.systemPackages = [ pkgs.sourcehut.coresrht ];
+
+    # PostgreSQL server
+    services.postgresql.enable = mkOverride 999 true;
+    # Mail server
+    services.postfix.enable = mkOverride 999 true;
+    # Cron daemon
+    services.cron.enable = mkOverride 999 true;
+    # Redis server
+    services.redis.enable = mkOverride 999 true;
+    services.redis.bind = mkOverride 999 "127.0.0.1";
+
+    services.sourcehut.settings = {
+      # The name of your network of sr.ht-based sites
+      "sr.ht".site-name = mkDefault "sourcehut";
+      # The top-level info page for your site
+      "sr.ht".site-info = mkDefault "https://sourcehut.org";
+      # {{ site-name }}, {{ site-blurb }}
+      "sr.ht".site-blurb = mkDefault "the hacker's forge";
+      # If this != production, we add a banner to each page
+      "sr.ht".environment = mkDefault "development";
+      # Contact information for the site owners
+      "sr.ht".owner-name = mkDefault "Drew DeVault";
+      "sr.ht".owner-email = mkDefault "sir@cmpwn.com";
+      # The source code for your fork of sr.ht
+      "sr.ht".source-url = mkDefault "https://git.sr.ht/~sircmpwn/srht";
+      # A secret key to encrypt session cookies with
+      "sr.ht".secret-key = mkDefault null;
+      "sr.ht".global-domain = mkDefault null;
+
+      # Outgoing SMTP settings
+      mail.smtp-host = mkDefault null;
+      mail.smtp-port = mkDefault null;
+      mail.smtp-user = mkDefault null;
+      mail.smtp-password = mkDefault null;
+      mail.smtp-from = mkDefault null;
+      # Application exceptions are emailed to this address
+      mail.error-to = mkDefault null;
+      mail.error-from = mkDefault null;
+      # Your PGP key information (DO NOT mix up pub and priv here)
+      # You must remove the password from your secret key, if present.
+      # You can do this with gpg --edit-key [key-id], then use the passwd
+      # command and do not enter a new password.
+      mail.pgp-privkey = mkDefault null;
+      mail.pgp-pubkey = mkDefault null;
+      mail.pgp-key-id = mkDefault null;
+
+      # base64-encoded Ed25519 key for signing webhook payloads. This should be
+      # consistent for all *.sr.ht sites, as we'll use this key to verify signatures
+      # from other sites in your network.
+      #
+      # Use the srht-webhook-keygen command to generate a key.
+      webhooks.private-key = mkDefault null;
+    };
+  };
+  meta.doc = ./sourcehut.xml;
+  meta.maintainers = with maintainers; [ tomberek ];
+}
diff --git a/nixos/modules/services/misc/sourcehut/dispatch.nix b/nixos/modules/services/misc/sourcehut/dispatch.nix
new file mode 100644
index 00000000000..a9db17bebe8
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/dispatch.nix
@@ -0,0 +1,125 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.dispatch;
+  iniKey = "dispatch.sr.ht";
+
+  drv = pkgs.sourcehut.dispatchsrht;
+in
+{
+  options.services.sourcehut.dispatch = {
+    user = mkOption {
+      type = types.str;
+      default = "dispatchsrht";
+      description = ''
+        User for dispatch.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5005;
+      description = ''
+        Port on which the "dispatch" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "dispatch.sr.ht";
+      description = ''
+        PostgreSQL database name for dispatch.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/dispatchsrht";
+      description = ''
+        State path for dispatch.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "dispatch" cfg.services) {
+
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          description = "dispatch.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services.dispatchsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+        after = [ "postgresql.service" "network.target" ];
+        requires = [ "postgresql.service" ];
+        wantedBy = [ "multi-user.target" ];
+
+        description = "dispatch.sr.ht website service";
+
+        serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL dispatch.sr.ht is being served at (protocol://domain)
+      "dispatch.sr.ht".origin = mkDefault "http://dispatch.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "dispatch.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "dispatch.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "dispatch.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "dispatch.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # dispatch.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "dispatch.sr.ht".oauth-client-id = mkDefault null;
+      "dispatch.sr.ht".oauth-client-secret = mkDefault null;
+
+      # Github Integration
+      "dispatch.sr.ht::github".oauth-client-id = mkDefault null;
+      "dispatch.sr.ht::github".oauth-client-secret = mkDefault null;
+
+      # Gitlab Integration
+      "dispatch.sr.ht::gitlab".enabled = mkDefault null;
+      "dispatch.sr.ht::gitlab".canonical-upstream = mkDefault "gitlab.com";
+      "dispatch.sr.ht::gitlab".repo-cache = mkDefault "./repo-cache";
+      # "dispatch.sr.ht::gitlab"."gitlab.com" = mkDefault "GitLab:application id:secret";
+    };
+
+    services.nginx.virtualHosts."dispatch.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.dispatchsrht}/${pkgs.sourcehut.python.sitePackages}/dispatchsrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/git.nix b/nixos/modules/services/misc/sourcehut/git.nix
new file mode 100644
index 00000000000..99b9aec0612
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/git.nix
@@ -0,0 +1,214 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  scfg = cfg.git;
+  iniKey = "git.sr.ht";
+
+  rcfg = config.services.redis;
+  drv = pkgs.sourcehut.gitsrht;
+in
+{
+  options.services.sourcehut.git = {
+    user = mkOption {
+      type = types.str;
+      visible = false;
+      internal = true;
+      readOnly = true;
+      default = "git";
+      description = ''
+        User for git.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5001;
+      description = ''
+        Port on which the "git" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "git.sr.ht";
+      description = ''
+        PostgreSQL database name for git.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/gitsrht";
+      description = ''
+        State path for git.sr.ht.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.git;
+      example = literalExample "pkgs.gitFull";
+      description = ''
+        Git package for git.sr.ht. This can help silence collisions.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "git" cfg.services) {
+    # sshd refuses to run with `Unsafe AuthorizedKeysCommand ... bad ownership or modes for directory /nix/store`
+    environment.etc."ssh/gitsrht-dispatch" = {
+      mode = "0755";
+      text = ''
+        #! ${pkgs.stdenv.shell}
+        ${cfg.python}/bin/gitsrht-dispatch "$@"
+      '';
+    };
+
+    # Needs this in the $PATH when sshing into the server
+    environment.systemPackages = [ cfg.git.package ];
+
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          # https://stackoverflow.com/questions/22314298/git-push-results-in-fatal-protocol-error-bad-line-length-character-this
+          # Probably could use gitsrht-shell if output is restricted to just parameters...
+          shell = pkgs.bash;
+          description = "git.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services = {
+      cron.systemCronJobs = [ "*/20 * * * * ${cfg.python}/bin/gitsrht-periodic" ];
+      fcgiwrap.enable = true;
+
+      openssh.authorizedKeysCommand = ''/etc/ssh/gitsrht-dispatch "%u" "%h" "%t" "%k"'';
+      openssh.authorizedKeysCommandUser = "root";
+      openssh.extraConfig = ''
+        PermitUserEnvironment SRHT_*
+      '';
+
+      postgresql = {
+        authentication = ''
+          local ${database} ${user} trust
+        '';
+        ensureDatabases = [ database ];
+        ensureUsers = [
+          {
+            name = user;
+            ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+          }
+        ];
+      };
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        # /var/log is owned by root
+        "f /var/log/git-srht-shell 0644 ${user} ${user} -"
+
+        "d ${statePath} 0750 ${user} ${user} -"
+        "d ${cfg.settings."${iniKey}".repos} 2755 ${user} ${user} -"
+      ];
+
+      services = {
+        gitsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+          after = [ "redis.service" "postgresql.service" "network.target" ];
+          requires = [ "redis.service" "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          # Needs internally to create repos at the very least
+          path = [ pkgs.git ];
+          description = "git.sr.ht website service";
+
+          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+        };
+
+        gitsrht-webhooks = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "git.sr.ht webhooks service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+          };
+
+          serviceConfig.ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
+        };
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL git.sr.ht is being served at (protocol://domain)
+      "git.sr.ht".origin = mkDefault "http://git.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "git.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "git.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "git.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "git.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # The redis connection used for the webhooks worker
+      "git.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1";
+
+      # A post-update script which is installed in every git repo.
+      "git.sr.ht".post-update-script = mkDefault "${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
+
+      # git.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "git.sr.ht".oauth-client-id = mkDefault null;
+      "git.sr.ht".oauth-client-secret = mkDefault null;
+      # Path to git repositories on disk
+      "git.sr.ht".repos = mkDefault "/var/lib/git";
+
+      "git.sr.ht".outgoing-domain = mkDefault "http://git.${cfg.originBase}";
+
+      # The authorized keys hook uses this to dispatch to various handlers
+      # The format is a program to exec into as the key, and the user to match as the
+      # value. When someone tries to log in as this user, this program is executed
+      # and is expected to omit an AuthorizedKeys file.
+      #
+      # Discard of the string context is in order to allow derivation-derived strings.
+      # This is safe if the relevant package is installed which will be the case if the setting is utilized.
+      "git.sr.ht::dispatch".${builtins.unsafeDiscardStringContext "${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys"} = mkDefault "${user}:${user}";
+    };
+
+    services.nginx.virtualHosts."git.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.gitsrht}/${pkgs.sourcehut.python.sitePackages}/gitsrht";
+      extraConfig = ''
+            location = /authorize {
+            proxy_pass http://${cfg.address}:${toString port};
+            proxy_pass_request_body off;
+            proxy_set_header Content-Length "";
+            proxy_set_header X-Original-URI $request_uri;
+        }
+            location ~ ^/([^/]+)/([^/]+)/(HEAD|info/refs|objects/info/.*|git-upload-pack).*$ {
+                auth_request /authorize;
+                root /var/lib/git;
+                fastcgi_pass unix:/run/fcgiwrap.sock;
+                fastcgi_param SCRIPT_FILENAME ${pkgs.git}/bin/git-http-backend;
+                fastcgi_param PATH_INFO $uri;
+                fastcgi_param GIT_PROJECT_ROOT $document_root;
+                fastcgi_read_timeout 500s;
+                include ${pkgs.nginx}/conf/fastcgi_params;
+                gzip off;
+            }
+      '';
+
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/hg.nix b/nixos/modules/services/misc/sourcehut/hg.nix
new file mode 100644
index 00000000000..5cd36bb0455
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/hg.nix
@@ -0,0 +1,173 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  scfg = cfg.hg;
+  iniKey = "hg.sr.ht";
+
+  rcfg = config.services.redis;
+  drv = pkgs.sourcehut.hgsrht;
+in
+{
+  options.services.sourcehut.hg = {
+    user = mkOption {
+      type = types.str;
+      internal = true;
+      readOnly = true;
+      default = "hg";
+      description = ''
+        User for hg.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5010;
+      description = ''
+        Port on which the "hg" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "hg.sr.ht";
+      description = ''
+        PostgreSQL database name for hg.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/hgsrht";
+      description = ''
+        State path for hg.sr.ht.
+      '';
+    };
+
+    cloneBundles = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Generate clonebundles (which require more disk space but dramatically speed up cloning large repositories).
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "hg" cfg.services) {
+    # In case it ever comes into being
+    environment.etc."ssh/hgsrht-dispatch" = {
+      mode = "0755";
+      text = ''
+        #! ${pkgs.stdenv.shell}
+        ${cfg.python}/bin/gitsrht-dispatch $@
+      '';
+    };
+
+    environment.systemPackages = [ pkgs.mercurial ];
+
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          # Assuming hg.sr.ht needs this too
+          shell = pkgs.bash;
+          description = "hg.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services = {
+      cron.systemCronJobs = [ "*/20 * * * * ${cfg.python}/bin/hgsrht-periodic" ]
+        ++ optional cloneBundles "0 * * * * ${cfg.python}/bin/hgsrht-clonebundles";
+
+      openssh.authorizedKeysCommand = ''/etc/ssh/hgsrht-dispatch "%u" "%h" "%t" "%k"'';
+      openssh.authorizedKeysCommandUser = "root";
+      openssh.extraConfig = ''
+        PermitUserEnvironment SRHT_*
+      '';
+
+      postgresql = {
+        authentication = ''
+          local ${database} ${user} trust
+        '';
+        ensureDatabases = [ database ];
+        ensureUsers = [
+          {
+            name = user;
+            ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+          }
+        ];
+      };
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        # /var/log is owned by root
+        "f /var/log/hg-srht-shell 0644 ${user} ${user} -"
+
+        "d ${statePath} 0750 ${user} ${user} -"
+        "d ${cfg.settings."${iniKey}".repos} 2755 ${user} ${user} -"
+      ];
+
+      services.hgsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+        after = [ "redis.service" "postgresql.service" "network.target" ];
+        requires = [ "redis.service" "postgresql.service" ];
+        wantedBy = [ "multi-user.target" ];
+
+        path = [ pkgs.mercurial ];
+        description = "hg.sr.ht website service";
+
+        serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL hg.sr.ht is being served at (protocol://domain)
+      "hg.sr.ht".origin = mkDefault "http://hg.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "hg.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "hg.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "hg.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # The redis connection used for the webhooks worker
+      "hg.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1";
+      # A post-update script which is installed in every mercurial repo.
+      "hg.sr.ht".changegroup-script = mkDefault "${cfg.python}/bin/hgsrht-hook-changegroup";
+      # hg.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "hg.sr.ht".oauth-client-id = mkDefault null;
+      "hg.sr.ht".oauth-client-secret = mkDefault null;
+      # Path to mercurial repositories on disk
+      "hg.sr.ht".repos = mkDefault "/var/lib/hg";
+      # Path to the srht mercurial extension
+      # (defaults to where the hgsrht code is)
+      # "hg.sr.ht".srhtext = mkDefault null;
+      # .hg/store size (in MB) past which the nightly job generates clone bundles.
+      # "hg.sr.ht".clone_bundle_threshold = mkDefault 50;
+      # Path to hg-ssh (if not in $PATH)
+      # "hg.sr.ht".hg_ssh = mkDefault /path/to/hg-ssh;
+
+      # The authorized keys hook uses this to dispatch to various handlers
+      # The format is a program to exec into as the key, and the user to match as the
+      # value. When someone tries to log in as this user, this program is executed
+      # and is expected to omit an AuthorizedKeys file.
+      #
+      # Uncomment the relevant lines to enable the various sr.ht dispatchers.
+      "hg.sr.ht::dispatch"."/run/current-system/sw/bin/hgsrht-keys" = mkDefault "${user}:${user}";
+    };
+
+    # TODO: requires testing and addition of hg-specific requirements
+    services.nginx.virtualHosts."hg.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.hgsrht}/${pkgs.sourcehut.python.sitePackages}/hgsrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/hub.nix b/nixos/modules/services/misc/sourcehut/hub.nix
new file mode 100644
index 00000000000..be3ea21011c
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/hub.nix
@@ -0,0 +1,118 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.hub;
+  iniKey = "hub.sr.ht";
+
+  drv = pkgs.sourcehut.hubsrht;
+in
+{
+  options.services.sourcehut.hub = {
+    user = mkOption {
+      type = types.str;
+      default = "hubsrht";
+      description = ''
+        User for hub.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5014;
+      description = ''
+        Port on which the "hub" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "hub.sr.ht";
+      description = ''
+        PostgreSQL database name for hub.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/hubsrht";
+      description = ''
+        State path for hub.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "hub" cfg.services) {
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          description = "hub.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services.hubsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+        after = [ "postgresql.service" "network.target" ];
+        requires = [ "postgresql.service" ];
+        wantedBy = [ "multi-user.target" ];
+
+        description = "hub.sr.ht website service";
+
+        serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL hub.sr.ht is being served at (protocol://domain)
+      "hub.sr.ht".origin = mkDefault "http://hub.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "hub.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "hub.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "hub.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "hub.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # hub.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "hub.sr.ht".oauth-client-id = mkDefault null;
+      "hub.sr.ht".oauth-client-secret = mkDefault null;
+    };
+
+    services.nginx.virtualHosts."${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.hubsrht}/${pkgs.sourcehut.python.sitePackages}/hubsrht";
+    };
+    services.nginx.virtualHosts."hub.${cfg.originBase}" = {
+      globalRedirect = "${cfg.originBase}";
+      forceSSL = true;
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/lists.nix b/nixos/modules/services/misc/sourcehut/lists.nix
new file mode 100644
index 00000000000..7b1fe9fd463
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/lists.nix
@@ -0,0 +1,185 @@
+# Email setup is fairly involved, useful references:
+# https://drewdevault.com/2018/08/05/Local-mail-server.html
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.lists;
+  iniKey = "lists.sr.ht";
+
+  rcfg = config.services.redis;
+  drv = pkgs.sourcehut.listssrht;
+in
+{
+  options.services.sourcehut.lists = {
+    user = mkOption {
+      type = types.str;
+      default = "listssrht";
+      description = ''
+        User for lists.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5006;
+      description = ''
+        Port on which the "lists" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "lists.sr.ht";
+      description = ''
+        PostgreSQL database name for lists.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/listssrht";
+      description = ''
+        State path for lists.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "lists" cfg.services) {
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          extraGroups = [ "postfix" ];
+          description = "lists.sr.ht user";
+        };
+      };
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services = {
+        listssrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "lists.sr.ht website service";
+
+          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+        };
+
+        listssrht-process = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "lists.sr.ht process service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.process worker --loglevel=info";
+          };
+        };
+
+        listssrht-lmtp = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "lists.sr.ht process service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+            ExecStart = "${cfg.python}/bin/listssrht-lmtp";
+          };
+        };
+
+
+        listssrht-webhooks = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "lists.sr.ht webhooks service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
+          };
+        };
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL lists.sr.ht is being served at (protocol://domain)
+      "lists.sr.ht".origin = mkDefault "http://lists.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "lists.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "lists.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "lists.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "lists.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # lists.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "lists.sr.ht".oauth-client-id = mkDefault null;
+      "lists.sr.ht".oauth-client-secret = mkDefault null;
+      # Outgoing email for notifications generated by users
+      "lists.sr.ht".notify-from = mkDefault "CHANGEME@example.org";
+      # The redis connection used for the webhooks worker
+      "lists.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/2";
+      # The redis connection used for the celery worker
+      "lists.sr.ht".redis = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/4";
+      # Network-key
+      "lists.sr.ht".network-key = mkDefault null;
+      # Allow creation
+      "lists.sr.ht".allow-new-lists = mkDefault "no";
+      # Posting Domain
+      "lists.sr.ht".posting-domain = mkDefault "lists.${cfg.originBase}";
+
+      # Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
+      # Alternatively, specify IP:PORT and an SMTP server will be run instead.
+      "lists.sr.ht::worker".sock = mkDefault "/tmp/lists.sr.ht-lmtp.sock";
+      # The lmtp daemon will make the unix socket group-read/write for users in this
+      # group.
+      "lists.sr.ht::worker".sock-group = mkDefault "postfix";
+      "lists.sr.ht::worker".reject-url = mkDefault "https://man.sr.ht/lists.sr.ht/etiquette.md";
+      "lists.sr.ht::worker".reject-mimetypes = mkDefault "text/html";
+
+    };
+
+    services.nginx.virtualHosts."lists.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.listssrht}/${pkgs.sourcehut.python.sitePackages}/listssrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/man.nix b/nixos/modules/services/misc/sourcehut/man.nix
new file mode 100644
index 00000000000..7693396d187
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/man.nix
@@ -0,0 +1,122 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.man;
+  iniKey = "man.sr.ht";
+
+  drv = pkgs.sourcehut.mansrht;
+in
+{
+  options.services.sourcehut.man = {
+    user = mkOption {
+      type = types.str;
+      default = "mansrht";
+      description = ''
+        User for man.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5004;
+      description = ''
+        Port on which the "man" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "man.sr.ht";
+      description = ''
+        PostgreSQL database name for man.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/mansrht";
+      description = ''
+        State path for man.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "man" cfg.services) {
+    assertions =
+      [
+        {
+          assertion = hasAttrByPath [ "git.sr.ht" "oauth-client-id" ] cfgIni;
+          message = "man.sr.ht needs access to git.sr.ht.";
+        }
+      ];
+
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          description = "man.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services.mansrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+        after = [ "postgresql.service" "network.target" ];
+        requires = [ "postgresql.service" ];
+        wantedBy = [ "multi-user.target" ];
+
+        description = "man.sr.ht website service";
+
+        serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL man.sr.ht is being served at (protocol://domain)
+      "man.sr.ht".origin = mkDefault "http://man.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "man.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "man.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "man.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "man.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # man.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "man.sr.ht".oauth-client-id = mkDefault null;
+      "man.sr.ht".oauth-client-secret = mkDefault null;
+    };
+
+    services.nginx.virtualHosts."man.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.mansrht}/${pkgs.sourcehut.python.sitePackages}/mansrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/meta.nix b/nixos/modules/services/misc/sourcehut/meta.nix
new file mode 100644
index 00000000000..56127a824eb
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/meta.nix
@@ -0,0 +1,211 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.meta;
+  iniKey = "meta.sr.ht";
+
+  rcfg = config.services.redis;
+  drv = pkgs.sourcehut.metasrht;
+in
+{
+  options.services.sourcehut.meta = {
+    user = mkOption {
+      type = types.str;
+      default = "metasrht";
+      description = ''
+        User for meta.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5000;
+      description = ''
+        Port on which the "meta" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "meta.sr.ht";
+      description = ''
+        PostgreSQL database name for meta.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/metasrht";
+      description = ''
+        State path for meta.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "meta" cfg.services) {
+    assertions =
+      [
+        {
+          assertion = with cfgIni."meta.sr.ht::billing"; enabled == "yes" -> (stripe-public-key != null && stripe-secret-key != null);
+          message = "If meta.sr.ht::billing is enabled, the keys should be defined.";
+        }
+      ];
+
+    users = {
+      users = {
+        ${user} = {
+          isSystemUser = true;
+          group = user;
+          description = "meta.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.cron.systemCronJobs = [ "0 0 * * * ${cfg.python}/bin/metasrht-daily" ];
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services = {
+        metasrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "meta.sr.ht website service";
+
+          preStart = ''
+            # Configure client(s) as "preauthorized"
+            ${concatMapStringsSep "\n\n"
+              (attr: ''
+                if ! test -e "${statePath}/${attr}.oauth" || [ "$(cat ${statePath}/${attr}.oauth)" != "${cfgIni."${attr}".oauth-client-id}" ]; then
+                  # Configure ${attr}'s OAuth client as "preauthorized"
+                  psql ${database} \
+                    -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${cfgIni."${attr}".oauth-client-id}'"
+
+                  printf "%s" "${cfgIni."${attr}".oauth-client-id}" > "${statePath}/${attr}.oauth"
+                fi
+              '')
+              (builtins.attrNames (filterAttrs
+                (k: v: !(hasInfix "::" k) && builtins.hasAttr "oauth-client-id" v && v.oauth-client-id != null)
+                cfg.settings))}
+          '';
+
+          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+        };
+
+        metasrht-api = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "meta.sr.ht api service";
+
+          preStart = ''
+            # Configure client(s) as "preauthorized"
+            ${concatMapStringsSep "\n\n"
+              (attr: ''
+                if ! test -e "${statePath}/${attr}.oauth" || [ "$(cat ${statePath}/${attr}.oauth)" != "${cfgIni."${attr}".oauth-client-id}" ]; then
+                  # Configure ${attr}'s OAuth client as "preauthorized"
+                  psql ${database} \
+                    -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${cfgIni."${attr}".oauth-client-id}'"
+
+                  printf "%s" "${cfgIni."${attr}".oauth-client-id}" > "${statePath}/${attr}.oauth"
+                fi
+              '')
+              (builtins.attrNames (filterAttrs
+                (k: v: !(hasInfix "::" k) && builtins.hasAttr "oauth-client-id" v && v.oauth-client-id != null)
+                cfg.settings))}
+          '';
+
+          serviceConfig.ExecStart = "${pkgs.sourcehut.metasrht}/bin/metasrht-api -b :${toString (port + 100)}";
+        };
+
+        metasrht-webhooks = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "meta.sr.ht webhooks service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
+          };
+
+        };
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL meta.sr.ht is being served at (protocol://domain)
+      "meta.sr.ht".origin = mkDefault "https://meta.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "meta.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "meta.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "meta.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "meta.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # If "yes", the user will be sent the stock sourcehut welcome emails after
+      # signup (requires cron to be configured properly). These are specific to the
+      # sr.ht instance so you probably want to patch these before enabling this.
+      "meta.sr.ht".welcome-emails = mkDefault "no";
+
+      # The redis connection used for the webhooks worker
+      "meta.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/6";
+
+      # If "no", public registration will not be permitted.
+      "meta.sr.ht::settings".registration = mkDefault "no";
+      # Where to redirect new users upon registration
+      "meta.sr.ht::settings".onboarding-redirect = mkDefault "https://meta.${cfg.originBase}";
+      # How many invites each user is issued upon registration (only applicable if
+      # open registration is disabled)
+      "meta.sr.ht::settings".user-invites = mkDefault 5;
+
+      # Origin URL for API, 100 more than web
+      "meta.sr.ht".api-origin = mkDefault "http://localhost:5100";
+
+      # You can add aliases for the client IDs of commonly used OAuth clients here.
+      #
+      # Example:
+      "meta.sr.ht::aliases" = mkDefault { };
+      # "meta.sr.ht::aliases"."git.sr.ht" = 12345;
+
+      # "yes" to enable the billing system
+      "meta.sr.ht::billing".enabled = mkDefault "no";
+      # Get your keys at https://dashboard.stripe.com/account/apikeys
+      "meta.sr.ht::billing".stripe-public-key = mkDefault null;
+      "meta.sr.ht::billing".stripe-secret-key = mkDefault null;
+    };
+
+    services.nginx.virtualHosts."meta.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.metasrht}/${pkgs.sourcehut.python.sitePackages}/metasrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/paste.nix b/nixos/modules/services/misc/sourcehut/paste.nix
new file mode 100644
index 00000000000..b2d5151969e
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/paste.nix
@@ -0,0 +1,133 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.paste;
+  iniKey = "paste.sr.ht";
+
+  rcfg = config.services.redis;
+  drv = pkgs.sourcehut.pastesrht;
+in
+{
+  options.services.sourcehut.paste = {
+    user = mkOption {
+      type = types.str;
+      default = "pastesrht";
+      description = ''
+        User for paste.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5011;
+      description = ''
+        Port on which the "paste" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "paste.sr.ht";
+      description = ''
+        PostgreSQL database name for paste.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/pastesrht";
+      description = ''
+        State path for pastesrht.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "paste" cfg.services) {
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          description = "paste.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services = {
+        pastesrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "paste.sr.ht website service";
+
+          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+        };
+
+        pastesrht-webhooks = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "paste.sr.ht webhooks service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
+          };
+
+        };
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL paste.sr.ht is being served at (protocol://domain)
+      "paste.sr.ht".origin = mkDefault "http://paste.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "paste.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "paste.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "paste.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "paste.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # paste.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "paste.sr.ht".oauth-client-id = mkDefault null;
+      "paste.sr.ht".oauth-client-secret = mkDefault null;
+      "paste.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/5";
+    };
+
+    services.nginx.virtualHosts."paste.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.pastesrht}/${pkgs.sourcehut.python.sitePackages}/pastesrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/service.nix b/nixos/modules/services/misc/sourcehut/service.nix
new file mode 100644
index 00000000000..65b4ad020f9
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/service.nix
@@ -0,0 +1,66 @@
+{ config, pkgs, lib }:
+serviceCfg: serviceDrv: iniKey: attrs:
+let
+  cfg = config.services.sourcehut;
+  cfgIni = cfg.settings."${iniKey}";
+  pgSuperUser = config.services.postgresql.superUser;
+
+  setupDB = pkgs.writeScript "${serviceDrv.pname}-gen-db" ''
+    #! ${cfg.python}/bin/python
+    from ${serviceDrv.pname}.app import db
+    db.create()
+  '';
+in
+with serviceCfg; with lib; recursiveUpdate
+{
+  environment.HOME = statePath;
+  path = [ config.services.postgresql.package ] ++ (attrs.path or [ ]);
+  restartTriggers = [ config.environment.etc."sr.ht/config.ini".source ];
+  serviceConfig = {
+    Type = "simple";
+    User = user;
+    Group = user;
+    Restart = "always";
+    WorkingDirectory = statePath;
+  } // (if (cfg.statePath == "/var/lib/sourcehut/${serviceDrv.pname}") then {
+          StateDirectory = [ "sourcehut/${serviceDrv.pname}" ];
+        } else {})
+  ;
+
+  preStart = ''
+    if ! test -e ${statePath}/db; then
+      # Setup the initial database
+      ${setupDB}
+
+      # Set the initial state of the database for future database upgrades
+      if test -e ${cfg.python}/bin/${serviceDrv.pname}-migrate; then
+        # Run alembic stamp head once to tell alembic the schema is up-to-date
+        ${cfg.python}/bin/${serviceDrv.pname}-migrate stamp head
+      fi
+
+      printf "%s" "${serviceDrv.version}" > ${statePath}/db
+    fi
+
+    # Update copy of each users' profile to the latest
+    # See https://lists.sr.ht/~sircmpwn/sr.ht-admins/<20190302181207.GA13778%40cirno.my.domain>
+    if ! test -e ${statePath}/webhook; then
+      # Update ${iniKey}'s users' profile copy to the latest
+      ${cfg.python}/bin/srht-update-profiles ${iniKey}
+
+      touch ${statePath}/webhook
+    fi
+
+    ${optionalString (builtins.hasAttr "migrate-on-upgrade" cfgIni && cfgIni.migrate-on-upgrade == "yes") ''
+      if [ "$(cat ${statePath}/db)" != "${serviceDrv.version}" ]; then
+        # Manage schema migrations using alembic
+        ${cfg.python}/bin/${serviceDrv.pname}-migrate -a upgrade head
+
+        # Mark down current package version
+        printf "%s" "${serviceDrv.version}" > ${statePath}/db
+      fi
+    ''}
+
+    ${attrs.preStart or ""}
+  '';
+}
+  (builtins.removeAttrs attrs [ "path" "preStart" ])
diff --git a/nixos/modules/services/misc/sourcehut/sourcehut.xml b/nixos/modules/services/misc/sourcehut/sourcehut.xml
new file mode 100644
index 00000000000..ab9a8c6cb4b
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/sourcehut.xml
@@ -0,0 +1,115 @@
+<chapter xmlns="http://docbook.org/ns/docbook"
+         xmlns:xlink="http://www.w3.org/1999/xlink"
+         xmlns:xi="http://www.w3.org/2001/XInclude"
+         version="5.0"
+         xml:id="module-services-sourcehut">
+ <title>Sourcehut</title>
+ <para>
+  <link xlink:href="https://sr.ht.com/">Sourcehut</link> is an open-source,
+  self-hostable software development platform. The server setup can be automated using
+  <link linkend="opt-services.sourcehut.enable">services.sourcehut</link>.
+ </para>
+
+ <section xml:id="module-services-sourcehut-basic-usage">
+  <title>Basic usage</title>
+  <para>
+   Sourcehut is a Python and Go based set of applications.
+   <literal><link linkend="opt-services.sourcehut.enable">services.sourcehut</link></literal>
+   by default will use
+   <literal><link linkend="opt-services.nginx.enable">services.nginx</link></literal>,
+   <literal><link linkend="opt-services.nginx.enable">services.redis</link></literal>,
+   <literal><link linkend="opt-services.nginx.enable">services.cron</link></literal>,
+   and
+   <literal><link linkend="opt-services.postgresql.enable">services.postgresql</link></literal>.
+  </para>
+
+  <para>
+   A very basic configuration may look like this:
+<programlisting>
+{ pkgs, ... }:
+let
+  fqdn =
+    let
+      join = hostName: domain: hostName + optionalString (domain != null) ".${domain}";
+    in join config.networking.hostName config.networking.domain;
+in {
+
+  networking = {
+    <link linkend="opt-networking.hostName">hostName</link> = "srht";
+    <link linkend="opt-networking.domain">domain</link> = "tld";
+    <link linkend="opt-networking.firewall.allowedTCPPorts">firewall.allowedTCPPorts</link> = [ 22 80 443 ];
+  };
+
+  services.sourcehut = {
+    <link linkend="opt-services.sourcehut.enable">enable</link> = true;
+    <link linkend="opt-services.sourcehut.originBase">originBase</link> = fqdn;
+    <link linkend="opt-services.sourcehut.services">services</link> = [ "meta" "man" "git" ];
+    <link linkend="opt-services.sourcehut.settings">settings</link> = {
+        "sr.ht" = {
+          environment = "production";
+          global-domain = fqdn;
+          origin = "https://${fqdn}";
+          # Produce keys with srht-keygen from <package>sourcehut.coresrht</package>.
+          network-key = "SECRET";
+          service-key = "SECRET";
+        };
+        webhooks.private-key= "SECRET";
+    };
+  };
+
+  <link linkend="opt-security.acme.certs._name_.extraDomainNames">security.acme.certs."${fqdn}".extraDomainNames</link> = [
+    "meta.${fqdn}"
+    "man.${fqdn}"
+    "git.${fqdn}"
+  ];
+
+  services.nginx = {
+    <link linkend="opt-services.nginx.enable">enable</link> = true;
+    # only recommendedProxySettings are strictly required, but the rest make sense as well.
+    <link linkend="opt-services.nginx.recommendedTlsSettings">recommendedTlsSettings</link> = true;
+    <link linkend="opt-services.nginx.recommendedOptimisation">recommendedOptimisation</link> = true;
+    <link linkend="opt-services.nginx.recommendedGzipSettings">recommendedGzipSettings</link> = true;
+    <link linkend="opt-services.nginx.recommendedProxySettings">recommendedProxySettings</link> = true;
+
+    # Settings to setup what certificates are used for which endpoint.
+    <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
+      <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">"${fqdn}".enableACME</link> = true;
+      <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">"meta.${fqdn}".useACMEHost</link> = fqdn:
+      <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">"man.${fqdn}".useACMEHost</link> = fqdn:
+      <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">"git.${fqdn}".useACMEHost</link> = fqdn:
+    };
+  };
+}
+</programlisting>
+  </para>
+
+  <para>
+   The <literal>hostName</literal> option is used internally to configure the nginx
+   reverse-proxy. The <literal>settings</literal> attribute set is
+   used by the configuration generator and the result is placed in <literal>/etc/sr.ht/config.ini</literal>.
+  </para>
+ </section>
+
+ <section xml:id="module-services-sourcehut-configuration">
+  <title>Configuration</title>
+
+  <para>
+   All configuration parameters are also stored in
+   <literal>/etc/sr.ht/config.ini</literal> which is generated by
+   the module and linked from the store to ensure that all values from <literal>config.ini</literal>
+   can be modified by the module.
+  </para>
+
+ </section>
+
+ <section xml:id="module-services-sourcehut-httpd">
+  <title>Using an alternative webserver as reverse-proxy (e.g. <literal>httpd</literal>)</title>
+  <para>
+   By default, <package>nginx</package> is used as reverse-proxy for <package>sourcehut</package>.
+   However, it's possible to use e.g. <package>httpd</package> by explicitly disabling
+   <package>nginx</package> using <xref linkend="opt-services.nginx.enable" /> and fixing the
+   <literal>settings</literal>.
+  </para>
+</section>
+
+</chapter>
diff --git a/nixos/modules/services/misc/sourcehut/todo.nix b/nixos/modules/services/misc/sourcehut/todo.nix
new file mode 100644
index 00000000000..aec773b0669
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/todo.nix
@@ -0,0 +1,161 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.todo;
+  iniKey = "todo.sr.ht";
+
+  rcfg = config.services.redis;
+  drv = pkgs.sourcehut.todosrht;
+in
+{
+  options.services.sourcehut.todo = {
+    user = mkOption {
+      type = types.str;
+      default = "todosrht";
+      description = ''
+        User for todo.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5003;
+      description = ''
+        Port on which the "todo" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "todo.sr.ht";
+      description = ''
+        PostgreSQL database name for todo.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/todosrht";
+      description = ''
+        State path for todo.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "todo" cfg.services) {
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          extraGroups = [ "postfix" ];
+          description = "todo.sr.ht user";
+        };
+      };
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services = {
+        todosrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "todo.sr.ht website service";
+
+          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+        };
+
+       todosrht-lmtp = {
+         after = [ "postgresql.service" "network.target" ];
+         bindsTo = [ "postgresql.service" ];
+         wantedBy = [ "multi-user.target" ];
+
+         description = "todo.sr.ht process service";
+         serviceConfig = {
+           Type = "simple";
+           User = user;
+           Restart = "always";
+           ExecStart = "${cfg.python}/bin/todosrht-lmtp";
+         };
+       };
+
+        todosrht-webhooks = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "todo.sr.ht webhooks service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
+          };
+
+        };
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL todo.sr.ht is being served at (protocol://domain)
+      "todo.sr.ht".origin = mkDefault "http://todo.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "todo.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "todo.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "todo.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "todo.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # todo.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "todo.sr.ht".oauth-client-id = mkDefault null;
+      "todo.sr.ht".oauth-client-secret = mkDefault null;
+      # Outgoing email for notifications generated by users
+      "todo.sr.ht".notify-from = mkDefault "CHANGEME@example.org";
+      # The redis connection used for the webhooks worker
+      "todo.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1";
+      # Network-key
+      "todo.sr.ht".network-key = mkDefault null;
+
+      # Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
+      # Alternatively, specify IP:PORT and an SMTP server will be run instead.
+      "todo.sr.ht::mail".sock = mkDefault "/tmp/todo.sr.ht-lmtp.sock";
+      # The lmtp daemon will make the unix socket group-read/write for users in this
+      # group.
+      "todo.sr.ht::mail".sock-group = mkDefault "postfix";
+
+      "todo.sr.ht::mail".posting-domain = mkDefault "todo.${cfg.originBase}";
+    };
+
+    services.nginx.virtualHosts."todo.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.todosrht}/${pkgs.sourcehut.python.sitePackages}/todosrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/ssm-agent.nix b/nixos/modules/services/misc/ssm-agent.nix
index 00e806695fd..c29d03d199b 100644
--- a/nixos/modules/services/misc/ssm-agent.nix
+++ b/nixos/modules/services/misc/ssm-agent.nix
@@ -22,15 +22,13 @@ in {
     package = mkOption {
       type = types.path;
       description = "The SSM agent package to use";
-      default = pkgs.ssm-agent;
-      defaultText = "pkgs.ssm-agent";
+      default = pkgs.ssm-agent.override { overrideEtc = false; };
+      defaultText = "pkgs.ssm-agent.override { overrideEtc = false; }";
     };
   };
 
   config = mkIf cfg.enable {
     systemd.services.ssm-agent = {
-      users.extraUsers.ssm-user = {};
-
       inherit (cfg.package.meta) description;
       after    = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
@@ -39,9 +37,37 @@ in {
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/amazon-ssm-agent";
         KillMode = "process";
-        Restart = "on-failure";
-        RestartSec = "15min";
+        # We want this restating pretty frequently. It could be our only means
+        # of accessing the instance.
+        Restart = "always";
+        RestartSec = "1min";
       };
     };
+
+    # Add user that Session Manager needs, and give it sudo.
+    # This is consistent with Amazon Linux 2 images.
+    security.sudo.extraRules = [
+      {
+        users = [ "ssm-user" ];
+        commands = [
+          {
+            command = "ALL";
+            options = [ "NOPASSWD" ];
+          }
+        ];
+      }
+    ];
+    # On Amazon Linux 2 images, the ssm-user user is pretty much a
+    # normal user with its own group. We do the same.
+    users.groups.ssm-user = {};
+    users.users.ssm-user = {
+      isNormalUser = true;
+      group = "ssm-user";
+    };
+
+    environment.etc."amazon/ssm/seelog.xml".source = "${cfg.package}/seelog.xml.template";
+
+    environment.etc."amazon/ssm/amazon-ssm-agent.json".source =  "${cfg.package}/etc/amazon/ssm/amazon-ssm-agent.json.template";
+
   };
 }
diff --git a/nixos/modules/services/misc/sssd.nix b/nixos/modules/services/misc/sssd.nix
index 3da99a3b38c..386281e2b7c 100644
--- a/nixos/modules/services/misc/sssd.nix
+++ b/nixos/modules/services/misc/sssd.nix
@@ -69,7 +69,7 @@ in {
         mode = "0400";
       };
 
-      system.nssModules = pkgs.sssd;
+      system.nssModules = [ pkgs.sssd ];
       system.nssDatabases = {
         group = [ "sss" ];
         passwd = [ "sss" ];
@@ -92,4 +92,6 @@ in {
     services.openssh.authorizedKeysCommand = "/etc/ssh/authorized_keys_command";
     services.openssh.authorizedKeysCommandUser = "nobody";
   })];
+
+  meta.maintainers = with maintainers; [ bbigras ];
 }
diff --git a/nixos/modules/services/misc/subsonic.nix b/nixos/modules/services/misc/subsonic.nix
index 152917d345c..e17a98a5e1d 100644
--- a/nixos/modules/services/misc/subsonic.nix
+++ b/nixos/modules/services/misc/subsonic.nix
@@ -28,7 +28,7 @@ let cfg = config.services.subsonic; in {
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 4040;
         description = ''
           The port on which Subsonic will listen for
@@ -37,7 +37,7 @@ let cfg = config.services.subsonic; in {
       };
 
       httpsPort = mkOption {
-        type = types.int;
+        type = types.port;
         default = 0;
         description = ''
           The port on which Subsonic will listen for
diff --git a/nixos/modules/services/misc/svnserve.nix b/nixos/modules/services/misc/svnserve.nix
index 3335ed09d40..5fa262ca3b9 100644
--- a/nixos/modules/services/misc/svnserve.nix
+++ b/nixos/modules/services/misc/svnserve.nix
@@ -24,8 +24,9 @@ in
       };
 
       svnBaseDir = mkOption {
+        type = types.str;
         default = "/repos";
-	description = "Base directory from which Subversion repositories are accessed.";
+        description = "Base directory from which Subversion repositories are accessed.";
       };
     };
 
diff --git a/nixos/modules/services/misc/synergy.nix b/nixos/modules/services/misc/synergy.nix
index 5b7cf3ac46c..d6cd5d7f0d6 100644
--- a/nixos/modules/services/misc/synergy.nix
+++ b/nixos/modules/services/misc/synergy.nix
@@ -23,12 +23,14 @@ in
 
         screenName = mkOption {
           default = "";
+          type = types.str;
           description = ''
             Use the given name instead of the hostname to identify
             ourselves to the server.
           '';
         };
         serverAddress = mkOption {
+          type = types.str;
           description = ''
             The server address is of the form: [hostname][:port].  The
             hostname must be the address or hostname of the server.  The
@@ -46,10 +48,12 @@ in
         enable = mkEnableOption "the Synergy server (send keyboard and mouse events)";
 
         configFile = mkOption {
+          type = types.path;
           default = "/etc/synergy-server.conf";
           description = "The Synergy server configuration file.";
         };
         screenName = mkOption {
+          type = types.str;
           default = "";
           description = ''
             Use the given name instead of the hostname to identify
@@ -57,6 +61,7 @@ in
           '';
         };
         address = mkOption {
+          type = types.str;
           default = "";
           description = "Address on which to listen for clients.";
         };
@@ -65,6 +70,26 @@ in
           type = types.bool;
           description = "Whether the Synergy server should be started automatically.";
         };
+        tls = {
+          enable = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Whether TLS encryption should be used.
+
+              Using this requires a TLS certificate that can be
+              generated by starting the Synergy GUI once and entering
+              a valid product key.
+            '';
+          };
+
+          cert = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "~/.synergy/SSL/Synergy.pem";
+            description = "The TLS certificate to use for encryption.";
+          };
+        };
       };
     };
 
@@ -90,7 +115,7 @@ in
         description = "Synergy server";
         wantedBy = optional cfgS.autoStart "graphical-session.target";
         path = [ pkgs.synergy ];
-        serviceConfig.ExecStart = ''${pkgs.synergy}/bin/synergys -c ${cfgS.configFile} -f ${optionalString (cfgS.address != "") "-a ${cfgS.address}"} ${optionalString (cfgS.screenName != "") "-n ${cfgS.screenName}" }'';
+        serviceConfig.ExecStart = ''${pkgs.synergy}/bin/synergys -c ${cfgS.configFile} -f${optionalString (cfgS.address != "") " -a ${cfgS.address}"}${optionalString (cfgS.screenName != "") " -n ${cfgS.screenName}"}${optionalString cfgS.tls.enable " --enable-crypto"}${optionalString (cfgS.tls.cert != null) (" --tls-cert=${cfgS.tls.cert}")}'';
         serviceConfig.Restart = "on-failure";
       };
     })
diff --git a/nixos/modules/services/misc/weechat.nix b/nixos/modules/services/misc/weechat.nix
index c6ff540ea12..b71250f62e0 100644
--- a/nixos/modules/services/misc/weechat.nix
+++ b/nixos/modules/services/misc/weechat.nix
@@ -20,6 +20,7 @@ in
       type = types.str;
     };
     binary = mkOption {
+      type = types.path;
       description = "Binary to execute (by default \${weechat}/bin/weechat).";
       example = literalExample ''
         ''${pkgs.weechat}/bin/weechat-headless
diff --git a/nixos/modules/services/misc/zigbee2mqtt.nix b/nixos/modules/services/misc/zigbee2mqtt.nix
index 0957920f1a0..4458da1346b 100644
--- a/nixos/modules/services/misc/zigbee2mqtt.nix
+++ b/nixos/modules/services/misc/zigbee2mqtt.nix
@@ -5,29 +5,17 @@ with lib;
 let
   cfg = config.services.zigbee2mqtt;
 
-  configJSON = pkgs.writeText "configuration.json"
-    (builtins.toJSON (recursiveUpdate defaultConfig cfg.config));
-  configFile = pkgs.runCommand "configuration.yaml" { preferLocalBuild = true; } ''
-    ${pkgs.remarshal}/bin/json2yaml -i ${configJSON} -o $out
-  '';
+  format = pkgs.formats.yaml { };
+  configFile = format.generate "zigbee2mqtt.yaml" cfg.settings;
 
-  # the default config contains all required settings,
-  # so the service starts up without crashing.
-  defaultConfig = {
-    homeassistant = false;
-    permit_join = false;
-    mqtt = {
-      base_topic = "zigbee2mqtt";
-      server = "mqtt://localhost:1883";
-    };
-    serial.port = "/dev/ttyACM0";
-    # put device configuration into separate file because configuration.yaml
-    # is copied from the store on startup
-    devices = "devices.yaml";
-  };
 in
 {
-  meta.maintainers = with maintainers; [ sweber ];
+  meta.maintainers = with maintainers; [ sweber hexa ];
+
+  imports = [
+    # Remove warning before the 21.11 release
+    (mkRenamedOptionModule [ "services" "zigbee2mqtt" "config" ] [ "services" "zigbee2mqtt" "settings" ])
+  ];
 
   options.services.zigbee2mqtt = {
     enable = mkEnableOption "enable zigbee2mqtt service";
@@ -37,7 +25,11 @@ in
       default = pkgs.zigbee2mqtt.override {
         dataDir = cfg.dataDir;
       };
-      defaultText = "pkgs.zigbee2mqtt";
+      defaultText = literalExample ''
+        pkgs.zigbee2mqtt {
+          dataDir = services.zigbee2mqtt.dataDir
+        }
+      '';
       type = types.package;
     };
 
@@ -47,9 +39,9 @@ in
       type = types.path;
     };
 
-    config = mkOption {
+    settings = mkOption {
+      type = format.type;
       default = {};
-      type = with types; nullOr attrs;
       example = literalExample ''
         {
           homeassistant = config.services.home-assistant.enable;
@@ -61,24 +53,80 @@ in
       '';
       description = ''
         Your <filename>configuration.yaml</filename> as a Nix attribute set.
+        Check the <link xlink:href="https://www.zigbee2mqtt.io/information/configuration.html">documentation</link>
+        for possible options.
       '';
     };
   };
 
   config = mkIf (cfg.enable) {
+
+    # preset config values
+    services.zigbee2mqtt.settings = {
+      homeassistant = mkDefault config.services.home-assistant.enable;
+      permit_join = mkDefault false;
+      mqtt = {
+        base_topic = mkDefault "zigbee2mqtt";
+        server = mkDefault "mqtt://localhost:1883";
+      };
+      serial.port = mkDefault "/dev/ttyACM0";
+      # reference device configuration, that is kept in a separate file
+      # to prevent it being overwritten in the units ExecStartPre script
+      devices = mkDefault "devices.yaml";
+    };
+
     systemd.services.zigbee2mqtt = {
       description = "Zigbee2mqtt Service";
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
+      environment.ZIGBEE2MQTT_DATA = cfg.dataDir;
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/zigbee2mqtt";
         User = "zigbee2mqtt";
         WorkingDirectory = cfg.dataDir;
         Restart = "on-failure";
+
+        # Hardening
+        CapabilityBoundingSet = "";
+        DeviceAllow = [
+          config.services.zigbee2mqtt.settings.serial.port
+        ];
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = false;
+        NoNewPrivileges = true;
+        PrivateDevices = false; # prevents access to /dev/serial, because it is set 0700 root:root
+        PrivateUsers = true;
+        PrivateTmp = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
         ProtectSystem = "strict";
         ReadWritePaths = cfg.dataDir;
-        PrivateTmp = true;
         RemoveIPC = true;
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SupplementaryGroups = [
+          "dialout"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+          "~@resources"
+        ];
+        UMask = "0077";
       };
       preStart = ''
         cp --no-preserve=mode ${configFile} "${cfg.dataDir}/configuration.yaml"
@@ -89,7 +137,6 @@ in
       home = cfg.dataDir;
       createHome = true;
       group = "zigbee2mqtt";
-      extraGroups = [ "dialout" ];
       uid = config.ids.uids.zigbee2mqtt;
     };
 
diff --git a/nixos/modules/services/misc/zookeeper.nix b/nixos/modules/services/misc/zookeeper.nix
index f6af7c75eba..1d12e81a9ec 100644
--- a/nixos/modules/services/misc/zookeeper.nix
+++ b/nixos/modules/services/misc/zookeeper.nix
@@ -76,6 +76,7 @@ in {
       default = ''
         zookeeper.root.logger=INFO, CONSOLE
         log4j.rootLogger=INFO, CONSOLE
+        log4j.logger.org.apache.zookeeper.audit.Log4jAuditLogger=INFO, CONSOLE
         log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
         log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
         log4j.appender.CONSOLE.layout.ConversionPattern=[myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n
@@ -128,11 +129,10 @@ in {
       description = "Zookeeper Daemon";
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
-      environment = { ZOOCFGDIR = configDir; };
       serviceConfig = {
         ExecStart = ''
           ${pkgs.jre}/bin/java \
-            -cp "${cfg.package}/lib/*:${cfg.package}/${cfg.package.name}.jar:${configDir}" \
+            -cp "${cfg.package}/lib/*:${configDir}" \
             ${escapeShellArgs cfg.extraCmdLineOptions} \
             -Dzookeeper.datadir.autocreate=false \
             ${optionalString cfg.preferIPv4 "-Djava.net.preferIPv4Stack=true"} \
@@ -143,6 +143,7 @@ in {
       };
       preStart = ''
         echo "${toString cfg.id}" > ${cfg.dataDir}/myid
+        mkdir -p ${cfg.dataDir}/version-2
       '';
     };