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.nix179
-rw-r--r--nixos/modules/services/misc/ananicy.nix107
-rw-r--r--nixos/modules/services/misc/ankisyncd.nix79
-rw-r--r--nixos/modules/services/misc/apache-kafka.nix151
-rw-r--r--nixos/modules/services/misc/autofs.nix100
-rw-r--r--nixos/modules/services/misc/autorandr.nix53
-rw-r--r--nixos/modules/services/misc/bazarr.nix77
-rw-r--r--nixos/modules/services/misc/beanstalkd.nix63
-rw-r--r--nixos/modules/services/misc/bees.nix132
-rw-r--r--nixos/modules/services/misc/bepasty.nix179
-rw-r--r--nixos/modules/services/misc/calibre-server.nix86
-rw-r--r--nixos/modules/services/misc/canto-daemon.nix37
-rw-r--r--nixos/modules/services/misc/cfdyndns.nix82
-rw-r--r--nixos/modules/services/misc/cgminer.nix148
-rw-r--r--nixos/modules/services/misc/clipcat.nix31
-rw-r--r--nixos/modules/services/misc/clipmenu.nix31
-rwxr-xr-xnixos/modules/services/misc/confd.nix90
-rw-r--r--nixos/modules/services/misc/cpuminer-cryptonight.nix66
-rw-r--r--nixos/modules/services/misc/dendrite.nix275
-rw-r--r--nixos/modules/services/misc/devmon.nix25
-rw-r--r--nixos/modules/services/misc/dictd.nix65
-rw-r--r--nixos/modules/services/misc/disnix.nix98
-rw-r--r--nixos/modules/services/misc/docker-registry.nix159
-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/dwm-status.nix73
-rw-r--r--nixos/modules/services/misc/dysnomia.nix265
-rw-r--r--nixos/modules/services/misc/errbot.nix104
-rw-r--r--nixos/modules/services/misc/etcd.nix205
-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/ethminer.nix117
-rw-r--r--nixos/modules/services/misc/exhibitor.nix422
-rw-r--r--nixos/modules/services/misc/felix.nix104
-rw-r--r--nixos/modules/services/misc/freeswitch.nix104
-rw-r--r--nixos/modules/services/misc/fstrim.nix46
-rw-r--r--nixos/modules/services/misc/gammu-smsd.nix253
-rw-r--r--nixos/modules/services/misc/geoipupdate.nix187
-rw-r--r--nixos/modules/services/misc/gitea.nix663
-rw-r--r--nixos/modules/services/misc/gitit.nix725
-rw-r--r--nixos/modules/services/misc/gitlab.nix1458
-rw-r--r--nixos/modules/services/misc/gitlab.xml151
-rw-r--r--nixos/modules/services/misc/gitolite.nix234
-rw-r--r--nixos/modules/services/misc/gitweb.nix60
-rw-r--r--nixos/modules/services/misc/gogs.nix274
-rw-r--r--nixos/modules/services/misc/gollum.nix121
-rw-r--r--nixos/modules/services/misc/gpsd.nix116
-rw-r--r--nixos/modules/services/misc/greenclip.nix31
-rw-r--r--nixos/modules/services/misc/headphones.nix89
-rw-r--r--nixos/modules/services/misc/heisenbridge.nix222
-rw-r--r--nixos/modules/services/misc/ihaskell.nix65
-rw-r--r--nixos/modules/services/misc/input-remapper.nix30
-rw-r--r--nixos/modules/services/misc/irkerd.nix67
-rw-r--r--nixos/modules/services/misc/jackett.nix82
-rw-r--r--nixos/modules/services/misc/jellyfin.nix122
-rw-r--r--nixos/modules/services/misc/klipper.nix117
-rw-r--r--nixos/modules/services/misc/leaps.nix62
-rw-r--r--nixos/modules/services/misc/libreddit.nix66
-rw-r--r--nixos/modules/services/misc/lidarr.nix89
-rw-r--r--nixos/modules/services/misc/lifecycled.nix164
-rw-r--r--nixos/modules/services/misc/logkeys.nix30
-rw-r--r--nixos/modules/services/misc/mame.nix69
-rw-r--r--nixos/modules/services/misc/matrix-appservice-discord.nix161
-rw-r--r--nixos/modules/services/misc/matrix-appservice-irc.nix232
-rw-r--r--nixos/modules/services/misc/matrix-conduit.nix149
-rw-r--r--nixos/modules/services/misc/mautrix-facebook.nix195
-rw-r--r--nixos/modules/services/misc/mautrix-telegram.nix181
-rw-r--r--nixos/modules/services/misc/mbpfan.nix107
-rw-r--r--nixos/modules/services/misc/mediatomb.nix394
-rw-r--r--nixos/modules/services/misc/metabase.nix103
-rw-r--r--nixos/modules/services/misc/moonraker.nix138
-rw-r--r--nixos/modules/services/misc/mx-puppet-discord.nix122
-rw-r--r--nixos/modules/services/misc/n8n.nix79
-rw-r--r--nixos/modules/services/misc/nitter.nix358
-rw-r--r--nixos/modules/services/misc/nix-daemon.nix818
-rw-r--r--nixos/modules/services/misc/nix-gc.nix100
-rw-r--r--nixos/modules/services/misc/nix-optimise.nix51
-rw-r--r--nixos/modules/services/misc/nix-ssh-serve.nix69
-rw-r--r--nixos/modules/services/misc/novacomd.nix31
-rw-r--r--nixos/modules/services/misc/nzbget.nix117
-rw-r--r--nixos/modules/services/misc/nzbhydra2.nix78
-rw-r--r--nixos/modules/services/misc/octoprint.nix133
-rw-r--r--nixos/modules/services/misc/ombi.nix81
-rw-r--r--nixos/modules/services/misc/osrm.nix86
-rw-r--r--nixos/modules/services/misc/owncast.nix98
-rw-r--r--nixos/modules/services/misc/packagekit.nix74
-rw-r--r--nixos/modules/services/misc/paperless-ng.nix322
-rw-r--r--nixos/modules/services/misc/parsoid.nix129
-rw-r--r--nixos/modules/services/misc/pinnwand.nix103
-rw-r--r--nixos/modules/services/misc/plex.nix180
-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/prowlarr.nix41
-rw-r--r--nixos/modules/services/misc/pykms.nix92
-rw-r--r--nixos/modules/services/misc/radarr.nix75
-rw-r--r--nixos/modules/services/misc/redmine.nix384
-rw-r--r--nixos/modules/services/misc/ripple-data-api.nix195
-rw-r--r--nixos/modules/services/misc/rippled.nix438
-rw-r--r--nixos/modules/services/misc/rmfakecloud.nix147
-rw-r--r--nixos/modules/services/misc/safeeyes.nix51
-rw-r--r--nixos/modules/services/misc/sdrplay.nix35
-rw-r--r--nixos/modules/services/misc/serviio.nix87
-rw-r--r--nixos/modules/services/misc/sickbeard.nix95
-rw-r--r--nixos/modules/services/misc/signald.nix105
-rw-r--r--nixos/modules/services/misc/siproxd.nix179
-rw-r--r--nixos/modules/services/misc/snapper.nix187
-rw-r--r--nixos/modules/services/misc/sonarr.nix76
-rw-r--r--nixos/modules/services/misc/sourcehut/builds.nix236
-rw-r--r--nixos/modules/services/misc/sourcehut/default.nix1386
-rw-r--r--nixos/modules/services/misc/sourcehut/dispatch.nix127
-rw-r--r--nixos/modules/services/misc/sourcehut/git.nix217
-rw-r--r--nixos/modules/services/misc/sourcehut/hg.nix175
-rw-r--r--nixos/modules/services/misc/sourcehut/hub.nix120
-rw-r--r--nixos/modules/services/misc/sourcehut/lists.nix187
-rw-r--r--nixos/modules/services/misc/sourcehut/man.nix124
-rw-r--r--nixos/modules/services/misc/sourcehut/meta.nix213
-rw-r--r--nixos/modules/services/misc/sourcehut/paste.nix135
-rw-r--r--nixos/modules/services/misc/sourcehut/service.nix375
-rw-r--r--nixos/modules/services/misc/sourcehut/sourcehut.xml119
-rw-r--r--nixos/modules/services/misc/sourcehut/todo.nix163
-rw-r--r--nixos/modules/services/misc/spice-vdagentd.nix30
-rw-r--r--nixos/modules/services/misc/ssm-agent.nix73
-rw-r--r--nixos/modules/services/misc/sssd.nix97
-rw-r--r--nixos/modules/services/misc/subsonic.nix169
-rw-r--r--nixos/modules/services/misc/sundtek.nix33
-rw-r--r--nixos/modules/services/misc/svnserve.nix46
-rw-r--r--nixos/modules/services/misc/synergy.nix149
-rw-r--r--nixos/modules/services/misc/sysprof.nix19
-rw-r--r--nixos/modules/services/misc/taskserver/default.nix569
-rw-r--r--nixos/modules/services/misc/taskserver/doc.xml135
-rw-r--r--nixos/modules/services/misc/taskserver/helper-tool.py688
-rw-r--r--nixos/modules/services/misc/tautulli.nix81
-rw-r--r--nixos/modules/services/misc/tiddlywiki.nix52
-rw-r--r--nixos/modules/services/misc/tp-auto-kbbl.nix58
-rw-r--r--nixos/modules/services/misc/tzupdate.nix45
-rw-r--r--nixos/modules/services/misc/uhub.nix112
-rw-r--r--nixos/modules/services/misc/weechat.nix63
-rw-r--r--nixos/modules/services/misc/weechat.xml66
-rw-r--r--nixos/modules/services/misc/xmr-stak.nix93
-rw-r--r--nixos/modules/services/misc/xmrig.nix76
-rw-r--r--nixos/modules/services/misc/zoneminder.nix370
-rw-r--r--nixos/modules/services/misc/zookeeper.nix158
142 files changed, 23475 insertions, 0 deletions
diff --git a/nixos/modules/services/misc/airsonic.nix b/nixos/modules/services/misc/airsonic.nix
new file mode 100644
index 00000000000..2b9c6d80abb
--- /dev/null
+++ b/nixos/modules/services/misc/airsonic.nix
@@ -0,0 +1,179 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.airsonic;
+  opt = options.services.airsonic;
+in {
+  options = {
+
+    services.airsonic = {
+      enable = mkEnableOption "Airsonic, the Free and Open Source media streaming server (fork of Subsonic and Libresonic)";
+
+      user = mkOption {
+        type = types.str;
+        default = "airsonic";
+        description = "User account under which airsonic runs.";
+      };
+
+      home = mkOption {
+        type = types.path;
+        default = "/var/lib/airsonic";
+        description = ''
+          The directory where Airsonic will create files.
+          Make sure it is writable.
+        '';
+      };
+
+      virtualHost = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost.
+        '';
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = ''
+          The host name or IP address on which to bind Airsonic.
+          The default value is appropriate for first launch, when the
+          default credentials are easy to guess. It is also appropriate
+          if you intend to use the virtualhost option in the service
+          module. In other cases, you may want to change this to a
+          specific IP or 0.0.0.0 to listen on all interfaces.
+        '';
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 4040;
+        description = ''
+          The port on which Airsonic will listen for
+          incoming HTTP traffic. Set to 0 to disable.
+        '';
+      };
+
+      contextPath = mkOption {
+        type = types.path;
+        default = "/";
+        description = ''
+          The context path, i.e., the last part of the Airsonic
+          URL. Typically '/' or '/airsonic'. Default '/'
+        '';
+      };
+
+      maxMemory = mkOption {
+        type = types.int;
+        default = 100;
+        description = ''
+          The memory limit (max Java heap size) in megabytes.
+          Default: 100
+        '';
+      };
+
+      transcoders = mkOption {
+        type = types.listOf types.path;
+        default = [ "${pkgs.ffmpeg.bin}/bin/ffmpeg" ];
+        defaultText = literalExpression ''[ "''${pkgs.ffmpeg.bin}/bin/ffmpeg" ]'';
+        description = ''
+          List of paths to transcoder executables that should be accessible
+          from Airsonic. Symlinks will be created to each executable inside
+          ''${config.${opt.home}}/transcoders.
+        '';
+      };
+
+      jre = mkOption {
+        type = types.package;
+        default = pkgs.jre8;
+        defaultText = literalExpression "pkgs.jre8";
+        description = ''
+          JRE package to use.
+
+          Airsonic only supports Java 8, airsonic-advanced requires at least
+          Java 11.
+        '';
+      };
+
+      war = mkOption {
+        type = types.path;
+        default = "${pkgs.airsonic}/webapps/airsonic.war";
+        defaultText = literalExpression ''"''${pkgs.airsonic}/webapps/airsonic.war"'';
+        description = "Airsonic war file to use.";
+      };
+
+      jvmOptions = mkOption {
+        description = ''
+          Extra command line options for the JVM running AirSonic.
+          Useful for sending jukebox output to non-default alsa
+          devices.
+        '';
+        default = [
+        ];
+        type = types.listOf types.str;
+        example = [
+          "-Djavax.sound.sampled.Clip='#CODEC [plughw:1,0]'"
+          "-Djavax.sound.sampled.Port='#Port CODEC [hw:1]'"
+          "-Djavax.sound.sampled.SourceDataLine='#CODEC [plughw:1,0]'"
+          "-Djavax.sound.sampled.TargetDataLine='#CODEC [plughw:1,0]'"
+        ];
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.airsonic = {
+      description = "Airsonic Media Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        # Install transcoders.
+        rm -rf ${cfg.home}/transcode
+        mkdir -p ${cfg.home}/transcode
+        for exe in ${toString cfg.transcoders}; do
+          ln -sf "$exe" ${cfg.home}/transcode
+        done
+      '';
+      serviceConfig = {
+        ExecStart = ''
+          ${cfg.jre}/bin/java -Xmx${toString cfg.maxMemory}m \
+          -Dairsonic.home=${cfg.home} \
+          -Dserver.address=${cfg.listenAddress} \
+          -Dserver.port=${toString cfg.port} \
+          -Dairsonic.contextPath=${cfg.contextPath} \
+          -Djava.awt.headless=true \
+          ${optionalString (cfg.virtualHost != null)
+            "-Dserver.use-forward-headers=true"} \
+          ${toString cfg.jvmOptions} \
+          -verbose:gc \
+          -jar ${cfg.war}
+        '';
+        Restart = "always";
+        User = "airsonic";
+        UMask = "0022";
+      };
+    };
+
+    services.nginx = mkIf (cfg.virtualHost != null) {
+      enable = true;
+      recommendedProxySettings = true;
+      virtualHosts.${cfg.virtualHost} = {
+        locations.${cfg.contextPath}.proxyPass = "http://${cfg.listenAddress}:${toString cfg.port}";
+      };
+    };
+
+    users.users.airsonic = {
+      description = "Airsonic service user";
+      group = "airsonic";
+      name = cfg.user;
+      home = cfg.home;
+      createHome = true;
+      isSystemUser = true;
+    };
+    users.groups.airsonic = {};
+  };
+}
diff --git a/nixos/modules/services/misc/ananicy.nix b/nixos/modules/services/misc/ananicy.nix
new file mode 100644
index 00000000000..191666bc362
--- /dev/null
+++ b/nixos/modules/services/misc/ananicy.nix
@@ -0,0 +1,107 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.ananicy;
+  configFile = pkgs.writeText "ananicy.conf" (generators.toKeyValue { } cfg.settings);
+  extraRules = pkgs.writeText "extraRules" cfg.extraRules;
+  servicename = if ((lib.getName cfg.package) == (lib.getName pkgs.ananicy-cpp)) then "ananicy-cpp" else "ananicy";
+in
+{
+  options = {
+    services.ananicy = {
+      enable = mkEnableOption "Ananicy, an auto nice daemon";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.ananicy;
+        defaultText = literalExpression "pkgs.ananicy";
+        example = literalExpression "pkgs.ananicy-cpp";
+        description = ''
+          Which ananicy package to use.
+        '';
+      };
+
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ int bool str ]);
+        default = { };
+        example = {
+          apply_nice = false;
+        };
+        description = ''
+          See <link xlink:href="https://github.com/Nefelim4ag/Ananicy/blob/master/ananicy.d/ananicy.conf"/>
+        '';
+      };
+
+      extraRules = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Extra rules in json format on separate lines. See:
+          <link xlink:href="https://github.com/Nefelim4ag/Ananicy#configuration"/>
+          <link xlink:href="https://gitlab.com/ananicy-cpp/ananicy-cpp/#global-configuration"/>
+        '';
+        example = literalExpression ''
+          '''
+            { "name": "eog", "type": "Image-View" }
+            { "name": "fdupes", "type": "BG_CPUIO" }
+          '''
+        '';
+
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment = {
+      systemPackages = [ cfg.package ];
+      etc."ananicy.d".source = pkgs.runCommandLocal "ananicyfiles" { } ''
+        mkdir -p $out
+        # ananicy-cpp does not include rules or settings on purpose
+        cp -r ${pkgs.ananicy}/etc/ananicy.d/* $out
+        rm $out/ananicy.conf
+        cp ${configFile} $out/ananicy.conf
+        ${optionalString (cfg.extraRules != "") "cp ${extraRules} $out/nixRules.rules"}
+      '';
+    };
+
+    # ananicy and ananicy-cpp have different default settings
+    services.ananicy.settings =
+      let
+        mkOD = mkOptionDefault;
+      in
+      {
+        cgroup_load = mkOD true;
+        type_load = mkOD true;
+        rule_load = mkOD true;
+        apply_nice = mkOD true;
+        apply_ioclass = mkOD true;
+        apply_ionice = mkOD true;
+        apply_sched = mkOD true;
+        apply_oom_score_adj = mkOD true;
+        apply_cgroup = mkOD true;
+      } // (if ((lib.getName cfg.package) == (lib.getName pkgs.ananicy-cpp)) then {
+        # https://gitlab.com/ananicy-cpp/ananicy-cpp/-/blob/master/src/config.cpp#L12
+        loglevel = mkOD "warn"; # default is info but its spammy
+        cgroup_realtime_workaround = mkOD config.systemd.enableUnifiedCgroupHierarchy;
+      } else {
+        # https://github.com/Nefelim4ag/Ananicy/blob/master/ananicy.d/ananicy.conf
+        check_disks_schedulers = mkOD true;
+        check_freq = mkOD 5;
+      });
+
+    systemd = {
+      # https://gitlab.com/ananicy-cpp/ananicy-cpp/#cgroups applies to both ananicy and -cpp
+      enableUnifiedCgroupHierarchy = mkDefault false;
+      packages = [ cfg.package ];
+      services."${servicename}" = {
+        wantedBy = [ "default.target" ];
+      };
+    };
+  };
+
+  meta = {
+    maintainers = with maintainers; [ artturin ];
+  };
+}
diff --git a/nixos/modules/services/misc/ankisyncd.nix b/nixos/modules/services/misc/ankisyncd.nix
new file mode 100644
index 00000000000..69e471f4f57
--- /dev/null
+++ b/nixos/modules/services/misc/ankisyncd.nix
@@ -0,0 +1,79 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.ankisyncd;
+
+  name = "ankisyncd";
+
+  stateDir = "/var/lib/${name}";
+
+  authDbPath = "${stateDir}/auth.db";
+
+  sessionDbPath = "${stateDir}/session.db";
+
+  configFile = pkgs.writeText "ankisyncd.conf" (lib.generators.toINI {} {
+    sync_app = {
+      host = cfg.host;
+      port = cfg.port;
+      data_root = stateDir;
+      auth_db_path = authDbPath;
+      session_db_path = sessionDbPath;
+
+      base_url = "/sync/";
+      base_media_url = "/msync/";
+    };
+  });
+in
+  {
+    options.services.ankisyncd = {
+      enable = mkEnableOption "ankisyncd";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.ankisyncd;
+        defaultText = literalExpression "pkgs.ankisyncd";
+        description = "The package to use for the ankisyncd command.";
+      };
+
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "ankisyncd host";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 27701;
+        description = "ankisyncd port";
+      };
+
+      openFirewall = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to open the firewall for the specified port.";
+      };
+    };
+
+    config = mkIf cfg.enable {
+      networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
+
+      environment.etc."ankisyncd/ankisyncd.conf".source = configFile;
+
+      systemd.services.ankisyncd = {
+        description = "ankisyncd - Anki sync server";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        path = [ cfg.package ];
+
+        serviceConfig = {
+          Type = "simple";
+          DynamicUser = true;
+          StateDirectory = name;
+          ExecStart = "${cfg.package}/bin/ankisyncd";
+          Restart = "always";
+        };
+      };
+    };
+  }
diff --git a/nixos/modules/services/misc/apache-kafka.nix b/nixos/modules/services/misc/apache-kafka.nix
new file mode 100644
index 00000000000..d1856fff4aa
--- /dev/null
+++ b/nixos/modules/services/misc/apache-kafka.nix
@@ -0,0 +1,151 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.apache-kafka;
+
+  serverProperties =
+    if cfg.serverProperties != null then
+      cfg.serverProperties
+    else
+      ''
+        # Generated by nixos
+        broker.id=${toString cfg.brokerId}
+        port=${toString cfg.port}
+        host.name=${cfg.hostname}
+        log.dirs=${concatStringsSep "," cfg.logDirs}
+        zookeeper.connect=${cfg.zookeeper}
+        ${toString cfg.extraProperties}
+      '';
+
+  serverConfig = pkgs.writeText "server.properties" serverProperties;
+  logConfig = pkgs.writeText "log4j.properties" cfg.log4jProperties;
+
+in {
+
+  options.services.apache-kafka = {
+    enable = mkOption {
+      description = "Whether to enable Apache Kafka.";
+      default = false;
+      type = types.bool;
+    };
+
+    brokerId = mkOption {
+      description = "Broker ID.";
+      default = -1;
+      type = types.int;
+    };
+
+    port = mkOption {
+      description = "Port number the broker should listen on.";
+      default = 9092;
+      type = types.int;
+    };
+
+    hostname = mkOption {
+      description = "Hostname the broker should bind to.";
+      default = "localhost";
+      type = types.str;
+    };
+
+    logDirs = mkOption {
+      description = "Log file directories";
+      default = [ "/tmp/kafka-logs" ];
+      type = types.listOf types.path;
+    };
+
+    zookeeper = mkOption {
+      description = "Zookeeper connection string";
+      default = "localhost:2181";
+      type = types.str;
+    };
+
+    extraProperties = mkOption {
+      description = "Extra properties for server.properties.";
+      type = types.nullOr types.lines;
+      default = null;
+    };
+
+    serverProperties = mkOption {
+      description = ''
+        Complete server.properties content. Other server.properties config
+        options will be ignored if this option is used.
+      '';
+      type = types.nullOr types.lines;
+      default = null;
+    };
+
+    log4jProperties = mkOption {
+      description = "Kafka log4j property configuration.";
+      default = ''
+        log4j.rootLogger=INFO, stdout
+
+        log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+        log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+        log4j.appender.stdout.layout.ConversionPattern=[%d] %p %m (%c)%n
+      '';
+      type = types.lines;
+    };
+
+    jvmOptions = mkOption {
+      description = "Extra command line options for the JVM running Kafka.";
+      default = [];
+      type = types.listOf types.str;
+      example = [
+        "-Djava.net.preferIPv4Stack=true"
+        "-Dcom.sun.management.jmxremote"
+        "-Dcom.sun.management.jmxremote.local.only=true"
+      ];
+    };
+
+    package = mkOption {
+      description = "The kafka package to use";
+      default = pkgs.apacheKafka;
+      defaultText = literalExpression "pkgs.apacheKafka";
+      type = types.package;
+    };
+
+    jre = mkOption {
+      description = "The JRE with which to run Kafka";
+      default = cfg.package.passthru.jre;
+      defaultText = literalExpression "pkgs.apacheKafka.passthru.jre";
+      type = types.package;
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [cfg.package];
+
+    users.users.apache-kafka = {
+      isSystemUser = true;
+      group = "apache-kafka";
+      description = "Apache Kafka daemon user";
+      home = head cfg.logDirs;
+    };
+    users.groups.apache-kafka = {};
+
+    systemd.tmpfiles.rules = map (logDir: "d '${logDir}' 0700 apache-kafka - - -") cfg.logDirs;
+
+    systemd.services.apache-kafka = {
+      description = "Apache Kafka Daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${cfg.jre}/bin/java \
+            -cp "${cfg.package}/libs/*" \
+            -Dlog4j.configuration=file:${logConfig} \
+            ${toString cfg.jvmOptions} \
+            kafka.Kafka \
+            ${serverConfig}
+        '';
+        User = "apache-kafka";
+        SuccessExitStatus = "0 143";
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/misc/autofs.nix b/nixos/modules/services/misc/autofs.nix
new file mode 100644
index 00000000000..5fce990afec
--- /dev/null
+++ b/nixos/modules/services/misc/autofs.nix
@@ -0,0 +1,100 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.autofs;
+
+  autoMaster = pkgs.writeText "auto.master" cfg.autoMaster;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.autofs = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Mount filesystems on demand. Unmount them automatically.
+          You may also be interested in afuse.
+        '';
+      };
+
+      autoMaster = mkOption {
+        type = types.str;
+        example = literalExpression ''
+          let
+            mapConf = pkgs.writeText "auto" '''
+             kernel    -ro,soft,intr       ftp.kernel.org:/pub/linux
+             boot      -fstype=ext2        :/dev/hda1
+             windoze   -fstype=smbfs       ://windoze/c
+             removable -fstype=ext2        :/dev/hdd
+             cd        -fstype=iso9660,ro  :/dev/hdc
+             floppy    -fstype=auto        :/dev/fd0
+             server    -rw,hard,intr       / -ro myserver.me.org:/ \
+                                           /usr myserver.me.org:/usr \
+                                           /home myserver.me.org:/home
+            ''';
+          in '''
+            /auto file:''${mapConf}
+          '''
+        '';
+        description = ''
+          Contents of <literal>/etc/auto.master</literal> file. See <command>auto.master(5)</command> and <command>autofs(5)</command>.
+        '';
+      };
+
+      timeout = mkOption {
+        type = types.int;
+        default = 600;
+        description = "Set the global minimum timeout, in seconds, until directories are unmounted";
+      };
+
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Pass -d and -7 to automount and write log to the system journal.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    boot.kernelModules = [ "autofs4" ];
+
+    systemd.services.autofs =
+      { description = "Automounts filesystems on demand";
+        after = [ "network.target" "ypbind.service" "sssd.service" "network-online.target" ];
+        wants = [ "network-online.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        preStart = ''
+          # There should be only one autofs service managed by systemd, so this should be safe.
+          rm -f /tmp/autofs-running
+        '';
+
+        serviceConfig = {
+          Type = "forking";
+          PIDFile = "/run/autofs.pid";
+          ExecStart = "${pkgs.autofs5}/bin/automount ${optionalString cfg.debug "-d"} -p /run/autofs.pid -t ${builtins.toString cfg.timeout} ${autoMaster}";
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        };
+      };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/autorandr.nix b/nixos/modules/services/misc/autorandr.nix
new file mode 100644
index 00000000000..a65c5c9d11c
--- /dev/null
+++ b/nixos/modules/services/misc/autorandr.nix
@@ -0,0 +1,53 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.autorandr;
+
+in {
+
+  options = {
+
+    services.autorandr = {
+      enable = mkEnableOption "handling of hotplug and sleep events by autorandr";
+
+      defaultTarget = mkOption {
+        default = "default";
+        type = types.str;
+        description = ''
+          Fallback if no monitor layout can be detected. See the docs
+          (https://github.com/phillipberndt/autorandr/blob/v1.0/README.md#how-to-use)
+          for further reference.
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    services.udev.packages = [ pkgs.autorandr ];
+
+    environment.systemPackages = [ pkgs.autorandr ];
+
+    systemd.services.autorandr = {
+      wantedBy = [ "sleep.target" ];
+      description = "Autorandr execution hook";
+      after = [ "sleep.target" ];
+
+      startLimitIntervalSec = 5;
+      startLimitBurst = 1;
+      serviceConfig = {
+        ExecStart = "${pkgs.autorandr}/bin/autorandr --batch --change --default ${cfg.defaultTarget}";
+        Type = "oneshot";
+        RemainAfterExit = false;
+        KillMode = "process";
+      };
+    };
+
+  };
+
+  meta.maintainers = with maintainers; [ ];
+}
diff --git a/nixos/modules/services/misc/bazarr.nix b/nixos/modules/services/misc/bazarr.nix
new file mode 100644
index 00000000000..99343a146a7
--- /dev/null
+++ b/nixos/modules/services/misc/bazarr.nix
@@ -0,0 +1,77 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.bazarr;
+in
+{
+  options = {
+    services.bazarr = {
+      enable = mkEnableOption "bazarr, a subtitle manager for Sonarr and Radarr";
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the bazarr web interface.";
+      };
+
+      listenPort = mkOption {
+        type = types.port;
+        default = 6767;
+        description = "Port on which the bazarr web interface should listen";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "bazarr";
+        description = "User account under which bazarr runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "bazarr";
+        description = "Group under which bazarr runs.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.bazarr = {
+      description = "bazarr";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = rec {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        StateDirectory = "bazarr";
+        SyslogIdentifier = "bazarr";
+        ExecStart = pkgs.writeShellScript "start-bazarr" ''
+          ${pkgs.bazarr}/bin/bazarr \
+            --config '/var/lib/${StateDirectory}' \
+            --port ${toString cfg.listenPort} \
+            --no-update True
+        '';
+        Restart = "on-failure";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.listenPort ];
+    };
+
+    users.users = mkIf (cfg.user == "bazarr") {
+      bazarr = {
+        isSystemUser = true;
+        group = cfg.group;
+        home = "/var/lib/${config.systemd.services.bazarr.serviceConfig.StateDirectory}";
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "bazarr") {
+      bazarr = {};
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/beanstalkd.nix b/nixos/modules/services/misc/beanstalkd.nix
new file mode 100644
index 00000000000..1c674a5b23b
--- /dev/null
+++ b/nixos/modules/services/misc/beanstalkd.nix
@@ -0,0 +1,63 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.beanstalkd;
+  pkg = pkgs.beanstalkd;
+in
+
+{
+  # interface
+
+  options = {
+    services.beanstalkd = {
+      enable = mkEnableOption "the Beanstalk work queue";
+
+      listen = {
+        port = mkOption {
+          type = types.int;
+          description = "TCP port that will be used to accept client connections.";
+          default = 11300;
+        };
+
+        address = mkOption {
+          type = types.str;
+          description = "IP address to listen on.";
+          default = "127.0.0.1";
+          example = "0.0.0.0";
+        };
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to open ports in the firewall for the server.";
+      };
+    };
+  };
+
+  # implementation
+
+  config = mkIf cfg.enable {
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.listen.port ];
+    };
+
+    environment.systemPackages = [ pkg ];
+
+    systemd.services.beanstalkd = {
+      description = "Beanstalk Work Queue";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        Restart = "always";
+        ExecStart = "${pkg}/bin/beanstalkd -l ${cfg.listen.address} -p ${toString cfg.listen.port} -b $STATE_DIRECTORY";
+        StateDirectory = "beanstalkd";
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/misc/bees.nix b/nixos/modules/services/misc/bees.nix
new file mode 100644
index 00000000000..fa00d7e4f55
--- /dev/null
+++ b/nixos/modules/services/misc/bees.nix
@@ -0,0 +1,132 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.beesd;
+
+  logLevels = { emerg = 0; alert = 1; crit = 2; err = 3; warning = 4; notice = 5; info = 6; debug = 7; };
+
+  fsOptions = with types; {
+    options.spec = mkOption {
+      type = str;
+      description = ''
+        Description of how to identify the filesystem to be duplicated by this
+        instance of bees. Note that deduplication crosses subvolumes; one must
+        not configure multiple instances for subvolumes of the same filesystem
+        (or block devices which are part of the same filesystem), but only for
+        completely independent btrfs filesystems.
+        </para>
+        <para>
+        This must be in a format usable by findmnt; that could be a key=value
+        pair, or a bare path to a mount point.
+        Using bare paths will allow systemd to start the beesd service only
+        after mounting the associated path.
+      '';
+      example = "LABEL=MyBulkDataDrive";
+    };
+    options.hashTableSizeMB = mkOption {
+      type = types.addCheck types.int (n: mod n 16 == 0);
+      default = 1024; # 1GB; default from upstream beesd script
+      description = ''
+        Hash table size in MB; must be a multiple of 16.
+        </para>
+        <para>
+        A larger ratio of index size to storage size means smaller blocks of
+        duplicate content are recognized.
+        </para>
+        <para>
+        If you have 1TB of data, a 4GB hash table (which is to say, a value of
+        4096) will permit 4KB extents (the smallest possible size) to be
+        recognized, whereas a value of 1024 -- creating a 1GB hash table --
+        will recognize only aligned duplicate blocks of 16KB.
+      '';
+    };
+    options.verbosity = mkOption {
+      type = types.enum (attrNames logLevels ++ attrValues logLevels);
+      apply = v: if isString v then logLevels.${v} else v;
+      default = "info";
+      description = "Log verbosity (syslog keyword/level).";
+    };
+    options.workDir = mkOption {
+      type = str;
+      default = ".beeshome";
+      description = ''
+        Name (relative to the root of the filesystem) of the subvolume where
+        the hash table will be stored.
+      '';
+    };
+    options.extraOptions = mkOption {
+      type = listOf str;
+      default = [ ];
+      description = ''
+        Extra command-line options passed to the daemon. See upstream bees documentation.
+      '';
+      example = literalExpression ''
+        [ "--thread-count" "4" ]
+      '';
+    };
+  };
+
+in
+{
+
+  options.services.beesd = {
+    filesystems = mkOption {
+      type = with types; attrsOf (submodule fsOptions);
+      description = "BTRFS filesystems to run block-level deduplication on.";
+      default = { };
+      example = literalExpression ''
+        {
+          root = {
+            spec = "LABEL=root";
+            hashTableSizeMB = 2048;
+            verbosity = "crit";
+            extraOptions = [ "--loadavg-target" "5.0" ];
+          };
+        }
+      '';
+    };
+  };
+  config = {
+    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.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"
+          };
+        unitConfig.RequiresMountsFor = lib.mkIf (lib.hasPrefix "/" fs.spec) fs.spec;
+        wantedBy = [ "multi-user.target" ];
+      })
+      cfg.filesystems;
+  };
+}
diff --git a/nixos/modules/services/misc/bepasty.nix b/nixos/modules/services/misc/bepasty.nix
new file mode 100644
index 00000000000..f69832e5b2b
--- /dev/null
+++ b/nixos/modules/services/misc/bepasty.nix
@@ -0,0 +1,179 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  gunicorn = pkgs.python3Packages.gunicorn;
+  bepasty = pkgs.bepasty;
+  gevent = pkgs.python3Packages.gevent;
+  python = pkgs.python3Packages.python;
+  cfg = config.services.bepasty;
+  user = "bepasty";
+  group = "bepasty";
+  default_home = "/var/lib/bepasty";
+in
+{
+  options.services.bepasty = {
+    enable = mkEnableOption "Bepasty servers";
+
+    servers = mkOption {
+      default = {};
+      description = ''
+        configure a number of bepasty servers which will be started with
+        gunicorn.
+        '';
+      type = with types ; attrsOf (submodule ({ config, ... } : {
+
+        options = {
+
+          bind = mkOption {
+            type = types.str;
+            description = ''
+              Bind address to be used for this server.
+              '';
+            example = "0.0.0.0:8000";
+            default = "127.0.0.1:8000";
+          };
+
+          dataDir = mkOption {
+            type = types.str;
+            description = ''
+              Path to the directory where the pastes will be saved to
+              '';
+            default = default_home+"/data";
+          };
+
+          defaultPermissions = mkOption {
+            type = types.str;
+            description = ''
+              default permissions for all unauthenticated accesses.
+              '';
+            example = "read,create,delete";
+            default = "read";
+          };
+
+          extraConfig = mkOption {
+            type = types.lines;
+            description = ''
+              Extra configuration for bepasty server to be appended on the
+              configuration.
+              see https://bepasty-server.readthedocs.org/en/latest/quickstart.html#configuring-bepasty
+              for all options.
+              '';
+            default = "";
+            example = ''
+              PERMISSIONS = {
+                'myadminsecret': 'admin,list,create,read,delete',
+              }
+              MAX_ALLOWED_FILE_SIZE = 5 * 1000 * 1000
+              '';
+          };
+
+          secretKey = mkOption {
+            type = types.str;
+            description = ''
+              server secret for safe session cookies, must be set.
+
+              Warning: this secret is stored in the WORLD-READABLE Nix store!
+
+              It's recommended to use <option>secretKeyFile</option>
+              which takes precedence over <option>secretKey</option>.
+              '';
+            default = "";
+          };
+
+          secretKeyFile = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            description = ''
+              A file that contains the server secret for safe session cookies, must be set.
+
+              <option>secretKeyFile</option> takes precedence over <option>secretKey</option>.
+
+              Warning: when <option>secretKey</option> is non-empty <option>secretKeyFile</option>
+              defaults to a file in the WORLD-READABLE Nix store containing that secret.
+              '';
+          };
+
+          workDir = mkOption {
+            type = types.str;
+            description = ''
+              Path to the working directory (used for config and pidfile).
+              Defaults to the users home directory.
+              '';
+            default = default_home;
+          };
+
+        };
+        config = {
+          secretKeyFile = mkDefault (
+            if config.secretKey != ""
+            then toString (pkgs.writeTextFile {
+              name = "bepasty-secret-key";
+              text = config.secretKey;
+            })
+            else null
+          );
+        };
+      }));
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ bepasty ];
+
+    # creates gunicorn systemd service for each configured server
+    systemd.services = mapAttrs' (name: server:
+      nameValuePair ("bepasty-server-${name}-gunicorn")
+        ({
+          description = "Bepasty Server ${name}";
+          wantedBy = [ "multi-user.target" ];
+          after = [ "network.target" ];
+          restartIfChanged = true;
+
+          environment = let
+            penv = python.buildEnv.override {
+              extraLibs = [ bepasty gevent ];
+            };
+          in {
+            BEPASTY_CONFIG = "${server.workDir}/bepasty-${name}.conf";
+            PYTHONPATH= "${penv}/${python.sitePackages}/";
+          };
+
+          serviceConfig = {
+            Type = "simple";
+            PrivateTmp = true;
+            ExecStartPre = assert server.secretKeyFile != null; pkgs.writeScript "bepasty-server.${name}-init" ''
+              #!/bin/sh
+              mkdir -p "${server.workDir}"
+              mkdir -p "${server.dataDir}"
+              chown ${user}:${group} "${server.workDir}" "${server.dataDir}"
+              cat > ${server.workDir}/bepasty-${name}.conf <<EOF
+              SITENAME="${name}"
+              STORAGE_FILESYSTEM_DIRECTORY="${server.dataDir}"
+              SECRET_KEY="$(cat "${server.secretKeyFile}")"
+              DEFAULT_PERMISSIONS="${server.defaultPermissions}"
+              ${server.extraConfig}
+              EOF
+            '';
+            ExecStart = ''${gunicorn}/bin/gunicorn bepasty.wsgi --name ${name} \
+              -u ${user} \
+              -g ${group} \
+              --workers 3 --log-level=info \
+              --bind=${server.bind} \
+              --pid ${server.workDir}/gunicorn-${name}.pid \
+              -k gevent
+            '';
+          };
+        })
+    ) cfg.servers;
+
+    users.users.${user} =
+      { uid = config.ids.uids.bepasty;
+        group = group;
+        home = default_home;
+      };
+
+    users.groups.${group}.gid = config.ids.gids.bepasty;
+  };
+}
diff --git a/nixos/modules/services/misc/calibre-server.nix b/nixos/modules/services/misc/calibre-server.nix
new file mode 100644
index 00000000000..2467d34b524
--- /dev/null
+++ b/nixos/modules/services/misc/calibre-server.nix
@@ -0,0 +1,86 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.calibre-server;
+
+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";
+
+      libraries = mkOption {
+        description = ''
+          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";
+      };
+
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.calibre-server = {
+        description = "Calibre Server";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          User = cfg.user;
+          Restart = "always";
+          ExecStart = "${pkgs.calibre}/bin/calibre-server ${lib.concatStringsSep " " cfg.libraries}";
+        };
+
+      };
+
+    environment.systemPackages = [ pkgs.calibre ];
+
+    users.users = optionalAttrs (cfg.user == "calibre-server") {
+      calibre-server = {
+        home = "/var/lib/calibre-server";
+        createHome = true;
+        uid = config.ids.uids.calibre-server;
+        group = cfg.group;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "calibre-server") {
+      calibre-server = {
+        gid = config.ids.gids.calibre-server;
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/canto-daemon.nix b/nixos/modules/services/misc/canto-daemon.nix
new file mode 100644
index 00000000000..db51a263aab
--- /dev/null
+++ b/nixos/modules/services/misc/canto-daemon.nix
@@ -0,0 +1,37 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+cfg = config.services.canto-daemon;
+
+in {
+
+##### interface
+
+  options = {
+
+    services.canto-daemon = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the canto RSS daemon.";
+      };
+    };
+
+  };
+
+##### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.user.services.canto-daemon = {
+      description = "Canto RSS Daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "default.target" ];
+      serviceConfig.ExecStart = "${pkgs.canto-daemon}/bin/canto-daemon";
+    };
+  };
+
+}
diff --git a/nixos/modules/services/misc/cfdyndns.nix b/nixos/modules/services/misc/cfdyndns.nix
new file mode 100644
index 00000000000..5885617d742
--- /dev/null
+++ b/nixos/modules/services/misc/cfdyndns.nix
@@ -0,0 +1,82 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+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";
+
+      email = mkOption {
+        type = types.str;
+        description = ''
+          The email address to use to authenticate to CloudFlare.
+        '';
+      };
+
+      apikeyFile = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        description = ''
+          The path to a file containing the API Key
+          used to authenticate with CloudFlare.
+        '';
+      };
+
+      records = mkOption {
+        default = [];
+        example = [ "host.tld" ];
+        type = types.listOf types.str;
+        description = ''
+          The records to update in CloudFlare.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.cfdyndns = {
+      description = "CloudFlare Dynamic DNS Client";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      startAt = "*:0/5";
+      serviceConfig = {
+        Type = "simple";
+        User = config.ids.uids.cfdyndns;
+        Group = config.ids.gids.cfdyndns;
+      };
+      environment = {
+        CLOUDFLARE_EMAIL="${cfg.email}";
+        CLOUDFLARE_RECORDS="${concatStringsSep "," cfg.records}";
+      };
+      script = ''
+        ${optionalString (cfg.apikeyFile != null) ''
+          export CLOUDFLARE_APIKEY="$(cat ${escapeShellArg cfg.apikeyFile})"
+        ''}
+        ${pkgs.cfdyndns}/bin/cfdyndns
+      '';
+    };
+
+    users.users = {
+      cfdyndns = {
+        group = "cfdyndns";
+        uid = config.ids.uids.cfdyndns;
+      };
+    };
+
+    users.groups = {
+      cfdyndns = {
+        gid = config.ids.gids.cfdyndns;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/cgminer.nix b/nixos/modules/services/misc/cgminer.nix
new file mode 100644
index 00000000000..60f75530723
--- /dev/null
+++ b/nixos/modules/services/misc/cgminer.nix
@@ -0,0 +1,148 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.cgminer;
+
+  convType = with builtins;
+    v: if isBool v then boolToString v else toString v;
+  mergedHwConfig =
+    mapAttrsToList (n: v: ''"${n}": "${(concatStringsSep "," (map convType v))}"'')
+      (foldAttrs (n: a: [n] ++ a) [] cfg.hardware);
+  mergedConfig = with builtins;
+    mapAttrsToList (n: v: ''"${n}":  ${if isBool v then "" else ''"''}${convType v}${if isBool v then "" else ''"''}'')
+      cfg.config;
+
+  cgminerConfig = pkgs.writeText "cgminer.conf" ''
+  {
+  ${concatStringsSep ",\n" mergedHwConfig},
+  ${concatStringsSep ",\n" mergedConfig},
+  "pools": [
+  ${concatStringsSep ",\n"
+    (map (v: ''{"url": "${v.url}", "user": "${v.user}", "pass": "${v.pass}"}'')
+          cfg.pools)}]
+  }
+  '';
+in
+{
+  ###### interface
+  options = {
+
+    services.cgminer = {
+
+      enable = mkEnableOption "cgminer, an ASIC/FPGA/GPU miner for bitcoin and litecoin";
+
+      package = mkOption {
+        default = pkgs.cgminer;
+        defaultText = literalExpression "pkgs.cgminer";
+        description = "Which cgminer derivation to use.";
+        type = types.package;
+      };
+
+      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";
+          username = "17EUZxTvs9uRmPsjPZSYUU3zCz9iwstudk";
+          password="X";
+        }];
+      };
+
+      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 = [
+        {
+          intensity = 9;
+          gpu-engine = "0-985";
+          gpu-fan = "0-85";
+          gpu-memclock = 860;
+          gpu-powertune = 20;
+          temp-cutoff = 95;
+          temp-overheat = 85;
+          temp-target = 75;
+        }
+        {
+          intensity = 9;
+          gpu-engine = "0-950";
+          gpu-fan = "0-85";
+          gpu-memclock = 825;
+          gpu-powertune = 20;
+          temp-cutoff = 95;
+          temp-overheat = 85;
+          temp-target = 75;
+        }];
+      };
+
+      config = mkOption {
+        default = {};
+        type = types.attrsOf (types.either types.bool types.int);
+        description = "Additional config";
+        example = {
+          auto-fan = true;
+          auto-gpu = true;
+          expiry = 120;
+          failover-only = true;
+          gpu-threads = 2;
+          log = 5;
+          queue = 1;
+          scan-time = 60;
+          temp-histeresys = 3;
+        };
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.cgminer.enable {
+
+    users.users = optionalAttrs (cfg.user == "cgminer") {
+      cgminer = {
+        isSystemUser = true;
+        group = "cgminer";
+        description = "Cgminer user";
+      };
+    };
+    users.groups = optionalAttrs (cfg.user == "cgminer") {
+      cgminer = {};
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services.cgminer = {
+      path = [ pkgs.cgminer ];
+
+      after = [ "network.target" "display-manager.service" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment = {
+        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";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/clipcat.nix b/nixos/modules/services/misc/clipcat.nix
new file mode 100644
index 00000000000..8b749aa7289
--- /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 = literalExpression "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/clipmenu.nix b/nixos/modules/services/misc/clipmenu.nix
new file mode 100644
index 00000000000..ef95985f8d8
--- /dev/null
+++ b/nixos/modules/services/misc/clipmenu.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.clipmenu;
+in {
+
+  options.services.clipmenu = {
+    enable = mkEnableOption "clipmenu, the clipboard management daemon";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.clipmenu;
+      defaultText = literalExpression "pkgs.clipmenu";
+      description = "clipmenu derivation to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.clipmenu = {
+      enable      = true;
+      description = "Clipboard management daemon";
+      wantedBy = [ "graphical-session.target" ];
+      after    = [ "graphical-session.target" ];
+      serviceConfig.ExecStart = "${cfg.package}/bin/clipmenud";
+    };
+
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/misc/confd.nix b/nixos/modules/services/misc/confd.nix
new file mode 100755
index 00000000000..6c66786524b
--- /dev/null
+++ b/nixos/modules/services/misc/confd.nix
@@ -0,0 +1,90 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.confd;
+
+  confdConfig = ''
+    backend = "${cfg.backend}"
+    confdir = "${cfg.confDir}"
+    interval = ${toString cfg.interval}
+    nodes = [ ${concatMapStringsSep "," (s: ''"${s}"'') cfg.nodes}, ]
+    prefix = "${cfg.prefix}"
+    log-level = "${cfg.logLevel}"
+    watch = ${boolToString cfg.watch}
+  '';
+
+in {
+  options.services.confd = {
+    enable = mkEnableOption "confd service";
+
+    backend = mkOption {
+      description = "Confd config storage backend to use.";
+      default = "etcd";
+      type = types.enum ["etcd" "consul" "redis" "zookeeper"];
+    };
+
+    interval = mkOption {
+      description = "Confd check interval.";
+      default = 10;
+      type = types.int;
+    };
+
+    nodes = mkOption {
+      description = "Confd list of nodes to connect to.";
+      default = [ "http://127.0.0.1:2379" ];
+      type = types.listOf types.str;
+    };
+
+    watch = mkOption {
+      description = "Confd, whether to watch etcd config for changes.";
+      default = true;
+      type = types.bool;
+    };
+
+    prefix = mkOption {
+      description = "The string to prefix to keys.";
+      default = "/";
+      type = types.path;
+    };
+
+    logLevel = mkOption {
+      description = "Confd log level.";
+      default = "info";
+      type = types.enum ["info" "debug"];
+    };
+
+    confDir = mkOption {
+      description = "The path to the confd configs.";
+      default = "/etc/confd";
+      type = types.path;
+    };
+
+    package = mkOption {
+      description = "Confd package to use.";
+      default = pkgs.confd;
+      defaultText = literalExpression "pkgs.confd";
+      type = types.package;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.confd = {
+      description = "Confd Service.";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/confd";
+      };
+    };
+
+    environment.etc = {
+      "confd/confd.toml".text = confdConfig;
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+    services.etcd.enable = mkIf (cfg.backend == "etcd") (mkDefault true);
+  };
+}
diff --git a/nixos/modules/services/misc/cpuminer-cryptonight.nix b/nixos/modules/services/misc/cpuminer-cryptonight.nix
new file mode 100644
index 00000000000..907b9d90da2
--- /dev/null
+++ b/nixos/modules/services/misc/cpuminer-cryptonight.nix
@@ -0,0 +1,66 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.cpuminer-cryptonight;
+
+  json = builtins.toJSON (
+    cfg // {
+       enable = null;
+       threads =
+         if cfg.threads == 0 then null else toString cfg.threads;
+    }
+  );
+
+  confFile = builtins.toFile "cpuminer.json" json;
+in
+{
+
+  options = {
+
+    services.cpuminer-cryptonight = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the cpuminer cryptonight miner.
+        '';
+      };
+      url = mkOption {
+        type = types.str;
+        description = "URL of mining server";
+      };
+      user = mkOption {
+        type = types.str;
+        description = "Username for mining server";
+      };
+      pass = mkOption {
+        type = types.str;
+        default = "x";
+        description = "Password for mining server";
+      };
+      threads = mkOption {
+        type = types.int;
+        default = 0;
+        description = "Number of miner threads, defaults to available processors";
+      };
+    };
+
+  };
+
+  config = mkIf config.services.cpuminer-cryptonight.enable {
+
+    systemd.services.cpuminer-cryptonight = {
+      description = "Cryptonight cpuminer";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.cpuminer-multi}/bin/minerd --syslog --config=${confFile}";
+        User = "nobody";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/dendrite.nix b/nixos/modules/services/misc/dendrite.nix
new file mode 100644
index 00000000000..b2885b09415
--- /dev/null
+++ b/nixos/modules/services/misc/dendrite.nix
@@ -0,0 +1,275 @@
+{ 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.app_service_api.database = {
+          connection_string = lib.mkOption {
+            type = lib.types.str;
+            default = "file:federationapi.db";
+            description = ''
+              Database for the Appservice API.
+            '';
+          };
+        };
+        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.
+            '';
+          };
+        };
+        options.federation_api.database = {
+          connection_string = lib.mkOption {
+            type = lib.types.str;
+            default = "file:federationapi.db";
+            description = ''
+              Database for the Federation API.
+            '';
+          };
+        };
+        options.key_server.database = {
+          connection_string = lib.mkOption {
+            type = lib.types.str;
+            default = "file:keyserver.db";
+            description = ''
+              Database for the Key Server (for end-to-end encryption).
+            '';
+          };
+        };
+        options.media_api = {
+          database = {
+            connection_string = lib.mkOption {
+              type = lib.types.str;
+              default = "file:mediaapi.db";
+              description = ''
+                Database for the Media API.
+              '';
+            };
+          };
+          base_path = lib.mkOption {
+            type = lib.types.str;
+            default = "${workingDir}/media_store";
+            description = ''
+              Storage path for uploaded media.
+            '';
+          };
+        };
+        options.room_server.database = {
+          connection_string = lib.mkOption {
+            type = lib.types.str;
+            default = "file:roomserver.db";
+            description = ''
+              Database for the Room Server.
+            '';
+          };
+        };
+        options.sync_api.database = {
+          connection_string = lib.mkOption {
+            type = lib.types.str;
+            default = "file:syncserver.db";
+            description = ''
+              Database for the Sync API.
+            '';
+          };
+        };
+        options.user_api = {
+          account_database = {
+            connection_string = lib.mkOption {
+              type = lib.types.str;
+              default = "file:userapi_accounts.db";
+              description = ''
+                Database for the User API, accounts.
+              '';
+            };
+          };
+          device_database = {
+            connection_string = lib.mkOption {
+              type = lib.types.str;
+              default = "file:userapi_devices.db";
+              description = ''
+                Database for the User API, devices.
+              '';
+            };
+          };
+        };
+        options.mscs = {
+          database = {
+            connection_string = lib.mkOption {
+              type = lib.types.str;
+              default = "file:mscs.db";
+              description = ''
+                Database for exerimental MSC's.
+              '';
+            };
+          };
+        };
+      };
+      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/devmon.nix b/nixos/modules/services/misc/devmon.nix
new file mode 100644
index 00000000000..e4a3348646b
--- /dev/null
+++ b/nixos/modules/services/misc/devmon.nix
@@ -0,0 +1,25 @@
+{ pkgs, config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.devmon;
+
+in {
+  options = {
+    services.devmon = {
+      enable = mkEnableOption "devmon, an automatic device mounting daemon";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.devmon = {
+      description = "devmon automatic device mounting daemon";
+      wantedBy = [ "default.target" ];
+      path = [ pkgs.udevil pkgs.procps pkgs.udisks2 pkgs.which ];
+      serviceConfig.ExecStart = "${pkgs.udevil}/bin/devmon";
+    };
+
+    services.udisks2.enable = true;
+  };
+}
diff --git a/nixos/modules/services/misc/dictd.nix b/nixos/modules/services/misc/dictd.nix
new file mode 100644
index 00000000000..96e2a4e7c26
--- /dev/null
+++ b/nixos/modules/services/misc/dictd.nix
@@ -0,0 +1,65 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.dictd;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.dictd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the DICT.org dictionary server.
+        '';
+      };
+
+      DBs = mkOption {
+        type = types.listOf types.package;
+        default = with pkgs.dictdDBs; [ wiktionary wordnet ];
+        defaultText = literalExpression "with pkgs.dictdDBs; [ wiktionary wordnet ]";
+        example = literalExpression "[ pkgs.dictdDBs.nld2eng ]";
+        description = "List of databases to make available.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = let dictdb = pkgs.dictDBCollector { dictlist = map (x: {
+               name = x.name;
+               filename = x; } ) cfg.DBs; };
+  in mkIf cfg.enable {
+
+    # get the command line client on system path to make some use of the service
+    environment.systemPackages = [ pkgs.dict ];
+
+    users.users.dictd =
+      { group = "dictd";
+        description = "DICT.org dictd server";
+        home = "${dictdb}/share/dictd";
+        uid = config.ids.uids.dictd;
+      };
+
+    users.groups.dictd.gid = config.ids.gids.dictd;
+
+    systemd.services.dictd = {
+      description = "DICT.org Dictionary Server";
+      wantedBy = [ "multi-user.target" ];
+      environment = { LOCALE_ARCHIVE = "/run/current-system/sw/lib/locale/locale-archive"; };
+      serviceConfig.Type = "forking";
+      script = "${pkgs.dict}/sbin/dictd -s -c ${dictdb}/share/dictd/dictd.conf --locale en_US.UTF-8";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/disnix.nix b/nixos/modules/services/misc/disnix.nix
new file mode 100644
index 00000000000..07c0613336a
--- /dev/null
+++ b/nixos/modules/services/misc/disnix.nix
@@ -0,0 +1,98 @@
+# Disnix server
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.disnix;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.disnix = {
+
+      enable = mkEnableOption "Disnix";
+
+      enableMultiUser = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to support multi-user mode by enabling the Disnix D-Bus service";
+      };
+
+      useWebServiceInterface = mkEnableOption "the DisnixWebService interface running on Apache Tomcat";
+
+      package = mkOption {
+        type = types.path;
+        description = "The Disnix package";
+        default = pkgs.disnix;
+        defaultText = literalExpression "pkgs.disnix";
+      };
+
+      enableProfilePath = mkEnableOption "exposing the Disnix profiles in the system's PATH";
+
+      profiles = mkOption {
+        type = types.listOf types.str;
+        default = [ "default" ];
+        description = "Names of the Disnix profiles to expose in the system's PATH";
+      };
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    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 ];
+
+    services.tomcat.enable = cfg.useWebServiceInterface;
+    services.tomcat.extraGroups = [ "disnix" ];
+    services.tomcat.javaOpts = "${optionalString cfg.useWebServiceInterface "-Djava.library.path=${pkgs.libmatthew_java}/lib/jni"} ";
+    services.tomcat.sharedLibs = optional cfg.useWebServiceInterface "${pkgs.DisnixWebService}/share/java/DisnixConnection.jar"
+      ++ optional cfg.useWebServiceInterface "${pkgs.dbus_java}/share/java/dbus.jar";
+    services.tomcat.webapps = optional cfg.useWebServiceInterface pkgs.DisnixWebService;
+
+    users.groups.disnix.gid = config.ids.gids.disnix;
+
+    systemd.services = {
+      disnix = mkIf cfg.enableMultiUser {
+        description = "Disnix server";
+        wants = [ "dysnomia.target" ];
+        wantedBy = [ "multi-user.target" ];
+        after = [ "dbus.service" ]
+          ++ optional config.services.httpd.enable "httpd.service"
+          ++ optional config.services.mysql.enable "mysql.service"
+          ++ 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.influxdb.enable "influxdb.service";
+
+        restartIfChanged = false;
+
+        path = [ config.nix.package cfg.package config.dysnomia.package "/run/current-system/sw" ];
+
+        environment = {
+          HOME = "/root";
+        }
+        // (if config.environment.variables ? DYSNOMIA_CONTAINERS_PATH then { inherit (config.environment.variables) DYSNOMIA_CONTAINERS_PATH; } else {})
+        // (if config.environment.variables ? DYSNOMIA_MODULES_PATH then { inherit (config.environment.variables) DYSNOMIA_MODULES_PATH; } else {});
+
+        serviceConfig.ExecStart = "${cfg.package}/bin/disnix-service";
+      };
+
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/docker-registry.nix b/nixos/modules/services/misc/docker-registry.nix
new file mode 100644
index 00000000000..cb68a29c530
--- /dev/null
+++ b/nixos/modules/services/misc/docker-registry.nix
@@ -0,0 +1,159 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.dockerRegistry;
+
+  blobCache = if cfg.enableRedisCache
+    then "redis"
+    else "inmemory";
+
+  registryConfig = {
+    version =  "0.1";
+    log.fields.service = "registry";
+    storage = {
+      cache.blobdescriptor = blobCache;
+      delete.enabled = cfg.enableDelete;
+    } // (if cfg.storagePath != null
+          then { filesystem.rootdirectory = cfg.storagePath; }
+          else {});
+    http = {
+      addr = "${cfg.listenAddress}:${builtins.toString cfg.port}";
+      headers.X-Content-Type-Options = ["nosniff"];
+    };
+    health.storagedriver = {
+      enabled = true;
+      interval = "10s";
+      threshold = 3;
+    };
+  };
+
+  registryConfig.redis = mkIf cfg.enableRedisCache {
+    addr = "${cfg.redisUrl}";
+    password = "${cfg.redisPassword}";
+    db = 0;
+    dialtimeout = "10ms";
+    readtimeout = "10ms";
+    writetimeout = "10ms";
+    pool = {
+      maxidle = 16;
+      maxactive = 64;
+      idletimeout = "300s";
+    };
+  };
+
+  configFile = pkgs.writeText "docker-registry-config.yml" (builtins.toJSON (recursiveUpdate registryConfig cfg.extraConfig));
+
+in {
+  options.services.dockerRegistry = {
+    enable = mkEnableOption "Docker Registry";
+
+    listenAddress = mkOption {
+      description = "Docker registry host or ip to bind to.";
+      default = "127.0.0.1";
+      type = types.str;
+    };
+
+    port = mkOption {
+      description = "Docker registry port to bind to.";
+      default = 5000;
+      type = types.port;
+    };
+
+    storagePath = mkOption {
+      type = types.nullOr types.path;
+      default = "/var/lib/docker-registry";
+      description = ''
+        Docker registry storage path for the filesystem storage backend. Set to
+        null to configure another backend via extraConfig.
+      '';
+    };
+
+    enableDelete = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Enable delete for manifests and blobs.";
+    };
+
+    enableRedisCache = mkEnableOption "redis as blob cache";
+
+    redisUrl = mkOption {
+      type = types.str;
+      default = "localhost:6379";
+      description = "Set redis host and port.";
+    };
+
+    redisPassword = mkOption {
+      type = types.str;
+      default = "";
+      description = "Set redis password.";
+    };
+
+    extraConfig = mkOption {
+      description = ''
+        Docker extra registry configuration via environment variables.
+      '';
+      default = {};
+      type = types.attrs;
+    };
+
+    enableGarbageCollect = mkEnableOption "garbage collect";
+
+    garbageCollectDates = mkOption {
+      default = "daily";
+      type = types.str;
+      description = ''
+        Specification (in the format described by
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry>) of the time at
+        which the garbage collect will occur.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.docker-registry = {
+      description = "Docker Container Registry";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      script = ''
+        ${pkgs.docker-distribution}/bin/registry serve ${configFile}
+      '';
+
+      serviceConfig = {
+        User = "docker-registry";
+        WorkingDirectory = cfg.storagePath;
+        AmbientCapabilities = mkIf (cfg.port < 1024) "cap_net_bind_service";
+      };
+    };
+
+    systemd.services.docker-registry-garbage-collect = {
+      description = "Run Garbage Collection for docker registry";
+
+      restartIfChanged = false;
+      unitConfig.X-StopOnRemoval = false;
+
+      serviceConfig.Type = "oneshot";
+
+      script = ''
+        ${pkgs.docker-distribution}/bin/registry garbage-collect ${configFile}
+        /run/current-system/systemd/bin/systemctl restart docker-registry.service
+      '';
+
+      startAt = optional cfg.enableGarbageCollect cfg.garbageCollectDates;
+    };
+
+    users.users.docker-registry =
+      (if cfg.storagePath != null
+      then {
+        createHome = true;
+        home = cfg.storagePath;
+      }
+      else {}) // {
+        group = "docker-registry";
+        isSystemUser = true;
+      };
+    users.groups.docker-registry = {};
+  };
+}
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/dwm-status.nix b/nixos/modules/services/misc/dwm-status.nix
new file mode 100644
index 00000000000..5f591b3c5d4
--- /dev/null
+++ b/nixos/modules/services/misc/dwm-status.nix
@@ -0,0 +1,73 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.dwm-status;
+
+  order = concatMapStringsSep "," (feature: ''"${feature}"'') cfg.order;
+
+  configFile = pkgs.writeText "dwm-status.toml" ''
+    order = [${order}]
+
+    ${cfg.extraConfig}
+  '';
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.dwm-status = {
+
+      enable = mkEnableOption "dwm-status user service";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.dwm-status;
+        defaultText = literalExpression "pkgs.dwm-status";
+        example = literalExpression "pkgs.dwm-status.override { enableAlsaUtils = false; }";
+        description = ''
+          Which dwm-status package to use.
+        '';
+      };
+
+      order = mkOption {
+        type = types.listOf (types.enum [ "audio" "backlight" "battery" "cpu_load" "network" "time" ]);
+        description = ''
+          List of enabled features in order.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra config in TOML format.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.upower.enable = elem "battery" cfg.order;
+
+    systemd.user.services.dwm-status = {
+      description = "Highly performant and configurable DWM status service";
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+
+      serviceConfig.ExecStart = "${cfg.package}/bin/dwm-status ${configFile}";
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/dysnomia.nix b/nixos/modules/services/misc/dysnomia.nix
new file mode 100644
index 00000000000..7d9c39a6973
--- /dev/null
+++ b/nixos/modules/services/misc/dysnomia.nix
@@ -0,0 +1,265 @@
+{pkgs, lib, config, ...}:
+
+with lib;
+
+let
+  cfg = config.dysnomia;
+
+  printProperties = properties:
+    concatMapStrings (propertyName:
+      let
+        property = properties.${propertyName};
+      in
+      if isList property then "${propertyName}=(${lib.concatMapStrings (elem: "\"${toString elem}\" ") (properties.${propertyName})})\n"
+      else "${propertyName}=\"${toString property}\"\n"
+    ) (builtins.attrNames properties);
+
+  properties = pkgs.stdenv.mkDerivation {
+    name = "dysnomia-properties";
+    buildCommand = ''
+      cat > $out << "EOF"
+      ${printProperties cfg.properties}
+      EOF
+    '';
+  };
+
+  containersDir = pkgs.stdenv.mkDerivation {
+    name = "dysnomia-containers";
+    buildCommand = ''
+      mkdir -p $out
+      cd $out
+
+      ${concatMapStrings (containerName:
+        let
+          containerProperties = cfg.containers.${containerName};
+        in
+        ''
+          cat > ${containerName} <<EOF
+          ${printProperties containerProperties}
+          type=${containerName}
+          EOF
+        ''
+      ) (builtins.attrNames cfg.containers)}
+    '';
+  };
+
+  linkMutableComponents = {containerName}:
+    ''
+      mkdir ${containerName}
+
+      ${concatMapStrings (componentName:
+        let
+          component = cfg.components.${containerName}.${componentName};
+        in
+        "ln -s ${component} ${containerName}/${componentName}\n"
+      ) (builtins.attrNames (cfg.components.${containerName} or {}))}
+    '';
+
+  componentsDir = pkgs.stdenv.mkDerivation {
+    name = "dysnomia-components";
+    buildCommand = ''
+      mkdir -p $out
+      cd $out
+
+      ${concatMapStrings (containerName:
+        linkMutableComponents { inherit containerName; }
+      ) (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 = {
+    dysnomia = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable Dysnomia";
+      };
+
+      enableAuthentication = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to publish privacy-sensitive authentication credentials";
+      };
+
+      package = mkOption {
+        type = types.path;
+        description = "The Dysnomia package";
+      };
+
+      properties = mkOption {
+        description = "An attribute set in which each attribute represents a machine property. Optionally, these values can be shell substitutions.";
+        default = {};
+        type = types.attrs;
+      };
+
+      containers = mkOption {
+        description = "An attribute set in which each key represents a container and each value an attribute set providing its configuration properties";
+        default = {};
+        type = types.attrsOf types.attrs;
+      };
+
+      components = mkOption {
+        description = "An atttribute set in which each key represents a container and each value an attribute set in which each key represents a component and each value a derivation constructing its initial state";
+        default = {};
+        type = types.attrsOf types.attrs;
+      };
+
+      extraContainerProperties = mkOption {
+        description = "An attribute set providing additional container settings in addition to the default properties";
+        default = {};
+        type = types.attrs;
+      };
+
+      extraContainerPaths = mkOption {
+        description = "A list of paths containing additional container configurations that are added to the search folders";
+        default = [];
+        type = types.listOf types.path;
+      };
+
+      extraModulePaths = mkOption {
+        description = "A list of paths containing additional modules that are added to the search folders";
+        default = [];
+        type = types.listOf types.path;
+      };
+
+      enableLegacyModules = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to enable Dysnomia legacy process and wrapper modules";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.etc = {
+      "dysnomia/containers" = {
+        source = containersDir;
+      };
+      "dysnomia/components" = {
+        source = componentsDir;
+      };
+      "dysnomia/properties" = {
+        source = properties;
+      };
+    };
+
+    environment.variables = {
+      DYSNOMIA_STATEDIR = "/var/state/dysnomia-nixos";
+      DYSNOMIA_CONTAINERS_PATH = "${lib.concatMapStrings (containerPath: "${containerPath}:") cfg.extraContainerPaths}/etc/dysnomia/containers";
+      DYSNOMIA_MODULES_PATH = "${lib.concatMapStrings (modulePath: "${modulePath}:") cfg.extraModulePaths}/etc/dysnomia/modules";
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+    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 = [
+        "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 ({
+      process = {};
+      wrapper = {};
+    }
+    // lib.optionalAttrs (config.services.httpd.enable) { apache-webapplication = {
+      documentRoot = config.services.httpd.virtualHosts.localhost.documentRoot;
+    }; }
+    // lib.optionalAttrs (config.services.tomcat.axis2.enable) { axis2-webservice = {}; }
+    // lib.optionalAttrs (config.services.ejabberd.enable) { ejabberd-dump = {
+      ejabberdUser = config.services.ejabberd.user;
+    }; }
+    // lib.optionalAttrs (config.services.mysql.enable) { mysql-database = {
+        mysqlPort = config.services.mysql.port;
+        mysqlSocket = "/run/mysqld/mysqld.sock";
+      } // lib.optionalAttrs cfg.enableAuthentication {
+        mysqlUsername = "root";
+      };
+    }
+    // lib.optionalAttrs (config.services.postgresql.enable) { postgresql-database = {
+      } // lib.optionalAttrs (cfg.enableAuthentication) {
+        postgresqlUsername = "postgres";
+      };
+    }
+    // lib.optionalAttrs (config.services.tomcat.enable) { tomcat-webapplication = {
+      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 ]
+      then
+          ( echo "[Unit]"
+            echo "Description=Services that are activated and deactivated by Dysnomia"
+            echo "After=final.target"
+          ) > /etc/systemd-mutable/system/dysnomia.target
+      fi
+    '';
+  };
+}
diff --git a/nixos/modules/services/misc/errbot.nix b/nixos/modules/services/misc/errbot.nix
new file mode 100644
index 00000000000..b447ba5d438
--- /dev/null
+++ b/nixos/modules/services/misc/errbot.nix
@@ -0,0 +1,104 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.errbot;
+  pluginEnv = plugins: pkgs.buildEnv {
+    name = "errbot-plugins";
+    paths = plugins;
+  };
+  mkConfigDir = instanceCfg: dataDir: pkgs.writeTextDir "config.py" ''
+    import logging
+    BACKEND = '${instanceCfg.backend}'
+    BOT_DATA_DIR = '${dataDir}'
+    BOT_EXTRA_PLUGIN_DIR = '${pluginEnv instanceCfg.plugins}'
+
+    BOT_LOG_LEVEL = logging.${instanceCfg.logLevel}
+    BOT_LOG_FILE = False
+
+    BOT_ADMINS = (${concatMapStringsSep "," (name: "'${name}'") instanceCfg.admins})
+
+    BOT_IDENTITY = ${builtins.toJSON instanceCfg.identity}
+
+    ${instanceCfg.extraConfig}
+  '';
+in {
+  options = {
+    services.errbot.instances = mkOption {
+      default = {};
+      description = "Errbot instance configs";
+      type = types.attrsOf (types.submodule {
+        options = {
+          dataDir = mkOption {
+            type = types.nullOr types.path;
+            default = null;
+            description = "Data directory for errbot instance.";
+          };
+
+          plugins = mkOption {
+            type = types.listOf types.package;
+            default = [];
+            description = "List of errbot plugin derivations.";
+          };
+
+          logLevel = mkOption {
+            type = types.str;
+            default = "INFO";
+            description = "Errbot log level";
+          };
+
+          admins = mkOption {
+            type = types.listOf types.str;
+            default = [];
+            description = "List of identifiers of errbot admins.";
+          };
+
+          backend = mkOption {
+            type = types.str;
+            default = "XMPP";
+            description = "Errbot backend name.";
+          };
+
+          identity = mkOption {
+            type = types.attrs;
+            description = "Errbot identity configuration";
+          };
+
+          extraConfig = mkOption {
+            type = types.lines;
+            default = "";
+            description = "String to be appended to the config verbatim";
+          };
+        };
+      });
+    };
+  };
+
+  config = mkIf (cfg.instances != {}) {
+    users.users.errbot = {
+      group = "errbot";
+      isSystemUser = true;
+    };
+    users.groups.errbot = {};
+
+    systemd.services = mapAttrs' (name: instanceCfg: nameValuePair "errbot-${name}" (
+    let
+      dataDir = if instanceCfg.dataDir != null then instanceCfg.dataDir else
+        "/var/lib/errbot/${name}";
+    in {
+      after = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      preStart = ''
+        mkdir -p ${dataDir}
+        chown -R errbot:errbot ${dataDir}
+      '';
+      serviceConfig = {
+        User = "errbot";
+        Restart = "on-failure";
+        ExecStart = "${pkgs.errbot}/bin/errbot -c ${mkConfigDir instanceCfg dataDir}/config.py";
+        PermissionsStartOnly = true;
+      };
+    })) cfg.instances;
+  };
+}
diff --git a/nixos/modules/services/misc/etcd.nix b/nixos/modules/services/misc/etcd.nix
new file mode 100644
index 00000000000..3925b7dd163
--- /dev/null
+++ b/nixos/modules/services/misc/etcd.nix
@@ -0,0 +1,205 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.etcd;
+  opt = options.services.etcd;
+
+in {
+
+  options.services.etcd = {
+    enable = mkOption {
+      description = "Whether to enable etcd.";
+      default = false;
+      type = types.bool;
+    };
+
+    name = mkOption {
+      description = "Etcd unique node name.";
+      default = config.networking.hostName;
+      defaultText = literalExpression "config.networking.hostName";
+      type = types.str;
+    };
+
+    advertiseClientUrls = mkOption {
+      description = "Etcd list of this member's client URLs to advertise to the rest of the cluster.";
+      default = cfg.listenClientUrls;
+      defaultText = literalExpression "config.${opt.listenClientUrls}";
+      type = types.listOf types.str;
+    };
+
+    listenClientUrls = mkOption {
+      description = "Etcd list of URLs to listen on for client traffic.";
+      default = ["http://127.0.0.1:2379"];
+      type = types.listOf types.str;
+    };
+
+    listenPeerUrls = mkOption {
+      description = "Etcd list of URLs to listen on for peer traffic.";
+      default = ["http://127.0.0.1:2380"];
+      type = types.listOf types.str;
+    };
+
+    initialAdvertisePeerUrls = mkOption {
+      description = "Etcd list of this member's peer URLs to advertise to rest of the cluster.";
+      default = cfg.listenPeerUrls;
+      defaultText = literalExpression "config.${opt.listenPeerUrls}";
+      type = types.listOf types.str;
+    };
+
+    initialCluster = mkOption {
+      description = "Etcd initial cluster configuration for bootstrapping.";
+      default = ["${cfg.name}=http://127.0.0.1:2380"];
+      defaultText = literalExpression ''["''${config.${opt.name}}=http://127.0.0.1:2380"]'';
+      type = types.listOf types.str;
+    };
+
+    initialClusterState = mkOption {
+      description = "Etcd initial cluster configuration for bootstrapping.";
+      default = "new";
+      type = types.enum ["new" "existing"];
+    };
+
+    initialClusterToken = mkOption {
+      description = "Etcd initial cluster token for etcd cluster during bootstrap.";
+      default = "etcd-cluster";
+      type = types.str;
+    };
+
+    discovery = mkOption {
+      description = "Etcd discovery url";
+      default = "";
+      type = types.str;
+    };
+
+    clientCertAuth = mkOption {
+      description = "Whether to use certs for client authentication";
+      default = false;
+      type = types.bool;
+    };
+
+    trustedCaFile = mkOption {
+      description = "Certificate authority file to use for clients";
+      default = null;
+      type = types.nullOr types.path;
+    };
+
+    certFile = mkOption {
+      description = "Cert file to use for clients";
+      default = null;
+      type = types.nullOr types.path;
+    };
+
+    keyFile = mkOption {
+      description = "Key file to use for clients";
+      default = null;
+      type = types.nullOr types.path;
+    };
+
+    peerCertFile = mkOption {
+      description = "Cert file to use for peer to peer communication";
+      default = cfg.certFile;
+      defaultText = literalExpression "config.${opt.certFile}";
+      type = types.nullOr types.path;
+    };
+
+    peerKeyFile = mkOption {
+      description = "Key file to use for peer to peer communication";
+      default = cfg.keyFile;
+      defaultText = literalExpression "config.${opt.keyFile}";
+      type = types.nullOr types.path;
+    };
+
+    peerTrustedCaFile = mkOption {
+      description = "Certificate authority file to use for peer to peer communication";
+      default = cfg.trustedCaFile;
+      defaultText = literalExpression "config.${opt.trustedCaFile}";
+      type = types.nullOr types.path;
+    };
+
+    peerClientCertAuth = mkOption {
+      description = "Whether to check all incoming peer requests from the cluster for valid client certificates signed by the supplied CA";
+      default = false;
+      type = types.bool;
+    };
+
+    extraConf = mkOption {
+      description = ''
+        Etcd extra configuration. See
+        <link xlink:href='https://github.com/coreos/etcd/blob/master/Documentation/op-guide/configuration.md#configuration-flags' />
+      '';
+      type = types.attrsOf types.str;
+      default = {};
+      example = literalExpression ''
+        {
+          "CORS" = "*";
+          "NAME" = "default-name";
+          "MAX_RESULT_BUFFER" = "1024";
+          "MAX_CLUSTER_SIZE" = "9";
+          "MAX_RETRY_ATTEMPTS" = "3";
+        }
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/etcd";
+      description = "Etcd data directory.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0700 etcd - - -"
+    ];
+
+    systemd.services.etcd = {
+      description = "etcd key-value store";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      environment = (filterAttrs (n: v: v != null) {
+        ETCD_NAME = cfg.name;
+        ETCD_DISCOVERY = cfg.discovery;
+        ETCD_DATA_DIR = cfg.dataDir;
+        ETCD_ADVERTISE_CLIENT_URLS = concatStringsSep "," cfg.advertiseClientUrls;
+        ETCD_LISTEN_CLIENT_URLS = concatStringsSep "," cfg.listenClientUrls;
+        ETCD_LISTEN_PEER_URLS = concatStringsSep "," cfg.listenPeerUrls;
+        ETCD_INITIAL_ADVERTISE_PEER_URLS = concatStringsSep "," cfg.initialAdvertisePeerUrls;
+        ETCD_PEER_TRUSTED_CA_FILE = cfg.peerTrustedCaFile;
+        ETCD_PEER_CERT_FILE = cfg.peerCertFile;
+        ETCD_PEER_KEY_FILE = cfg.peerKeyFile;
+        ETCD_CLIENT_CERT_AUTH = toString cfg.peerClientCertAuth;
+        ETCD_TRUSTED_CA_FILE = cfg.trustedCaFile;
+        ETCD_CERT_FILE = cfg.certFile;
+        ETCD_KEY_FILE = cfg.keyFile;
+      }) // (optionalAttrs (cfg.discovery == ""){
+        ETCD_INITIAL_CLUSTER = concatStringsSep "," cfg.initialCluster;
+        ETCD_INITIAL_CLUSTER_STATE = cfg.initialClusterState;
+        ETCD_INITIAL_CLUSTER_TOKEN = cfg.initialClusterToken;
+      }) // (mapAttrs' (n: v: nameValuePair "ETCD_${n}" v) cfg.extraConf);
+
+      unitConfig = {
+        Documentation = "https://github.com/coreos/etcd";
+      };
+
+      serviceConfig = {
+        Type = "notify";
+        ExecStart = "${pkgs.etcd}/bin/etcd";
+        User = "etcd";
+        LimitNOFILE = 40000;
+      };
+    };
+
+    environment.systemPackages = [ pkgs.etcd ];
+
+    users.users.etcd = {
+      isSystemUser = true;
+      group = "etcd";
+      description = "Etcd daemon user";
+      home = cfg.dataDir;
+    };
+    users.groups.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..dd84ac37b0d
--- /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 = literalExpression ''"''${config.services.etebase-server.dataDir}/static"'';
+                description = "The directory for static files.";
+              };
+              media_root = mkOption {
+                type = types.str;
+                default = "${cfg.dataDir}/media";
+                defaultText = literalExpression ''"''${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 = literalExpression ''"''${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/ethminer.nix b/nixos/modules/services/misc/ethminer.nix
new file mode 100644
index 00000000000..95afb0460fb
--- /dev/null
+++ b/nixos/modules/services/misc/ethminer.nix
@@ -0,0 +1,117 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.ethminer;
+  poolUrl = escapeShellArg "stratum1+tcp://${cfg.wallet}@${cfg.pool}:${toString cfg.stratumPort}/${cfg.rig}/${cfg.registerMail}";
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.ethminer = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable ethminer ether mining.";
+      };
+
+      recheckInterval = mkOption {
+        type = types.int;
+        default = 2000;
+        description = "Interval in milliseconds between farm rechecks.";
+      };
+
+      toolkit = mkOption {
+        type = types.enum [ "cuda" "opencl" ];
+        default = "cuda";
+        description = "Cuda or opencl toolkit.";
+      };
+
+      apiPort = mkOption {
+        type = types.int;
+        default = -3333;
+        description = "Ethminer api port. minus sign puts api in read-only mode.";
+      };
+
+      wallet = mkOption {
+        type = types.str;
+        example = "0x0123456789abcdef0123456789abcdef01234567";
+        description = "Ethereum wallet address.";
+      };
+
+      pool = mkOption {
+        type = types.str;
+        example = "eth-us-east1.nanopool.org";
+        description = "Mining pool address.";
+      };
+
+      stratumPort = mkOption {
+        type = types.port;
+        default = 9999;
+        description = "Stratum protocol tcp port.";
+      };
+
+      rig = mkOption {
+        type = types.str;
+        default = "mining-rig-name";
+        description = "Mining rig name.";
+      };
+
+      registerMail = mkOption {
+        type = types.str;
+        example = "email%40example.org";
+        description = "Url encoded email address to register with pool.";
+      };
+
+      maxPower = mkOption {
+        type = types.int;
+        default = 113;
+        description = "Miner max watt usage.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.ethminer = {
+      path = [ pkgs.cudatoolkit ];
+      description = "ethminer ethereum mining service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStartPre = "${pkgs.ethminer}/bin/.ethminer-wrapped --list-devices";
+        ExecStartPost = optional (cfg.toolkit == "cuda") "+${getBin config.boot.kernelPackages.nvidia_x11}/bin/nvidia-smi -pl ${toString cfg.maxPower}";
+        Restart = "always";
+      };
+
+      environment = {
+        LD_LIBRARY_PATH = "${config.boot.kernelPackages.nvidia_x11}/lib";
+      };
+
+      script = ''
+        ${pkgs.ethminer}/bin/.ethminer-wrapped \
+          --farm-recheck ${toString cfg.recheckInterval} \
+          --report-hashrate \
+          --${cfg.toolkit} \
+          --api-port ${toString cfg.apiPort} \
+          --pool ${poolUrl}
+      '';
+
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/exhibitor.nix b/nixos/modules/services/misc/exhibitor.nix
new file mode 100644
index 00000000000..4c935efbd84
--- /dev/null
+++ b/nixos/modules/services/misc/exhibitor.nix
@@ -0,0 +1,422 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.exhibitor;
+  opt = options.services.exhibitor;
+  exhibitorConfig = ''
+    zookeeper-install-directory=${cfg.baseDir}/zookeeper
+    zookeeper-data-directory=${cfg.zkDataDir}
+    zookeeper-log-directory=${cfg.zkLogDir}
+    zoo-cfg-extra=${cfg.zkExtraCfg}
+    client-port=${toString cfg.zkClientPort}
+    connect-port=${toString cfg.zkConnectPort}
+    election-port=${toString cfg.zkElectionPort}
+    cleanup-period-ms=${toString cfg.zkCleanupPeriod}
+    servers-spec=${concatStringsSep "," cfg.zkServersSpec}
+    auto-manage-instances=${toString cfg.autoManageInstances}
+    ${cfg.extraConf}
+  '';
+  # NB: toString rather than lib.boolToString on cfg.autoManageInstances is intended.
+  # Exhibitor tests if it's an integer not equal to 0, so the empty string (toString false)
+  # will operate in the same fashion as a 0.
+  configDir = pkgs.writeTextDir "exhibitor.properties" exhibitorConfig;
+  cliOptionsCommon = {
+    configtype = cfg.configType;
+    defaultconfig = "${configDir}/exhibitor.properties";
+    port = toString cfg.port;
+    hostname = cfg.hostname;
+    headingtext = if (cfg.headingText != null) then (lib.escapeShellArg cfg.headingText) else null;
+    nodemodification = lib.boolToString cfg.nodeModification;
+    configcheckms = toString cfg.configCheckMs;
+    jquerystyle = cfg.jqueryStyle;
+    loglines = toString cfg.logLines;
+    servo = lib.boolToString cfg.servo;
+    timeout = toString cfg.timeout;
+  };
+  s3CommonOptions = { s3region = cfg.s3Region; s3credentials = cfg.s3Credentials; };
+  cliOptionsPerConfig = {
+    s3 = {
+      s3config = "${cfg.s3Config.bucketName}:${cfg.s3Config.objectKey}";
+      s3configprefix = cfg.s3Config.configPrefix;
+    };
+    zookeeper = {
+      zkconfigconnect = concatStringsSep "," cfg.zkConfigConnect;
+      zkconfigexhibitorpath = cfg.zkConfigExhibitorPath;
+      zkconfigpollms = toString cfg.zkConfigPollMs;
+      zkconfigretry = "${toString cfg.zkConfigRetry.sleepMs}:${toString cfg.zkConfigRetry.retryQuantity}";
+      zkconfigzpath = cfg.zkConfigZPath;
+      zkconfigexhibitorport = toString cfg.zkConfigExhibitorPort; # NB: This might be null
+    };
+    file = {
+      fsconfigdir = cfg.fsConfigDir;
+      fsconfiglockprefix = cfg.fsConfigLockPrefix;
+      fsConfigName = fsConfigName;
+    };
+    none = {
+      noneconfigdir = configDir;
+    };
+  };
+  cliOptions = concatStringsSep " " (mapAttrsToList (k: v: "--${k} ${v}") (filterAttrs (k: v: v != null && v != "") (cliOptionsCommon //
+               cliOptionsPerConfig.${cfg.configType} //
+               s3CommonOptions //
+               optionalAttrs cfg.s3Backup { s3backup = "true"; } //
+               optionalAttrs cfg.fileSystemBackup { filesystembackup = "true"; }
+               )));
+in
+{
+  options = {
+    services.exhibitor = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "
+          Whether to enable the exhibitor server.
+        ";
+      };
+      # See https://github.com/soabase/exhibitor/wiki/Running-Exhibitor for what these mean
+      # General options for any type of config
+      port = mkOption {
+        type = types.int;
+        default = 8080;
+        description = ''
+          The port for exhibitor to listen on and communicate with other exhibitors.
+        '';
+      };
+      baseDir = mkOption {
+        type = types.str;
+        default = "/var/exhibitor";
+        description = ''
+          Baseline directory for exhibitor runtime config.
+        '';
+      };
+      configType = mkOption {
+        type = types.enum [ "file" "s3" "zookeeper" "none" ];
+        description = ''
+          Which configuration type you want to use. Additional config will be
+          required depending on which type you are using.
+        '';
+      };
+      hostname = mkOption {
+        type = types.nullOr types.str;
+        description = ''
+          Hostname to use and advertise
+        '';
+        default = null;
+      };
+      nodeModification = mkOption {
+        type = types.bool;
+        description = ''
+          Whether the Explorer UI will allow nodes to be modified (use with caution).
+        '';
+        default = true;
+      };
+      configCheckMs = mkOption {
+        type = types.int;
+        description = ''
+          Period (ms) to check for shared config updates.
+        '';
+        default = 30000;
+      };
+      headingText = mkOption {
+        type = types.nullOr types.str;
+        description = ''
+          Extra text to display in UI header
+        '';
+        default = null;
+      };
+      jqueryStyle = mkOption {
+        type = types.enum [ "red" "black" "custom" ];
+        description = ''
+          Styling used for the JQuery-based UI.
+        '';
+        default = "red";
+      };
+      logLines = mkOption {
+        type = types.int;
+        description = ''
+        Max lines of logging to keep in memory for display.
+        '';
+        default = 1000;
+      };
+      servo = mkOption {
+        type = types.bool;
+        description = ''
+          ZooKeeper will be queried once a minute for its state via the 'mntr' four
+          letter word (this requires ZooKeeper 3.4.x+). Servo will be used to publish
+          this data via JMX.
+        '';
+        default = false;
+      };
+      timeout = mkOption {
+        type = types.int;
+        description = ''
+          Connection timeout (ms) for ZK connections.
+        '';
+        default = 30000;
+      };
+      autoManageInstances = mkOption {
+        type = types.bool;
+        description = ''
+          Automatically manage ZooKeeper instances in the ensemble
+        '';
+        default = false;
+      };
+      zkDataDir = mkOption {
+        type = types.str;
+        default = "${cfg.baseDir}/zkData";
+        defaultText = literalExpression ''"''${config.${opt.baseDir}}/zkData"'';
+        description = ''
+          The Zookeeper data directory
+        '';
+      };
+      zkLogDir = mkOption {
+        type = types.path;
+        default = "${cfg.baseDir}/zkLogs";
+        defaultText = literalExpression ''"''${config.${opt.baseDir}}/zkLogs"'';
+        description = ''
+          The Zookeeper logs directory
+        '';
+      };
+      extraConf = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Extra Exhibitor configuration to put in the ZooKeeper config file.
+        '';
+      };
+      zkExtraCfg = mkOption {
+        type = types.str;
+        default = "initLimit=5&syncLimit=2&tickTime=2000";
+        description = ''
+          Extra options to pass into Zookeeper
+        '';
+      };
+      zkClientPort = mkOption {
+        type = types.int;
+        default = 2181;
+        description = ''
+          Zookeeper client port
+        '';
+      };
+      zkConnectPort = mkOption {
+        type = types.int;
+        default = 2888;
+        description = ''
+          The port to use for followers to talk to each other.
+        '';
+      };
+      zkElectionPort = mkOption {
+        type = types.int;
+        default = 3888;
+        description = ''
+          The port for Zookeepers to use for leader election.
+        '';
+      };
+      zkCleanupPeriod = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          How often (in milliseconds) to run the Zookeeper log cleanup task.
+        '';
+      };
+      zkServersSpec = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Zookeeper server spec for all servers in the ensemble.
+        '';
+        example = [ "S:1:zk1.example.com" "S:2:zk2.example.com" "S:3:zk3.example.com" "O:4:zk-observer.example.com" ];
+      };
+
+      # Backup options
+      s3Backup = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable backups to S3
+        '';
+      };
+      fileSystemBackup = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enables file system backup of ZooKeeper log files
+        '';
+      };
+
+      # Options for using zookeeper configType
+      zkConfigConnect = mkOption {
+        type = types.listOf types.str;
+        description = ''
+          The initial connection string for ZooKeeper shared config storage
+        '';
+        example = ["host1:2181" "host2:2181"];
+      };
+      zkConfigExhibitorPath = mkOption {
+        type = types.str;
+        description = ''
+          If the ZooKeeper shared config is also running Exhibitor, the URI path for the REST call
+        '';
+        default = "/";
+      };
+      zkConfigExhibitorPort = mkOption {
+        type = types.nullOr types.int;
+        description = ''
+          If the ZooKeeper shared config is also running Exhibitor, the port that
+          Exhibitor is listening on. IMPORTANT: if this value is not set it implies
+          that Exhibitor is not being used on the ZooKeeper shared config.
+        '';
+      };
+      zkConfigPollMs = mkOption {
+        type = types.int;
+        description = ''
+          The period in ms to check for changes in the config ensemble
+        '';
+        default = 10000;
+      };
+      zkConfigRetry = {
+        sleepMs = mkOption {
+          type = types.int;
+          default = 1000;
+          description = ''
+            Retry sleep time connecting to the ZooKeeper config
+          '';
+        };
+        retryQuantity = mkOption {
+          type = types.int;
+          default = 3;
+          description = ''
+            Retries connecting to the ZooKeeper config
+          '';
+        };
+      };
+      zkConfigZPath = mkOption {
+        type = types.str;
+        description = ''
+          The base ZPath that Exhibitor should use
+        '';
+        example = "/exhibitor/config";
+      };
+
+      # Config options for s3 configType
+      s3Config = {
+        bucketName = mkOption {
+          type = types.str;
+          description = ''
+            Bucket name to store config
+          '';
+        };
+        objectKey = mkOption {
+          type = types.str;
+          description = ''
+            S3 key name to store the config
+          '';
+        };
+        configPrefix = mkOption {
+          type = types.str;
+          description = ''
+            When using AWS S3 shared config files, the prefix to use for values such as locks
+          '';
+          default = "exhibitor-";
+        };
+      };
+
+      # The next two are used for either s3backup or s3 configType
+      s3Credentials = mkOption {
+        type = types.nullOr types.path;
+        description = ''
+          Optional credentials to use for s3backup or s3config. Argument is the path
+          to an AWS credential properties file with two properties:
+          com.netflix.exhibitor.s3.access-key-id and com.netflix.exhibitor.s3.access-secret-key
+        '';
+        default = null;
+      };
+      s3Region = mkOption {
+        type = types.nullOr types.str;
+        description = ''
+          Optional region for S3 calls
+        '';
+        default = null;
+      };
+
+      # Config options for file config type
+      fsConfigDir = mkOption {
+        type = types.path;
+        description = ''
+          Directory to store Exhibitor properties (cannot be used with s3config).
+          Exhibitor uses file system locks so you can specify a shared location
+          so as to enable complete ensemble management.
+        '';
+      };
+      fsConfigLockPrefix = mkOption {
+        type = types.str;
+        description = ''
+          A prefix for a locking mechanism used in conjunction with fsconfigdir
+        '';
+        default = "exhibitor-lock-";
+      };
+      fsConfigName = mkOption {
+        type = types.str;
+        description = ''
+          The name of the file to store config in
+        '';
+        default = "exhibitor.properties";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.exhibitor = {
+      description = "Exhibitor Daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      environment = {
+        ZOO_LOG_DIR = cfg.baseDir;
+      };
+      serviceConfig = {
+        /***
+          Exhibitor is a bit un-nixy. It wants to present to you a user interface in order to
+          mutate the configuration of both itself and ZooKeeper, and to coordinate changes
+          among the members of the Zookeeper ensemble. I'm going for a different approach here,
+          which is to manage all the configuration via nix and have it write out the configuration
+          files that exhibitor will use, and to reduce the amount of inter-exhibitor orchestration.
+        ***/
+        ExecStart = ''
+          ${pkgs.exhibitor}/bin/startExhibitor.sh ${cliOptions}
+        '';
+        User = "zookeeper";
+        PermissionsStartOnly = true;
+      };
+      # This is a bit wonky, but the reason for this is that Exhibitor tries to write to
+      # ${cfg.baseDir}/zookeeper/bin/../conf/zoo.cfg
+      # I want everything but the conf directory to be in the immutable nix store, and I want defaults
+      # from the nix store
+      # If I symlink the bin directory in, then bin/../ will resolve to the parent of the symlink in the
+      # immutable nix store. Bind mounting a writable conf over the existing conf might work, but it gets very
+      # messy with trying to copy the existing out into a mutable store.
+      # Another option is to try to patch upstream exhibitor, but the current package just pulls down the
+      # prebuild JARs off of Maven, rather than building them ourselves, as Maven support in Nix isn't
+      # very mature. So, it seems like a reasonable compromise is to just copy out of the immutable store
+      # just before starting the service, so we're running binaries from the immutable store, but we work around
+      # Exhibitor's desire to mutate its current installation.
+      preStart = ''
+        mkdir -m 0700 -p ${cfg.baseDir}/zookeeper
+        # Not doing a chown -R to keep the base ZK files owned by root
+        chown zookeeper ${cfg.baseDir} ${cfg.baseDir}/zookeeper
+        cp -Rf ${pkgs.zookeeper}/* ${cfg.baseDir}/zookeeper
+        chown -R zookeeper ${cfg.baseDir}/zookeeper/conf
+        chmod -R u+w ${cfg.baseDir}/zookeeper/conf
+        replace_what=$(echo ${pkgs.zookeeper} | sed 's/[\/&]/\\&/g')
+        replace_with=$(echo ${cfg.baseDir}/zookeeper | sed 's/[\/&]/\\&/g')
+        sed -i 's/'"$replace_what"'/'"$replace_with"'/g' ${cfg.baseDir}/zookeeper/bin/zk*.sh
+      '';
+    };
+    users.users.zookeeper = {
+      uid = config.ids.uids.zookeeper;
+      description = "Zookeeper daemon user";
+      home = cfg.baseDir;
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/felix.nix b/nixos/modules/services/misc/felix.nix
new file mode 100644
index 00000000000..0283de128af
--- /dev/null
+++ b/nixos/modules/services/misc/felix.nix
@@ -0,0 +1,104 @@
+# Felix server
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.felix;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.felix = {
+
+      enable = mkEnableOption "the Apache Felix OSGi service";
+
+      bundles = mkOption {
+        type = types.listOf types.package;
+        default = [ pkgs.felix_remoteshell ];
+        defaultText = literalExpression "[ pkgs.felix_remoteshell ]";
+        description = "List of bundles that should be activated on startup";
+      };
+
+      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.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    users.groups.osgi.gid = config.ids.gids.osgi;
+
+    users.users.osgi =
+      { uid = config.ids.uids.osgi;
+        description = "OSGi user";
+        home = "/homeless-shelter";
+      };
+
+    systemd.services.felix = {
+      description = "Felix server";
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        # Initialise felix instance on first startup
+        if [ ! -d /var/felix ]
+        then
+          # Symlink system files
+
+          mkdir -p /var/felix
+          chown ${cfg.user}:${cfg.group} /var/felix
+
+          for i in ${pkgs.felix}/*
+          do
+              if [ "$i" != "${pkgs.felix}/bundle" ]
+              then
+                  ln -sfn $i /var/felix/$(basename $i)
+              fi
+          done
+
+          # Symlink bundles
+          mkdir -p /var/felix/bundle
+          chown ${cfg.user}:${cfg.group} /var/felix/bundle
+
+          for i in ${pkgs.felix}/bundle/* ${toString cfg.bundles}
+          do
+              if [ -f $i ]
+              then
+                  ln -sfn $i /var/felix/bundle/$(basename $i)
+              elif [ -d $i ]
+              then
+                  for j in $i/bundle/*
+              do
+                  ln -sfn $j /var/felix/bundle/$(basename $j)
+              done
+              fi
+          done
+        fi
+      '';
+
+      script = ''
+        cd /var/felix
+        ${pkgs.su}/bin/su -s ${pkgs.bash}/bin/sh ${cfg.user} -c '${pkgs.jre}/bin/java -jar bin/felix.jar'
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/freeswitch.nix b/nixos/modules/services/misc/freeswitch.nix
new file mode 100644
index 00000000000..472b0b73ff6
--- /dev/null
+++ b/nixos/modules/services/misc/freeswitch.nix
@@ -0,0 +1,104 @@
+{ config, lib, pkgs, ...}:
+with lib;
+let
+  cfg = config.services.freeswitch;
+  pkg = cfg.package;
+  configDirectory = pkgs.runCommand "freeswitch-config-d" { } ''
+    mkdir -p $out
+    cp -rT ${cfg.configTemplate} $out
+    chmod -R +w $out
+    ${concatStringsSep "\n" (mapAttrsToList (fileName: filePath: ''
+      mkdir -p $out/$(dirname ${fileName})
+      cp ${filePath} $out/${fileName}
+    '') cfg.configDir)}
+  '';
+  configPath = if cfg.enableReload
+    then "/etc/freeswitch"
+    else configDirectory;
+in {
+  options = {
+    services.freeswitch = {
+      enable = mkEnableOption "FreeSWITCH";
+      enableReload = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Issue the <literal>reloadxml</literal> command to FreeSWITCH when configuration directory changes (instead of restart).
+          See <link xlink:href="https://freeswitch.org/confluence/display/FREESWITCH/Reloading">FreeSWITCH documentation</link> for more info.
+          The configuration directory is exposed at <filename>/etc/freeswitch</filename>.
+          See also <literal>systemd.services.*.restartIfChanged</literal>.
+        '';
+      };
+      configTemplate = mkOption {
+        type = types.path;
+        default = "${config.services.freeswitch.package}/share/freeswitch/conf/vanilla";
+        defaultText = literalExpression ''"''${config.services.freeswitch.package}/share/freeswitch/conf/vanilla"'';
+        example = literalExpression ''"''${config.services.freeswitch.package}/share/freeswitch/conf/minimal"'';
+        description = ''
+          Configuration template to use.
+          See available templates in <link xlink:href="https://github.com/signalwire/freeswitch/tree/master/conf">FreeSWITCH repository</link>.
+          You can also set your own configuration directory.
+        '';
+      };
+      configDir = mkOption {
+        type = with types; attrsOf path;
+        default = { };
+        example = literalExpression ''
+          {
+            "freeswitch.xml" = ./freeswitch.xml;
+            "dialplan/default.xml" = pkgs.writeText "dialplan-default.xml" '''
+              [xml lines]
+            ''';
+          }
+        '';
+        description = ''
+          Override file in FreeSWITCH config template directory.
+          Each top-level attribute denotes a file path in the configuration directory, its value is the file path.
+          See <link xlink:href="https://freeswitch.org/confluence/display/FREESWITCH/Default+Configuration">FreeSWITCH documentation</link> for more info.
+          Also check available templates in <link xlink:href="https://github.com/signalwire/freeswitch/tree/master/conf">FreeSWITCH repository</link>.
+        '';
+      };
+      package = mkOption {
+        type = types.package;
+        default = pkgs.freeswitch;
+        defaultText = literalExpression "pkgs.freeswitch";
+        description = ''
+          FreeSWITCH package.
+        '';
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+    environment.etc.freeswitch = mkIf cfg.enableReload {
+      source = configDirectory;
+    };
+    systemd.services.freeswitch-config-reload = mkIf cfg.enableReload {
+      before = [ "freeswitch.service" ];
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ configDirectory ];
+      serviceConfig = {
+        ExecStart = "/run/current-system/systemd/bin/systemctl try-reload-or-restart freeswitch.service";
+        RemainAfterExit = true;
+        Type = "oneshot";
+      };
+    };
+    systemd.services.freeswitch = {
+      description = "Free and open-source application server for real-time communication";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "freeswitch";
+        ExecStart = "${pkg}/bin/freeswitch -nf \\
+          -mod ${pkg}/lib/freeswitch/mod \\
+          -conf ${configPath} \\
+          -base /var/lib/freeswitch";
+        ExecReload = "${pkg}/bin/fs_cli -x reloadxml";
+        Restart = "on-failure";
+        RestartSec = "5s";
+        CPUSchedulingPolicy = "fifo";
+      };
+    };
+    environment.systemPackages = [ pkg ];
+  };
+}
diff --git a/nixos/modules/services/misc/fstrim.nix b/nixos/modules/services/misc/fstrim.nix
new file mode 100644
index 00000000000..a9fc04b46f0
--- /dev/null
+++ b/nixos/modules/services/misc/fstrim.nix
@@ -0,0 +1,46 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.fstrim;
+
+in {
+
+  options = {
+
+    services.fstrim = {
+      enable = mkEnableOption "periodic SSD TRIM of mounted partitions in background";
+
+      interval = mkOption {
+        type = types.str;
+        default = "weekly";
+        description = ''
+          How often we run fstrim. For most desktop and server systems
+          a sufficient trimming frequency is once a week.
+
+          The format is described in
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>.
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.packages = [ pkgs.util-linux ];
+
+    systemd.timers.fstrim = {
+      timerConfig = {
+        OnCalendar = cfg.interval;
+      };
+      wantedBy = [ "timers.target" ];
+    };
+
+  };
+
+  meta.maintainers = with maintainers; [ ];
+}
diff --git a/nixos/modules/services/misc/gammu-smsd.nix b/nixos/modules/services/misc/gammu-smsd.nix
new file mode 100644
index 00000000000..d4bb58d81dd
--- /dev/null
+++ b/nixos/modules/services/misc/gammu-smsd.nix
@@ -0,0 +1,253 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+let
+  cfg = config.services.gammu-smsd;
+
+  configFile = pkgs.writeText "gammu-smsd.conf" ''
+    [gammu]
+    Device = ${cfg.device.path}
+    Connection = ${cfg.device.connection}
+    SynchronizeTime = ${if cfg.device.synchronizeTime then "yes" else "no"}
+    LogFormat = ${cfg.log.format}
+    ${if (cfg.device.pin != null) then "PIN = ${cfg.device.pin}" else ""}
+    ${cfg.extraConfig.gammu}
+
+
+    [smsd]
+    LogFile = ${cfg.log.file}
+    Service = ${cfg.backend.service}
+
+    ${optionalString (cfg.backend.service == "files") ''
+      InboxPath = ${cfg.backend.files.inboxPath}
+      OutboxPath = ${cfg.backend.files.outboxPath}
+      SentSMSPath = ${cfg.backend.files.sentSMSPath}
+      ErrorSMSPath = ${cfg.backend.files.errorSMSPath}
+    ''}
+
+    ${optionalString (cfg.backend.service == "sql" && cfg.backend.sql.driver == "sqlite") ''
+      Driver = ${cfg.backend.sql.driver}
+      DBDir = ${cfg.backend.sql.database}
+    ''}
+
+    ${optionalString (cfg.backend.service == "sql" && cfg.backend.sql.driver == "native_pgsql") (
+      with cfg.backend; ''
+        Driver = ${sql.driver}
+        ${if (sql.database!= null) then "Database = ${sql.database}" else ""}
+        ${if (sql.host != null) then "Host = ${sql.host}" else ""}
+        ${if (sql.user != null) then "User = ${sql.user}" else ""}
+        ${if (sql.password != null) then "Password = ${sql.password}" else ""}
+      '')}
+
+    ${cfg.extraConfig.smsd}
+  '';
+
+  initDBDir = "share/doc/gammu/examples/sql";
+
+  gammuPackage = with cfg.backend; (pkgs.gammu.override {
+    dbiSupport = (service == "sql" && sql.driver == "sqlite");
+    postgresSupport = (service == "sql" && sql.driver == "native_pgsql");
+  });
+
+in {
+  options = {
+    services.gammu-smsd = {
+
+      enable = mkEnableOption "gammu-smsd daemon";
+
+      user = mkOption {
+        type = types.str;
+        default = "smsd";
+        description = "User that has access to the device";
+      };
+
+      device = {
+        path = mkOption {
+          type = types.path;
+          description = "Device node or address of the phone";
+          example = "/dev/ttyUSB2";
+        };
+
+        group = mkOption {
+          type = types.str;
+          default = "root";
+          description = "Owner group of the device";
+          example = "dialout";
+        };
+
+        connection = mkOption {
+          type = types.str;
+          default = "at";
+          description = "Protocol which will be used to talk to the phone";
+        };
+
+        synchronizeTime = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Whether to set time from computer to the phone during starting connection";
+        };
+
+        pin = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = "PIN code for the simcard";
+        };
+      };
+
+
+      log = {
+        file = mkOption {
+          type = types.str;
+          default = "syslog";
+          description = "Path to file where information about communication will be stored";
+        };
+
+        format = mkOption {
+          type = types.enum [ "nothing" "text" "textall" "textalldate" "errors" "errorsdate" "binary" ];
+          default = "errors";
+          description = "Determines what will be logged to the LogFile";
+        };
+      };
+
+
+      extraConfig = {
+        gammu = mkOption {
+          type = types.lines;
+          default = "";
+          description = "Extra config lines to be added into [gammu] section";
+        };
+
+
+        smsd = mkOption {
+          type = types.lines;
+          default = "";
+          description = "Extra config lines to be added into [smsd] section";
+        };
+      };
+
+
+      backend = {
+        service = mkOption {
+          type = types.enum [ "null" "files" "sql" ];
+          default = "null";
+          description = "Service to use to store sms data.";
+        };
+
+        files = {
+          inboxPath = mkOption {
+            type = types.path;
+            default = "/var/spool/sms/inbox/";
+            description = "Where the received SMSes are stored";
+          };
+
+          outboxPath = mkOption {
+            type = types.path;
+            default = "/var/spool/sms/outbox/";
+            description = "Where SMSes to be sent should be placed";
+          };
+
+          sentSMSPath = mkOption {
+            type = types.path;
+            default = "/var/spool/sms/sent/";
+            description = "Where the transmitted SMSes are placed";
+          };
+
+          errorSMSPath = mkOption {
+            type = types.path;
+            default = "/var/spool/sms/error/";
+            description = "Where SMSes with error in transmission is placed";
+          };
+        };
+
+        sql = {
+          driver = mkOption {
+            type = types.enum [ "native_mysql" "native_pgsql" "odbc" "dbi" ];
+            description = "DB driver to use";
+          };
+
+          sqlDialect = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            description = "SQL dialect to use (odbc driver only)";
+          };
+
+          database = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            description = "Database name to store sms data";
+          };
+
+          host = mkOption {
+            type = types.str;
+            default = "localhost";
+            description = "Database server address";
+          };
+
+          user = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            description = "User name used for connection to the database";
+          };
+
+          password = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            description = "User password used for connetion to the database";
+          };
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.${cfg.user} = {
+      description = "gammu-smsd user";
+      isSystemUser = true;
+      group = cfg.device.group;
+    };
+
+    environment.systemPackages = with cfg.backend; [ gammuPackage ]
+    ++ optionals (service == "sql" && sql.driver == "sqlite")  [ pkgs.sqlite ];
+
+    systemd.services.gammu-smsd = {
+      description = "gammu-smsd daemon";
+
+      wantedBy = [ "multi-user.target" ];
+
+      wants = with cfg.backend; [ ]
+      ++ optionals (service == "sql" && sql.driver == "native_pgsql") [ "postgresql.service" ];
+
+      preStart = with cfg.backend;
+
+        optionalString (service == "files") (with files; ''
+          mkdir -m 755 -p ${inboxPath} ${outboxPath} ${sentSMSPath} ${errorSMSPath}
+          chown ${cfg.user} -R ${inboxPath}
+          chown ${cfg.user} -R ${outboxPath}
+          chown ${cfg.user} -R ${sentSMSPath}
+          chown ${cfg.user} -R ${errorSMSPath}
+        '')
+      + optionalString (service == "sql" && sql.driver == "sqlite") ''
+         cat "${gammuPackage}/${initDBDir}/sqlite.sql" \
+         | ${pkgs.sqlite.bin}/bin/sqlite3 ${sql.database}
+        ''
+      + (let execPsql = extraArgs: concatStringsSep " " [
+          (optionalString (sql.password != null) "PGPASSWORD=${sql.password}")
+          "${config.services.postgresql.package}/bin/psql"
+          (optionalString (sql.host != null) "-h ${sql.host}")
+          (optionalString (sql.user != null) "-U ${sql.user}")
+          "$extraArgs"
+          "${sql.database}"
+        ]; in optionalString (service == "sql" && sql.driver == "native_pgsql") ''
+         echo '\i '"${gammuPackage}/${initDBDir}/pgsql.sql" | ${execPsql ""}
+       '');
+
+      serviceConfig = {
+        User = "${cfg.user}";
+        Group = "${cfg.device.group}";
+        PermissionsStartOnly = true;
+        ExecStart = "${gammuPackage}/bin/gammu-smsd -c ${configFile}";
+      };
+
+    };
+  };
+}
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
new file mode 100644
index 00000000000..bc7bb663ee0
--- /dev/null
+++ b/nixos/modules/services/misc/gitea.nix
@@ -0,0 +1,663 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gitea;
+  opt = options.services.gitea;
+  gitea = cfg.package;
+  pg = config.services.postgresql;
+  useMysql = cfg.database.type == "mysql";
+  usePostgresql = cfg.database.type == "postgres";
+  useSqlite = cfg.database.type == "sqlite3";
+  configFile = pkgs.writeText "app.ini" ''
+    APP_NAME = ${cfg.appName}
+    RUN_USER = ${cfg.user}
+    RUN_MODE = prod
+
+    ${generators.toINI {} cfg.settings}
+
+    ${optionalString (cfg.extraConfig != null) cfg.extraConfig}
+  '';
+in
+
+{
+  options = {
+    services.gitea = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Enable Gitea Service.";
+      };
+
+      package = mkOption {
+        default = pkgs.gitea;
+        type = types.package;
+        defaultText = literalExpression "pkgs.gitea";
+        description = "gitea derivation to use";
+      };
+
+      useWizard = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Do not generate a configuration and use gitea' installation wizard instead. The first registered user will be administrator.";
+      };
+
+      stateDir = mkOption {
+        default = "/var/lib/gitea";
+        type = types.str;
+        description = "gitea data directory.";
+      };
+
+      log = {
+        rootPath = mkOption {
+          default = "${cfg.stateDir}/log";
+          defaultText = literalExpression ''"''${config.${opt.stateDir}}/log"'';
+          type = types.str;
+          description = "Root path for log files.";
+        };
+        level = mkOption {
+          default = "Info";
+          type = types.enum [ "Trace" "Debug" "Info" "Warn" "Error" "Critical" ];
+          description = "General log level.";
+        };
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "gitea";
+        description = "User account under which gitea runs.";
+      };
+
+      database = {
+        type = mkOption {
+          type = types.enum [ "sqlite3" "mysql" "postgres" ];
+          example = "mysql";
+          default = "sqlite3";
+          description = "Database engine to use.";
+        };
+
+        host = mkOption {
+          type = types.str;
+          default = "127.0.0.1";
+          description = "Database host address.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = (if !usePostgresql then 3306 else pg.port);
+          defaultText = literalExpression ''
+            if config.${opt.database.type} != "postgresql"
+            then 3306
+            else config.${options.services.postgresql.port}
+          '';
+          description = "Database host port.";
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "gitea";
+          description = "Database name.";
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "gitea";
+          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;
+          example = "/run/keys/gitea-dbpassword";
+          description = ''
+            A file containing the password corresponding to
+            <option>database.user</option>.
+          '';
+        };
+
+        socket = mkOption {
+          type = types.nullOr types.path;
+          default = if (cfg.database.createDatabase && usePostgresql) then "/run/postgresql" else if (cfg.database.createDatabase && useMysql) then "/run/mysqld/mysqld.sock" else null;
+          defaultText = literalExpression "null";
+          example = "/run/mysqld/mysqld.sock";
+          description = "Path to the unix socket file to use for authentication.";
+        };
+
+        path = mkOption {
+          type = types.str;
+          default = "${cfg.stateDir}/data/gitea.db";
+          defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/gitea.db"'';
+          description = "Path to the sqlite3 database file.";
+        };
+
+        createDatabase = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Whether to create a local database automatically.";
+        };
+      };
+
+      dump = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Enable a timer that runs gitea dump to generate backup-files of the
+            current gitea database and repositories.
+          '';
+        };
+
+        interval = mkOption {
+          type = types.str;
+          default = "04:31";
+          example = "hourly";
+          description = ''
+            Run a gitea dump at this interval. Runs by default at 04:31 every day.
+
+            The format is described in
+            <citerefentry><refentrytitle>systemd.time</refentrytitle>
+            <manvolnum>7</manvolnum></citerefentry>.
+          '';
+        };
+
+        backupDir = mkOption {
+          type = types.str;
+          default = "${cfg.stateDir}/dump";
+          defaultText = literalExpression ''"''${config.${opt.stateDir}}/dump"'';
+          description = "Path to the dump files.";
+        };
+
+        type = mkOption {
+          type = types.enum [ "zip" "rar" "tar" "sz" "tar.gz" "tar.xz" "tar.bz2" "tar.br" "tar.lz4" ];
+          default = "zip";
+          description = "Archive format used to store the dump file.";
+        };
+
+        file = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = "Filename to be used for the dump. If `null` a default name is choosen by gitea.";
+          example = "gitea-dump";
+        };
+      };
+
+      ssh = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Enable external SSH feature.";
+        };
+
+        clonePort = mkOption {
+          type = types.int;
+          default = 22;
+          example = 2222;
+          description = ''
+            SSH port displayed in clone URL.
+            The option is required to configure a service when the external visible port
+            differs from the local listening port i.e. if port forwarding is used.
+          '';
+        };
+      };
+
+      lfs = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Enables git-lfs support.";
+        };
+
+        contentDir = mkOption {
+          type = types.str;
+          default = "${cfg.stateDir}/data/lfs";
+          defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/lfs"'';
+          description = "Where to store LFS files.";
+        };
+      };
+
+      appName = mkOption {
+        type = types.str;
+        default = "gitea: Gitea Service";
+        description = "Application name.";
+      };
+
+      repositoryRoot = mkOption {
+        type = types.str;
+        default = "${cfg.stateDir}/repositories";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/repositories"'';
+        description = "Path to the git repositories.";
+      };
+
+      domain = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Domain name of your server.";
+      };
+
+      rootUrl = mkOption {
+        type = types.str;
+        default = "http://localhost:3000/";
+        description = "Full public URL of gitea server.";
+      };
+
+      httpAddress = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = "HTTP listen address.";
+      };
+
+      httpPort = mkOption {
+        type = types.int;
+        default = 3000;
+        description = "HTTP listen port.";
+      };
+
+      enableUnixSocket = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Configure Gitea to listen on a unix socket instead of the default TCP port.";
+      };
+
+      cookieSecure = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Marks session cookies as "secure" as a hint for browsers to only send
+          them via HTTPS. This option is recommend, if gitea is being served over HTTPS.
+        '';
+      };
+
+      staticRootPath = mkOption {
+        type = types.either types.str types.path;
+        default = gitea.data;
+        defaultText = literalExpression "package.data";
+        example = "/var/lib/gitea/data";
+        description = "Upper level of template and static files path.";
+      };
+
+      mailerPasswordFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/var/lib/secrets/gitea/mailpw";
+        description = "Path to a file containing the SMTP password.";
+      };
+
+      disableRegistration = mkEnableOption "the registration lock" // {
+        description = ''
+          By default any user can create an account on this <literal>gitea</literal> instance.
+          This can be disabled by using this option.
+
+          <emphasis>Note:</emphasis> please keep in mind that this should be added after the initial
+          deploy unless <link linkend="opt-services.gitea.useWizard">services.gitea.useWizard</link>
+          is <literal>true</literal> as the first registered user will be the administrator if
+          no install wizard is used.
+        '';
+      };
+
+      settings = mkOption {
+        type = with types; attrsOf (attrsOf (oneOf [ bool int str ]));
+        default = {};
+        description = ''
+          Gitea configuration. Refer to <link xlink:href="https://docs.gitea.io/en-us/config-cheat-sheet/"/>
+          for details on supported values.
+        '';
+        example = literalExpression ''
+          {
+            "cron.sync_external_users" = {
+              RUN_AT_START = true;
+              SCHEDULE = "@every 24h";
+              UPDATE_EXISTING = true;
+            };
+            mailer = {
+              ENABLED = true;
+              MAILER_TYPE = "sendmail";
+              FROM = "do-not-reply@example.org";
+              SENDMAIL_PATH = "''${pkgs.system-sendmail}/bin/sendmail";
+            };
+            other = {
+              SHOW_FOOTER_VERSION = false;
+            };
+          }
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "Configuration lines appended to the generated gitea configuration file.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.database.createDatabase -> cfg.database.user == cfg.user;
+        message = "services.gitea.database.user must match services.gitea.user if the database is to be automatically provisioned";
+      }
+    ];
+
+    services.gitea.settings = {
+      database = mkMerge [
+        {
+          DB_TYPE = cfg.database.type;
+        }
+        (mkIf (useMysql || usePostgresql) {
+          HOST = if cfg.database.socket != null then cfg.database.socket else cfg.database.host + ":" + toString cfg.database.port;
+          NAME = cfg.database.name;
+          USER = cfg.database.user;
+          PASSWD = "#dbpass#";
+        })
+        (mkIf useSqlite {
+          PATH = cfg.database.path;
+        })
+        (mkIf usePostgresql {
+          SSL_MODE = "disable";
+        })
+      ];
+
+      repository = {
+        ROOT = cfg.repositoryRoot;
+      };
+
+      server = mkMerge [
+        {
+          DOMAIN = cfg.domain;
+          STATIC_ROOT_PATH = toString cfg.staticRootPath;
+          LFS_JWT_SECRET = "#lfsjwtsecret#";
+          ROOT_URL = cfg.rootUrl;
+        }
+        (mkIf cfg.enableUnixSocket {
+          PROTOCOL = "unix";
+          HTTP_ADDR = "/run/gitea/gitea.sock";
+        })
+        (mkIf (!cfg.enableUnixSocket) {
+          HTTP_ADDR = cfg.httpAddress;
+          HTTP_PORT = cfg.httpPort;
+        })
+        (mkIf cfg.ssh.enable {
+          DISABLE_SSH = false;
+          SSH_PORT = cfg.ssh.clonePort;
+        })
+        (mkIf (!cfg.ssh.enable) {
+          DISABLE_SSH = true;
+        })
+        (mkIf cfg.lfs.enable {
+          LFS_START_SERVER = true;
+          LFS_CONTENT_PATH = cfg.lfs.contentDir;
+        })
+
+      ];
+
+      session = {
+        COOKIE_NAME = "session";
+        COOKIE_SECURE = cfg.cookieSecure;
+      };
+
+      security = {
+        SECRET_KEY = "#secretkey#";
+        INTERNAL_TOKEN = "#internaltoken#";
+        INSTALL_LOCK = true;
+      };
+
+      log = {
+        ROOT_PATH = cfg.log.rootPath;
+        LEVEL = cfg.log.level;
+      };
+
+      service = {
+        DISABLE_REGISTRATION = cfg.disableRegistration;
+      };
+
+      mailer = mkIf (cfg.mailerPasswordFile != null) {
+        PASSWD = "#mailerpass#";
+      };
+
+      oauth2 = {
+        JWT_SECRET = "#oauth2jwtsecret#";
+      };
+    };
+
+    services.postgresql = optionalAttrs (usePostgresql && cfg.database.createDatabase) {
+      enable = mkDefault true;
+
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.database.user;
+          ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    services.mysql = optionalAttrs (useMysql && cfg.database.createDatabase) {
+      enable = mkDefault true;
+      package = mkDefault pkgs.mariadb;
+
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.database.user;
+          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -"
+      "z '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -"
+      "Z '${cfg.dump.backupDir}' - ${cfg.user} gitea - -"
+      "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -"
+      "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -"
+      "Z '${cfg.lfs.contentDir}' - ${cfg.user} gitea - -"
+      "d '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -"
+      "z '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -"
+      "Z '${cfg.repositoryRoot}' - ${cfg.user} gitea - -"
+      "d '${cfg.stateDir}' 0750 ${cfg.user} gitea - -"
+      "d '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -"
+      "d '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -"
+      "d '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -"
+      "d '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -"
+      "z '${cfg.stateDir}' 0750 ${cfg.user} gitea - -"
+      "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} gitea - -"
+      "z '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -"
+      "z '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -"
+      "z '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -"
+      "z '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -"
+      "Z '${cfg.stateDir}' - ${cfg.user} gitea - -"
+
+      # If we have a folder or symlink with gitea locales, remove it
+      # And symlink the current gitea locales in place
+      "L+ '${cfg.stateDir}/conf/locale' - - - - ${gitea.out}/locale"
+    ];
+
+    systemd.services.gitea = {
+      description = "gitea";
+      after = [ "network.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
+      wantedBy = [ "multi-user.target" ];
+      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";
+        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) ''
+          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)
+        ''}
+
+        # run migrations/init the database
+        ${gitea}/bin/gitea migrate
+
+        # update all hooks' binary paths
+        ${gitea}/bin/gitea admin regenerate hooks
+
+        # update command option in authorized_keys
+        if [ -r ${cfg.stateDir}/.ssh/authorized_keys ]
+        then
+          ${gitea}/bin/gitea admin regenerate keys
+        fi
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = "gitea";
+        WorkingDirectory = cfg.stateDir;
+        ExecStart = "${gitea}/bin/gitea web --pid /run/gitea/gitea.pid";
+        Restart = "always";
+        # Runtime directory and mode
+        RuntimeDirectory = "gitea";
+        RuntimeDirectoryMode = "0755";
+        # Access write directories
+        ReadWritePaths = [ cfg.dump.backupDir cfg.repositoryRoot cfg.stateDir cfg.lfs.contentDir ];
+        UMask = "0027";
+        # Capabilities
+        CapabilityBoundingSet = "";
+        # Security
+        NoNewPrivileges = true;
+        # Sandboxing
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_UNIX AF_INET AF_INET6" ];
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        PrivateMounts = true;
+        # System Call Filtering
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @raw-io @reboot @resources @setuid @swap";
+      };
+
+      environment = {
+        USER = cfg.user;
+        HOME = cfg.stateDir;
+        GITEA_WORK_DIR = cfg.stateDir;
+      };
+    };
+
+    users.users = mkIf (cfg.user == "gitea") {
+      gitea = {
+        description = "Gitea Service";
+        home = cfg.stateDir;
+        useDefaultShell = true;
+        group = "gitea";
+        isSystemUser = true;
+      };
+    };
+
+    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.extraConfig != null) ''
+        services.gitea.`extraConfig` is deprecated, please use services.gitea.`settings`.
+      '';
+
+    # Create database passwordFile default when password is configured.
+    services.gitea.database.passwordFile =
+      (mkDefault (toString (pkgs.writeTextFile {
+        name = "gitea-database-password";
+        text = cfg.database.password;
+      })));
+
+    systemd.services.gitea-dump = mkIf cfg.dump.enable {
+       description = "gitea dump";
+       after = [ "gitea.service" ];
+       wantedBy = [ "default.target" ];
+       path = [ gitea ];
+
+       environment = {
+         USER = cfg.user;
+         HOME = cfg.stateDir;
+         GITEA_WORK_DIR = cfg.stateDir;
+       };
+
+       serviceConfig = {
+         Type = "oneshot";
+         User = cfg.user;
+         ExecStart = "${gitea}/bin/gitea dump --type ${cfg.dump.type}" + optionalString (cfg.dump.file != null) " --file ${cfg.dump.file}";
+         WorkingDirectory = cfg.dump.backupDir;
+       };
+    };
+
+    systemd.timers.gitea-dump = mkIf cfg.dump.enable {
+      description = "Update timer for gitea-dump";
+      partOf = [ "gitea-dump.service" ];
+      wantedBy = [ "timers.target" ];
+      timerConfig.OnCalendar = cfg.dump.interval;
+    };
+  };
+  meta.maintainers = with lib.maintainers; [ srhb ma27 ];
+}
diff --git a/nixos/modules/services/misc/gitit.nix b/nixos/modules/services/misc/gitit.nix
new file mode 100644
index 00000000000..ceb186c0f04
--- /dev/null
+++ b/nixos/modules/services/misc/gitit.nix
@@ -0,0 +1,725 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.gitit;
+
+  homeDir = "/var/lib/gitit";
+
+  toYesNo = b: if b then "yes" else "no";
+
+  gititShared = with cfg.haskellPackages; gitit + "/share/" + pkgs.stdenv.hostPlatform.system + "-" + ghc.name + "/" + gitit.pname + "-" + gitit.version;
+
+  gititWithPkgs = hsPkgs: extras: hsPkgs.ghcWithPackages (self: with self; [ gitit ] ++ (extras self));
+
+  gititSh = hsPkgs: extras: with pkgs; let
+    env = gititWithPkgs hsPkgs extras;
+  in writeScript "gitit" ''
+    #!${runtimeShell}
+    cd $HOME
+    export NIX_GHC="${env}/bin/ghc"
+    export NIX_GHCPKG="${env}/bin/ghc-pkg"
+    export NIX_GHC_DOCDIR="${env}/share/doc/ghc/html"
+    export NIX_GHC_LIBDIR=$( $NIX_GHC --print-libdir )
+    ${env}/bin/gitit -f ${configFile}
+  '';
+
+  gititOptions = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable the gitit service.";
+      };
+
+      haskellPackages = mkOption {
+        default = pkgs.haskellPackages;
+        defaultText = literalExpression "pkgs.haskellPackages";
+        example = literalExpression "pkgs.haskell.packages.ghc784";
+        description = "haskellPackages used to build gitit and plugins.";
+      };
+
+      extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
+        default = self: [];
+        example = literalExpression ''
+          haskellPackages: [
+            haskellPackages.wreq
+          ]
+        '';
+        description = ''
+          Extra packages available to ghc when running gitit. The
+          value must be a function which receives the attrset defined
+          in <varname>haskellPackages</varname> as the sole argument.
+        '';
+      };
+
+      address = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = "IP address on which the web server will listen.";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 5001;
+        description = "Port on which the web server will run.";
+      };
+
+      wikiTitle = mkOption {
+        type = types.str;
+        default = "Gitit!";
+        description = "The wiki title.";
+      };
+
+      repositoryType = mkOption {
+        type = types.enum ["git" "darcs" "mercurial"];
+        default = "git";
+        description = "Specifies the type of repository used for wiki content.";
+      };
+
+      repositoryPath = mkOption {
+        type = types.path;
+        default = homeDir + "/wiki";
+        description = ''
+          Specifies the path of the repository directory. If it does not
+          exist, gitit will create it on startup.
+        '';
+      };
+
+      requireAuthentication = mkOption {
+        type = types.enum [ "none" "modify" "read" ];
+        default = "modify";
+        description = ''
+          If 'none', login is never required, and pages can be edited
+          anonymously.  If 'modify', login is required to modify the wiki
+          (edit, add, delete pages, upload files).  If 'read', login is
+          required to see any wiki pages.
+        '';
+      };
+
+      authenticationMethod = mkOption {
+        type = types.enum [ "form" "http" "generic" "github" ];
+        default = "form";
+        description = ''
+          'form' means that users will be logged in and registered using forms
+          in the gitit web interface.  'http' means that gitit will assume that
+          HTTP authentication is in place and take the logged in username from
+          the "Authorization" field of the HTTP request header (in addition,
+          the login/logout and registration links will be suppressed).
+          'generic' means that gitit will assume that some form of
+          authentication is in place that directly sets REMOTE_USER to the name
+          of the authenticated user (e.g. mod_auth_cas on apache).  'rpx' means
+          that gitit will attempt to log in through https://rpxnow.com.  This
+          requires that 'rpx-domain', 'rpx-key', and 'base-url' be set below,
+          and that 'curl' be in the system path.
+        '';
+      };
+
+      userFile = mkOption {
+        type = types.path;
+        default = homeDir + "/gitit-users";
+        description = ''
+          Specifies the path of the file containing user login information.  If
+          it does not exist, gitit will create it (with an empty user list).
+          This file is not used if 'http' is selected for
+          authentication-method.
+        '';
+      };
+
+      sessionTimeout = mkOption {
+        type = types.int;
+        default = 60;
+        description = ''
+          Number of minutes of inactivity before a session expires.
+        '';
+      };
+
+      staticDir = mkOption {
+        type = types.path;
+        default = gititShared + "/data/static";
+        description = ''
+          Specifies the path of the static directory (containing javascript,
+          css, and images).  If it does not exist, gitit will create it and
+          populate it with required scripts, stylesheets, and images.
+        '';
+      };
+
+      defaultPageType = mkOption {
+        type = types.enum [ "markdown" "rst" "latex" "html" "markdown+lhs" "rst+lhs" "latex+lhs" ];
+        default = "markdown";
+        description = ''
+          Specifies the type of markup used to interpret pages in the wiki.
+          Possible values are markdown, rst, latex, html, markdown+lhs,
+          rst+lhs, and latex+lhs. (the +lhs variants treat the input as
+          literate Haskell. See pandoc's documentation for more details.) If
+          Markdown is selected, pandoc's syntax extensions (for footnotes,
+          delimited code blocks, etc.) will be enabled. Note that pandoc's
+          restructuredtext parser is not complete, so some pages may not be
+          rendered correctly if rst is selected. The same goes for latex and
+          html.
+        '';
+      };
+
+      math = mkOption {
+        type = types.enum [ "mathml" "raw" "mathjax" "jsmath" "google" ];
+        default = "mathml";
+        description = ''
+          Specifies how LaTeX math is to be displayed.  Possible values are
+          mathml, raw, mathjax, jsmath, and google.  If mathml is selected,
+          gitit will convert LaTeX math to MathML and link in a script,
+          MathMLinHTML.js, that allows the MathML to be seen in Gecko browsers,
+          IE + mathplayer, and Opera. In other browsers you may get a jumble of
+          characters.  If raw is selected, the LaTeX math will be displayed as
+          raw LaTeX math.  If mathjax is selected, gitit will link to the
+          remote mathjax script.  If jsMath is selected, gitit will link to the
+          script /js/jsMath/easy/load.js, and will assume that jsMath has been
+          installed into the js/jsMath directory.  This is the most portable
+          solution. If google is selected, the google chart API is called to
+          render the formula as an image. This requires a connection to google,
+          and might raise a technical or a privacy problem.
+        '';
+      };
+
+      mathJaxScript = mkOption {
+        type = types.str;
+        default = "https://d3eoax9i5htok0.cloudfront.net/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML";
+        description = ''
+          Specifies the path to MathJax rendering script.  You might want to
+          use your own MathJax script to render formulas without Internet
+          connection or if you want to use some special LaTeX packages.  Note:
+          path specified there cannot be an absolute path to a script on your
+          hdd, instead you should run your (local if you wish) HTTP server
+          which will serve the MathJax.js script. You can easily (in four lines
+          of code) serve MathJax.js using
+          http://happstack.com/docs/crashcourse/FileServing.html Do not forget
+          the "http://" prefix (e.g. http://localhost:1234/MathJax.js).
+        '';
+      };
+
+      showLhsBirdTracks = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Specifies whether to show Haskell code blocks in "bird style", with
+          "> " at the beginning of each line.
+        '';
+      };
+
+      templatesDir = mkOption {
+        type = types.path;
+        default = gititShared + "/data/templates";
+        description = ''
+          Specifies the path of the directory containing page templates.  If it
+          does not exist, gitit will create it with default templates.  Users
+          may wish to edit the templates to customize the appearance of their
+          wiki. The template files are HStringTemplate templates.  Variables to
+          be interpolated appear between $\'s. Literal $\'s must be
+          backslash-escaped.
+        '';
+      };
+
+      logFile = mkOption {
+        type = types.path;
+        default = homeDir + "/gitit.log";
+        description = ''
+          Specifies the path of gitit's log file.  If it does not exist, gitit
+          will create it. The log is in Apache combined log format.
+        '';
+      };
+
+      logLevel = mkOption {
+        type = types.enum [ "DEBUG" "INFO" "NOTICE" "WARNING" "ERROR" "CRITICAL" "ALERT" "EMERGENCY" ];
+        default = "ERROR";
+        description = ''
+          Determines how much information is logged.  Possible values (from
+          most to least verbose) are DEBUG, INFO, NOTICE, WARNING, ERROR,
+          CRITICAL, ALERT, EMERGENCY.
+        '';
+      };
+
+      frontPage = mkOption {
+        type = types.str;
+        default = "Front Page";
+        description = ''
+          Specifies which wiki page is to be used as the wiki's front page.
+          Gitit creates a default front page on startup, if one does not exist
+          already.
+        '';
+      };
+
+      noDelete = mkOption {
+        type = types.str;
+        default = "Front Page, Help";
+        description = ''
+          Specifies pages that cannot be deleted through the web interface.
+          (They can still be deleted directly using git or darcs.) A
+          comma-separated list of page names.  Leave blank to allow every page
+          to be deleted.
+        '';
+      };
+
+      noEdit = mkOption {
+        type = types.str;
+        default = "Help";
+        description = ''
+          Specifies pages that cannot be edited through the web interface.
+          Leave blank to allow every page to be edited.
+        '';
+      };
+
+      defaultSummary = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Specifies text to be used in the change description if the author
+          leaves the "description" field blank.  If default-summary is blank
+          (the default), the author will be required to fill in the description
+          field.
+        '';
+      };
+
+      tableOfContents = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Specifies whether to print a tables of contents (with links to
+          sections) on each wiki page.
+        '';
+      };
+
+      plugins = mkOption {
+        type = with types; listOf str;
+        default = [ (gititShared + "/plugins/Dot.hs") ];
+        description = ''
+          Specifies a list of plugins to load. Plugins may be specified either
+          by their path or by their module name. If the plugin name starts
+          with Gitit.Plugin., gitit will assume that the plugin is an installed
+          module and will not try to find a source file.
+        '';
+      };
+
+      useCache = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Specifies whether to cache rendered pages.  Note that if use-feed is
+          selected, feeds will be cached regardless of the value of use-cache.
+        '';
+      };
+
+      cacheDir = mkOption {
+        type = types.path;
+        default = homeDir + "/cache";
+        description = "Path where rendered pages will be cached.";
+      };
+
+      maxUploadSize = mkOption {
+        type = types.str;
+        default = "1000K";
+        description = ''
+          Specifies an upper limit on the size (in bytes) of files uploaded
+          through the wiki's web interface.  To disable uploads, set this to
+          0K.  This will result in the uploads link disappearing and the
+          _upload url becoming inactive.
+        '';
+      };
+
+      maxPageSize = mkOption {
+        type = types.str;
+        default = "1000K";
+        description = "Specifies an upper limit on the size (in bytes) of pages.";
+      };
+
+      debugMode = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Causes debug information to be logged while gitit is running.";
+      };
+
+      compressResponses = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Specifies whether HTTP responses should be compressed.";
+      };
+
+      mimeTypesFile = mkOption {
+        type = types.path;
+        default = "/etc/mime/types.info";
+        description = ''
+          Specifies the path of a file containing mime type mappings.  Each
+          line of the file should contain two fields, separated by whitespace.
+          The first field is the mime type, the second is a file extension.
+          For example:
+<programlisting>
+video/x-ms-wmx  wmx
+</programlisting>
+          If the file is not found, some simple defaults will be used.
+        '';
+      };
+
+      useReCaptcha = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If true, causes gitit to use the reCAPTCHA service
+          (http://recaptcha.net) to prevent bots from creating accounts.
+        '';
+      };
+
+      reCaptchaPrivateKey = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Specifies the private key for the reCAPTCHA service.  To get
+          these, you need to create an account at http://recaptcha.net.
+        '';
+      };
+
+      reCaptchaPublicKey = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Specifies the public key for the reCAPTCHA service.  To get
+          these, you need to create an account at http://recaptcha.net.
+        '';
+      };
+
+      accessQuestion = mkOption {
+        type = types.str;
+        default = "What is the code given to you by Ms. X?";
+        description = ''
+          Specifies a question that users must answer when they attempt to
+          create an account
+        '';
+      };
+
+      accessQuestionAnswers = mkOption {
+        type = types.str;
+        default = "RED DOG, red dog";
+        description = ''
+          Specifies a question that users must answer when they attempt to
+          create an account, along with a comma-separated list of acceptable
+          answers.  This can be used to institute a rudimentary password for
+          signing up as a user on the wiki, or as an alternative to reCAPTCHA.
+          Example:
+          access-question:  What is the code given to you by Ms. X?
+          access-question-answers:  RED DOG, red dog
+        '';
+      };
+
+      rpxDomain = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          Specifies the domain and key of your RPX account.  The domain is just
+          the prefix of the complete RPX domain, so if your full domain is
+          'https://foo.rpxnow.com/', use 'foo' as the value of rpx-domain.
+        '';
+      };
+
+      rpxKey = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "RPX account access key.";
+      };
+
+      mailCommand = mkOption {
+        type = types.str;
+        default = "sendmail %s";
+        description = ''
+          Specifies the command to use to send notification emails.  '%s' will
+          be replaced by the destination email address.  The body of the
+          message will be read from stdin.  If this field is left blank,
+          password reset will not be offered.
+        '';
+      };
+
+      resetPasswordMessage = mkOption {
+        type = types.lines;
+        default = ''
+          > From: gitit@$hostname$
+          > To: $useremail$
+          > Subject: Wiki password reset
+          >
+          > Hello $username$,
+          >
+          > To reset your password, please follow the link below:
+          > http://$hostname$:$port$$resetlink$
+          >
+          > Regards
+        '';
+        description = ''
+          Gives the text of the message that will be sent to the user should
+          she want to reset her password, or change other registration info.
+          The lines must be indented, and must begin with '>'.  The initial
+          spaces and '> ' will be stripped off.  $username$ will be replaced by
+          the user's username, $useremail$ by her email address, $hostname$ by
+          the hostname on which the wiki is running (as returned by the
+          hostname system call), $port$ by the port on which the wiki is
+          running, and $resetlink$ by the relative path of a reset link derived
+          from the user's existing hashed password. If your gitit wiki is being
+          proxied to a location other than the root path of $port$, you should
+          change the link to reflect this: for example, to
+          http://$hostname$/path/to/wiki$resetlink$ or
+          http://gitit.$hostname$$resetlink$
+        '';
+      };
+
+      useFeed = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Specifies whether an ATOM feed should be enabled (for the site and
+          for individual pages).
+        '';
+      };
+
+      baseUrl = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          The base URL of the wiki, to be used in constructing feed IDs and RPX
+          token_urls.  Set this if useFeed is false or authentication-method
+          is 'rpx'.
+        '';
+      };
+
+      absoluteUrls = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Make wikilinks absolute with respect to the base-url.  So, for
+          example, in a wiki served at the base URL '/wiki', on a page
+          Sub/Page, the wikilink '[Cactus]()' will produce a link to
+          '/wiki/Cactus' if absoluteUrls is true, and a relative link to
+          'Cactus' (referring to '/wiki/Sub/Cactus') if absolute-urls is 'no'.
+        '';
+      };
+
+      feedDays = mkOption {
+        type = types.int;
+        default = 14;
+        description = "Number of days to be included in feeds.";
+      };
+
+      feedRefreshTime = mkOption {
+        type = types.int;
+        default = 60;
+        description = "Number of minutes to cache feeds before refreshing.";
+      };
+
+      pdfExport = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If true, PDF will appear in export options. PDF will be created using
+          pdflatex, which must be installed and in the path. Note that PDF
+          exports create significant additional server load.
+        '';
+      };
+
+      pandocUserData = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        description = ''
+          If a directory is specified, this will be searched for pandoc
+          customizations. These can include a templates/ directory for custom
+          templates for various export formats, an S5 directory for custom S5
+          styles, and a reference.odt for ODT exports. If no directory is
+          specified, $HOME/.pandoc will be searched. See pandoc's README for
+          more information.
+        '';
+      };
+
+      xssSanitize = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          If true, all HTML (including that produced by pandoc) is filtered
+          through xss-sanitize.  Set to no only if you trust all of your users.
+        '';
+      };
+
+      oauthClientId = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "OAuth client ID";
+      };
+
+      oauthClientSecret = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "OAuth client secret";
+      };
+
+      oauthCallback = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "OAuth callback URL";
+      };
+
+      oauthAuthorizeEndpoint = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "OAuth authorize endpoint";
+      };
+
+      oauthAccessTokenEndpoint = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "OAuth access token endpoint";
+      };
+
+      githubOrg = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "Github organization";
+      };
+  };
+
+  configFile = pkgs.writeText "gitit.conf" ''
+    address: ${cfg.address}
+    port: ${toString cfg.port}
+    wiki-title: ${cfg.wikiTitle}
+    repository-type: ${cfg.repositoryType}
+    repository-path: ${cfg.repositoryPath}
+    require-authentication: ${cfg.requireAuthentication}
+    authentication-method: ${cfg.authenticationMethod}
+    user-file: ${cfg.userFile}
+    session-timeout: ${toString cfg.sessionTimeout}
+    static-dir: ${cfg.staticDir}
+    default-page-type: ${cfg.defaultPageType}
+    math: ${cfg.math}
+    mathjax-script: ${cfg.mathJaxScript}
+    show-lhs-bird-tracks: ${toYesNo cfg.showLhsBirdTracks}
+    templates-dir: ${cfg.templatesDir}
+    log-file: ${cfg.logFile}
+    log-level: ${cfg.logLevel}
+    front-page: ${cfg.frontPage}
+    no-delete: ${cfg.noDelete}
+    no-edit: ${cfg.noEdit}
+    default-summary: ${cfg.defaultSummary}
+    table-of-contents: ${toYesNo cfg.tableOfContents}
+    plugins: ${concatStringsSep "," cfg.plugins}
+    use-cache: ${toYesNo cfg.useCache}
+    cache-dir: ${cfg.cacheDir}
+    max-upload-size: ${cfg.maxUploadSize}
+    max-page-size: ${cfg.maxPageSize}
+    debug-mode: ${toYesNo cfg.debugMode}
+    compress-responses: ${toYesNo cfg.compressResponses}
+    mime-types-file: ${cfg.mimeTypesFile}
+    use-recaptcha: ${toYesNo cfg.useReCaptcha}
+    recaptcha-private-key: ${toString cfg.reCaptchaPrivateKey}
+    recaptcha-public-key: ${toString cfg.reCaptchaPublicKey}
+    access-question: ${cfg.accessQuestion}
+    access-question-answers: ${cfg.accessQuestionAnswers}
+    rpx-domain: ${toString cfg.rpxDomain}
+    rpx-key: ${toString cfg.rpxKey}
+    mail-command: ${cfg.mailCommand}
+    reset-password-message: ${cfg.resetPasswordMessage}
+    use-feed: ${toYesNo cfg.useFeed}
+    base-url: ${toString cfg.baseUrl}
+    absolute-urls: ${toYesNo cfg.absoluteUrls}
+    feed-days: ${toString cfg.feedDays}
+    feed-refresh-time: ${toString cfg.feedRefreshTime}
+    pdf-export: ${toYesNo cfg.pdfExport}
+    pandoc-user-data: ${toString cfg.pandocUserData}
+    xss-sanitize: ${toYesNo cfg.xssSanitize}
+
+    [Github]
+    oauthclientid: ${toString cfg.oauthClientId}
+    oauthclientsecret: ${toString cfg.oauthClientSecret}
+    oauthcallback: ${toString cfg.oauthCallback}
+    oauthauthorizeendpoint: ${toString cfg.oauthAuthorizeEndpoint}
+    oauthaccesstokenendpoint: ${toString cfg.oauthAccessTokenEndpoint}
+    github-org: ${toString cfg.githubOrg}
+  '';
+
+in
+
+{
+
+  options.services.gitit = gititOptions;
+
+  config = mkIf cfg.enable {
+
+    users.users.gitit = {
+      group = config.users.groups.gitit.name;
+      description = "Gitit user";
+      home = homeDir;
+      createHome = true;
+      uid = config.ids.uids.gitit;
+    };
+
+    users.groups.gitit.gid = config.ids.gids.gitit;
+
+    systemd.services.gitit = let
+      uid = toString config.ids.uids.gitit;
+      gid = toString config.ids.gids.gitit;
+    in {
+      description = "Git and Pandoc Powered Wiki";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = with pkgs; [ curl ]
+             ++ optional cfg.pdfExport texlive.combined.scheme-basic
+             ++ optional (cfg.repositoryType == "darcs") darcs
+             ++ optional (cfg.repositoryType == "mercurial") mercurial
+             ++ optional (cfg.repositoryType == "git") git;
+
+      preStart = let
+        gm = "gitit@${config.networking.hostName}";
+      in
+      with cfg; ''
+        chown ${uid}:${gid} -R ${homeDir}
+        for dir in ${repositoryPath} ${staticDir} ${templatesDir} ${cacheDir}
+        do
+          if [ ! -d $dir ]
+          then
+            mkdir -p $dir
+            find $dir -type d -exec chmod 0750 {} +
+            find $dir -type f -exec chmod 0640 {} +
+          fi
+        done
+        cd ${repositoryPath}
+        ${
+          if repositoryType == "darcs" then
+          ''
+          if [ ! -d _darcs ]
+          then
+            ${pkgs.darcs}/bin/darcs initialize
+            echo "${gm}" > _darcs/prefs/email
+          ''
+          else if repositoryType == "mercurial" then
+          ''
+          if [ ! -d .hg ]
+          then
+            ${pkgs.mercurial}/bin/hg init
+            cat >> .hg/hgrc <<NAMED
+[ui]
+username = gitit ${gm}
+NAMED
+          ''
+          else
+          ''
+          if [ ! -d  .git ]
+          then
+            ${pkgs.git}/bin/git init
+            ${pkgs.git}/bin/git config user.email "${gm}"
+            ${pkgs.git}/bin/git config user.name "gitit"
+          ''}
+          chown ${uid}:${gid} -R ${repositoryPath}
+          fi
+        cd -
+      '';
+
+      serviceConfig = {
+        User = config.users.users.gitit.name;
+        Group = config.users.groups.gitit.name;
+        ExecStart = with cfg; gititSh haskellPackages extraPackages;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix
new file mode 100644
index 00000000000..e48444f7161
--- /dev/null
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -0,0 +1,1458 @@
+{ config, lib, options, pkgs, utils, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gitlab;
+  opt = options.services.gitlab;
+
+  ruby = cfg.packages.gitlab.ruby;
+
+  postgresqlPackage = if config.services.postgresql.enable then
+                        config.services.postgresql.package
+                      else
+                        pkgs.postgresql_12;
+
+  gitlabSocket = "${cfg.statePath}/tmp/sockets/gitlab.socket";
+  gitalySocket = "${cfg.statePath}/tmp/sockets/gitaly.socket";
+  pathUrlQuote = url: replaceStrings ["/"] ["%2F"] url;
+
+  databaseConfig = {
+    production = {
+      adapter = "postgresql";
+      database = cfg.databaseName;
+      host = cfg.databaseHost;
+      username = cfg.databaseUsername;
+      encoding = "utf8";
+      pool = cfg.databasePool;
+    } // cfg.extraDatabaseConfig;
+  };
+
+  # We only want to create a database if we're actually going to connect to it.
+  databaseActuallyCreateLocally = cfg.databaseCreateLocally && cfg.databaseHost == "";
+
+  gitalyToml = pkgs.writeText "gitaly.toml" ''
+    socket_path = "${lib.escape ["\""] gitalySocket}"
+    bin_dir = "${cfg.packages.gitaly}/bin"
+    prometheus_listen_addr = "localhost:9236"
+
+    [git]
+    bin_path = "${pkgs.git}/bin/git"
+
+    [gitaly-ruby]
+    dir = "${cfg.packages.gitaly.ruby}"
+
+    [gitlab-shell]
+    dir = "${cfg.packages.gitlab-shell}"
+
+    [hooks]
+    custom_hooks_dir = "${cfg.statePath}/custom_hooks"
+
+    [gitlab]
+    secret_file = "${cfg.statePath}/gitlab_shell_secret"
+    url = "http+unix://${pathUrlQuote gitlabSocket}"
+
+    [gitlab.http-settings]
+    self_signed_cert = false
+
+    ${concatStringsSep "\n" (attrValues (mapAttrs (k: v: ''
+    [[storage]]
+    name = "${lib.escape ["\""] k}"
+    path = "${lib.escape ["\""] v.path}"
+    '') gitlabConfig.production.repositories.storages))}
+  '';
+
+  gitlabShellConfig = flip recursiveUpdate cfg.extraShellConfig {
+    user = cfg.user;
+    gitlab_url = "http+unix://${pathUrlQuote gitlabSocket}";
+    http_settings.self_signed_cert = false;
+    repos_path = "${cfg.statePath}/repositories";
+    secret_file = "${cfg.statePath}/gitlab_shell_secret";
+    log_file = "${cfg.statePath}/log/gitlab-shell.log";
+    redis = {
+      bin = "${pkgs.redis}/bin/redis-cli";
+      host = "127.0.0.1";
+      port = config.services.redis.servers.gitlab.port;
+      database = 0;
+      namespace = "resque:gitlab";
+    };
+  };
+
+  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 {
+      gitlab = {
+        host = cfg.host;
+        port = cfg.port;
+        https = cfg.https;
+        user = cfg.user;
+        email_enabled = true;
+        email_display_name = "GitLab";
+        email_reply_to = "noreply@localhost";
+        default_theme = 2;
+        default_projects_features = {
+          issues = true;
+          merge_requests = true;
+          wiki = true;
+          snippets = true;
+          builds = true;
+          container_registry = true;
+        };
+      };
+      repositories.storages.default.path = "${cfg.statePath}/repositories";
+      repositories.storages.default.gitaly_address = "unix:${gitalySocket}";
+      artifacts.enabled = true;
+      lfs.enabled = true;
+      gravatar.enabled = true;
+      cron_jobs = { };
+      gitlab_ci.builds_path = "${cfg.statePath}/builds";
+      ldap.enabled = false;
+      omniauth.enabled = false;
+      shared.path = "${cfg.statePath}/shared";
+      gitaly.client_path = "${cfg.packages.gitaly}/bin";
+      backup = {
+        gitaly_backup_path = "${cfg.packages.gitaly}/bin/gitaly-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";
+        secret_file = "${cfg.statePath}/gitlab_shell_secret";
+        upload_pack = true;
+        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" ];
+        sidekiq_exporter = {
+          enable = true;
+          address = "localhost";
+          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 = cfg.packages.gitlab.gitlabEnv // {
+    HOME = "${cfg.statePath}/home";
+    PUMA_PATH = "${cfg.statePath}/";
+    GITLAB_PATH = "${cfg.packages.gitlab}/share/gitlab/";
+    SCHEMA = "${cfg.statePath}/db/structure.sql";
+    GITLAB_UPLOADS_PATH = "${cfg.statePath}/uploads";
+    GITLAB_LOG_PATH = "${cfg.statePath}/log";
+    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";
+    buildInputs = [ pkgs.makeWrapper ];
+    dontBuild = true;
+    dontUnpack = true;
+    installPhase = ''
+      mkdir -p $out/bin
+      makeWrapper ${cfg.packages.gitlab.rubyEnv}/bin/rake $out/bin/gitlab-rake \
+          ${concatStrings (mapAttrsToList (name: value: "--set ${name} '${value}' ") gitlabEnv)} \
+          --set PATH '${lib.makeBinPath [ pkgs.nodejs pkgs.gzip pkgs.git pkgs.gnutar postgresqlPackage pkgs.coreutils pkgs.procps ]}:$PATH' \
+          --set RAKEOPT '-f ${cfg.packages.gitlab}/share/gitlab/Rakefile' \
+          --run 'cd ${cfg.packages.gitlab}/share/gitlab'
+     '';
+  };
+
+  gitlab-rails = pkgs.stdenv.mkDerivation {
+    name = "gitlab-rails";
+    buildInputs = [ pkgs.makeWrapper ];
+    dontBuild = true;
+    dontUnpack = true;
+    installPhase = ''
+      mkdir -p $out/bin
+      makeWrapper ${cfg.packages.gitlab.rubyEnv}/bin/rails $out/bin/gitlab-rails \
+          ${concatStrings (mapAttrsToList (name: value: "--set ${name} '${value}' ") gitlabEnv)} \
+          --set PATH '${lib.makeBinPath [ pkgs.nodejs pkgs.gzip pkgs.git pkgs.gnutar postgresqlPackage pkgs.coreutils pkgs.procps ]}:$PATH' \
+          --run 'cd ${cfg.packages.gitlab}/share/gitlab'
+     '';
+  };
+
+  extraGitlabRb = pkgs.writeText "extra-gitlab.rb" cfg.extraGitlabRb;
+
+  smtpSettings = pkgs.writeText "gitlab-smtp-settings.rb" ''
+    if Rails.env.production?
+      Rails.application.config.action_mailer.delivery_method = :smtp
+
+      ActionMailer::Base.delivery_method = :smtp
+      ActionMailer::Base.smtp_settings = {
+        address: "${cfg.smtp.address}",
+        port: ${toString cfg.smtp.port},
+        ${optionalString (cfg.smtp.username != null) ''user_name: "${cfg.smtp.username}",''}
+        ${optionalString (cfg.smtp.passwordFile != null) ''password: "@smtpPassword@",''}
+        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}'
+      }
+    end
+  '';
+
+in {
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "gitlab" "stateDir" ] [ "services" "gitlab" "statePath" ])
+    (mkRenamedOptionModule [ "services" "gitlab" "backupPath" ] [ "services" "gitlab" "backup" "path" ])
+    (mkRemovedOptionModule [ "services" "gitlab" "satelliteDir" ] "")
+  ];
+
+  options = {
+    services.gitlab = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the gitlab service.
+        '';
+      };
+
+      packages.gitlab = mkOption {
+        type = types.package;
+        default = pkgs.gitlab;
+        defaultText = literalExpression "pkgs.gitlab";
+        description = "Reference to the gitlab package";
+        example = literalExpression "pkgs.gitlab-ee";
+      };
+
+      packages.gitlab-shell = mkOption {
+        type = types.package;
+        default = pkgs.gitlab-shell;
+        defaultText = literalExpression "pkgs.gitlab-shell";
+        description = "Reference to the gitlab-shell package";
+      };
+
+      packages.gitlab-workhorse = mkOption {
+        type = types.package;
+        default = pkgs.gitlab-workhorse;
+        defaultText = literalExpression "pkgs.gitlab-workhorse";
+        description = "Reference to the gitlab-workhorse package";
+      };
+
+      packages.gitaly = mkOption {
+        type = types.package;
+        default = pkgs.gitaly;
+        defaultText = literalExpression "pkgs.gitaly";
+        description = "Reference to the gitaly package";
+      };
+
+      packages.pages = mkOption {
+        type = types.package;
+        default = pkgs.gitlab-pages;
+        defaultText = literalExpression "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
+          logs, among other things, are stored here.
+
+          The directory will be created automatically if it doesn't
+          exist already. Its parent directories must be owned by
+          either <literal>root</literal> or the user set in
+          <option>services.gitlab.user</option>.
+        '';
+      };
+
+      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";
+        defaultText = literalExpression ''config.${opt.statePath} + "/backup"'';
+        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 = literalExpression ''
+          {
+            # 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
+          local unix socket connection</quote>.
+        '';
+      };
+
+      databasePasswordFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        description = ''
+          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.
+        '';
+      };
+
+      databaseCreateLocally = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether a database should be automatically created on the
+          local host. Set this to <literal>false</literal> if you plan
+          on provisioning a local database yourself. This has no effect
+          if <option>services.gitlab.databaseHost</option> is customized.
+        '';
+      };
+
+      databaseName = mkOption {
+        type = types.str;
+        default = "gitlab";
+        description = "GitLab database name.";
+      };
+
+      databaseUsername = mkOption {
+        type = types.str;
+        default = "gitlab";
+        description = "GitLab database user.";
+      };
+
+      databasePool = mkOption {
+        type = types.int;
+        default = 5;
+        description = "Database connection pool size.";
+      };
+
+      extraDatabaseConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        description = "Extra configuration in config/database.yml.";
+      };
+
+      redisUrl = mkOption {
+        type = types.str;
+        default = "redis://localhost:${toString config.services.redis.servers.gitlab.port}/";
+        defaultText = literalExpression ''redis://localhost:''${toString config.services.redis.servers.gitlab.port}/'';
+        description = "Redis URL for all GitLab services except gitlab-shell";
+      };
+
+      extraGitlabRb = mkOption {
+        type = types.str;
+        default = "";
+        example = ''
+          if Rails.env.production?
+            Rails.application.config.action_mailer.delivery_method = :sendmail
+            ActionMailer::Base.delivery_method = :sendmail
+            ActionMailer::Base.sendmail_settings = {
+              location: "/run/wrappers/bin/sendmail",
+              arguments: "-i -t"
+            }
+          end
+        '';
+        description = ''
+          Extra configuration to be placed in config/extra-gitlab.rb. This can
+          be used to add configuration not otherwise exposed through this module's
+          options.
+        '';
+      };
+
+      host = mkOption {
+        type = types.str;
+        default = config.networking.hostName;
+        defaultText = literalExpression "config.networking.hostName";
+        description = "GitLab host name. Used e.g. for copy-paste URLs.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 8080;
+        description = ''
+          GitLab server port for copy-paste URLs, e.g. 80 or 443 if you're
+          service over https.
+        '';
+      };
+
+      https = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether gitlab prints URLs with https as scheme.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "gitlab";
+        description = "User to run gitlab and all related services.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "gitlab";
+        description = "Group to run gitlab and all related services.";
+      };
+
+      initialRootEmail = mkOption {
+        type = types.str;
+        default = "admin@local.host";
+        description = ''
+          Initial email address of the root account if this is a new install.
+        '';
+      };
+
+      initialRootPasswordFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        description = ''
+          File containing the initial password of the root account if
+          this is a new install.
+
+          This should be a string, not a nix path, since nix paths are
+          copied into the world-readable nix store.
+        '';
+      };
+
+      registry = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Enable GitLab container registry.";
+        };
+        host = mkOption {
+          type = types.str;
+          default = config.services.gitlab.host;
+          defaultText = literalExpression "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;
+          description = "Path to GitLab container registry certificate.";
+        };
+        keyFile = mkOption {
+          type = types.path;
+          description = "Path to GitLab container registry certificate-key.";
+        };
+        defaultForProjects = mkOption {
+          type = types.bool;
+          default = cfg.registry.enable;
+          defaultText = literalExpression "config.${opt.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;
+          default = false;
+          description = "Enable gitlab mail delivery over SMTP.";
+        };
+
+        address = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = "Address of the SMTP server for GitLab.";
+        };
+
+        port = mkOption {
+          type = types.int;
+          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.";
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          description = ''
+            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.
+          '';
+        };
+
+        domain = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = "HELO domain to use for outgoing mail.";
+        };
+
+        authentication = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          description = "Authentication type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
+        };
+
+        enableStartTLSAuto = mkOption {
+          type = types.bool;
+          default = true;
+          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";
+          description = "How OpenSSL checks the certificate, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
+        };
+      };
+
+      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;
+        description = ''
+          A file containing the secret used to encrypt variables 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 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
+          copied into the world-readable nix store.
+        '';
+      };
+
+      secrets.dbFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        description = ''
+          A file containing the secret used to encrypt variables 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 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
+          copied into the world-readable nix store.
+        '';
+      };
+
+      secrets.otpFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        description = ''
+          A file containing the secret used to encrypt secrets for OTP
+          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 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
+          copied into the world-readable nix store.
+        '';
+      };
+
+      secrets.jwsFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        description = ''
+          A file containing the secret used to encrypt session
+          keys. If you change or lose this key, users will be
+          disconnected.
+
+          Make sure the secret is an RSA private key in PEM format. You can
+          generate one with
+
+          openssl genrsa 2048
+
+          This should be a string, not a nix path, since nix paths are
+          copied into the world-readable nix store.
+        '';
+      };
+
+      extraShellConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        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.
+        '';
+      };
+
+      logrotate = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Enable rotation of log files.
+          '';
+        };
+
+        frequency = mkOption {
+          type = types.str;
+          default = "daily";
+          description = "How often to rotate the logs.";
+        };
+
+        keep = mkOption {
+          type = types.int;
+          default = 30;
+          description = "How many rotations to keep.";
+        };
+
+        extraConfig = mkOption {
+          type = types.lines;
+          default = ''
+            copytruncate
+            compress
+          '';
+          description = ''
+            Extra logrotate config options for this path. Refer to
+            <link xlink:href="https://linux.die.net/man/8/logrotate"/> for details.
+          '';
+        };
+      };
+
+      extraConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        example = literalExpression ''
+          {
+            gitlab = {
+              default_projects_features = {
+                builds = false;
+              };
+            };
+            omniauth = {
+              enabled = true;
+              auto_sign_in_with_provider = "openid_connect";
+              allow_single_sign_on = ["openid_connect"];
+              block_auto_created_users = false;
+              providers = [
+                {
+                  name = "openid_connect";
+                  label = "OpenID Connect";
+                  args = {
+                    name = "openid_connect";
+                    scope = ["openid" "profile"];
+                    response_type = "code";
+                    issuer = "https://keycloak.example.com/auth/realms/My%20Realm";
+                    discovery = true;
+                    client_auth_method = "query";
+                    uid_field = "preferred_username";
+                    client_options = {
+                      identifier = "gitlab";
+                      secret = { _secret = "/var/keys/gitlab_oidc_secret"; };
+                      redirect_uri = "https://git.example.com/users/auth/openid_connect/callback";
+                    };
+                  };
+                }
+              ];
+            };
+          };
+        '';
+        description = ''
+          Extra options to be added under
+          <literal>production</literal> in
+          <filename>config/gitlab.yml</filename>, as a nix attribute
+          set.
+
+          Options containing secret data should be set to an attribute
+          set containing the attribute <literal>_secret</literal> - a
+          string pointing to a file containing the value the option
+          should be set to. See the example to get a better picture of
+          this: in the resulting
+          <filename>config/gitlab.yml</filename> file, the
+          <literal>production.omniauth.providers[0].args.client_options.secret</literal>
+          key will be set to the contents of the
+          <filename>/var/keys/gitlab_oidc_secret</filename> file.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      {
+        assertion = databaseActuallyCreateLocally -> (cfg.user == cfg.databaseUsername);
+        message = ''For local automatic database provisioning (services.gitlab.databaseCreateLocally == true) with peer authentication (services.gitlab.databaseHost == "") to work services.gitlab.user and services.gitlab.databaseUsername must be identical.'';
+      }
+      {
+        assertion = (cfg.databaseHost != "") -> (cfg.databasePasswordFile != null);
+        message = "When services.gitlab.databaseHost is customized, services.gitlab.databasePasswordFile must be set!";
+      }
+      {
+        assertion = cfg.initialRootPasswordFile != null;
+        message = "services.gitlab.initialRootPasswordFile must be set!";
+      }
+      {
+        assertion = cfg.secrets.secretFile != null;
+        message = "services.gitlab.secrets.secretFile must be set!";
+      }
+      {
+        assertion = cfg.secrets.dbFile != null;
+        message = "services.gitlab.secrets.dbFile must be set!";
+      }
+      {
+        assertion = cfg.secrets.otpFile != null;
+        message = "services.gitlab.secrets.otpFile must be set!";
+      }
+      {
+        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.servers.gitlab = {
+      enable = mkDefault true;
+      port = mkDefault 31636;
+      bind = mkDefault "127.0.0.1";
+    };
+
+    # We use postgres as the main data store.
+    services.postgresql = optionalAttrs databaseActuallyCreateLocally {
+      enable = true;
+      ensureUsers = singleton { name = cfg.databaseUsername; };
+    };
+
+    # Enable rotation of log files
+    services.logrotate = {
+      enable = cfg.logrotate.enable;
+      paths = {
+        gitlab = {
+          path = "${cfg.statePath}/log/*.log";
+          user = cfg.user;
+          group = cfg.group;
+          frequency = cfg.logrotate.frequency;
+          keep = cfg.logrotate.keep;
+          extraConfig = cfg.logrotate.extraConfig;
+        };
+      };
+    };
+
+    # The postgresql module doesn't currently support concepts like
+    # objects owners and extensions; for now we tack on what's needed
+    # here.
+    systemd.services.gitlab-postgresql = let pgsql = config.services.postgresql; in mkIf databaseActuallyCreateLocally {
+      after = [ "postgresql.service" ];
+      bindsTo = [ "postgresql.service" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      path = [
+        pgsql.package
+        pkgs.util-linux
+      ];
+      script = ''
+        set -eu
+
+        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}'")
+        if [[ "$current_owner" != "${cfg.databaseUsername}" ]]; then
+            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}\""
+            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 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 (cfg.smtp.enable && cfg.smtp.address == "localhost");
+
+    users.users.${cfg.user} =
+      { group = cfg.group;
+        home = "${cfg.statePath}/home";
+        shell = "${pkgs.bash}/bin/bash";
+        uid = config.ids.uids.gitlab;
+      };
+
+    users.groups.${cfg.group}.gid = config.ids.gids.gitlab;
+
+    systemd.tmpfiles.rules = [
+      "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.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}/db 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/log 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/repositories 2770 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/shell 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/tmp 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/tmp/pids 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/tmp/sockets 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/uploads 0700 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/custom_hooks 0700 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/custom_hooks/pre-receive.d 0700 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/custom_hooks/post-receive.d 0700 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.statePath}/custom_hooks/update.d 0700 ${cfg.user} ${cfg.group} -"
+      "d ${gitlabConfig.production.shared.path} 0750 ${cfg.user} ${cfg.group} -"
+      "d ${gitlabConfig.production.shared.path}/artifacts 0750 ${cfg.user} ${cfg.group} -"
+      "d ${gitlabConfig.production.shared.path}/lfs-objects 0750 ${cfg.user} ${cfg.group} -"
+      "d ${gitlabConfig.production.shared.path}/packages 0750 ${cfg.user} ${cfg.group} -"
+      "d ${gitlabConfig.production.shared.path}/pages 0750 ${cfg.user} ${cfg.group} -"
+      "d ${gitlabConfig.production.shared.path}/terraform_state 0750 ${cfg.user} ${cfg.group} -"
+      "L+ /run/gitlab/config - - - - ${cfg.statePath}/config"
+      "L+ /run/gitlab/log - - - - ${cfg.statePath}/log"
+      "L+ /run/gitlab/tmp - - - - ${cfg.statePath}/tmp"
+      "L+ /run/gitlab/uploads - - - - ${cfg.statePath}/uploads"
+
+      "L+ /run/gitlab/shell-config.yml - - - - ${pkgs.writeText "config.yml" (builtins.toJSON gitlabShellConfig)}"
+    ];
+
+
+    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 = ''
+            set -o errexit -o pipefail -o nounset
+            shopt -s dotglob nullglob inherit_errexit
+
+            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 -o errexit -o pipefail -o nounset
+          shopt -s inherit_errexit
+
+          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 ''
+                db_password="$(<'${cfg.databasePasswordFile}')"
+                export db_password
+
+                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'
+
+            secret="$(<'${cfg.secrets.secretFile}')"
+            db="$(<'${cfg.secrets.dbFile}')"
+            otp="$(<'${cfg.secrets.otpFile}')"
+            jws="$(<'${cfg.secrets.jwsFile}')"
+            export secret db otp jws
+            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 -o errexit -o pipefail -o nounset
+          shopt -s inherit_errexit
+          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-gitlab.service"
+        "postgresql.service"
+        "gitlab-config.service"
+        "gitlab-db-config.service"
+      ];
+      bindsTo = [
+        "redis-gitlab.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
+        git
+        ruby
+        openssh
+        nodejs
+        gnupg
+
+        # Needed for GitLab project imports
+        gnutar
+        gzip
+
+        procps # Sidekiq MemoryKiller
+      ];
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        TimeoutSec = "infinity";
+        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" "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
+        git
+        cfg.packages.gitaly.rubyEnv
+        cfg.packages.gitaly.rubyEnv.wrappedRuby
+        gzip
+        bzip2
+      ];
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+        WorkingDirectory = gitlabEnv.HOME;
+        ExecStart = "${cfg.packages.gitaly}/bin/gitaly ${gitalyToml}";
+      };
+    };
+
+    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 = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      path = with pkgs; [
+        exiftool
+        git
+        gnutar
+        gzip
+        openssh
+        gitlab-workhorse
+      ];
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+        WorkingDirectory = gitlabEnv.HOME;
+        ExecStart =
+          "${cfg.packages.gitlab-workhorse}/bin/workhorse "
+          + "-listenUmask 0 "
+          + "-listenNetwork unix "
+          + "-listenAddr /run/gitlab/gitlab-workhorse.socket "
+          + "-authSocket ${gitlabSocket} "
+          + "-documentRoot ${cfg.packages.gitlab}/share/gitlab/public "
+          + "-secretPath ${cfg.statePath}/.gitlab_workhorse_secret";
+      };
+    };
+
+    systemd.services.gitlab-mailroom = mkIf (gitlabConfig.production.incoming_email.enabled or false) {
+      description = "GitLab incoming mail daemon";
+      after = [ "network.target" "redis-gitlab.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"
+        "network.target"
+        "redis-gitlab.service"
+        "gitlab-config.service"
+        "gitlab-db-config.service"
+      ];
+      bindsTo = [
+        "redis-gitlab.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
+        git
+        openssh
+        nodejs
+        procps
+        gnupg
+      ];
+      serviceConfig = {
+        Type = "notify";
+        User = cfg.user;
+        Group = cfg.group;
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+        WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
+        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}"
+        ];
+      };
+
+    };
+
+    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
new file mode 100644
index 00000000000..40424c5039a
--- /dev/null
+++ b/nixos/modules/services/misc/gitlab.xml
@@ -0,0 +1,151 @@
+<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-gitlab">
+ <title>GitLab</title>
+ <para>
+  GitLab is a feature-rich git hosting service.
+ </para>
+ <section xml:id="module-services-gitlab-prerequisites">
+  <title>Prerequisites</title>
+
+  <para>
+   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>
+
+  <para>
+   For instance, the following configuration could be used to use nginx as
+   frontend proxy:
+<programlisting>
+<link linkend="opt-services.nginx.enable">services.nginx</link> = {
+  <link linkend="opt-services.nginx.enable">enable</link> = true;
+  <link linkend="opt-services.nginx.recommendedGzipSettings">recommendedGzipSettings</link> = true;
+  <link linkend="opt-services.nginx.recommendedOptimisation">recommendedOptimisation</link> = true;
+  <link linkend="opt-services.nginx.recommendedProxySettings">recommendedProxySettings</link> = true;
+  <link linkend="opt-services.nginx.recommendedTlsSettings">recommendedTlsSettings</link> = true;
+  <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link>."git.example.com" = {
+    <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_.proxyPass">locations."/".proxyPass</link> = "http://unix:/run/gitlab/gitlab-workhorse.socket";
+  };
+};
+</programlisting>
+  </para>
+ </section>
+ <section xml:id="module-services-gitlab-configuring">
+  <title>Configuring</title>
+
+  <para>
+   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>
+
+  <para>
+   The default state dir is <literal>/var/gitlab/state</literal>. This is where
+   all data like the repositories and uploads will be stored.
+  </para>
+
+  <para>
+   A basic configuration with some custom settings could look like this:
+<programlisting>
+services.gitlab = {
+  <link linkend="opt-services.gitlab.enable">enable</link> = true;
+  <link linkend="opt-services.gitlab.databasePasswordFile">databasePasswordFile</link> = "/var/keys/gitlab/db_password";
+  <link linkend="opt-services.gitlab.initialRootPasswordFile">initialRootPasswordFile</link> = "/var/keys/gitlab/root_password";
+  <link linkend="opt-services.gitlab.https">https</link> = true;
+  <link linkend="opt-services.gitlab.host">host</link> = "git.example.com";
+  <link linkend="opt-services.gitlab.port">port</link> = 443;
+  <link linkend="opt-services.gitlab.user">user</link> = "git";
+  <link linkend="opt-services.gitlab.group">group</link> = "git";
+  smtp = {
+    <link linkend="opt-services.gitlab.smtp.enable">enable</link> = true;
+    <link linkend="opt-services.gitlab.smtp.address">address</link> = "localhost";
+    <link linkend="opt-services.gitlab.smtp.port">port</link> = 25;
+  };
+  secrets = {
+    <link linkend="opt-services.gitlab.secrets.dbFile">dbFile</link> = "/var/keys/gitlab/db";
+    <link linkend="opt-services.gitlab.secrets.secretFile">secretFile</link> = "/var/keys/gitlab/secret";
+    <link linkend="opt-services.gitlab.secrets.otpFile">otpFile</link> = "/var/keys/gitlab/otp";
+    <link linkend="opt-services.gitlab.secrets.jwsFile">jwsFile</link> = "/var/keys/gitlab/jws";
+  };
+  <link linkend="opt-services.gitlab.extraConfig">extraConfig</link> = {
+    gitlab = {
+      email_from = "gitlab-no-reply@example.com";
+      email_display_name = "Example GitLab";
+      email_reply_to = "gitlab-no-reply@example.com";
+      default_projects_features = { builds = false; };
+    };
+  };
+};
+</programlisting>
+  </para>
+
+  <para>
+   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
+   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
+   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.
+  </para>
+ </section>
+ <section xml:id="module-services-gitlab-maintenance">
+  <title>Maintenance</title>
+
+  <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>
+     To run a manual backup, start the <literal>gitlab-backup</literal> service:
+<screen>
+<prompt>$ </prompt>systemctl start gitlab-backup.service
+</screen>
+   </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>
+  </section>
+ </section>
+</chapter>
diff --git a/nixos/modules/services/misc/gitolite.nix b/nixos/modules/services/misc/gitolite.nix
new file mode 100644
index 00000000000..810ef1f21b9
--- /dev/null
+++ b/nixos/modules/services/misc/gitolite.nix
@@ -0,0 +1,234 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gitolite;
+  # Use writeTextDir to not leak Nix store hash into file name
+  pubkeyFile = (pkgs.writeTextDir "gitolite-admin.pub" cfg.adminPubkey) + "/gitolite-admin.pub";
+  hooks = lib.concatMapStrings (hook: "${hook} ") cfg.commonHooks;
+in
+{
+  options = {
+    services.gitolite = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable gitolite management under the
+          <literal>gitolite</literal> user. After
+          switching to a configuration with Gitolite enabled, you can
+          then run <literal>git clone
+          gitolite@host:gitolite-admin.git</literal> to manage it further.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/gitolite";
+        description = ''
+          The gitolite home directory used to store all repositories. If left as the default value
+          this directory will automatically be created before the gitolite server starts, otherwise
+          the sysadmin is responsible for ensuring the directory exists with appropriate ownership
+          and permissions.
+        '';
+      };
+
+      adminPubkey = mkOption {
+        type = types.str;
+        description = ''
+          Initial administrative public key for Gitolite. This should
+          be an SSH Public Key. Note that this key will only be used
+          once, upon the first initialization of the Gitolite user.
+          The key string cannot have any line breaks in it.
+        '';
+      };
+
+      enableGitAnnex = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable git-annex support. Uses the <literal>extraGitoliteRc</literal> option
+          to apply the necessary configuration.
+        '';
+      };
+
+      commonHooks = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          A list of custom git hooks that get copied to <literal>~/.gitolite/hooks/common</literal>.
+        '';
+      };
+
+      extraGitoliteRc = mkOption {
+        type = types.lines;
+        default = "";
+        example = literalExpression ''
+          '''
+            $RC{UMASK} = 0027;
+            $RC{SITE_INFO} = 'This is our private repository host';
+            push( @{$RC{ENABLE}}, 'Kindergarten' ); # enable the command/feature
+            @{$RC{ENABLE}} = grep { $_ ne 'desc' } @{$RC{ENABLE}}; # disable the command/feature
+          '''
+        '';
+        description = ''
+          Extra configuration to append to the default <literal>~/.gitolite.rc</literal>.
+
+          This should be Perl code that modifies the <literal>%RC</literal>
+          configuration variable. The default <literal>~/.gitolite.rc</literal>
+          content is generated by invoking <literal>gitolite print-default-rc</literal>,
+          and extra configuration from this option is appended to it. The result
+          is placed to Nix store, and the <literal>~/.gitolite.rc</literal> file
+          becomes a symlink to it.
+
+          If you already have a customized (or otherwise changed)
+          <literal>~/.gitolite.rc</literal> file, NixOS will refuse to replace
+          it with a symlink, and the `gitolite-init` initialization service
+          will fail. In this situation, in order to use this option, you
+          will need to take any customizations you may have in
+          <literal>~/.gitolite.rc</literal>, convert them to appropriate Perl
+          statements, add them to this option, and remove the file.
+
+          See also the <literal>enableGitAnnex</literal> option.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "gitolite";
+        description = ''
+          Gitolite user account. This is the username of the gitolite endpoint.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "gitolite";
+        description = ''
+          Primary group of the Gitolite user account.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable (
+  let
+    manageGitoliteRc = cfg.extraGitoliteRc != "";
+    rcDir = pkgs.runCommand "gitolite-rc" { preferLocalBuild = true; } rcDirScript;
+    rcDirScript =
+      ''
+        mkdir "$out"
+        export HOME=temp-home
+        mkdir -p "$HOME/.gitolite/logs" # gitolite can't run without it
+        '${pkgs.gitolite}'/bin/gitolite print-default-rc >>"$out/gitolite.rc.default"
+        cat <<END >>"$out/gitolite.rc"
+        # This file is managed by NixOS.
+        # Use services.gitolite options to control it.
+
+        END
+        cat "$out/gitolite.rc.default" >>"$out/gitolite.rc"
+      '' +
+      optionalString (cfg.extraGitoliteRc != "") ''
+        echo -n ${escapeShellArg ''
+
+          # Added by NixOS:
+          ${removeSuffix "\n" cfg.extraGitoliteRc}
+
+          # per perl rules, this should be the last line in such a file:
+          1;
+        ''} >>"$out/gitolite.rc"
+      '';
+  in {
+    services.gitolite.extraGitoliteRc = optionalString cfg.enableGitAnnex ''
+      # Enable git-annex support:
+      push( @{$RC{ENABLE}}, 'git-annex-shell ua');
+    '';
+
+    users.users.${cfg.user} = {
+      description     = "Gitolite user";
+      home            = cfg.dataDir;
+      uid             = config.ids.uids.gitolite;
+      group           = cfg.group;
+      useDefaultShell = true;
+    };
+    users.groups.${cfg.group}.gid = config.ids.gids.gitolite;
+
+    systemd.services.gitolite-init = {
+      description = "Gitolite initialization";
+      wantedBy    = [ "multi-user.target" ];
+      unitConfig.RequiresMountsFor = cfg.dataDir;
+
+      environment = {
+        GITOLITE_RC = ".gitolite.rc";
+        GITOLITE_RC_DEFAULT = "${rcDir}/gitolite.rc.default";
+      };
+
+      serviceConfig = mkMerge [
+        (mkIf (cfg.dataDir == "/var/lib/gitolite") {
+          StateDirectory = "gitolite gitolite/.gitolite gitolite/.gitolite/logs";
+          StateDirectoryMode = "0750";
+        })
+        {
+          Type = "oneshot";
+          User = cfg.user;
+          Group = cfg.group;
+          WorkingDirectory = "~";
+          RemainAfterExit = true;
+        }
+      ];
+
+      path = [ pkgs.gitolite pkgs.git pkgs.perl pkgs.bash pkgs.diffutils config.programs.ssh.package ];
+      script =
+      let
+        rcSetupScriptIfCustomFile =
+          if manageGitoliteRc then ''
+            cat <<END
+            <3>ERROR: NixOS can't apply declarative configuration
+            <3>to your .gitolite.rc file, because it seems to be
+            <3>already customized manually.
+            <3>See the services.gitolite.extraGitoliteRc option
+            <3>in "man configuration.nix" for more information.
+            END
+            # Not sure if the line below addresses the issue directly or just
+            # adds a delay, but without it our error message often doesn't
+            # show up in `systemctl status gitolite-init`.
+            journalctl --flush
+            exit 1
+          '' else ''
+            :
+          '';
+        rcSetupScriptIfDefaultFileOrStoreSymlink =
+          if manageGitoliteRc then ''
+            ln -sf "${rcDir}/gitolite.rc" "$GITOLITE_RC"
+          '' else ''
+            [[ -L "$GITOLITE_RC" ]] && rm -f "$GITOLITE_RC"
+          '';
+      in
+        ''
+          if ( [[ ! -e "$GITOLITE_RC" ]] && [[ ! -L "$GITOLITE_RC" ]] ) ||
+             ( [[ -f "$GITOLITE_RC" ]] && diff -q "$GITOLITE_RC" "$GITOLITE_RC_DEFAULT" >/dev/null ) ||
+             ( [[ -L "$GITOLITE_RC" ]] && [[ "$(readlink "$GITOLITE_RC")" =~ ^/nix/store/ ]] )
+          then
+        '' + rcSetupScriptIfDefaultFileOrStoreSymlink +
+        ''
+          else
+        '' + rcSetupScriptIfCustomFile +
+        ''
+          fi
+
+          if [ ! -d repositories ]; then
+            gitolite setup -pk ${pubkeyFile}
+          fi
+          if [ -n "${hooks}" ]; then
+            cp -f ${hooks} .gitolite/hooks/common/
+            chmod +x .gitolite/hooks/common/*
+          fi
+          gitolite setup # Upgrade if needed
+        '';
+    };
+
+    environment.systemPackages = [ pkgs.gitolite pkgs.git ]
+        ++ optional cfg.enableGitAnnex pkgs.git-annex;
+  });
+}
diff --git a/nixos/modules/services/misc/gitweb.nix b/nixos/modules/services/misc/gitweb.nix
new file mode 100644
index 00000000000..a1180716e36
--- /dev/null
+++ b/nixos/modules/services/misc/gitweb.nix
@@ -0,0 +1,60 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gitweb;
+
+in
+{
+
+  options.services.gitweb = {
+
+    projectroot = mkOption {
+      default = "/srv/git";
+      type = types.path;
+      description = ''
+        Path to git projects (bare repositories) that should be served by
+        gitweb. Must not end with a slash.
+      '';
+    };
+
+    extraConfig = mkOption {
+      default = "";
+      type = types.lines;
+      description = ''
+        Verbatim configuration text appended to the generated gitweb.conf file.
+      '';
+      example = ''
+        $feature{'highlight'}{'default'} = [1];
+        $feature{'ctags'}{'default'} = [1];
+        $feature{'avatar'}{'default'} = ['gravatar'];
+      '';
+    };
+
+    gitwebTheme = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Use an alternative theme for gitweb, strongly inspired by GitHub.
+      '';
+    };
+
+    gitwebConfigFile = mkOption {
+      default = pkgs.writeText "gitweb.conf" ''
+        # path to git projects (<project>.git)
+        $projectroot = "${cfg.projectroot}";
+        $highlight_bin = "${pkgs.highlight}/bin/highlight";
+        ${cfg.extraConfig}
+      '';
+      defaultText = literalDocBook "generated config file";
+      type = types.path;
+      readOnly = true;
+      internal = true;
+    };
+
+  };
+
+  meta.maintainers = with maintainers; [ ];
+
+}
diff --git a/nixos/modules/services/misc/gogs.nix b/nixos/modules/services/misc/gogs.nix
new file mode 100644
index 00000000000..c7ae4f49407
--- /dev/null
+++ b/nixos/modules/services/misc/gogs.nix
@@ -0,0 +1,274 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gogs;
+  opt = options.services.gogs;
+  configFile = pkgs.writeText "app.ini" ''
+    APP_NAME = ${cfg.appName}
+    RUN_USER = ${cfg.user}
+    RUN_MODE = prod
+
+    [database]
+    DB_TYPE = ${cfg.database.type}
+    HOST = ${cfg.database.host}:${toString cfg.database.port}
+    NAME = ${cfg.database.name}
+    USER = ${cfg.database.user}
+    PASSWD = #dbpass#
+    PATH = ${cfg.database.path}
+
+    [repository]
+    ROOT = ${cfg.repositoryRoot}
+
+    [server]
+    DOMAIN = ${cfg.domain}
+    HTTP_ADDR = ${cfg.httpAddress}
+    HTTP_PORT = ${toString cfg.httpPort}
+    ROOT_URL = ${cfg.rootUrl}
+
+    [session]
+    COOKIE_NAME = session
+    COOKIE_SECURE = ${boolToString cfg.cookieSecure}
+
+    [security]
+    SECRET_KEY = #secretkey#
+    INSTALL_LOCK = true
+
+    [log]
+    ROOT_PATH = ${cfg.stateDir}/log
+
+    ${cfg.extraConfig}
+  '';
+in
+
+{
+  options = {
+    services.gogs = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Enable Go Git Service.";
+      };
+
+      useWizard = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Do not generate a configuration and use Gogs' installation wizard instead. The first registered user will be administrator.";
+      };
+
+      stateDir = mkOption {
+        default = "/var/lib/gogs";
+        type = types.str;
+        description = "Gogs data directory.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "gogs";
+        description = "User account under which Gogs runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "gogs";
+        description = "Group account under which Gogs runs.";
+      };
+
+      database = {
+        type = mkOption {
+          type = types.enum [ "sqlite3" "mysql" "postgres" ];
+          example = "mysql";
+          default = "sqlite3";
+          description = "Database engine to use.";
+        };
+
+        host = mkOption {
+          type = types.str;
+          default = "127.0.0.1";
+          description = "Database host address.";
+        };
+
+        port = mkOption {
+          type = types.int;
+          default = 3306;
+          description = "Database host port.";
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "gogs";
+          description = "Database name.";
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "gogs";
+          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;
+          example = "/run/keys/gogs-dbpassword";
+          description = ''
+            A file containing the password corresponding to
+            <option>database.user</option>.
+          '';
+        };
+
+        path = mkOption {
+          type = types.str;
+          default = "${cfg.stateDir}/data/gogs.db";
+          defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/gogs.db"'';
+          description = "Path to the sqlite3 database file.";
+        };
+      };
+
+      appName = mkOption {
+        type = types.str;
+        default = "Gogs: Go Git Service";
+        description = "Application name.";
+      };
+
+      repositoryRoot = mkOption {
+        type = types.str;
+        default = "${cfg.stateDir}/repositories";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/repositories"'';
+        description = "Path to the git repositories.";
+      };
+
+      domain = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Domain name of your server.";
+      };
+
+      rootUrl = mkOption {
+        type = types.str;
+        default = "http://localhost:3000/";
+        description = "Full public URL of Gogs server.";
+      };
+
+      httpAddress = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = "HTTP listen address.";
+      };
+
+      httpPort = mkOption {
+        type = types.int;
+        default = 3000;
+        description = "HTTP listen port.";
+      };
+
+      cookieSecure = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Marks session cookies as "secure" as a hint for browsers to only send
+          them via HTTPS. This option is recommend, if Gogs is being served over HTTPS.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.str;
+        default = "";
+        description = "Configuration lines appended to the generated Gogs configuration file.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services.gogs = {
+      description = "Gogs (Go Git Service)";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.gogs ];
+
+      preStart = let
+        runConfig = "${cfg.stateDir}/custom/conf/app.ini";
+        secretKey = "${cfg.stateDir}/custom/conf/secret_key";
+      in ''
+        mkdir -p ${cfg.stateDir}
+
+        # copy custom configuration and generate a random secret key if needed
+        ${optionalString (cfg.useWizard == false) ''
+          mkdir -p ${cfg.stateDir}/custom/conf
+          cp -f ${configFile} ${runConfig}
+
+          if [ ! -e ${secretKey} ]; then
+              head -c 16 /dev/urandom | base64 > ${secretKey}
+          fi
+
+          KEY=$(head -n1 ${secretKey})
+          DBPASS=$(head -n1 ${cfg.database.passwordFile})
+          sed -e "s,#secretkey#,$KEY,g" \
+              -e "s,#dbpass#,$DBPASS,g" \
+              -i ${runConfig}
+          chmod 440 ${runConfig} ${secretKey}
+        ''}
+
+        mkdir -p ${cfg.repositoryRoot}
+        # update all hooks' binary paths
+        HOOKS=$(find ${cfg.repositoryRoot} -mindepth 4 -maxdepth 4 -type f -wholename "*git/hooks/*")
+        if [ "$HOOKS" ]
+        then
+          sed -ri 's,/nix/store/[a-z0-9.-]+/bin/gogs,${pkgs.gogs}/bin/gogs,g' $HOOKS
+          sed -ri 's,/nix/store/[a-z0-9.-]+/bin/env,${pkgs.coreutils}/bin/env,g' $HOOKS
+          sed -ri 's,/nix/store/[a-z0-9.-]+/bin/bash,${pkgs.bash}/bin/bash,g' $HOOKS
+          sed -ri 's,/nix/store/[a-z0-9.-]+/bin/perl,${pkgs.perl}/bin/perl,g' $HOOKS
+        fi
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        WorkingDirectory = cfg.stateDir;
+        ExecStart = "${pkgs.gogs}/bin/gogs web";
+        Restart = "always";
+      };
+
+      environment = {
+        USER = cfg.user;
+        HOME = cfg.stateDir;
+        GOGS_WORK_DIR = cfg.stateDir;
+      };
+    };
+
+    users = mkIf (cfg.user == "gogs") {
+      users.gogs = {
+        description = "Go Git Service";
+        uid = config.ids.uids.gogs;
+        group = "gogs";
+        home = cfg.stateDir;
+        createHome = true;
+        shell = pkgs.bash;
+      };
+      groups.gogs.gid = config.ids.gids.gogs;
+    };
+
+    warnings = optional (cfg.database.password != "")
+      ''config.services.gogs.database.password will be stored as plaintext
+        in the Nix store. Use database.passwordFile instead.'';
+
+    # Create database passwordFile default when password is configured.
+    services.gogs.database.passwordFile =
+      (mkDefault (toString (pkgs.writeTextFile {
+        name = "gogs-database-password";
+        text = cfg.database.password;
+      })));
+  };
+}
diff --git a/nixos/modules/services/misc/gollum.nix b/nixos/modules/services/misc/gollum.nix
new file mode 100644
index 00000000000..cad73a871ba
--- /dev/null
+++ b/nixos/modules/services/misc/gollum.nix
@@ -0,0 +1,121 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gollum;
+in
+
+{
+  options.services.gollum = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Enable the Gollum service.";
+    };
+
+    address = mkOption {
+      type = types.str;
+      default = "0.0.0.0";
+      description = "IP address on which the web server will listen.";
+    };
+
+    port = mkOption {
+      type = types.int;
+      default = 4567;
+      description = "Port on which the web server will run.";
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = "Content of the configuration file";
+    };
+
+    mathjax = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Enable support for math rendering using MathJax";
+    };
+
+    allowUploads = mkOption {
+      type = types.nullOr (types.enum [ "dir" "page" ]);
+      default = null;
+      description = "Enable uploads of external files";
+    };
+
+    emoji = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Parse and interpret emoji tags";
+    };
+
+    h1-title = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Use the first h1 as page title";
+    };
+
+    branch = mkOption {
+      type = types.str;
+      default = "master";
+      example = "develop";
+      description = "Git branch to serve";
+    };
+
+    stateDir = mkOption {
+      type = types.path;
+      default = "/var/lib/gollum";
+      description = "Specifies the path of the repository directory. If it does not exist, Gollum will create it on startup.";
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    users.users.gollum = {
+      group = config.users.users.gollum.name;
+      description = "Gollum user";
+      createHome = false;
+      isSystemUser = true;
+    };
+
+    users.groups.gollum = { };
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.stateDir}' - ${config.users.users.gollum.name} ${config.users.groups.gollum.name} - -"
+    ];
+
+    systemd.services.gollum = {
+      description = "Gollum wiki";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.git ];
+
+      preStart = ''
+        # This is safe to be run on an existing repo
+        git init ${cfg.stateDir}
+      '';
+
+      serviceConfig = {
+        User = config.users.users.gollum.name;
+        Group = config.users.groups.gollum.name;
+        WorkingDirectory = cfg.stateDir;
+        ExecStart = ''
+          ${pkgs.gollum}/bin/gollum \
+            --port ${toString cfg.port} \
+            --host ${cfg.address} \
+            --config ${pkgs.writeText "gollum-config.rb" cfg.extraConfig} \
+            --ref ${cfg.branch} \
+            ${optionalString cfg.mathjax "--mathjax"} \
+            ${optionalString cfg.emoji "--emoji"} \
+            ${optionalString cfg.h1-title "--h1-title"} \
+            ${optionalString (cfg.allowUploads != null) "--allow-uploads ${cfg.allowUploads}"} \
+            ${cfg.stateDir}
+        '';
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ erictapen ];
+}
diff --git a/nixos/modules/services/misc/gpsd.nix b/nixos/modules/services/misc/gpsd.nix
new file mode 100644
index 00000000000..6494578f764
--- /dev/null
+++ b/nixos/modules/services/misc/gpsd.nix
@@ -0,0 +1,116 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  uid = config.ids.uids.gpsd;
+  gid = config.ids.gids.gpsd;
+  cfg = config.services.gpsd;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.gpsd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable `gpsd', a GPS service daemon.
+        '';
+      };
+
+      device = mkOption {
+        type = types.str;
+        default = "/dev/ttyUSB0";
+        description = ''
+          A device may be a local serial device for GPS input, or a URL of the form:
+               <literal>[{dgpsip|ntrip}://][user:passwd@]host[:port][/stream]</literal>
+          in which case it specifies an input source for DGPS or ntrip data.
+        '';
+      };
+
+      readonly = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable the broken-device-safety, otherwise
+          known as read-only mode.  Some popular bluetooth and USB
+          receivers lock up or become totally inaccessible when
+          probed or reconfigured.  This switch prevents gpsd from
+          writing to a receiver.  This means that gpsd cannot
+          configure the receiver for optimal performance, but it
+          also means that gpsd cannot break the receiver.  A better
+          solution would be for Bluetooth to not be so fragile.  A
+          platform independent method to identify
+          serial-over-Bluetooth devices would also be nice.
+        '';
+      };
+
+      nowait = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          don't wait for client connects to poll GPS
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 2947;
+        description = ''
+          The port where to listen for TCP connections.
+        '';
+      };
+
+      debugLevel = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          The debugging level.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.gpsd =
+      { inherit uid;
+        group = "gpsd";
+        description = "gpsd daemon user";
+        home = "/var/empty";
+      };
+
+    users.groups.gpsd = { inherit gid; };
+
+    systemd.services.gpsd = {
+      description = "GPSD daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = ''
+          ${pkgs.gpsd}/sbin/gpsd -D "${toString cfg.debugLevel}"  \
+            -S "${toString cfg.port}"                             \
+            ${optionalString cfg.readonly "-b"}                   \
+            ${optionalString cfg.nowait "-n"}                     \
+            "${cfg.device}"
+        '';
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/greenclip.nix b/nixos/modules/services/misc/greenclip.nix
new file mode 100644
index 00000000000..32e8d746cb5
--- /dev/null
+++ b/nixos/modules/services/misc/greenclip.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.greenclip;
+in {
+
+  options.services.greenclip = {
+    enable = mkEnableOption "Greenclip daemon";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.haskellPackages.greenclip;
+      defaultText = literalExpression "pkgs.haskellPackages.greenclip";
+      description = "greenclip derivation to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.greenclip = {
+      enable      = true;
+      description = "greenclip daemon";
+      wantedBy = [ "graphical-session.target" ];
+      after    = [ "graphical-session.target" ];
+      serviceConfig.ExecStart = "${cfg.package}/bin/greenclip daemon";
+    };
+
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/misc/headphones.nix b/nixos/modules/services/misc/headphones.nix
new file mode 100644
index 00000000000..31bd61cb4c2
--- /dev/null
+++ b/nixos/modules/services/misc/headphones.nix
@@ -0,0 +1,89 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+
+  name = "headphones";
+
+  cfg = config.services.headphones;
+  opt = options.services.headphones;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+    services.headphones = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the headphones server.";
+      };
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/${name}";
+        description = "Path where to store data files.";
+      };
+      configFile = mkOption {
+        type = types.path;
+        default = "${cfg.dataDir}/config.ini";
+        defaultText = literalExpression ''"''${config.${opt.dataDir}}/config.ini"'';
+        description = "Path to config file.";
+      };
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Host to listen on.";
+      };
+      port = mkOption {
+        type = types.ints.u16;
+        default = 8181;
+        description = "Port to bind to.";
+      };
+      user = mkOption {
+        type = types.str;
+        default = name;
+        description = "User to run the service as";
+      };
+      group = mkOption {
+        type = types.str;
+        default = name;
+        description = "Group to run the service as";
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users = optionalAttrs (cfg.user == name) {
+      ${name} = {
+        uid = config.ids.uids.headphones;
+        group = cfg.group;
+        description = "headphones user";
+        home = cfg.dataDir;
+        createHome = true;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == name) {
+      ${name}.gid = config.ids.gids.headphones;
+    };
+
+    systemd.services.headphones = {
+        description = "Headphones Server";
+        wantedBy    = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        serviceConfig = {
+          User = cfg.user;
+          Group = cfg.group;
+          ExecStart = "${pkgs.headphones}/bin/headphones --datadir ${cfg.dataDir} --config ${cfg.configFile} --host ${cfg.host} --port ${toString cfg.port}";
+        };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/heisenbridge.nix b/nixos/modules/services/misc/heisenbridge.nix
new file mode 100644
index 00000000000..7ce8a23d9af
--- /dev/null
+++ b/nixos/modules/services/misc/heisenbridge.nix
@@ -0,0 +1,222 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.heisenbridge;
+
+  pkg = config.services.heisenbridge.package;
+  bin = "${pkg}/bin/heisenbridge";
+
+  jsonType = (pkgs.formats.json { }).type;
+
+  registrationFile = "/var/lib/heisenbridge/registration.yml";
+  # JSON is a proper subset of YAML
+  bridgeConfig = builtins.toFile "heisenbridge-registration.yml" (builtins.toJSON {
+    id = "heisenbridge";
+    url = cfg.registrationUrl;
+    # Don't specify as_token and hs_token
+    rate_limited = false;
+    sender_localpart = "heisenbridge";
+    namespaces = cfg.namespaces;
+  });
+in
+{
+  options.services.heisenbridge = {
+    enable = mkEnableOption "the Matrix to IRC bridge";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.heisenbridge;
+      defaultText = "pkgs.heisenbridge";
+      example = "pkgs.heisenbridge.override { … = …; }";
+      description = ''
+        Package of the application to run, exposed for overriding purposes.
+      '';
+    };
+
+    homeserver = mkOption {
+      type = types.str;
+      description = "The URL to the home server for client-server API calls";
+      example = "http://localhost:8008";
+    };
+
+    registrationUrl = mkOption {
+      type = types.str;
+      description = ''
+        The URL where the application service is listening for HS requests, from the Matrix HS perspective.#
+        The default value assumes the bridge runs on the same host as the home server, in the same network.
+      '';
+      example = "https://matrix.example.org";
+      default = "http://${cfg.address}:${toString cfg.port}";
+      defaultText = "http://$${cfg.address}:$${toString cfg.port}";
+    };
+
+    address = mkOption {
+      type = types.str;
+      description = "Address to listen on. IPv6 does not seem to be supported.";
+      default = "127.0.0.1";
+      example = "0.0.0.0";
+    };
+
+    port = mkOption {
+      type = types.port;
+      description = "The port to listen on";
+      default = 9898;
+    };
+
+    debug = mkOption {
+      type = types.bool;
+      description = "More verbose logging. Recommended during initial setup.";
+      default = false;
+    };
+
+    owner = mkOption {
+      type = types.nullOr types.str;
+      description = ''
+        Set owner MXID otherwise first talking local user will claim the bridge
+      '';
+      default = null;
+      example = "@admin:example.org";
+    };
+
+    namespaces = mkOption {
+      description = "Configure the 'namespaces' section of the registration.yml for the bridge and the server";
+      # TODO link to Matrix documentation of the format
+      type = types.submodule {
+        freeformType = jsonType;
+      };
+
+      default = {
+        users = [
+          {
+            regex = "@irc_.*";
+            exclusive = true;
+          }
+        ];
+        aliases = [ ];
+        rooms = [ ];
+      };
+    };
+
+    identd.enable = mkEnableOption "identd service support";
+    identd.port = mkOption {
+      type = types.port;
+      description = "identd listen port";
+      default = 113;
+    };
+
+    extraArgs = mkOption {
+      type = types.listOf types.str;
+      description = "Heisenbridge is configured over the command line. Append extra arguments here";
+      default = [ ];
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.heisenbridge = {
+      description = "Matrix<->IRC bridge";
+      before = [ "matrix-synapse.service" ]; # So the registration file can be used by Synapse
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        umask 077
+        set -e -u -o pipefail
+
+        if ! [ -f "${registrationFile}" ]; then
+          # Generate registration file if not present (actually, we only care about the tokens in it)
+          ${bin} --generate --config ${registrationFile}
+        fi
+
+        # Overwrite the registration file with our generated one (the config may have changed since then),
+        # but keep the tokens. Two step procedure to be failure safe
+        ${pkgs.yq}/bin/yq --slurp \
+          '.[0] + (.[1] | {as_token, hs_token})' \
+          ${bridgeConfig} \
+          ${registrationFile} \
+          > ${registrationFile}.new
+        mv -f ${registrationFile}.new ${registrationFile}
+
+        # Grant Synapse access to the registration
+        if ${getBin pkgs.glibc}/bin/getent group matrix-synapse > /dev/null; then
+          chgrp -v matrix-synapse ${registrationFile}
+          chmod -v g+r ${registrationFile}
+        fi
+      '';
+
+      serviceConfig = rec {
+        Type = "simple";
+        ExecStart = lib.concatStringsSep " " (
+          [
+            bin
+            (if cfg.debug then "-vvv" else "-v")
+            "--config"
+            registrationFile
+            "--listen-address"
+            (lib.escapeShellArg cfg.address)
+            "--listen-port"
+            (toString cfg.port)
+          ]
+          ++ (lib.optionals (cfg.owner != null) [
+            "--owner"
+            (lib.escapeShellArg cfg.owner)
+          ])
+          ++ (lib.optionals cfg.identd.enable [
+            "--identd"
+            "--identd-port"
+            (toString cfg.identd.port)
+          ])
+          ++ [
+            (lib.escapeShellArg cfg.homeserver)
+          ]
+          ++ (map (lib.escapeShellArg) cfg.extraArgs)
+        );
+
+        # Hardening options
+
+        User = "heisenbridge";
+        Group = "heisenbridge";
+        RuntimeDirectory = "heisenbridge";
+        RuntimeDirectoryMode = "0700";
+        StateDirectory = "heisenbridge";
+        StateDirectoryMode = "0755";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        RestrictSUIDSGID = true;
+        PrivateMounts = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
+        RestrictNamespaces = true;
+        RemoveIPC = true;
+        UMask = "0077";
+
+        CapabilityBoundingSet = [ "CAP_CHOWN" ] ++ optional (cfg.port < 1024 || (cfg.identd.enable && cfg.identd.port < 1024)) "CAP_NET_BIND_SERVICE";
+        AmbientCapabilities = CapabilityBoundingSet;
+        NoNewPrivileges = true;
+        LockPersonality = true;
+        RestrictRealtime = true;
+        SystemCallFilter = ["@system-service" "~@priviledged" "@chown"];
+        SystemCallArchitectures = "native";
+        RestrictAddressFamilies = "AF_INET AF_INET6";
+      };
+    };
+
+    users.groups.heisenbridge = {};
+    users.users.heisenbridge = {
+      description = "Service user for the Heisenbridge";
+      group = "heisenbridge";
+      isSystemUser = true;
+    };
+  };
+
+  meta.maintainers = [ lib.maintainers.piegames ];
+}
diff --git a/nixos/modules/services/misc/ihaskell.nix b/nixos/modules/services/misc/ihaskell.nix
new file mode 100644
index 00000000000..9978e8a4653
--- /dev/null
+++ b/nixos/modules/services/misc/ihaskell.nix
@@ -0,0 +1,65 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.ihaskell;
+  ihaskell = pkgs.ihaskell.override {
+    packages = cfg.extraPackages;
+  };
+
+in
+
+{
+  options = {
+    services.ihaskell = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Autostart an IHaskell notebook service.";
+      };
+
+      extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
+        default = haskellPackages: [];
+        defaultText = literalExpression "haskellPackages: []";
+        example = literalExpression ''
+          haskellPackages: [
+            haskellPackages.wreq
+            haskellPackages.lens
+          ]
+        '';
+        description = ''
+          Extra packages available to ghc when running ihaskell. The
+          value must be a function which receives the attrset defined
+          in <varname>haskellPackages</varname> as the sole argument.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    users.users.ihaskell = {
+      group = config.users.groups.ihaskell.name;
+      description = "IHaskell user";
+      home = "/var/lib/ihaskell";
+      createHome = true;
+      uid = config.ids.uids.ihaskell;
+    };
+
+    users.groups.ihaskell.gid = config.ids.gids.ihaskell;
+
+    systemd.services.ihaskell = {
+      description = "IHaskell notebook instance";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        User = config.users.users.ihaskell.name;
+        Group = config.users.groups.ihaskell.name;
+        ExecStart = "${pkgs.runtimeShell} -c \"cd $HOME;${ihaskell}/bin/ihaskell-notebook\"";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/input-remapper.nix b/nixos/modules/services/misc/input-remapper.nix
new file mode 100644
index 00000000000..f5fb2bf5308
--- /dev/null
+++ b/nixos/modules/services/misc/input-remapper.nix
@@ -0,0 +1,30 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+
+let cfg = config.services.input-remapper; in
+{
+  options = {
+    services.input-remapper = {
+      enable = mkEnableOption "input-remapper, an easy to use tool to change the mapping of your input device buttons.";
+      package = options.mkPackageOption pkgs "input-remapper" { };
+      enableUdevRules = mkEnableOption "udev rules added by input-remapper to handle hotplugged devices. Currently disabled by default due to https://github.com/sezanzeb/input-remapper/issues/140";
+      serviceWantedBy = mkOption {
+        default = [ "graphical.target" ];
+        example = [ "multi-user.target" ];
+        type = types.listOf types.str;
+        description = "Specifies the WantedBy setting for the input-remapper service.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.packages = mkIf cfg.enableUdevRules [ cfg.package ];
+    services.dbus.packages = [ cfg.package ];
+    systemd.packages = [ cfg.package ];
+    environment.systemPackages = [ cfg.package ];
+    systemd.services.input-remapper.wantedBy = cfg.serviceWantedBy;
+  };
+
+  meta.maintainers = with lib.maintainers; [ LunNova ];
+}
diff --git a/nixos/modules/services/misc/irkerd.nix b/nixos/modules/services/misc/irkerd.nix
new file mode 100644
index 00000000000..993d77ba424
--- /dev/null
+++ b/nixos/modules/services/misc/irkerd.nix
@@ -0,0 +1,67 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.irkerd;
+  ports = [ 6659 ];
+in
+{
+  options.services.irkerd = {
+    enable = mkOption {
+      description = "Whether to enable irker, an IRC notification daemon.";
+      default = false;
+      type = types.bool;
+    };
+
+    openPorts = mkOption {
+      description = "Open ports in the firewall for irkerd";
+      default = false;
+      type = types.bool;
+    };
+
+    listenAddress = mkOption {
+      default = "localhost";
+      example = "0.0.0.0";
+      type = types.str;
+      description = ''
+        Specifies the bind address on which the irker daemon listens.
+        The default is localhost.
+
+        Irker authors strongly warn about the risks of running this on
+        a publicly accessible interface, so change this with caution.
+      '';
+    };
+
+    nick = mkOption {
+      default = "irker";
+      type = types.str;
+      description = "Nick to use for irker";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.irkerd = {
+      description = "Internet Relay Chat (IRC) notification daemon";
+      documentation = [ "man:irkerd(8)" "man:irkerhook(1)" "man:irk(1)" ];
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.irker}/bin/irkerd -H ${cfg.listenAddress} -n ${cfg.nick}";
+        User = "irkerd";
+      };
+    };
+
+    environment.systemPackages = [ pkgs.irker ];
+
+    users.users.irkerd = {
+      description = "Irker daemon user";
+      isSystemUser = true;
+      group = "irkerd";
+    };
+    users.groups.irkerd = {};
+
+    networking.firewall.allowedTCPPorts = mkIf cfg.openPorts ports;
+    networking.firewall.allowedUDPPorts = mkIf cfg.openPorts ports;
+  };
+}
diff --git a/nixos/modules/services/misc/jackett.nix b/nixos/modules/services/misc/jackett.nix
new file mode 100644
index 00000000000..c2144d4a9a9
--- /dev/null
+++ b/nixos/modules/services/misc/jackett.nix
@@ -0,0 +1,82 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.jackett;
+
+in
+{
+  options = {
+    services.jackett = {
+      enable = mkEnableOption "Jackett";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/jackett/.config/Jackett";
+        description = "The directory where Jackett stores its data files.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the Jackett web interface.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "jackett";
+        description = "User account under which Jackett runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "jackett";
+        description = "Group under which Jackett runs.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.jackett;
+        defaultText = literalExpression "pkgs.jackett";
+        description = "Jackett package to use.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0700 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.jackett = {
+      description = "Jackett";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${cfg.package}/bin/Jackett --NoUpdates --DataFolder '${cfg.dataDir}'";
+        Restart = "on-failure";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 9117 ];
+    };
+
+    users.users = mkIf (cfg.user == "jackett") {
+      jackett = {
+        group = cfg.group;
+        home = cfg.dataDir;
+        uid = config.ids.uids.jackett;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "jackett") {
+      jackett.gid = config.ids.gids.jackett;
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/jellyfin.nix b/nixos/modules/services/misc/jellyfin.nix
new file mode 100644
index 00000000000..04cf82f8a46
--- /dev/null
+++ b/nixos/modules/services/misc/jellyfin.nix
@@ -0,0 +1,122 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.jellyfin;
+in
+{
+  options = {
+    services.jellyfin = {
+      enable = mkEnableOption "Jellyfin Media Server";
+
+      user = mkOption {
+        type = types.str;
+        default = "jellyfin";
+        description = "User account under which Jellyfin runs.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.jellyfin;
+        defaultText = literalExpression "pkgs.jellyfin";
+        description = ''
+          Jellyfin package to use.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        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.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.jellyfin = {
+      description = "Jellyfin Media Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = rec {
+        User = cfg.user;
+        Group = cfg.group;
+        StateDirectory = "jellyfin";
+        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;
+        # Disabled to allow Jellyfin to access hw accel devices endpoints
+        # PrivateDevices = true;
+        PrivateUsers = true;
+
+        # Disabled as it does not allow Jellyfin to interface with CUDA devices
+        # 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" "AF_UNIX" ];
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = [
+          "@system-service"
+          "~@cpu-emulation" "~@debug" "~@keyring" "~@memlock" "~@obsolete" "~@privileged" "~@setuid"
+        ];
+      };
+    };
+
+    users.users = mkIf (cfg.user == "jellyfin") {
+      jellyfin = {
+        group = cfg.group;
+        isSystemUser = true;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "jellyfin") {
+      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..7b3780b5cc9
--- /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;
+        defaultText = literalExpression "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 = "/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
new file mode 100644
index 00000000000..f797218522c
--- /dev/null
+++ b/nixos/modules/services/misc/leaps.nix
@@ -0,0 +1,62 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.leaps;
+  stateDir = "/var/lib/leaps/";
+in
+{
+  options = {
+    services.leaps = {
+      enable = mkEnableOption "leaps";
+      port = mkOption {
+        type = types.port;
+        default = 8080;
+        description = "A port where leaps listens for incoming http requests";
+      };
+      address = mkOption {
+        default = "";
+        type = types.str;
+        example = "127.0.0.1";
+        description = "Hostname or IP-address to listen to. By default it will listen on all interfaces.";
+      };
+      path = mkOption {
+        default = "/";
+        type = types.path;
+        description = "Subdirectory used for reverse proxy setups";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users = {
+      users.leaps = {
+        uid             = config.ids.uids.leaps;
+        description     = "Leaps server user";
+        group           = "leaps";
+        home            = stateDir;
+        createHome      = true;
+      };
+
+      groups.leaps = {
+        gid = config.ids.gids.leaps;
+      };
+    };
+
+    systemd.services.leaps = {
+      description   = "leaps service";
+      wantedBy      = [ "multi-user.target" ];
+      after         = [ "network.target" ];
+
+      serviceConfig = {
+        User = "leaps";
+        Group = "leaps";
+        Restart = "on-failure";
+        WorkingDirectory = stateDir;
+        PrivateTmp = true;
+        ExecStart = "${pkgs.leaps}/bin/leaps -path ${toString cfg.path} -address ${cfg.address}:${toString cfg.port}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/libreddit.nix b/nixos/modules/services/misc/libreddit.nix
new file mode 100644
index 00000000000..77b34a85620
--- /dev/null
+++ b/nixos/modules/services/misc/libreddit.nix
@@ -0,0 +1,66 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+  let
+    cfg = config.services.libreddit;
+
+    args = concatStringsSep " " ([
+      "--port ${toString cfg.port}"
+      "--address ${cfg.address}"
+    ] ++ optional cfg.redirect "--redirect-https");
+
+in
+{
+  options = {
+    services.libreddit = {
+      enable = mkEnableOption "Private front-end for Reddit";
+
+      address = mkOption {
+        default = "0.0.0.0";
+        example = "127.0.0.1";
+        type =  types.str;
+        description = "The address to listen on";
+      };
+
+      port = mkOption {
+        default = 8080;
+        example = 8000;
+        type = types.port;
+        description = "The port to listen on";
+      };
+
+      redirect = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable the redirecting to HTTPS";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the libreddit web interface";
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.libreddit = {
+        description = "Private front-end for Reddit";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        serviceConfig = {
+          DynamicUser = true;
+          ExecStart = "${pkgs.libreddit}/bin/libreddit ${args}";
+          AmbientCapabilities = lib.mkIf (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
+          Restart = "on-failure";
+          RestartSec = "2s";
+        };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/lidarr.nix b/nixos/modules/services/misc/lidarr.nix
new file mode 100644
index 00000000000..20153c7e61a
--- /dev/null
+++ b/nixos/modules/services/misc/lidarr.nix
@@ -0,0 +1,89 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.lidarr;
+in
+{
+  options = {
+    services.lidarr = {
+      enable = mkEnableOption "Lidarr";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/lidarr/.config/Lidarr";
+        description = "The directory where Lidarr stores its data files.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.lidarr;
+        defaultText = literalExpression "pkgs.lidarr";
+        description = "The Lidarr package to use";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports in the firewall for Lidarr
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "lidarr";
+        description = ''
+          User account under which Lidarr runs.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "lidarr";
+        description = ''
+          Group under which Lidarr runs.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0700 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.lidarr = {
+      description = "Lidarr";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${cfg.package}/bin/Lidarr -nobrowser -data='${cfg.dataDir}'";
+        Restart = "on-failure";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 8686 ];
+    };
+
+    users.users = mkIf (cfg.user == "lidarr") {
+      lidarr = {
+        group = cfg.group;
+        home = "/var/lib/lidarr";
+        uid = config.ids.uids.lidarr;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "lidarr") {
+      lidarr = {
+        gid = config.ids.gids.lidarr;
+      };
+    };
+  };
+}
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/logkeys.nix b/nixos/modules/services/misc/logkeys.nix
new file mode 100644
index 00000000000..0082db63a06
--- /dev/null
+++ b/nixos/modules/services/misc/logkeys.nix
@@ -0,0 +1,30 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.logkeys;
+in {
+  options.services.logkeys = {
+    enable = mkEnableOption "logkeys service";
+
+    device = mkOption {
+      description = "Use the given device as keyboard input event device instead of /dev/input/eventX default.";
+      default = null;
+      type = types.nullOr types.str;
+      example = "/dev/input/event15";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.logkeys = {
+      description = "LogKeys Keylogger Daemon";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.logkeys}/bin/logkeys -s${lib.optionalString (cfg.device != null) " -d ${cfg.device}"}";
+        ExecStop = "${pkgs.logkeys}/bin/logkeys -k";
+        Type = "forking";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/mame.nix b/nixos/modules/services/misc/mame.nix
new file mode 100644
index 00000000000..dd6c5ef9aa0
--- /dev/null
+++ b/nixos/modules/services/misc/mame.nix
@@ -0,0 +1,69 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mame;
+  mame = "mame${lib.optionalString pkgs.stdenv.is64bit "64"}";
+in
+{
+  options = {
+    services.mame = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to setup TUN/TAP Ethernet interface for MAME emulator.
+        '';
+      };
+      user = mkOption {
+        type = types.str;
+        description = ''
+          User from which you run MAME binary.
+        '';
+      };
+      hostAddr = mkOption {
+        type = types.str;
+        description = ''
+          IP address of the host system. Usually an address of the main network
+          adapter or the adapter through which you get an internet connection.
+        '';
+        example = "192.168.31.156";
+      };
+      emuAddr = mkOption {
+        type = types.str;
+        description = ''
+          IP address of the guest system. The same you set inside guest OS under
+          MAME. Should be on the same subnet as <option>services.mame.hostAddr</option>.
+        '';
+        example = "192.168.31.155";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.mame ];
+
+    security.wrappers."${mame}" = {
+      owner = "root";
+      group = "root";
+      capabilities = "cap_net_admin,cap_net_raw+eip";
+      source = "${pkgs.mame}/bin/${mame}";
+    };
+
+    systemd.services.mame = {
+      description = "MAME TUN/TAP Ethernet interface";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.iproute2 ];
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        ExecStart = "${pkgs.mame}/bin/taputil.sh -c ${cfg.user} ${cfg.emuAddr} ${cfg.hostAddr} -";
+        ExecStop = "${pkgs.mame}/bin/taputil.sh -d ${cfg.user}";
+      };
+    };
+  };
+
+  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
new file mode 100644
index 00000000000..8a8c7f41e3c
--- /dev/null
+++ b/nixos/modules/services/misc/matrix-appservice-discord.nix
@@ -0,0 +1,161 @@
+{ config, options, pkgs, lib, ... }:
+
+with lib;
+
+let
+  dataDir = "/var/lib/matrix-appservice-discord";
+  registrationFile = "${dataDir}/discord-registration.yaml";
+  appDir = "${pkgs.matrix-appservice-discord}/${pkgs.matrix-appservice-discord.passthru.nodeAppDir}";
+  cfg = config.services.matrix-appservice-discord;
+  opt = options.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);
+
+in {
+  options = {
+    services.matrix-appservice-discord = {
+      enable = mkEnableOption "a bridge between Matrix and Discord";
+
+      settings = mkOption rec {
+        # TODO: switch to types.config.json as prescribed by RFC42 once it's implemented
+        type = types.attrs;
+        apply = recursiveUpdate default;
+        default = {
+          database = {
+            filename = "${dataDir}/discord.db";
+          };
+
+          # empty values necessary for registration file generation
+          # actual values defined in environmentFile
+          auth = {
+            clientID = "";
+            botToken = "";
+          };
+        };
+        example = literalExpression ''
+          {
+            bridge = {
+              domain = "public-domain.tld";
+              homeserverUrl = "http://public-domain.tld:8008";
+            };
+          }
+        '';
+        description = ''
+          <filename>config.yaml</filename> configuration as a Nix attribute set.
+          </para>
+
+          <para>
+          Configuration options should match those described in
+          <link xlink:href="https://github.com/Half-Shot/matrix-appservice-discord/blob/master/config/config.sample.yaml">
+          config.sample.yaml</link>.
+          </para>
+
+          <para>
+          <option>config.bridge.domain</option> and <option>config.bridge.homeserverUrl</option>
+          should be set to match the public host name of the Matrix homeserver for webhooks and avatars to work.
+          </para>
+
+          <para>
+          Secret tokens should be specified using <option>environmentFile</option>
+          instead of this world-readable attribute set.
+        '';
+      };
+
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          File containing environment variables to be passed to the matrix-appservice-discord service,
+          in which secret tokens can be specified securely by defining values for
+          <literal>APPSERVICE_DISCORD_AUTH_CLIENT_I_D</literal> and
+          <literal>APPSERVICE_DISCORD_AUTH_BOT_TOKEN</literal>.
+        '';
+      };
+
+      url = mkOption {
+        type = types.str;
+        default = "http://localhost:${toString cfg.port}";
+        defaultText = literalExpression ''"http://localhost:''${toString config.${opt.port}}"'';
+        description = ''
+          The URL where the application service is listening for HS requests.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 9005; # from https://github.com/Half-Shot/matrix-appservice-discord/blob/master/package.json#L11
+        description = ''
+          Port number on which the bridge should listen for internal communication with the Matrix homeserver.
+        '';
+      };
+
+      localpart = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          The user_id localpart to assign to the AS.
+        '';
+      };
+
+      serviceDependencies = mkOption {
+        type = with types; listOf str;
+        default = optional config.services.matrix-synapse.enable "matrix-synapse.service";
+        defaultText = literalExpression ''
+          optional config.services.matrix-synapse.enable "matrix-synapse.service"
+        '';
+        description = ''
+          List of Systemd services to require and wait for when starting the application service,
+          such as the Matrix homeserver if it's running on the same host.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.matrix-appservice-discord = {
+      description = "A bridge between Matrix and Discord.";
+
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
+      after = [ "network-online.target" ] ++ cfg.serviceDependencies;
+
+      preStart = ''
+        if [ ! -f '${registrationFile}' ]; then
+          ${pkgs.matrix-appservice-discord}/bin/matrix-appservice-discord \
+            --generate-registration \
+            --url=${escapeShellArg cfg.url} \
+            ${optionalString (cfg.localpart != null) "--localpart=${escapeShellArg cfg.localpart}"} \
+            --config='${settingsFile}' \
+            --file='${registrationFile}'
+        fi
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+
+        DynamicUser = true;
+        PrivateTmp = true;
+        WorkingDirectory = appDir;
+        StateDirectory = baseNameOf dataDir;
+        UMask = 0027;
+        EnvironmentFile = cfg.environmentFile;
+
+        ExecStart = ''
+          ${pkgs.matrix-appservice-discord}/bin/matrix-appservice-discord \
+            --file='${registrationFile}' \
+            --config='${settingsFile}' \
+            --port='${toString cfg.port}'
+        '';
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ pacien ];
+}
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..b041c9c82c5
--- /dev/null
+++ b/nixos/modules/services/misc/matrix-appservice-irc.nix
@@ -0,0 +1,232 @@
+{ 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.runCommand "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;
+    };
+  };
+
+  # uses attributes of the linked package
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/misc/matrix-conduit.nix b/nixos/modules/services/misc/matrix-conduit.nix
new file mode 100644
index 00000000000..108f64de7aa
--- /dev/null
+++ b/nixos/modules/services/misc/matrix-conduit.nix
@@ -0,0 +1,149 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.matrix-conduit;
+
+  format = pkgs.formats.toml {};
+  configFile = format.generate "conduit.toml" cfg.settings;
+in
+  {
+    meta.maintainers = with maintainers; [ pstn piegames ];
+    options.services.matrix-conduit = {
+      enable = mkEnableOption "matrix-conduit";
+
+      extraEnvironment = mkOption {
+        type = types.attrsOf types.str;
+        description = "Extra Environment variables to pass to the conduit server.";
+        default = {};
+        example = { RUST_BACKTRACE="yes"; };
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.matrix-conduit;
+        defaultText = "pkgs.matrix-conduit";
+        example = "pkgs.matrix-conduit";
+        description = ''
+          Package of the conduit matrix server to use.
+        '';
+      };
+
+      settings = mkOption {
+        type = types.submodule {
+          freeformType = format.type;
+          options = {
+            global.server_name = mkOption {
+              type = types.str;
+              example = "example.com";
+              description = "The server_name is the name of this server. It is used as a suffix for user # and room ids.";
+            };
+            global.port = mkOption {
+              type = types.port;
+              default = 6167;
+              description = "The port Conduit will be running on. You need to set up a reverse proxy in your web server (e.g. apache or nginx), so all requests to /_matrix on port 443 and 8448 will be forwarded to the Conduit instance running on this port";
+            };
+            global.max_request_size = mkOption {
+              type = types.ints.positive;
+              default = 20000000;
+              description = "Max request size in bytes. Don't forget to also change it in the proxy.";
+            };
+            global.allow_registration = mkOption {
+              type = types.bool;
+              default = false;
+              description = "Whether new users can register on this server.";
+            };
+            global.allow_encryption = mkOption {
+              type = types.bool;
+              default = true;
+              description = "Whether new encrypted rooms can be created. Note: existing rooms will continue to work.";
+            };
+            global.allow_federation = mkOption {
+              type = types.bool;
+              default = true;
+              description = ''
+                Whether this server federates with other servers.
+              '';
+            };
+            global.trusted_servers = mkOption {
+              type = types.listOf types.str;
+              default = [ "matrix.org" ];
+              description = "Servers trusted with signing server keys.";
+            };
+            global.address = mkOption {
+              type = types.str;
+              default = "::1";
+              description = "Address to listen on for connections by the reverse proxy/tls terminator.";
+            };
+            global.database_path = mkOption {
+              type = types.str;
+              default = "/var/lib/matrix-conduit/";
+              readOnly = true;
+              description = ''
+                Path to the conduit database, the directory where conduit will save its data.
+                Note that due to using the DynamicUser feature of systemd, this value should not be changed
+                and is set to be read only.
+              '';
+            };
+            global.database_backend = mkOption {
+              type = types.enum [ "sqlite" "rocksdb" ];
+              default = "sqlite";
+              example = "rocksdb";
+              description = ''
+                The database backend for the service. Switching it on an existing
+                instance will require manual migration of data.
+              '';
+            };
+          };
+        };
+        default = {};
+        description = ''
+            Generates the conduit.toml configuration file. Refer to
+            <link xlink:href="https://gitlab.com/famedly/conduit/-/blob/master/conduit-example.toml"/>
+            for details on supported values.
+            Note that database_path can not be edited because the service's reliance on systemd StateDir.
+        '';
+      };
+    };
+
+    config = mkIf cfg.enable {
+      systemd.services.conduit = {
+        description = "Conduit Matrix Server";
+        documentation = [ "https://gitlab.com/famedly/conduit/" ];
+        wantedBy = [ "multi-user.target" ];
+        environment = lib.mkMerge ([
+          { CONDUIT_CONFIG = configFile; }
+          cfg.extraEnvironment
+        ]);
+        serviceConfig = {
+          DynamicUser = true;
+          User = "conduit";
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          ProtectClock = true;
+          ProtectControlGroups = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          PrivateDevices = true;
+          PrivateMounts = true;
+          PrivateUsers = true;
+          RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [
+            "@system-service"
+            "~@privileged"
+          ];
+          StateDirectory = "matrix-conduit";
+          ExecStart = "${cfg.package}/bin/conduit";
+          Restart = "on-failure";
+          RestartSec = 10;
+          StartLimitBurst = 5;
+        };
+      };
+    };
+  }
diff --git a/nixos/modules/services/misc/mautrix-facebook.nix b/nixos/modules/services/misc/mautrix-facebook.nix
new file mode 100644
index 00000000000..e046c791ac0
--- /dev/null
+++ b/nixos/modules/services/misc/mautrix-facebook.nix
@@ -0,0 +1,195 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mautrix-facebook;
+  settingsFormat = pkgs.formats.json {};
+  settingsFile = settingsFormat.generate "mautrix-facebook-config.json" cfg.settings;
+
+  puppetRegex = concatStringsSep
+    ".*"
+    (map
+      escapeRegex
+      (splitString
+        "{userid}"
+        cfg.settings.bridge.username_template));
+in {
+  options = {
+    services.mautrix-facebook = {
+      enable = mkEnableOption "Mautrix-Facebook, a Matrix-Facebook hybrid puppeting/relaybot bridge";
+
+      settings = mkOption rec {
+        apply = recursiveUpdate default;
+        type = settingsFormat.type;
+        default = {
+          homeserver = {
+            address = "http://localhost:8008";
+          };
+
+          appservice = rec {
+            address = "http://${hostname}:${toString port}";
+            hostname = "localhost";
+            port = 29319;
+
+            database = "postgresql://";
+
+            bot_username = "facebookbot";
+          };
+
+          metrics.enabled = false;
+          manhole.enabled = false;
+
+          bridge = {
+            encryption = {
+              allow = true;
+              default = true;
+            };
+            username_template = "facebook_{userid}";
+          };
+
+          logging = {
+            version = 1;
+            formatters.journal_fmt.format = "%(name)s: %(message)s";
+            handlers.journal = {
+              class = "systemd.journal.JournalHandler";
+              formatter = "journal_fmt";
+              SYSLOG_IDENTIFIER = "mautrix-facebook";
+            };
+            root = {
+              level = "INFO";
+              handlers = ["journal"];
+            };
+          };
+        };
+        example = literalExpression ''
+          {
+            homeserver = {
+              address = "http://localhost:8008";
+              domain = "mydomain.example";
+            };
+
+            bridge.permissions = {
+              "@admin:mydomain.example" = "admin";
+              "mydomain.example" = "user";
+            };
+          }
+        '';
+        description = ''
+          <filename>config.yaml</filename> configuration as a Nix attribute set.
+          Configuration options should match those described in
+          <link xlink:href="https://github.com/mautrix/facebook/blob/master/mautrix_facebook/example-config.yaml">
+          example-config.yaml</link>.
+          </para>
+
+          <para>
+          Secret tokens should be specified using <option>environmentFile</option>
+          instead of this world-readable attribute set.
+        '';
+      };
+
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          File containing environment variables to be passed to the mautrix-telegram service.
+
+          Any config variable can be overridden by setting <literal>MAUTRIX_FACEBOOK_SOME_KEY</literal> to override the <literal>some.key</literal> variable.
+        '';
+      };
+
+      configurePostgresql = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Enable PostgreSQL and create a user and database for mautrix-facebook. The default <literal>settings</literal> reference this database, if you disable this option you must provide a database URL.
+        '';
+      };
+
+      registrationData = mkOption {
+        type = types.attrs;
+        default = {};
+        description = ''
+          Output data for appservice registration. Simply make any desired changes and serialize to JSON. Note that this data contains secrets so think twice before putting it into the nix store.
+
+          Currently <literal>as_token</literal> and <literal>hs_token</literal> need to be added as they are not known to this module.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.mautrix-facebook = {
+      group = "mautrix-facebook";
+      isSystemUser = true;
+    };
+
+    services.postgresql = mkIf cfg.configurePostgresql {
+      ensureDatabases = ["mautrix-facebook"];
+      ensureUsers = [{
+        name = "mautrix-facebook";
+        ensurePermissions = {
+          "DATABASE \"mautrix-facebook\"" = "ALL PRIVILEGES";
+        };
+      }];
+    };
+
+    systemd.services.mautrix-facebook = rec {
+      wantedBy = [ "multi-user.target" ];
+      wants = [
+        "network-online.target"
+      ] ++ optional config.services.matrix-synapse.enable "matrix-synapse.service"
+        ++ optional cfg.configurePostgresql "postgresql.service";
+      after = wants;
+
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+
+        User = "mautrix-facebook";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        PrivateTmp = true;
+
+        EnvironmentFile = cfg.environmentFile;
+
+        ExecStart = ''
+          ${pkgs.mautrix-facebook}/bin/mautrix-facebook --config=${settingsFile}
+        '';
+      };
+    };
+
+    services.mautrix-facebook = {
+      registrationData = {
+        id = "mautrix-facebook";
+
+        namespaces = {
+          users = [
+            {
+              exclusive = true;
+              regex = escapeRegex "@${cfg.settings.appservice.bot_username}:${cfg.settings.homeserver.domain}";
+            }
+            {
+              exclusive = true;
+              regex = "@${puppetRegex}:${escapeRegex cfg.settings.homeserver.domain}";
+            }
+          ];
+          aliases = [];
+        };
+
+        url = cfg.settings.appservice.address;
+        sender_localpart = "mautrix-facebook-sender";
+
+        rate_limited = false;
+        "de.sorunome.msc2409.push_ephemeral" = true;
+        push_ephemeral = true;
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ kevincox ];
+}
diff --git a/nixos/modules/services/misc/mautrix-telegram.nix b/nixos/modules/services/misc/mautrix-telegram.nix
new file mode 100644
index 00000000000..794c4dd9ddc
--- /dev/null
+++ b/nixos/modules/services/misc/mautrix-telegram.nix
@@ -0,0 +1,181 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  dataDir = "/var/lib/mautrix-telegram";
+  registrationFile = "${dataDir}/telegram-registration.yaml";
+  cfg = config.services.mautrix-telegram;
+  settingsFormat = pkgs.formats.json {};
+  settingsFileUnsubstituted = settingsFormat.generate "mautrix-telegram-config-unsubstituted.json" cfg.settings;
+  settingsFile = "${dataDir}/config.json";
+
+in {
+  options = {
+    services.mautrix-telegram = {
+      enable = mkEnableOption "Mautrix-Telegram, a Matrix-Telegram hybrid puppeting/relaybot bridge";
+
+      settings = mkOption rec {
+        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}";
+          };
+
+          bridge = {
+            permissions."*" = "relaybot";
+            relaybot.whitelist = [ ];
+            double_puppet_server_map = {};
+            login_shared_secret_map = {};
+          };
+
+          logging = {
+            version = 1;
+
+            formatters.precise.format = "[%(levelname)s@%(name)s] %(message)s";
+
+            handlers.console = {
+              class = "logging.StreamHandler";
+              formatter = "precise";
+            };
+
+            loggers = {
+              mau.level = "INFO";
+              telethon.level = "INFO";
+
+              # prevent tokens from leaking in the logs:
+              # https://github.com/tulir/mautrix-telegram/issues/351
+              aiohttp.level = "WARNING";
+            };
+
+            # log to console/systemd instead of file
+            root = {
+              level = "INFO";
+              handlers = [ "console" ];
+            };
+          };
+        };
+        example = literalExpression ''
+          {
+            homeserver = {
+              address = "http://localhost:8008";
+              domain = "public-domain.tld";
+            };
+
+            appservice.public = {
+              prefix = "/public";
+              external = "https://public-appservice-address/public";
+            };
+
+            bridge.permissions = {
+              "example.com" = "full";
+              "@admin:example.com" = "admin";
+            };
+          }
+        '';
+        description = ''
+          <filename>config.yaml</filename> configuration as a Nix attribute set.
+          Configuration options should match those described in
+          <link xlink:href="https://github.com/tulir/mautrix-telegram/blob/master/example-config.yaml">
+          example-config.yaml</link>.
+          </para>
+
+          <para>
+          Secret tokens should be specified using <option>environmentFile</option>
+          instead of this world-readable attribute set.
+        '';
+      };
+
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          File containing environment variables to be passed to the mautrix-telegram service,
+          in which secret tokens can be specified securely by defining values for
+          <literal>MAUTRIX_TELEGRAM_APPSERVICE_AS_TOKEN</literal>,
+          <literal>MAUTRIX_TELEGRAM_APPSERVICE_HS_TOKEN</literal>,
+          <literal>MAUTRIX_TELEGRAM_TELEGRAM_API_ID</literal>,
+          <literal>MAUTRIX_TELEGRAM_TELEGRAM_API_HASH</literal> and optionally
+          <literal>MAUTRIX_TELEGRAM_TELEGRAM_BOT_TOKEN</literal>.
+        '';
+      };
+
+      serviceDependencies = mkOption {
+        type = with types; listOf str;
+        default = optional config.services.matrix-synapse.enable "matrix-synapse.service";
+        defaultText = literalExpression ''
+          optional config.services.matrix-synapse.enable "matrix-synapse.service"
+        '';
+        description = ''
+          List of Systemd services to require and wait for when starting the application service.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.mautrix-telegram = {
+      description = "Mautrix-Telegram, a Matrix-Telegram hybrid puppeting/relaybot bridge.";
+
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
+      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 0177
+        ${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 \
+            --generate-registration \
+            --base-config='${pkgs.mautrix-telegram}/${pkgs.mautrix-telegram.pythonModule.sitePackages}/mautrix_telegram/example-config.yaml' \
+            --config='${settingsFile}' \
+            --registration='${registrationFile}'
+        fi
+      '' + lib.optionalString (pkgs.mautrix-telegram ? alembic) ''
+        # run automatic database init and migration scripts
+        ${pkgs.mautrix-telegram.alembic}/bin/alembic -x config='${settingsFile}' upgrade head
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+
+        DynamicUser = true;
+        PrivateTmp = true;
+        WorkingDirectory = pkgs.mautrix-telegram; # necessary for the database migration scripts to be found
+        StateDirectory = baseNameOf dataDir;
+        UMask = 0027;
+        EnvironmentFile = cfg.environmentFile;
+
+        ExecStart = ''
+          ${pkgs.mautrix-telegram}/bin/mautrix-telegram \
+            --config='${settingsFile}'
+        '';
+      };
+
+      restartTriggers = [ settingsFileUnsubstituted ];
+    };
+  };
+
+  meta.maintainers = with maintainers; [ pacien vskilet ];
+}
diff --git a/nixos/modules/services/misc/mbpfan.nix b/nixos/modules/services/misc/mbpfan.nix
new file mode 100644
index 00000000000..e0a4d8a13e7
--- /dev/null
+++ b/nixos/modules/services/misc/mbpfan.nix
@@ -0,0 +1,107 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mbpfan;
+  verbose = if cfg.verbose then "v" else "";
+  settingsFormat = pkgs.formats.ini {};
+  settingsFile = settingsFormat.generate "mbpfan.ini" cfg.settings;
+
+in {
+  options.services.mbpfan = {
+    enable = mkEnableOption "mbpfan, fan controller daemon for Apple Macs and MacBooks";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.mbpfan;
+      defaultText = literalExpression "pkgs.mbpfan";
+      description = ''
+        The package used for the mbpfan daemon.
+      '';
+    };
+
+    verbose = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        If true, sets the log level to verbose.
+      '';
+    };
+
+    settings = mkOption {
+      default = {};
+      description = "The INI configuration for Mbpfan.";
+      type = types.submodule {
+        freeformType = settingsFormat.type;
+
+        options.general.min_fan1_speed = mkOption {
+          type = types.nullOr types.int;
+          default = 2000;
+          description = ''
+            The minimum fan speed. Setting to null enables automatic detection.
+            Check minimum fan limits with "cat /sys/devices/platform/applesmc.768/fan*_min".
+          '';
+        };
+        options.general.max_fan1_speed = mkOption {
+          type = types.nullOr types.int;
+          default = 6199;
+          description = ''
+            The maximum fan speed. Setting to null enables automatic detection.
+            Check maximum fan limits with "cat /sys/devices/platform/applesmc.768/fan*_max".
+          '';
+        };
+        options.general.low_temp = mkOption {
+          type = types.int;
+          default = 55;
+          description = "Temperature below which fan speed will be at minimum. Try ranges 55-63.";
+        };
+        options.general.high_temp = mkOption {
+          type = types.int;
+          default = 58;
+          description = "Fan will increase speed when higher than this temperature. Try ranges 58-66.";
+        };
+        options.general.max_temp = mkOption {
+          type = types.int;
+          default = 86;
+          description = "Fan will run at full speed above this temperature. Do not set it > 90.";
+        };
+        options.general.polling_interval = mkOption {
+          type = types.int;
+          default = 1;
+          description = "The polling interval.";
+        };
+      };
+    };
+  };
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "mbpfan" "pollingInterval" ] [ "services" "mbpfan" "settings" "general" "polling_interval" ])
+    (mkRenamedOptionModule [ "services" "mbpfan" "maxTemp" ] [ "services" "mbpfan" "settings" "general" "max_temp" ])
+    (mkRenamedOptionModule [ "services" "mbpfan" "lowTemp" ] [ "services" "mbpfan" "settings" "general" "low_temp" ])
+    (mkRenamedOptionModule [ "services" "mbpfan" "highTemp" ] [ "services" "mbpfan" "settings" "general" "high_temp" ])
+    (mkRenamedOptionModule [ "services" "mbpfan" "minFanSpeed" ] [ "services" "mbpfan" "settings" "general" "min_fan1_speed" ])
+    (mkRenamedOptionModule [ "services" "mbpfan" "maxFanSpeed" ] [ "services" "mbpfan" "settings" "general" "max_fan1_speed" ])
+  ];
+
+  config = mkIf cfg.enable {
+    boot.kernelModules = [ "coretemp" "applesmc" ];
+
+    environment.etc."mbpfan.conf".source = settingsFile;
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services.mbpfan = {
+      description = "A fan manager daemon for MacBook Pro";
+      wantedBy = [ "sysinit.target" ];
+      after = [ "syslog.target" "sysinit.target" ];
+      restartTriggers = [ config.environment.etc."mbpfan.conf".source ];
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${cfg.package}/bin/mbpfan -f${verbose}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        PIDFile = "/run/mbpfan.pid";
+        Restart = "always";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/mediatomb.nix b/nixos/modules/services/misc/mediatomb.nix
new file mode 100644
index 00000000000..ee5c0ef8d27
--- /dev/null
+++ b/nixos/modules/services/misc/mediatomb.nix
@@ -0,0 +1,394 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+
+  gid = config.ids.gids.mediatomb;
+  cfg = config.services.mediatomb;
+  opt = options.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";
+
+  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="${name}" password="${name}"/>
+        </accounts>
+      </ui>
+      <name>${cfg.serverName}</name>
+      <udn>uuid:${cfg.uuid}</udn>
+      <home>${cfg.dataDir}</home>
+      <interface>${cfg.interface}</interface>
+      <webroot>${pkg}/share/${name}/web</webroot>
+      <pc-directory upnp-hide="${optionYesNo cfg.pcDirectoryHide}"/>
+      <storage>
+        <sqlite3 enabled="yes">
+          <database-file>${name}.db</database-file>
+        </sqlite3>
+      </storage>
+      <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>
+      ''}
+        ${optionalString cfg.tg100Support ''
+      <upnp-string-limit>101</upnp-string-limit>
+      ''}
+      <extended-runtime-options>
+        <mark-played-items enabled="yes" suppress-cds-updates="yes">
+          <string mode="prepend">*</string>
+          <mark>
+            <content>video</content>
+          </mark>
+        </mark-played-items>
+      </extended-runtime-options>
+    </server>
+    <import hidden-files="no">
+      <autoscan use-inotify="auto">
+      ${concatMapStrings toMediaDirectory cfg.mediaDirectories}
+      </autoscan>
+      <scripting script-charset="UTF-8">
+        <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>${pkg}/share/${name}/js/import.js</import-script>
+        </virtual-layout>
+      </scripting>
+      <mappings>
+        <extension-mimetype ignore-unknown="no">
+          <map from="mp3" to="audio/mpeg"/>
+          <map from="ogx" to="application/ogg"/>
+          <map from="ogv" to="video/ogg"/>
+          <map from="oga" to="audio/ogg"/>
+          <map from="ogg" to="audio/ogg"/>
+          <map from="ogm" to="video/ogg"/>
+          <map from="asf" to="video/x-ms-asf"/>
+          <map from="asx" to="video/x-ms-asf"/>
+          <map from="wma" to="audio/x-ms-wma"/>
+          <map from="wax" to="audio/x-ms-wax"/>
+          <map from="wmv" to="video/x-ms-wmv"/>
+          <map from="wvx" to="video/x-ms-wvx"/>
+          <map from="wm" to="video/x-ms-wm"/>
+          <map from="wmx" to="video/x-ms-wmx"/>
+          <map from="m3u" to="audio/x-mpegurl"/>
+          <map from="pls" to="audio/x-scpls"/>
+          <map from="flv" to="video/x-flv"/>
+          <map from="mkv" to="video/x-matroska"/>
+          <map from="mka" to="audio/x-matroska"/>
+          ${optionalString cfg.ps3Support ''
+          <map from="avi" to="video/divx"/>
+          ''}
+          ${optionalString cfg.dsmSupport ''
+          <map from="avi" to="video/avi"/>
+          ''}
+        </extension-mimetype>
+        <mimetype-upnpclass>
+          <map from="audio/*" to="object.item.audioItem.musicTrack"/>
+          <map from="video/*" to="object.item.videoItem"/>
+          <map from="image/*" to="object.item.imageItem"/>
+        </mimetype-upnpclass>
+        <mimetype-contenttype>
+          <treat mimetype="audio/mpeg" as="mp3"/>
+          <treat mimetype="application/ogg" as="ogg"/>
+          <treat mimetype="audio/ogg" as="ogg"/>
+          <treat mimetype="audio/x-flac" as="flac"/>
+          <treat mimetype="audio/x-ms-wma" as="wma"/>
+          <treat mimetype="audio/x-wavpack" as="wv"/>
+          <treat mimetype="image/jpeg" as="jpg"/>
+          <treat mimetype="audio/x-mpegurl" as="playlist"/>
+          <treat mimetype="audio/x-scpls" as="playlist"/>
+          <treat mimetype="audio/x-wav" as="pcm"/>
+          <treat mimetype="audio/L16" as="pcm"/>
+          <treat mimetype="video/x-msvideo" as="avi"/>
+          <treat mimetype="video/mp4" as="mp4"/>
+          <treat mimetype="audio/mp4" as="mp4"/>
+          <treat mimetype="application/x-iso9660" as="dvd"/>
+          <treat mimetype="application/x-iso9660-image" as="dvd"/>
+        </mimetype-contenttype>
+      </mappings>
+      <online-content>
+        <YouTube enabled="no" refresh="28800" update-at-start="no" purge-after="604800" racy-content="exclude" format="mp4" hd="no">
+          <favorites user="${name}"/>
+          <standardfeed feed="most_viewed" time-range="today"/>
+          <playlists user="${name}"/>
+          <uploads user="${name}"/>
+          <standardfeed feed="recently_featured" time-range="today"/>
+        </YouTube>
+      </online-content>
+    </import>
+    ${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 = {
+
+    services.mediatomb = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the Gerbera/Mediatomb DLNA server.
+        '';
+      };
+
+      serverName = mkOption {
+        type = types.str;
+        default = "Gerbera (Mediatomb)";
+        description = ''
+          How to identify the server on the network.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.gerbera;
+        defaultText = literalExpression "pkgs.gerbera";
+        description = ''
+          Underlying package to be used with the module.
+        '';
+      };
+
+      ps3Support = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable ps3 specific tweaks.
+          WARNING: incompatible with DSM 320 support.
+        '';
+      };
+
+      dsmSupport = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable D-Link DSM 320 specific tweaks.
+          WARNING: incompatible with ps3 support.
+        '';
+      };
+
+      tg100Support = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Telegent TG100 specific tweaks.
+        '';
+      };
+
+      transcoding = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable transcoding.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/${name}";
+        defaultText = literalExpression ''"/var/lib/''${config.${opt.package}.pname}"'';
+        description = ''
+          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 the service runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "mediatomb";
+        description = "Group account under which the service runs.";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 49152;
+        description = ''
+          The network port to listen on.
+        '';
+      };
+
+      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 the service 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 = 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 = "${cfg.serverName} media Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig.ExecStart = "${binaryCommand} --port ${toString cfg.port} ${interfaceFlag} ${configFlag} --home ${cfg.dataDir}";
+      serviceConfig.User = cfg.user;
+      serviceConfig.Group = cfg.group;
+    };
+
+    users.groups = optionalAttrs (cfg.group == "mediatomb") {
+      mediatomb.gid = gid;
+    };
+
+    users.users = optionalAttrs (cfg.user == "mediatomb") {
+      mediatomb = {
+        isSystemUser = true;
+        group = cfg.group;
+        home = cfg.dataDir;
+        createHome = true;
+        description = "${name} DLNA Server User";
+      };
+    };
+
+    # 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/metabase.nix b/nixos/modules/services/misc/metabase.nix
new file mode 100644
index 00000000000..e78100a046a
--- /dev/null
+++ b/nixos/modules/services/misc/metabase.nix
@@ -0,0 +1,103 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.metabase;
+
+  inherit (lib) mkEnableOption mkIf mkOption;
+  inherit (lib) optional optionalAttrs types;
+
+  dataDir = "/var/lib/metabase";
+
+in {
+
+  options = {
+
+    services.metabase = {
+      enable = mkEnableOption "Metabase service";
+
+      listen = {
+        ip = mkOption {
+          type = types.str;
+          default = "0.0.0.0";
+          description = ''
+            IP address that Metabase should listen on.
+          '';
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 3000;
+          description = ''
+            Listen port for Metabase.
+          '';
+        };
+      };
+
+      ssl = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Whether to enable SSL (https) support.
+          '';
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8443;
+          description = ''
+            Listen port over SSL (https) for Metabase.
+          '';
+        };
+
+        keystore = mkOption {
+          type = types.nullOr types.path;
+          default = "${dataDir}/metabase.jks";
+          example = "/etc/secrets/keystore.jks";
+          description = ''
+            <link xlink:href="https://www.digitalocean.com/community/tutorials/java-keytool-essentials-working-with-java-keystores">Java KeyStore</link> file containing the certificates.
+          '';
+        };
+
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports in the firewall for Metabase.
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services.metabase = {
+      description = "Metabase server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      environment = {
+        MB_PLUGINS_DIR = "${dataDir}/plugins";
+        MB_DB_FILE = "${dataDir}/metabase.db";
+        MB_JETTY_HOST = cfg.listen.ip;
+        MB_JETTY_PORT = toString cfg.listen.port;
+      } // optionalAttrs (cfg.ssl.enable) {
+        MB_JETTY_SSL = true;
+        MB_JETTY_SSL_PORT = toString cfg.ssl.port;
+        MB_JETTY_SSL_KEYSTORE = cfg.ssl.keystore;
+      };
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = baseNameOf dataDir;
+        ExecStart = "${pkgs.metabase}/bin/metabase";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.listen.port ] ++ optional cfg.ssl.enable cfg.ssl.port;
+    };
+
+  };
+}
diff --git a/nixos/modules/services/misc/moonraker.nix b/nixos/modules/services/misc/moonraker.nix
new file mode 100644
index 00000000000..ae57aaa6d47
--- /dev/null
+++ b/nixos/modules/services/misc/moonraker.nix
@@ -0,0 +1,138 @@
+{ config, lib, options, pkgs, ... }:
+with lib;
+let
+  pkg = pkgs.moonraker;
+  cfg = config.services.moonraker;
+  opt = options.services.moonraker;
+  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 {
+  options = {
+    services.moonraker = {
+      enable = mkEnableOption "Moonraker, an API web server for Klipper";
+
+      klipperSocket = mkOption {
+        type = types.path;
+        default = config.services.klipper.apiSocket;
+        defaultText = literalExpression "config.services.klipper.apiSocket";
+        description = "Path to Klipper's API socket.";
+      };
+
+      stateDir = mkOption {
+        type = types.path;
+        default = "/var/lib/moonraker";
+        description = "The directory containing the Moonraker databases.";
+      };
+
+      configDir = mkOption {
+        type = types.path;
+        default = cfg.stateDir + "/config";
+        defaultText = literalExpression ''config.${opt.stateDir} + "/config"'';
+        description = ''
+          The directory containing client-writable configuration files.
+
+          Clients will be able to edit files in this directory via the API. This directory must be writable.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "moonraker";
+        description = "User account under which Moonraker runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "moonraker";
+        description = "Group account under which Moonraker runs.";
+      };
+
+      address = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        example = "0.0.0.0";
+        description = "The IP or host to listen on.";
+      };
+
+      port = mkOption {
+        type = types.ints.unsigned;
+        default = 7125;
+        description = "The port to listen on.";
+      };
+
+      settings = mkOption {
+        type = format.type;
+        default = { };
+        example = {
+          authorization = {
+            trusted_clients = [ "10.0.0.0/24" ];
+            cors_domains = [ "https://app.fluidd.xyz" ];
+          };
+        };
+        description = ''
+          Configuration for Moonraker. See the <link xlink:href="https://moonraker.readthedocs.io/en/latest/configuration/">documentation</link>
+          for supported values.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    warnings = optional (cfg.settings ? update_manager)
+      ''Enabling update_manager is not supported on NixOS and will lead to non-removable warnings in some clients.'';
+
+    users.users = optionalAttrs (cfg.user == "moonraker") {
+      moonraker = {
+        group = cfg.group;
+        uid = config.ids.uids.moonraker;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "moonraker") {
+      moonraker.gid = config.ids.gids.moonraker;
+    };
+
+    environment.etc."moonraker.cfg".source = let
+      forcedConfig = {
+        server = {
+          host = cfg.address;
+          port = cfg.port;
+          klippy_uds_address = cfg.klipperSocket;
+          config_path = cfg.configDir;
+          database_path = "${cfg.stateDir}/database";
+        };
+      };
+      fullConfig = recursiveUpdate cfg.settings forcedConfig;
+    in format.generate "moonraker.cfg" fullConfig;
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.configDir}' - ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.moonraker = {
+      description = "Moonraker, an API web server for Klipper";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ]
+        ++ optional config.services.klipper.enable "klipper.service";
+
+      # Moonraker really wants its own config to be writable...
+      script = ''
+        cp /etc/moonraker.cfg ${cfg.configDir}/moonraker-temp.cfg
+        chmod u+w ${cfg.configDir}/moonraker-temp.cfg
+        exec ${pkg}/bin/moonraker -c ${cfg.configDir}/moonraker-temp.cfg
+      '';
+
+      serviceConfig = {
+        WorkingDirectory = cfg.stateDir;
+        Group = cfg.group;
+        User = cfg.user;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/mx-puppet-discord.nix b/nixos/modules/services/misc/mx-puppet-discord.nix
new file mode 100644
index 00000000000..6214f7f7eb6
--- /dev/null
+++ b/nixos/modules/services/misc/mx-puppet-discord.nix
@@ -0,0 +1,122 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  dataDir = "/var/lib/mx-puppet-discord";
+  registrationFile = "${dataDir}/discord-registration.yaml";
+  cfg = config.services.mx-puppet-discord;
+  settingsFormat = pkgs.formats.json {};
+  settingsFile = settingsFormat.generate "mx-puppet-discord-config.json" cfg.settings;
+
+in {
+  options = {
+    services.mx-puppet-discord = {
+      enable = mkEnableOption ''
+        mx-puppet-discord is a discord puppeting bridge for matrix.
+        It handles bridging private and group DMs, as well as Guilds (servers)
+      '';
+
+      settings = mkOption rec {
+        apply = recursiveUpdate default;
+        inherit (settingsFormat) type;
+        default = {
+          bridge.port = 8434;
+          presence = {
+            enabled = true;
+            interval = 500;
+          };
+          provisioning.whitelist = [ ];
+          relay.whitelist = [ ];
+
+          # variables are preceded by a colon.
+          namePatterns = {
+            user = ":name";
+            userOverride = ":displayname";
+            room = ":name";
+            group = ":name";
+          };
+
+          #defaults to sqlite but can be configured to use postgresql with
+          #connstring
+          database.filename = "${dataDir}/database.db";
+          logging = {
+            console = "info";
+            lineDateFormat = "MMM-D HH:mm:ss.SSS";
+          };
+        };
+        example = literalExpression ''
+          {
+            bridge = {
+              bindAddress = "localhost";
+              domain = "example.com";
+              homeserverUrl = "https://example.com";
+            };
+
+            provisioning.whitelist = [ "@admin:example.com" ];
+            relay.whitelist = [ "@.*:example.com" ];
+          }
+        '';
+        description = ''
+          <filename>config.yaml</filename> configuration as a Nix attribute set.
+          Configuration options should match those described in
+          <link xlink:href="https://github.com/matrix-discord/mx-puppet-discord/blob/master/sample.config.yaml">
+          sample.config.yaml</link>.
+        '';
+      };
+      serviceDependencies = mkOption {
+        type = with types; listOf str;
+        default = optional config.services.matrix-synapse.enable "matrix-synapse.service";
+        defaultText = literalExpression ''
+          optional config.services.matrix-synapse.enable "matrix-synapse.service"
+        '';
+        description = ''
+          List of Systemd services to require and wait for when starting the application service.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.mx-puppet-discord = {
+      description = "Matrix to Discord puppeting bridge";
+
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
+      after = [ "network-online.target" ] ++ cfg.serviceDependencies;
+
+      preStart = ''
+        # generate the appservice's registration file if absent
+        if [ ! -f '${registrationFile}' ]; then
+          ${pkgs.mx-puppet-discord}/bin/mx-puppet-discord -r -c ${settingsFile} \
+          -f ${registrationFile}
+        fi
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+
+        DynamicUser = true;
+        PrivateTmp = true;
+        WorkingDirectory = pkgs.mx-puppet-discord;
+        StateDirectory = baseNameOf dataDir;
+        UMask = 0027;
+
+        ExecStart = ''
+          ${pkgs.mx-puppet-discord}/bin/mx-puppet-discord \
+            -c ${settingsFile} \
+            -f ${registrationFile}
+        '';
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ govanify ];
+}
diff --git a/nixos/modules/services/misc/n8n.nix b/nixos/modules/services/misc/n8n.nix
new file mode 100644
index 00000000000..77e717eeff9
--- /dev/null
+++ b/nixos/modules/services/misc/n8n.nix
@@ -0,0 +1,79 @@
+{ 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";
+        HOME = "/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 = "no"; # v8 JIT requires memory segments to be Writable-Executable.
+        LockPersonality = "yes";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.settings.port ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/nitter.nix b/nixos/modules/services/misc/nitter.nix
new file mode 100644
index 00000000000..97005c9d914
--- /dev/null
+++ b/nixos/modules/services/misc/nitter.nix
@@ -0,0 +1,358 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.nitter;
+  configFile = pkgs.writeText "nitter.conf" ''
+    ${generators.toINI {
+      # String values need to be quoted
+      mkKeyValue = generators.mkKeyValueDefault {
+        mkValueString = v:
+          if isString v then "\"" + (strings.escape ["\""] (toString v)) + "\""
+          else generators.mkValueStringDefault {} v;
+      } " = ";
+    } (lib.recursiveUpdate {
+      Server = cfg.server;
+      Cache = cfg.cache;
+      Config = cfg.config // { hmacKey = "@hmac@"; };
+      Preferences = cfg.preferences;
+    } cfg.settings)}
+  '';
+  # `hmac` is a secret used for cryptographic signing of video URLs.
+  # Generate it on first launch, then copy configuration and replace
+  # `@hmac@` with this value.
+  # We are not using sed as it would leak the value in the command line.
+  preStart = pkgs.writers.writePython3 "nitter-prestart" {} ''
+    import os
+    import secrets
+
+    state_dir = os.environ.get("STATE_DIRECTORY")
+    if not os.path.isfile(f"{state_dir}/hmac"):
+        # Generate hmac on first launch
+        hmac = secrets.token_hex(32)
+        with open(f"{state_dir}/hmac", "w") as f:
+            f.write(hmac)
+    else:
+        # Load previously generated hmac
+        with open(f"{state_dir}/hmac", "r") as f:
+            hmac = f.read()
+
+    configFile = "${configFile}"
+    with open(configFile, "r") as f_in:
+        with open(f"{state_dir}/nitter.conf", "w") as f_out:
+            f_out.write(f_in.read().replace("@hmac@", hmac))
+  '';
+in
+{
+  options = {
+    services.nitter = {
+      enable = mkEnableOption "If enabled, start Nitter.";
+
+      package = mkOption {
+        default = pkgs.nitter;
+        type = types.package;
+        defaultText = literalExpression "pkgs.nitter";
+        description = "The nitter derivation to use.";
+      };
+
+      server = {
+        address = mkOption {
+          type =  types.str;
+          default = "0.0.0.0";
+          example = "127.0.0.1";
+          description = "The address to listen on.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8080;
+          example = 8000;
+          description = "The port to listen on.";
+        };
+
+        https = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Set secure attribute on cookies. Keep it disabled to enable cookies when not using HTTPS.";
+        };
+
+        httpMaxConnections = mkOption {
+          type = types.int;
+          default = 100;
+          description = "Maximum number of HTTP connections.";
+        };
+
+        staticDir = mkOption {
+          type = types.path;
+          default = "${cfg.package}/share/nitter/public";
+          defaultText = literalExpression ''"''${config.services.nitter.package}/share/nitter/public"'';
+          description = "Path to the static files directory.";
+        };
+
+        title = mkOption {
+          type = types.str;
+          default = "nitter";
+          description = "Title of the instance.";
+        };
+
+        hostname = mkOption {
+          type = types.str;
+          default = "localhost";
+          example = "nitter.net";
+          description = "Hostname of the instance.";
+        };
+      };
+
+      cache = {
+        listMinutes = mkOption {
+          type = types.int;
+          default = 240;
+          description = "How long to cache list info (not the tweets, so keep it high).";
+        };
+
+        rssMinutes = mkOption {
+          type = types.int;
+          default = 10;
+          description = "How long to cache RSS queries.";
+        };
+
+        redisHost = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = "Redis host.";
+        };
+
+        redisPort = mkOption {
+          type = types.port;
+          default = 6379;
+          description = "Redis port.";
+        };
+
+        redisConnections = mkOption {
+          type = types.int;
+          default = 20;
+          description = "Redis connection pool size.";
+        };
+
+        redisMaxConnections = mkOption {
+          type = types.int;
+          default = 30;
+          description = ''
+            Maximum number of connections to Redis.
+
+            New connections are opened when none are available, but if the
+            pool size goes above this, they are closed when released, do not
+            worry about this unless you receive tons of requests per second.
+          '';
+        };
+      };
+
+      config = {
+        base64Media = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Use base64 encoding for proxied media URLs.";
+        };
+
+        tokenCount = mkOption {
+          type = types.int;
+          default = 10;
+          description = ''
+            Minimum amount of usable tokens.
+
+            Tokens are used to authorize API requests, but they expire after
+            ~1 hour, and have a limit of 187 requests. The limit gets reset
+            every 15 minutes, and the pool is filled up so there is always at
+            least tokenCount usable tokens. Only increase this if you receive
+            major bursts all the time.
+          '';
+        };
+      };
+
+      preferences = {
+        replaceTwitter = mkOption {
+          type = types.str;
+          default = "";
+          example = "nitter.net";
+          description = "Replace Twitter links with links to this instance (blank to disable).";
+        };
+
+        replaceYouTube = mkOption {
+          type = types.str;
+          default = "";
+          example = "piped.kavin.rocks";
+          description = "Replace YouTube links with links to this instance (blank to disable).";
+        };
+
+        replaceInstagram = mkOption {
+          type = types.str;
+          default = "";
+          description = "Replace Instagram links with links to this instance (blank to disable).";
+        };
+
+        mp4Playback = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Enable MP4 video playback.";
+        };
+
+        hlsPlayback = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Enable HLS video streaming (requires JavaScript).";
+        };
+
+        proxyVideos = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Proxy video streaming through the server (might be slow).";
+        };
+
+        muteVideos = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Mute videos by default.";
+        };
+
+        autoplayGifs = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Autoplay GIFs.";
+        };
+
+        theme = mkOption {
+          type = types.str;
+          default = "Nitter";
+          description = "Instance theme.";
+        };
+
+        infiniteScroll = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Infinite scrolling (requires JavaScript, experimental!).";
+        };
+
+        stickyProfile = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Make profile sidebar stick to top.";
+        };
+
+        bidiSupport = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Support bidirectional text (makes clicking on tweets harder).";
+        };
+
+        hideTweetStats = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Hide tweet stats (replies, retweets, likes).";
+        };
+
+        hideBanner = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Hide profile banner.";
+        };
+
+        hidePins = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Hide pinned tweets.";
+        };
+
+        hideReplies = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Hide tweet replies.";
+        };
+      };
+
+      settings = mkOption {
+        type = types.attrs;
+        default = {};
+        description = ''
+          Add settings here to override NixOS module generated settings.
+
+          Check the official repository for the available settings:
+          https://github.com/zedeus/nitter/blob/master/nitter.conf
+        '';
+      };
+
+      redisCreateLocally = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Configure local Redis server for Nitter.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for Nitter web interface.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = !cfg.redisCreateLocally || (cfg.cache.redisHost == "localhost" && cfg.cache.redisPort == 6379);
+        message = "When services.nitter.redisCreateLocally is enabled, you need to use localhost:6379 as a cache server.";
+      }
+    ];
+
+    systemd.services.nitter = {
+        description = "Nitter (An alternative Twitter front-end)";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        serviceConfig = {
+          DynamicUser = true;
+          StateDirectory = "nitter";
+          Environment = [ "NITTER_CONF_FILE=/var/lib/nitter/nitter.conf" ];
+          # Some parts of Nitter expect `public` folder in working directory,
+          # see https://github.com/zedeus/nitter/issues/414
+          WorkingDirectory = "${cfg.package}/share/nitter";
+          ExecStart = "${cfg.package}/bin/nitter";
+          ExecStartPre = "${preStart}";
+          AmbientCapabilities = lib.mkIf (cfg.server.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
+          Restart = "on-failure";
+          RestartSec = "5s";
+          # Hardening
+          CapabilityBoundingSet = if (cfg.server.port < 1024) then [ "CAP_NET_BIND_SERVICE" ] else [ "" ];
+          DeviceAllow = [ "" ];
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          PrivateDevices = true;
+          # A private user cannot have process capabilities on the host's user
+          # namespace and thus CAP_NET_BIND_SERVICE has no effect.
+          PrivateUsers = (cfg.server.port >= 1024);
+          ProcSubset = "pid";
+          ProtectClock = true;
+          ProtectControlGroups = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectProc = "invisible";
+          RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+          UMask = "0077";
+        };
+    };
+
+    services.redis = lib.mkIf (cfg.redisCreateLocally) {
+      enable = true;
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.server.port ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix
new file mode 100644
index 00000000000..2b21df91b82
--- /dev/null
+++ b/nixos/modules/services/misc/nix-daemon.nix
@@ -0,0 +1,818 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.nix;
+
+  nixPackage = cfg.package.out;
+
+  isNixAtLeast = versionAtLeast (getVersion nixPackage);
+
+  makeNixBuildUser = nr: {
+    name = "nixbld${toString nr}";
+    value = {
+      description = "Nix build user ${toString nr}";
+
+      /*
+        For consistency with the setgid(2), setuid(2), and setgroups(2)
+        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" ];
+    };
+  };
+
+  nixbldUsers = listToAttrs (map makeNixBuildUser (range 1 cfg.nrBuildUsers));
+
+  nixConf =
+    assert isNixAtLeast "2.2";
+    let
+
+      mkValueString = v:
+        if v == null then ""
+        else if isInt v then toString v
+        else if isBool v then boolToString v
+        else if isFloat v then floatToString v
+        else if isList v then toString v
+        else if isDerivation v then toString v
+        else if builtins.isPath v then toString v
+        else if isString v then v
+        else if isCoercibleToString v then toString v
+        else abort "The nix conf value: ${toPretty {} v} can not be encoded";
+
+      mkKeyValue = k: v: "${escape [ "=" ] k} = ${mkValueString v}";
+
+      mkKeyValuePairs = attrs: concatStringsSep "\n" (mapAttrsToList mkKeyValue attrs);
+
+    in
+    pkgs.writeTextFile {
+      name = "nix.conf";
+      text = ''
+        # WARNING: this file is generated from the nix.* options in
+        # your NixOS configuration, typically
+        # /etc/nixos/configuration.nix.  Do not edit it!
+        ${mkKeyValuePairs cfg.settings}
+        ${cfg.extraOptions}
+      '';
+      checkPhase =
+        if pkgs.stdenv.hostPlatform != pkgs.stdenv.buildPlatform then ''
+          echo "Ignoring validation for cross-compilation"
+        ''
+        else ''
+          echo "Validating generated nix.conf"
+          ln -s $out ./nix.conf
+          set -e
+          set +o pipefail
+          NIX_CONF_DIR=$PWD \
+            ${cfg.package}/bin/nix show-config ${optionalString (isNixAtLeast "2.3pre") "--no-net"} \
+              ${optionalString (isNixAtLeast "2.4pre") "--option experimental-features nix-command"} \
+            |& sed -e 's/^warning:/error:/' \
+            | (! grep '${if cfg.checkConfig then "^error:" else "^error: unknown setting"}')
+          set -o pipefail
+        '';
+    };
+
+  legacyConfMappings = {
+    useSandbox = "sandbox";
+    buildCores = "cores";
+    maxJobs = "max-jobs";
+    sandboxPaths = "extra-sandbox-paths";
+    binaryCaches = "substituters";
+    trustedBinaryCaches = "trusted-substituters";
+    binaryCachePublicKeys = "trusted-public-keys";
+    autoOptimiseStore = "auto-optimise-store";
+    requireSignedBinaryCaches = "require-sigs";
+    trustedUsers = "trusted-users";
+    allowedUsers = "allowed-users";
+    systemFeatures = "system-features";
+  };
+
+  semanticConfType = with types;
+    let
+      confAtom = nullOr
+        (oneOf [
+          bool
+          int
+          float
+          str
+          path
+          package
+        ]) // {
+        description = "Nix config atom (null, bool, int, float, str, path or package)";
+      };
+    in
+    attrsOf (either confAtom (listOf confAtom));
+
+in
+
+{
+  imports = [
+    (mkRenamedOptionModule [ "nix" "useChroot" ] [ "nix" "useSandbox" ])
+    (mkRenamedOptionModule [ "nix" "chrootDirs" ] [ "nix" "sandboxPaths" ])
+    (mkRenamedOptionModule [ "nix" "daemonIONiceLevel" ] [ "nix" "daemonIOSchedPriority" ])
+    (mkRemovedOptionModule [ "nix" "daemonNiceLevel" ] "Consider nix.daemonCPUSchedPolicy instead.")
+  ] ++ mapAttrsToList (oldConf: newConf: mkRenamedOptionModule [ "nix" oldConf ] [ "nix" "settings" newConf ]) legacyConfMappings;
+
+  ###### interface
+
+  options = {
+
+    nix = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable Nix.
+          Disabling Nix makes the system hard to modify and the Nix programs and configuration will not be made available by NixOS itself.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.nix;
+        defaultText = literalExpression "pkgs.nix";
+        description = ''
+          This option specifies the Nix package instance to use throughout the system.
+        '';
+      };
+
+      distributedBuilds = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to distribute builds to the machines listed in
+          <option>nix.buildMachines</option>.
+        '';
+      };
+
+      daemonCPUSchedPolicy = mkOption {
+        type = types.enum [ "other" "batch" "idle" ];
+        default = "other";
+        example = "batch";
+        description = ''
+          Nix daemon process CPU scheduling policy. This policy propagates to
+          build processes. <literal>other</literal> is the default scheduling
+          policy for regular tasks. The <literal>batch</literal> policy is
+          similar to <literal>other</literal>, but optimised for
+          non-interactive tasks. <literal>idle</literal> is for extremely
+          low-priority tasks that should only be run when no other task
+          requires CPU time.
+
+          Please note that while using the <literal>idle</literal> policy may
+          greatly improve responsiveness of a system performing expensive
+          builds, it may also slow down and potentially starve crucial
+          configuration updates during load.
+
+          <literal>idle</literal> may therefore be a sensible policy for
+          systems that experience only intermittent phases of high CPU load,
+          such as desktop or portable computers used interactively. Other
+          systems should use the <literal>other</literal> or
+          <literal>batch</literal> policy instead.
+
+          For more fine-grained resource control, please refer to
+          <citerefentry><refentrytitle>systemd.resource-control
+          </refentrytitle><manvolnum>5</manvolnum></citerefentry> and adjust
+          <option>systemd.services.nix-daemon</option> directly.
+      '';
+      };
+
+      daemonIOSchedClass = mkOption {
+        type = types.enum [ "best-effort" "idle" ];
+        default = "best-effort";
+        example = "idle";
+        description = ''
+          Nix daemon process I/O scheduling class. This class propagates to
+          build processes. <literal>best-effort</literal> is the default
+          class for regular tasks. The <literal>idle</literal> class is for
+          extremely low-priority tasks that should only perform I/O when no
+          other task does.
+
+          Please note that while using the <literal>idle</literal> scheduling
+          class can improve responsiveness of a system performing expensive
+          builds, it might also slow down or starve crucial configuration
+          updates during load.
+
+          <literal>idle</literal> may therefore be a sensible class for
+          systems that experience only intermittent phases of high I/O load,
+          such as desktop or portable computers used interactively. Other
+          systems should use the <literal>best-effort</literal> class.
+      '';
+      };
+
+      daemonIOSchedPriority = mkOption {
+        type = types.int;
+        default = 0;
+        example = 1;
+        description = ''
+          Nix daemon process I/O scheduling priority. This priority propagates
+          to build processes. The supported priorities depend on the
+          scheduling policy: With idle, priorities are not used in scheduling
+          decisions. best-effort supports values in the range 0 (high) to 7
+          (low).
+        '';
+      };
+
+      buildMachines = mkOption {
+        type = types.listOf (types.submodule {
+          options = {
+            hostName = mkOption {
+              type = types.str;
+              example = "nixbuilder.example.org";
+              description = ''
+                The hostname of the build machine.
+              '';
+            };
+            system = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "x86_64-linux";
+              description = ''
+                The system type the build machine can execute derivations on.
+                Either this attribute or <varname>systems</varname> must be
+                present, where <varname>system</varname> takes precedence if
+                both are set.
+              '';
+            };
+            systems = mkOption {
+              type = types.listOf types.str;
+              default = [ ];
+              example = [ "x86_64-linux" "aarch64-linux" ];
+              description = ''
+                The system types the build machine can execute derivations on.
+                Either this attribute or <varname>system</varname> must be
+                present, where <varname>system</varname> takes precedence if
+                both are set.
+              '';
+            };
+            sshUser = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "builder";
+              description = ''
+                The username to log in as on the remote host. This user must be
+                able to log in and run nix commands non-interactively. It must
+                also be privileged to build derivations, so must be included in
+                <option>nix.settings.trusted-users</option>.
+              '';
+            };
+            sshKey = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "/root/.ssh/id_buildhost_builduser";
+              description = ''
+                The path to the SSH private key with which to authenticate on
+                the build machine. The private key must not have a passphrase.
+                If null, the building user (root on NixOS machines) must have an
+                appropriate ssh configuration to log in non-interactively.
+
+                Note that for security reasons, this path must point to a file
+                in the local filesystem, *not* to the nix store.
+              '';
+            };
+            maxJobs = mkOption {
+              type = types.int;
+              default = 1;
+              description = ''
+                The number of concurrent jobs the build machine supports. The
+                build machine will enforce its own limits, but this allows hydra
+                to schedule better since there is no work-stealing between build
+                machines.
+              '';
+            };
+            speedFactor = mkOption {
+              type = types.int;
+              default = 1;
+              description = ''
+                The relative speed of this builder. This is an arbitrary integer
+                that indicates the speed of this builder, relative to other
+                builders. Higher is faster.
+              '';
+            };
+            mandatoryFeatures = mkOption {
+              type = types.listOf types.str;
+              default = [ ];
+              example = [ "big-parallel" ];
+              description = ''
+                A list of features mandatory for this builder. The builder will
+                be ignored for derivations that don't require all features in
+                this list. All mandatory features are automatically included in
+                <varname>supportedFeatures</varname>.
+              '';
+            };
+            supportedFeatures = mkOption {
+              type = types.listOf types.str;
+              default = [ ];
+              example = [ "kvm" "big-parallel" ];
+              description = ''
+                A list of features supported by this builder. The builder will
+                be ignored for derivations that require features not in this
+                list.
+              '';
+            };
+            publicHostKey = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = ''
+                The (base64-encoded) public host key of this builder. The field
+                is calculated via <command>base64 -w0 /etc/ssh/ssh_host_type_key.pub</command>.
+                If null, SSH will use its regular known-hosts file when connecting.
+              '';
+            };
+          };
+        });
+        default = [ ];
+        description = ''
+          This option lists the machines to be used if distributed builds are
+          enabled (see <option>nix.distributedBuilds</option>).
+          Nix will perform derivations on those machines via SSH by copying the
+          inputs to the Nix store on the remote machine, starting the build,
+          then copying the output back to the local Nix store.
+        '';
+      };
+
+      # Environment variables for running Nix.
+      envVars = mkOption {
+        type = types.attrs;
+        internal = true;
+        default = { };
+        description = "Environment variables used by Nix.";
+      };
+
+      nrBuildUsers = mkOption {
+        type = types.int;
+        description = ''
+          Number of <literal>nixbld</literal> user accounts created to
+          perform secure concurrent builds.  If you receive an error
+          message saying that “all build users are currently in use”,
+          you should increase this value.
+        '';
+      };
+
+      readOnlyStore = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          If set, NixOS will enforce the immutability of the Nix store
+          by making <filename>/nix/store</filename> a read-only bind
+          mount.  Nix will automatically make the store writable when
+          needed.
+        '';
+      };
+
+      nixPath = mkOption {
+        type = types.listOf types.str;
+        default = [
+          "nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos"
+          "nixos-config=/etc/nixos/configuration.nix"
+          "/nix/var/nix/profiles/per-user/root/channels"
+        ];
+        description = ''
+          The default Nix expression search path, used by the Nix
+          evaluator to look up paths enclosed in angle brackets
+          (e.g. <literal>&lt;nixpkgs&gt;</literal>).
+        '';
+      };
+
+      checkConfig = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          If enabled (the default), checks for data type mismatches and that Nix
+          can parse the generated nix.conf.
+        '';
+      };
+
+      registry = mkOption {
+        type = types.attrsOf (types.submodule (
+          let
+            referenceAttrs = with types; attrsOf (oneOf [
+              str
+              int
+              bool
+              package
+            ]);
+          in
+          { config, name, ... }:
+          {
+            options = {
+              from = mkOption {
+                type = referenceAttrs;
+                example = { type = "indirect"; id = "nixpkgs"; };
+                description = "The flake reference to be rewritten.";
+              };
+              to = mkOption {
+                type = referenceAttrs;
+                example = { type = "github"; owner = "my-org"; repo = "my-nixpkgs"; };
+                description = "The flake reference <option>from></option> is rewritten to.";
+              };
+              flake = mkOption {
+                type = types.nullOr types.attrs;
+                default = null;
+                example = literalExpression "nixpkgs";
+                description = ''
+                  The flake input <option>from></option> is rewritten to.
+                '';
+              };
+              exact = mkOption {
+                type = types.bool;
+                default = true;
+                description = ''
+                  Whether the <option>from</option> reference needs to match exactly. If set,
+                  a <option>from</option> reference like <literal>nixpkgs</literal> does not
+                  match with a reference like <literal>nixpkgs/nixos-20.03</literal>.
+                '';
+              };
+            };
+            config = {
+              from = mkDefault { type = "indirect"; id = name; };
+              to = mkIf (config.flake != null) (mkDefault
+                {
+                  type = "path";
+                  path = config.flake.outPath;
+                } // filterAttrs
+                (n: _: n == "lastModified" || n == "rev" || n == "revCount" || n == "narHash")
+                config.flake);
+            };
+          }
+        ));
+        default = { };
+        description = ''
+          A system-wide flake registry.
+        '';
+      };
+
+      extraOptions = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          keep-outputs = true
+          keep-derivations = true
+        '';
+        description = "Additional text appended to <filename>nix.conf</filename>.";
+      };
+
+      settings = mkOption {
+        type = types.submodule {
+          freeformType = semanticConfType;
+
+          options = {
+            max-jobs = mkOption {
+              type = types.either types.int (types.enum [ "auto" ]);
+              default = "auto";
+              example = 64;
+              description = ''
+                This option defines the maximum number of jobs that Nix will try to
+                build in parallel. The default is auto, which means it will use all
+                available logical cores. It is recommend to set it to the total
+                number of logical cores in your system (e.g., 16 for two CPUs with 4
+                cores each and hyper-threading).
+              '';
+            };
+
+            auto-optimise-store = mkOption {
+              type = types.bool;
+              default = false;
+              example = true;
+              description = ''
+                If set to true, Nix automatically detects files in the store that have
+                identical contents, and replaces them with hard links to a single copy.
+                This saves disk space. If set to false (the default), you can still run
+                nix-store --optimise to get rid of duplicate files.
+              '';
+            };
+
+            cores = mkOption {
+              type = types.int;
+              default = 0;
+              example = 64;
+              description = ''
+                This option defines the maximum number of concurrent tasks during
+                one build. It affects, e.g., -j option for make.
+                The special value 0 means that the builder should use all
+                available CPU cores in the system. Some builds may become
+                non-deterministic with this option; use with care! Packages will
+                only be affected if enableParallelBuilding is set for them.
+              '';
+            };
+
+            sandbox = mkOption {
+              type = types.either types.bool (types.enum [ "relaxed" ]);
+              default = true;
+              description = ''
+                If set, Nix will perform builds in a sandboxed environment that it
+                will set up automatically for each build. This prevents impurities
+                in builds by disallowing access to dependencies outside of the Nix
+                store by using network and mount namespaces in a chroot environment.
+                This is enabled by default even though it has a possible performance
+                impact due to the initial setup time of a sandbox for each build. It
+                doesn't affect derivation hashes, so changing this option will not
+                trigger a rebuild of packages.
+              '';
+            };
+
+            extra-sandbox-paths = mkOption {
+              type = types.listOf types.str;
+              default = [ ];
+              example = [ "/dev" "/proc" ];
+              description = ''
+                Directories from the host filesystem to be included
+                in the sandbox.
+              '';
+            };
+
+            substituters = mkOption {
+              type = types.listOf types.str;
+              description = ''
+                List of binary cache URLs used to obtain pre-built binaries
+                of Nix packages.
+
+                By default https://cache.nixos.org/ is added.
+              '';
+            };
+
+            trusted-substituters = mkOption {
+              type = types.listOf types.str;
+              default = [ ];
+              example = [ "https://hydra.nixos.org/" ];
+              description = ''
+                List of binary cache URLs that non-root users can use (in
+                addition to those specified using
+                <option>nix.settings.substituters</option>) by passing
+                <literal>--option binary-caches</literal> to Nix commands.
+              '';
+            };
+
+            require-sigs = mkOption {
+              type = types.bool;
+              default = true;
+              description = ''
+                If enabled (the default), Nix will only download binaries from binary caches if
+                they are cryptographically signed with any of the keys listed in
+                <option>nix.settings.trusted-public-keys</option>. If disabled, signatures are neither
+                required nor checked, so it's strongly recommended that you use only
+                trustworthy caches and https to prevent man-in-the-middle attacks.
+              '';
+            };
+
+            trusted-public-keys = mkOption {
+              type = types.listOf types.str;
+              example = [ "hydra.nixos.org-1:CNHJZBh9K4tP3EKF6FkkgeVYsS3ohTl+oS0Qa8bezVs=" ];
+              description = ''
+                List of public keys used to sign binary caches. If
+                <option>nix.settings.trusted-public-keys</option> is enabled,
+                then Nix will use a binary from a binary cache if and only
+                if it is signed by <emphasis>any</emphasis> of the keys
+                listed here. By default, only the key for
+                <uri>cache.nixos.org</uri> is included.
+              '';
+            };
+
+            trusted-users = mkOption {
+              type = types.listOf types.str;
+              default = [ "root" ];
+              example = [ "root" "alice" "@wheel" ];
+              description = ''
+                A list of names of users that have additional rights when
+                connecting to the Nix daemon, such as the ability to specify
+                additional binary caches, or to import unsigned NARs. You
+                can also specify groups by prefixing them with
+                <literal>@</literal>; for instance,
+                <literal>@wheel</literal> means all users in the wheel
+                group.
+              '';
+            };
+
+            system-features = mkOption {
+              type = types.listOf types.str;
+              example = [ "kvm" "big-parallel" "gccarch-skylake" ];
+              description = ''
+                The set of features supported by the machine. Derivations
+                can express dependencies on system features through the
+                <literal>requiredSystemFeatures</literal> attribute.
+
+                By default, pseudo-features <literal>nixos-test</literal>, <literal>benchmark</literal>,
+                and <literal>big-parallel</literal> used in Nixpkgs are set, <literal>kvm</literal>
+                is also included in it is avaliable.
+              '';
+            };
+
+            allowed-users = mkOption {
+              type = types.listOf types.str;
+              default = [ "*" ];
+              example = [ "@wheel" "@builders" "alice" "bob" ];
+              description = ''
+                A list of names of users (separated by whitespace) that are
+                allowed to connect to the Nix daemon. As with
+                <option>nix.settings.trusted-users</option>, you can specify groups by
+                prefixing them with <literal>@</literal>. Also, you can
+                allow all users by specifying <literal>*</literal>. The
+                default is <literal>*</literal>. Note that trusted users are
+                always allowed to connect.
+              '';
+            };
+          };
+        };
+        default = { };
+        example = literalExpression ''
+          {
+            use-sandbox = true;
+            show-trace = true;
+
+            system-features = [ "big-parallel" "kvm" "recursive-nix" ];
+            sandbox-paths = { "/bin/sh" = "''${pkgs.busybox-sandbox-shell.out}/bin/busybox"; };
+          }
+        '';
+        description = ''
+          Configuration for Nix, see
+          <link xlink:href="https://nixos.org/manual/nix/stable/#sec-conf-file"/> or
+          <citerefentry>
+            <refentrytitle>nix.conf</refentrytitle>
+            <manvolnum>5</manvolnum>
+          </citerefentry> for avalaible options.
+          The value declared here will be translated directly to the key-value pairs Nix expects.
+          </para>
+          <para>
+          You can use <command>nix-instantiate --eval --strict '&lt;nixpkgs/nixos&gt;' -A config.nix.settings</command>
+          to view the current value. By default it is empty.
+          </para>
+          <para>
+          Nix configurations defined under <option>nix.*</option> will be translated and applied to this
+          option. In addition, configuration specified in <option>nix.extraOptions</option> which will be appended
+          verbatim to the resulting config file.
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.systemPackages =
+      [
+        nixPackage
+        pkgs.nix-info
+      ]
+      ++ optional (config.programs.bash.enableCompletion) pkgs.nix-bash-completions;
+
+    environment.etc."nix/nix.conf".source = nixConf;
+
+    environment.etc."nix/registry.json".text = builtins.toJSON {
+      version = 2;
+      flakes = mapAttrsToList (n: v: { inherit (v) from to exact; }) cfg.registry;
+    };
+
+    # List of machines for distributed Nix builds in the format
+    # expected by build-remote.pl.
+    environment.etc."nix/machines" = mkIf (cfg.buildMachines != [ ]) {
+      text =
+        concatMapStrings
+          (machine:
+            (concatStringsSep " " ([
+              "${optionalString (machine.sshUser != null) "${machine.sshUser}@"}${machine.hostName}"
+              (if machine.system != null then machine.system else if machine.systems != [ ] then concatStringsSep "," machine.systems else "-")
+              (if machine.sshKey != null then machine.sshKey else "-")
+              (toString machine.maxJobs)
+              (toString machine.speedFactor)
+              (concatStringsSep "," (machine.supportedFeatures ++ machine.mandatoryFeatures))
+              (concatStringsSep "," machine.mandatoryFeatures)
+            ]
+            ++ optional (isNixAtLeast "2.4pre") (if machine.publicHostKey != null then machine.publicHostKey else "-")))
+            + "\n"
+          )
+          cfg.buildMachines;
+    };
+
+    assertions =
+      let badMachine = m: m.system == null && m.systems == [ ];
+      in
+      [
+        {
+          assertion = !(any badMachine cfg.buildMachines);
+          message = ''
+            At least one system type (via <varname>system</varname> or
+              <varname>systems</varname>) must be set for every build machine.
+              Invalid machine specifications:
+          '' + "      " +
+          (concatStringsSep "\n      "
+            (map (m: m.hostName)
+              (filter (badMachine) cfg.buildMachines)));
+        }
+      ];
+
+    systemd.packages = [ nixPackage ];
+
+    systemd.sockets.nix-daemon.wantedBy = [ "sockets.target" ];
+
+    systemd.services.nix-daemon =
+      {
+        path = [ nixPackage pkgs.util-linux config.programs.ssh.package ]
+          ++ optionals cfg.distributedBuilds [ pkgs.gzip ];
+
+        environment = cfg.envVars
+          // { CURL_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"; }
+          // config.networking.proxy.envVars;
+
+        unitConfig.RequiresMountsFor = "/nix/store";
+
+        serviceConfig =
+          {
+            CPUSchedulingPolicy = cfg.daemonCPUSchedPolicy;
+            IOSchedulingClass = cfg.daemonIOSchedClass;
+            IOSchedulingPriority = cfg.daemonIOSchedPriority;
+            LimitNOFILE = 4096;
+          };
+
+        restartTriggers = [ nixConf ];
+
+        # `stopIfChanged = false` changes to switch behavior
+        # from   stop -> update units -> start
+        #   to   update units -> restart
+        #
+        # The `stopIfChanged` setting therefore controls a trade-off between a
+        # more predictable lifecycle, which runs the correct "version" of
+        # the `ExecStop` line, and on the other hand the availability of
+        # sockets during the switch, as the effectiveness of the stop operation
+        # depends on the socket being stopped as well.
+        #
+        # As `nix-daemon.service` does not make use of `ExecStop`, we prefer
+        # to keep the socket up and available. This is important for machines
+        # that run Nix-based services, such as automated build, test, and deploy
+        # services, that expect the daemon socket to be available at all times.
+        #
+        # Notably, the Nix client does not retry on failure to connect to the
+        # daemon socket, and the in-process RemoteStore instance will disable
+        # itself. This makes retries infeasible even for services that are
+        # aware of the issue. Failure to connect can affect not only new client
+        # processes, but also new RemoteStore instances in existing processes,
+        # as well as existing RemoteStore instances that have not saturated
+        # their connection pool.
+        #
+        # Also note that `stopIfChanged = true` does not kill existing
+        # connection handling daemons, as one might wish to happen before a
+        # breaking Nix upgrade (which is rare). The daemon forks that handle
+        # the individual connections split off into their own sessions, causing
+        # them not to be stopped by systemd.
+        # If a Nix upgrade does require all existing daemon processes to stop,
+        # nix-daemon must do so on its own accord, and only when the new version
+        # starts and detects that Nix's persistent state needs an upgrade.
+        stopIfChanged = false;
+
+      };
+
+    # Set up the environment variables for running Nix.
+    environment.sessionVariables = cfg.envVars // { NIX_PATH = cfg.nixPath; };
+
+    environment.extraInit =
+      ''
+        if [ -e "$HOME/.nix-defexpr/channels" ]; then
+          export NIX_PATH="$HOME/.nix-defexpr/channels''${NIX_PATH:+:$NIX_PATH}"
+        fi
+      '';
+
+    nix.nrBuildUsers = mkDefault (max 32 (if cfg.settings.max-jobs == "auto" then 0 else cfg.settings.max-jobs));
+
+    users.users = nixbldUsers;
+
+    services.xserver.displayManager.hiddenUsers = attrNames nixbldUsers;
+
+    system.activationScripts.nix = stringAfter [ "etc" "users" ]
+      ''
+        install -m 0755 -d /nix/var/nix/{gcroots,profiles}/per-user
+
+        # Subscribe the root user to the NixOS channel by default.
+        if [ ! -e "/root/.nix-channels" ]; then
+            echo "${config.system.defaultChannel} nixos" > "/root/.nix-channels"
+        fi
+      '';
+
+    # Legacy configuration conversion.
+    nix.settings = mkMerge [
+      {
+        trusted-public-keys = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ];
+        substituters = mkAfter [ "https://cache.nixos.org/" ];
+
+        system-features = mkDefault (
+          [ "nixos-test" "benchmark" "big-parallel" "kvm" ] ++
+          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}") systems.architectures.inferiors.${pkgs.hostPlatform.gcc.arch}
+          )
+        );
+      }
+
+      (mkIf (!cfg.distributedBuilds) { builders = null; })
+
+      (mkIf (isNixAtLeast "2.3pre") { sandbox-fallback = false; })
+    ];
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/nix-gc.nix b/nixos/modules/services/misc/nix-gc.nix
new file mode 100644
index 00000000000..a7a6a3b5964
--- /dev/null
+++ b/nixos/modules/services/misc/nix-gc.nix
@@ -0,0 +1,100 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.nix.gc;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    nix.gc = {
+
+      automatic = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Automatically run the garbage collector at a specific time.";
+      };
+
+      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 = ''
+          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>
+        '';
+      };
+
+      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.
+        '';
+      };
+
+      options = mkOption {
+        default = "";
+        example = "--max-freed $((64 * 1024**3))";
+        type = types.str;
+        description = ''
+          Options given to <filename>nix-collect-garbage</filename> when the
+          garbage collector is run automatically.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  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.timers.nix-gc = lib.mkIf cfg.automatic {
+      timerConfig = {
+        RandomizedDelaySec = cfg.randomizedDelaySec;
+        Persistent = cfg.persistent;
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/nix-optimise.nix b/nixos/modules/services/misc/nix-optimise.nix
new file mode 100644
index 00000000000..e02026d5f76
--- /dev/null
+++ b/nixos/modules/services/misc/nix-optimise.nix
@@ -0,0 +1,51 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.nix.optimise;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    nix.optimise = {
+
+      automatic = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Automatically run the nix store optimiser at a specific time.";
+      };
+
+      dates = mkOption {
+        default = ["03:45"];
+        type = types.listOf types.str;
+        description = ''
+          Specification (in the format described by
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>) of the time at
+          which the optimiser will run.
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = {
+
+    systemd.services.nix-optimise =
+      { description = "Nix Store Optimiser";
+        # No point this if the nix daemon (and thus the nix store) is outside
+        unitConfig.ConditionPathIsReadWrite = "/nix/var/nix/daemon-socket";
+        serviceConfig.ExecStart = "${config.nix.package}/bin/nix-store --optimise";
+        startAt = optionals cfg.automatic cfg.dates;
+      };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/nix-ssh-serve.nix b/nixos/modules/services/misc/nix-ssh-serve.nix
new file mode 100644
index 00000000000..355fad5db46
--- /dev/null
+++ b/nixos/modules/services/misc/nix-ssh-serve.nix
@@ -0,0 +1,69 @@
+{ config, lib, ... }:
+
+with lib;
+let cfg = config.nix.sshServe;
+    command =
+      if cfg.protocol == "ssh"
+        then "nix-store --serve ${lib.optionalString cfg.write "--write"}"
+      else "nix-daemon --stdio";
+in {
+  options = {
+
+    nix.sshServe = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable serving the Nix store as a remote store via SSH.";
+      };
+
+      write = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable writing to the Nix store as a remote store via SSH. Note: the sshServe user is named nix-ssh and is not a trusted-user. nix-ssh should be added to the <option>nix.settings.trusted-users</option> option in most use cases, such as allowing remote building of derivations.";
+      };
+
+      keys = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "ssh-dss AAAAB3NzaC1k... alice@example.org" ];
+        description = "A list of SSH public keys allowed to access the binary cache via SSH.";
+      };
+
+      protocol = mkOption {
+        type = types.enum [ "ssh" "ssh-ng" ];
+        default = "ssh";
+        description = "The specific Nix-over-SSH protocol to use.";
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    users.users.nix-ssh = {
+      description = "Nix SSH store user";
+      isSystemUser = true;
+      group = "nix-ssh";
+      useDefaultShell = true;
+    };
+    users.groups.nix-ssh = {};
+
+    services.openssh.enable = true;
+
+    services.openssh.extraConfig = ''
+      Match User nix-ssh
+        AllowAgentForwarding no
+        AllowTcpForwarding no
+        PermitTTY no
+        PermitTunnel no
+        X11Forwarding no
+        ForceCommand ${config.nix.package.out}/bin/${command}
+      Match All
+    '';
+
+    users.users.nix-ssh.openssh.authorizedKeys.keys = cfg.keys;
+
+  };
+}
diff --git a/nixos/modules/services/misc/novacomd.nix b/nixos/modules/services/misc/novacomd.nix
new file mode 100644
index 00000000000..7cfc68d2b67
--- /dev/null
+++ b/nixos/modules/services/misc/novacomd.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.novacomd;
+
+in {
+
+  options = {
+    services.novacomd = {
+      enable = mkEnableOption "Novacom service for connecting to WebOS devices";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.webos.novacom ];
+
+    systemd.services.novacomd = {
+      description = "Novacom WebOS daemon";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = "${pkgs.webos.novacomd}/sbin/novacomd";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ dtzWill ];
+}
diff --git a/nixos/modules/services/misc/nzbget.nix b/nixos/modules/services/misc/nzbget.nix
new file mode 100644
index 00000000000..27c5f2e395f
--- /dev/null
+++ b/nixos/modules/services/misc/nzbget.nix
@@ -0,0 +1,117 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.nzbget;
+  pkg = pkgs.nzbget;
+  stateDir = "/var/lib/nzbget";
+  configFile = "${stateDir}/nzbget.conf";
+  configOpts = concatStringsSep " " (mapAttrsToList (name: value: "-o ${name}=${escapeShellArg (toStr value)}") cfg.settings);
+  toStr = v:
+    if v == true then "yes"
+    else if v == false then "no"
+    else if isInt v then toString v
+    else v;
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "misc" "nzbget" "configFile" ] "The configuration of nzbget is now managed by users through the web interface.")
+    (mkRemovedOptionModule [ "services" "misc" "nzbget" "dataDir" ] "The data directory for nzbget is now /var/lib/nzbget.")
+    (mkRemovedOptionModule [ "services" "misc" "nzbget" "openFirewall" ] "The port used by nzbget is managed through the web interface so you should adjust your firewall rules accordingly.")
+  ];
+
+  # interface
+
+  options = {
+    services.nzbget = {
+      enable = mkEnableOption "NZBGet";
+
+      user = mkOption {
+        type = types.str;
+        default = "nzbget";
+        description = "User account under which NZBGet runs";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "nzbget";
+        description = "Group under which NZBGet runs";
+      };
+
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ bool int str ]);
+        default = {};
+        description = ''
+          NZBGet configuration, passed via command line using switch -o. Refer to
+          <link xlink:href="https://github.com/nzbget/nzbget/blob/master/nzbget.conf"/>
+          for details on supported values.
+        '';
+        example = {
+          MainDir = "/data";
+        };
+      };
+    };
+  };
+
+  # implementation
+
+  config = mkIf cfg.enable {
+    services.nzbget.settings = {
+      # allows nzbget to run as a "simple" service
+      OutputMode = "loggable";
+      # use journald for logging
+      WriteLog = "none";
+      ErrorTarget = "screen";
+      WarningTarget = "screen";
+      InfoTarget = "screen";
+      DetailTarget = "screen";
+      # required paths
+      ConfigTemplate = "${pkg}/share/nzbget/nzbget.conf";
+      WebDir = "${pkg}/share/nzbget/webui";
+      # nixos handles package updates
+      UpdateCheck = "none";
+    };
+
+    systemd.services.nzbget = {
+      description = "NZBGet Daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = with pkgs; [
+        unrar
+        p7zip
+      ];
+
+      preStart = ''
+        if [ ! -f ${configFile} ]; then
+          ${pkgs.coreutils}/bin/install -m 0700 ${pkg}/share/nzbget/nzbget.conf ${configFile}
+        fi
+      '';
+
+      serviceConfig = {
+        StateDirectory = "nzbget";
+        StateDirectoryMode = "0750";
+        User = cfg.user;
+        Group = cfg.group;
+        UMask = "0002";
+        Restart = "on-failure";
+        ExecStart = "${pkg}/bin/nzbget --server --configfile ${stateDir}/nzbget.conf ${configOpts}";
+        ExecStop = "${pkg}/bin/nzbget --quit";
+      };
+    };
+
+    users.users = mkIf (cfg.user == "nzbget") {
+      nzbget = {
+        home = stateDir;
+        group = cfg.group;
+        uid = config.ids.uids.nzbget;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "nzbget") {
+      nzbget = {
+        gid = config.ids.gids.nzbget;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/nzbhydra2.nix b/nixos/modules/services/misc/nzbhydra2.nix
new file mode 100644
index 00000000000..500c40f117d
--- /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 = literalExpression "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
new file mode 100644
index 00000000000..cd846d3f268
--- /dev/null
+++ b/nixos/modules/services/misc/octoprint.nix
@@ -0,0 +1,133 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.octoprint;
+
+  baseConfig = {
+    plugins.curalegacy.cura_engine = "${pkgs.curaengine_stable}/bin/CuraEngine";
+    server.host = cfg.host;
+    server.port = cfg.port;
+    webcam.ffmpeg = "${pkgs.ffmpeg.bin}/bin/ffmpeg";
+  };
+
+  fullConfig = recursiveUpdate cfg.extraConfig baseConfig;
+
+  cfgUpdate = pkgs.writeText "octoprint-config.yaml" (builtins.toJSON fullConfig);
+
+  pluginsEnv = package.python.withPackages (ps: [ps.octoprint] ++ (cfg.plugins ps));
+
+  package = pkgs.octoprint;
+
+in
+{
+  ##### interface
+
+  options = {
+
+    services.octoprint = {
+
+      enable = mkEnableOption "OctoPrint, web interface for 3D printers";
+
+      host = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = ''
+          Host to bind OctoPrint to.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 5000;
+        description = ''
+          Port to bind OctoPrint to.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "octoprint";
+        description = "User for the daemon.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "octoprint";
+        description = "Group for the daemon.";
+      };
+
+      stateDir = mkOption {
+        type = types.path;
+        default = "/var/lib/octoprint";
+        description = "State directory of the daemon.";
+      };
+
+      plugins = mkOption {
+        type = types.functionTo (types.listOf types.package);
+        default = plugins: [];
+        defaultText = literalExpression "plugins: []";
+        example = literalExpression "plugins: with plugins; [ themeify stlviewer ]";
+        description = "Additional plugins to be used. Available plugins are passed through the plugins input.";
+      };
+
+      extraConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        description = "Extra options which are added to OctoPrint's YAML configuration file.";
+      };
+
+    };
+
+  };
+
+  ##### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users = optionalAttrs (cfg.user == "octoprint") {
+      octoprint = {
+        group = cfg.group;
+        uid = config.ids.uids.octoprint;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "octoprint") {
+      octoprint.gid = config.ids.gids.octoprint;
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.octoprint = {
+      description = "OctoPrint, web interface for 3D printers";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = [ pluginsEnv ];
+
+      preStart = ''
+        if [ -e "${cfg.stateDir}/config.yaml" ]; then
+          ${pkgs.yaml-merge}/bin/yaml-merge "${cfg.stateDir}/config.yaml" "${cfgUpdate}" > "${cfg.stateDir}/config.yaml.tmp"
+          mv "${cfg.stateDir}/config.yaml.tmp" "${cfg.stateDir}/config.yaml"
+        else
+          cp "${cfgUpdate}" "${cfg.stateDir}/config.yaml"
+          chmod 600 "${cfg.stateDir}/config.yaml"
+        fi
+      '';
+
+      serviceConfig = {
+        ExecStart = "${pluginsEnv}/bin/octoprint serve -b ${cfg.stateDir}";
+        User = cfg.user;
+        Group = cfg.group;
+        SupplementaryGroups = [
+          "dialout"
+        ];
+      };
+    };
+
+  };
+
+}
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/osrm.nix b/nixos/modules/services/misc/osrm.nix
new file mode 100644
index 00000000000..79c347ab7e0
--- /dev/null
+++ b/nixos/modules/services/misc/osrm.nix
@@ -0,0 +1,86 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.osrm;
+in
+
+{
+  options.services.osrm = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Enable the OSRM service.";
+    };
+
+    address = mkOption {
+      type = types.str;
+      default = "0.0.0.0";
+      description = "IP address on which the web server will listen.";
+    };
+
+    port = mkOption {
+      type = types.int;
+      default = 5000;
+      description = "Port on which the web server will run.";
+    };
+
+    threads = mkOption {
+      type = types.int;
+      default = 4;
+      description = "Number of threads to use.";
+    };
+
+    algorithm = mkOption {
+      type = types.enum [ "CH" "CoreCH" "MLD" ];
+      default = "MLD";
+      description = "Algorithm to use for the data. Must be one of CH, CoreCH, MLD";
+    };
+
+    extraFlags = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "--max-table-size 1000" "--max-matching-size 1000" ];
+      description = "Extra command line arguments passed to osrm-routed";
+    };
+
+    dataFile = mkOption {
+      type = types.path;
+      example = "/var/lib/osrm/berlin-latest.osrm";
+      description = "Data file location";
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    users.users.osrm = {
+      group = config.users.users.osrm.name;
+      description = "OSRM user";
+      createHome = false;
+      isSystemUser = true;
+    };
+
+    users.groups.osrm = { };
+
+    systemd.services.osrm = {
+      description = "OSRM service";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        User = config.users.users.osrm.name;
+        ExecStart = ''
+          ${pkgs.osrm-backend}/bin/osrm-routed \
+            --ip ${cfg.address} \
+            --port ${toString cfg.port} \
+            --threads ${toString cfg.threads} \
+            --algorithm ${cfg.algorithm} \
+            ${toString cfg.extraFlags} \
+            ${cfg.dataFile}
+        '';
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/owncast.nix b/nixos/modules/services/misc/owncast.nix
new file mode 100644
index 00000000000..0852335238f
--- /dev/null
+++ b/nixos/modules/services/misc/owncast.nix
@@ -0,0 +1,98 @@
+{ lib, pkgs, config, ... }:
+with lib;
+let cfg = config.services.owncast;
+in {
+
+  options.services.owncast = {
+
+    enable = mkEnableOption "owncast";
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/owncast";
+      description = ''
+        The directory where owncast stores its data files. If left as the default value this directory will automatically be created before the owncast server starts, otherwise the sysadmin is responsible for ensuring the directory exists with appropriate ownership and permissions.
+      '';
+    };
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Open the appropriate ports in the firewall for owncast.
+      '';
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = "owncast";
+      description = "User account under which owncast runs.";
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "owncast";
+      description = "Group under which owncast runs.";
+    };
+
+    listen = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      example = "0.0.0.0";
+      description = "The IP address to bind the owncast web server to.";
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 8080;
+      description = ''
+        TCP port where owncast web-gui listens.
+      '';
+    };
+
+    rtmp-port = mkOption {
+      type = types.port;
+      default = 1935;
+      description = ''
+        TCP port where owncast rtmp service listens.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services.owncast = {
+      description = "A self-hosted live video and web chat server";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = mkMerge [
+        {
+          User = cfg.user;
+          Group = cfg.group;
+          WorkingDirectory = cfg.dataDir;
+          ExecStart = "${pkgs.owncast}/bin/owncast -webserverport ${toString cfg.port} -rtmpport ${toString cfg.rtmp-port} -webserverip ${cfg.listen}";
+          Restart = "on-failure";
+        }
+        (mkIf (cfg.dataDir == "/var/lib/owncast") {
+          StateDirectory = "owncast";
+        })
+      ];
+    };
+
+    users.users = mkIf (cfg.user == "owncast") {
+      owncast = {
+        isSystemUser = true;
+        group = cfg.group;
+        description = "owncast system user";
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "owncast") { owncast = { }; };
+
+    networking.firewall =
+      mkIf cfg.openFirewall { allowedTCPPorts = [ cfg.rtmp-port ] ++ optional (cfg.listen != "127.0.0.1") cfg.port; };
+
+  };
+  meta = { maintainers = with lib.maintainers; [ MayNiklas ]; };
+}
diff --git a/nixos/modules/services/misc/packagekit.nix b/nixos/modules/services/misc/packagekit.nix
new file mode 100644
index 00000000000..9191078ef9c
--- /dev/null
+++ b/nixos/modules/services/misc/packagekit.nix
@@ -0,0 +1,74 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.packagekit;
+
+  inherit (lib)
+    mkEnableOption mkOption mkIf mkRemovedOptionModule types
+    listToAttrs recursiveUpdate;
+
+  iniFmt = pkgs.formats.ini { };
+
+  confFiles = [
+    (iniFmt.generate "PackageKit.conf" (recursiveUpdate
+      {
+        Daemon = {
+          DefaultBackend = "nix";
+          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" ] "Always set to Nix.")
+  ];
+
+  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.
+    '';
+
+    settings = mkOption {
+      type = iniFmt.type;
+      default = { };
+      description = "Additional settings passed straight through to PackageKit.conf";
+    };
+
+    vendorSettings = mkOption {
+      type = iniFmt.type;
+      default = { };
+      description = "Additional settings passed straight through to Vendor.conf";
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    services.dbus.packages = with pkgs; [ packagekit ];
+
+    environment.systemPackages = with pkgs; [ packagekit ];
+
+    systemd.packages = with pkgs; [ packagekit ];
+
+    environment.etc = listToAttrs (map
+      (e:
+        lib.nameValuePair "PackageKit/${e.name}" { source = e; })
+      confFiles);
+  };
+}
diff --git a/nixos/modules/services/misc/paperless-ng.nix b/nixos/modules/services/misc/paperless-ng.nix
new file mode 100644
index 00000000000..11e44f5ece5
--- /dev/null
+++ b/nixos/modules/services/misc/paperless-ng.nix
@@ -0,0 +1,322 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.paperless-ng;
+
+  defaultUser = "paperless";
+
+  hasCustomRedis = hasAttr "PAPERLESS_REDIS" cfg.extraConfig;
+
+  env = {
+    PAPERLESS_DATA_DIR = cfg.dataDir;
+    PAPERLESS_MEDIA_ROOT = cfg.mediaDir;
+    PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
+    GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
+  } // (
+    lib.mapAttrs (_: toString) cfg.extraConfig
+  ) // (optionalAttrs (!hasCustomRedis) {
+    PAPERLESS_REDIS = "unix://${config.services.redis.servers.paperless-ng.unixSocket}";
+  });
+
+  manage = let
+    setupEnv = lib.concatStringsSep "\n" (mapAttrsToList (name: val: "export ${name}=\"${val}\"") env);
+  in pkgs.writeShellScript "manage" ''
+    ${setupEnv}
+    exec ${cfg.package}/bin/paperless-ng "$@"
+  '';
+
+  # Secure the services
+  defaultServiceConfig = {
+    TemporaryFileSystem = "/:ro";
+    BindReadOnlyPaths = [
+      "/nix/store"
+      "-/etc/resolv.conf"
+      "-/etc/nsswitch.conf"
+      "-/etc/hosts"
+      "-/etc/localtime"
+      "-/run/postgresql"
+    ] ++ (optional (!hasCustomRedis) config.services.redis.servers.paperless-ng.unixSocket);
+    BindPaths = [
+      cfg.consumptionDir
+      cfg.dataDir
+      cfg.mediaDir
+    ];
+    CapabilityBoundingSet = "";
+    # ProtectClock adds DeviceAllow=char-rtc r
+    DeviceAllow = "";
+    LockPersonality = true;
+    MemoryDenyWriteExecute = true;
+    NoNewPrivileges = true;
+    PrivateDevices = true;
+    PrivateMounts = true;
+    PrivateNetwork = true;
+    PrivateTmp = true;
+    PrivateUsers = true;
+    ProcSubset = "pid";
+    ProtectClock = true;
+    # Breaks if the home dir of the user is in /home
+    # Also does not add much value in combination with the TemporaryFileSystem.
+    # ProtectHome = true;
+    ProtectHostname = true;
+    # Would re-mount paths ignored by temporary root
+    #ProtectSystem = "strict";
+    ProtectControlGroups = true;
+    ProtectKernelLogs = true;
+    ProtectKernelModules = true;
+    ProtectKernelTunables = true;
+    ProtectProc = "invisible";
+    RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+    RestrictNamespaces = true;
+    RestrictRealtime = true;
+    RestrictSUIDSGID = true;
+    SupplementaryGroups = optional (!hasCustomRedis) config.services.redis.servers.paperless-ng.user;
+    SystemCallArchitectures = "native";
+    SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
+    # Does not work well with the temporary root
+    #UMask = "0066";
+  };
+in
+{
+  meta.maintainers = with maintainers; [ earvstedt Flakebi ];
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "paperless"] ''
+      The paperless module has been removed as the upstream project died.
+      Users should migrate to the paperless-ng module (services.paperless-ng).
+      More information can be found in the NixOS 21.11 release notes.
+    '')
+  ];
+
+  options.services.paperless-ng = {
+    enable = mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = ''
+        Enable Paperless-ng.
+
+        When started, the Paperless database is automatically created if it doesn't
+        exist and updated if the Paperless package has changed.
+        Both tasks are achieved by running a Django migration.
+
+        A script to manage the Paperless instance (by wrapping Django's manage.py) is linked to
+        <literal>''${dataDir}/paperless-ng-manage</literal>.
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/paperless";
+      description = "Directory to store the Paperless data.";
+    };
+
+    mediaDir = mkOption {
+      type = types.str;
+      default = "${cfg.dataDir}/media";
+      defaultText = literalExpression ''"''${dataDir}/media"'';
+      description = "Directory to store the Paperless documents.";
+    };
+
+    consumptionDir = mkOption {
+      type = types.str;
+      default = "${cfg.dataDir}/consume";
+      defaultText = literalExpression ''"''${dataDir}/consume"'';
+      description = "Directory from which new documents are imported.";
+    };
+
+    consumptionDirIsPublic = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Whether all users can write to the consumption dir.";
+    };
+
+    passwordFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/run/keys/paperless-ng-password";
+      description = ''
+        A file containing the superuser password.
+
+        A superuser is required to access the web interface.
+        If unset, you can create a superuser manually by running
+        <literal>''${dataDir}/paperless-ng-manage createsuperuser</literal>.
+
+        The default superuser name is <literal>admin</literal>. To change it, set
+        option <option>extraConfig.PAPERLESS_ADMIN_USER</option>.
+        WARNING: When changing the superuser name after the initial setup, the old superuser
+        will continue to exist.
+
+        To disable login for the web interface, set the following:
+        <literal>extraConfig.PAPERLESS_AUTO_LOGIN_USERNAME = "admin";</literal>.
+        WARNING: Only use this on a trusted system without internet access to Paperless.
+      '';
+    };
+
+    address = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = "Web interface address.";
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 28981;
+      description = "Web interface port.";
+    };
+
+    extraConfig = mkOption {
+      type = types.attrs;
+      default = {};
+      description = ''
+        Extra paperless-ng config options.
+
+        See <link xlink:href="https://paperless-ng.readthedocs.io/en/latest/configuration.html">the documentation</link>
+        for available options.
+      '';
+      example = literalExpression ''
+        {
+          PAPERLESS_OCR_LANGUAGE = "deu+eng";
+        }
+      '';
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = defaultUser;
+      description = "User under which Paperless runs.";
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.paperless-ng;
+      defaultText = literalExpression "pkgs.paperless-ng";
+      description = "The Paperless package to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # Enable redis if no special url is set
+    services.redis.servers.paperless-ng.enable = mkIf (!hasCustomRedis) true;
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
+      "d '${cfg.mediaDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
+      (if cfg.consumptionDirIsPublic then
+        "d '${cfg.consumptionDir}' 777 - - - -"
+      else
+        "d '${cfg.consumptionDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
+      )
+    ];
+
+    systemd.services.paperless-ng-server = {
+      description = "Paperless document server";
+      serviceConfig = defaultServiceConfig // {
+        User = cfg.user;
+        ExecStart = "${cfg.package}/bin/paperless-ng qcluster";
+        Restart = "on-failure";
+        # The `mbind` syscall is needed for running the classifier.
+        SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "mbind" ];
+      };
+      environment = env;
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "paperless-ng-consumer.service" "paperless-ng-web.service" ];
+
+      preStart = ''
+        ln -sf ${manage} ${cfg.dataDir}/paperless-ng-manage
+
+        # Auto-migrate on first run or if the package has changed
+        versionFile="${cfg.dataDir}/src-version"
+        if [[ $(cat "$versionFile" 2>/dev/null) != ${cfg.package} ]]; then
+          ${cfg.package}/bin/paperless-ng migrate
+          echo ${cfg.package} > "$versionFile"
+        fi
+      ''
+      + optionalString (cfg.passwordFile != null) ''
+        export PAPERLESS_ADMIN_USER="''${PAPERLESS_ADMIN_USER:-admin}"
+        export PAPERLESS_ADMIN_PASSWORD=$(cat "${cfg.dataDir}/superuser-password")
+        superuserState="$PAPERLESS_ADMIN_USER:$PAPERLESS_ADMIN_PASSWORD"
+        superuserStateFile="${cfg.dataDir}/superuser-state"
+
+        if [[ $(cat "$superuserStateFile" 2>/dev/null) != $superuserState ]]; then
+          ${cfg.package}/bin/paperless-ng manage_superuser
+          echo "$superuserState" > "$superuserStateFile"
+        fi
+      '';
+    } // optionalAttrs (!hasCustomRedis) {
+      after = [ "redis-paperless-ng.service" ];
+    };
+
+    # Password copying can't be implemented as a privileged preStart script
+    # in 'paperless-ng-server' because 'defaultServiceConfig' limits the filesystem
+    # paths accessible by the service.
+    systemd.services.paperless-ng-copy-password = mkIf (cfg.passwordFile != null) {
+      requiredBy = [ "paperless-ng-server.service" ];
+      before = [ "paperless-ng-server.service" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${pkgs.coreutils}/bin/install --mode 600 --owner '${cfg.user}' --compare \
+            '${cfg.passwordFile}' '${cfg.dataDir}/superuser-password'
+        '';
+        Type = "oneshot";
+        # Needs to talk to mail server for automated import rules
+        PrivateNetwork = false;
+      };
+    };
+
+    systemd.services.paperless-ng-consumer = {
+      description = "Paperless document consumer";
+      serviceConfig = defaultServiceConfig // {
+        User = cfg.user;
+        ExecStart = "${cfg.package}/bin/paperless-ng document_consumer";
+        Restart = "on-failure";
+      };
+      environment = env;
+      # Bind to `paperless-ng-server` so that the consumer never runs
+      # during migrations
+      bindsTo = [ "paperless-ng-server.service" ];
+      after = [ "paperless-ng-server.service" ];
+    };
+
+    systemd.services.paperless-ng-web = {
+      description = "Paperless web server";
+      serviceConfig = defaultServiceConfig // {
+        User = cfg.user;
+        ExecStart = ''
+          ${pkgs.python3Packages.gunicorn}/bin/gunicorn \
+            -c ${cfg.package}/lib/paperless-ng/gunicorn.conf.py paperless.asgi:application
+        '';
+        Restart = "on-failure";
+
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+        CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
+        # gunicorn needs setuid
+        SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "@setuid" ];
+        # Needs to serve web page
+        PrivateNetwork = false;
+      };
+      environment = env // {
+        PATH = mkForce cfg.package.path;
+        PYTHONPATH = "${cfg.package.pythonPath}:${cfg.package}/lib/paperless-ng/src";
+      };
+      # Allow the web interface to access the private /tmp directory of the server.
+      # This is required to support uploading files via the web interface.
+      unitConfig.JoinsNamespaceOf = "paperless-ng-server.service";
+      # Bind to `paperless-ng-server` so that the web server never runs
+      # during migrations
+      bindsTo = [ "paperless-ng-server.service" ];
+      after = [ "paperless-ng-server.service" ];
+    };
+
+    users = optionalAttrs (cfg.user == defaultUser) {
+      users.${defaultUser} = {
+        group = defaultUser;
+        uid = config.ids.uids.paperless;
+        home = cfg.dataDir;
+      };
+
+      groups.${defaultUser} = {
+        gid = config.ids.gids.paperless;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/parsoid.nix b/nixos/modules/services/misc/parsoid.nix
new file mode 100644
index 00000000000..09b7f977bfb
--- /dev/null
+++ b/nixos/modules/services/misc/parsoid.nix
@@ -0,0 +1,129 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.parsoid;
+
+  parsoid = pkgs.nodePackages.parsoid;
+
+  confTree = {
+    worker_heartbeat_timeout = 300000;
+    logging = { level = "info"; };
+    services = [{
+      module = "lib/index.js";
+      entrypoint = "apiServiceWorker";
+      conf = {
+        mwApis = map (x: if isAttrs x then x else { uri = x; }) cfg.wikis;
+        serverInterface = cfg.interface;
+        serverPort = cfg.port;
+      };
+    }];
+  };
+
+  confFile = pkgs.writeText "config.yml" (builtins.toJSON (recursiveUpdate confTree cfg.extraConfig));
+
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "parsoid" "interwikis" ] "Use services.parsoid.wikis instead")
+  ];
+
+  ##### interface
+
+  options = {
+
+    services.parsoid = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Parsoid -- bidirectional
+          wikitext parser.
+        '';
+      };
+
+      wikis = mkOption {
+        type = types.listOf (types.either types.str types.attrs);
+        example = [ "http://localhost/api.php" ];
+        description = ''
+          Used MediaWiki API endpoints.
+        '';
+      };
+
+      workers = mkOption {
+        type = types.int;
+        default = 2;
+        description = ''
+          Number of Parsoid workers.
+        '';
+      };
+
+      interface = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = ''
+          Interface to listen on.
+        '';
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 8000;
+        description = ''
+          Port to listen on.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        description = ''
+          Extra configuration to add to parsoid configuration.
+        '';
+      };
+
+    };
+
+  };
+
+  ##### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.parsoid = {
+      description = "Bidirectional wikitext parser";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = "${parsoid}/lib/node_modules/parsoid/bin/server.js -c ${confFile} -n ${toString cfg.workers}";
+
+        DynamicUser = true;
+        User = "parsoid";
+        Group = "parsoid";
+
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        #MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        RemoveIPC = true;
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/pinnwand.nix b/nixos/modules/services/misc/pinnwand.nix
new file mode 100644
index 00000000000..cbc796c9a7c
--- /dev/null
+++ b/nixos/modules/services/misc/pinnwand.nix
@@ -0,0 +1,103 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.pinnwand;
+
+  format = pkgs.formats.toml {};
+  configFile = format.generate "pinnwand.toml" cfg.settings;
+in
+{
+  options.services.pinnwand = {
+    enable = mkEnableOption "Pinnwand";
+
+    port = mkOption {
+      type = types.port;
+      description = "The port to listen on.";
+      default = 8000;
+    };
+
+    settings = mkOption {
+      type = format.type;
+      description = ''
+        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 = {};
+    };
+  };
+
+  config = mkIf cfg.enable {
+    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;
+
+        StateDirectory = "pinnwand";
+        StateDirectoryMode = "0700";
+
+        AmbientCapabilities = [];
+        CapabilityBoundingSet = "";
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        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/plex.nix b/nixos/modules/services/misc/plex.nix
new file mode 100644
index 00000000000..1cd8da768f4
--- /dev/null
+++ b/nixos/modules/services/misc/plex.nix
@@ -0,0 +1,180 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.plex;
+in
+{
+  imports = [
+    (mkRemovedOptionModule [ "services" "plex" "managePlugins" ] "Please omit or define the option: `services.plex.extraPlugins' instead.")
+  ];
+
+  options = {
+    services.plex = {
+      enable = mkEnableOption "Plex Media Server";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/plex";
+        description = ''
+          The directory where Plex stores its data files.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports in the firewall for the media server.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "plex";
+        description = ''
+          User account under which Plex runs.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "plex";
+        description = ''
+          Group under which Plex runs.
+        '';
+      };
+
+      extraPlugins = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          A list of paths to extra plugin bundles to install in Plex's plugin
+          directory. Every time the systemd unit for Plex starts up, all of the
+          symlinks in Plex's plugin directory will be cleared and this module
+          will symlink all of the paths specified here to that directory.
+        '';
+        example = literalExpression ''
+          [
+            (builtins.path {
+              name = "Audnexus.bundle";
+              path = pkgs.fetchFromGitHub {
+                owner = "djdembeck";
+                repo = "Audnexus.bundle";
+                rev = "v0.2.8";
+                sha256 = "sha256-IWOSz3vYL7zhdHan468xNc6C/eQ2C2BukQlaJNLXh7E=";
+              };
+            })
+          ]
+        '';
+      };
+
+      extraScanners = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          A list of paths to extra scanners to install in Plex's scanners
+          directory.
+
+          Every time the systemd unit for Plex starts up, all of the symlinks
+          in Plex's scanners directory will be cleared and this module will
+          symlink all of the paths specified here to that directory.
+        '';
+        example = literalExpression ''
+          [
+            (fetchFromGitHub {
+              owner = "ZeroQI";
+              repo = "Absolute-Series-Scanner";
+              rev = "773a39f502a1204b0b0255903cee4ed02c46fde0";
+              sha256 = "4l+vpiDdC8L/EeJowUgYyB3JPNTZ1sauN8liFAcK+PY=";
+            })
+          ]
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.plex;
+        defaultText = literalExpression "pkgs.plex";
+        description = ''
+          The Plex package to use. Plex subscribers may wish to use their own
+          package here, pointing to subscriber-only server versions.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # Most of this is just copied from the RPM package's systemd service file.
+    systemd.services.plex = {
+      description = "Plex Media Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+
+        # Run the pre-start script with full permissions (the "!" prefix) so it
+        # can create the data directory if necessary.
+        ExecStartPre = let
+          preStartScript = pkgs.writeScript "plex-run-prestart" ''
+            #!${pkgs.bash}/bin/bash
+
+            # Create data directory if it doesn't exist
+            if ! test -d "$PLEX_DATADIR"; then
+              echo "Creating initial Plex data directory in: $PLEX_DATADIR"
+              install -d -m 0755 -o "${cfg.user}" -g "${cfg.group}" "$PLEX_DATADIR"
+            fi
+         '';
+        in
+          "!${preStartScript}";
+
+        ExecStart = "${cfg.package}/bin/plexmediaserver";
+        KillSignal = "SIGQUIT";
+        Restart = "on-failure";
+      };
+
+      environment = {
+        # Configuration for our FHS userenv script
+        PLEX_DATADIR=cfg.dataDir;
+        PLEX_PLUGINS=concatMapStringsSep ":" builtins.toString cfg.extraPlugins;
+        PLEX_SCANNERS=concatMapStringsSep ":" builtins.toString cfg.extraScanners;
+
+        # The following variables should be set by the FHS userenv script:
+        #   PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR
+        #   PLEX_MEDIA_SERVER_HOME
+
+        # Allow access to GPU acceleration; the Plex LD_LIBRARY_PATH is added
+        # by the FHS userenv script.
+        LD_LIBRARY_PATH="/run/opengl-driver/lib";
+
+        PLEX_MEDIA_SERVER_MAX_PLUGIN_PROCS="6";
+        PLEX_MEDIA_SERVER_TMPDIR="/tmp";
+        PLEX_MEDIA_SERVER_USE_SYSLOG="true";
+        LC_ALL="en_US.UTF-8";
+        LANG="en_US.UTF-8";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 32400 3005 8324 32469 ];
+      allowedUDPPorts = [ 1900 5353 32410 32412 32413 32414 ];
+    };
+
+    users.users = mkIf (cfg.user == "plex") {
+      plex = {
+        group = cfg.group;
+        uid = config.ids.uids.plex;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "plex") {
+      plex = {
+        gid = config.ids.gids.plex;
+      };
+    };
+  };
+}
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/prowlarr.nix b/nixos/modules/services/misc/prowlarr.nix
new file mode 100644
index 00000000000..ef820b4022d
--- /dev/null
+++ b/nixos/modules/services/misc/prowlarr.nix
@@ -0,0 +1,41 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.prowlarr;
+
+in
+{
+  options = {
+    services.prowlarr = {
+      enable = mkEnableOption "Prowlarr";
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the Prowlarr web interface.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.prowlarr = {
+      description = "Prowlarr";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        DynamicUser = true;
+        StateDirectory = "prowlarr";
+        ExecStart = "${pkgs.prowlarr}/bin/Prowlarr -nobrowser -data=/var/lib/prowlarr";
+        Restart = "on-failure";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 9696 ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/pykms.nix b/nixos/modules/services/misc/pykms.nix
new file mode 100644
index 00000000000..2f752bcc7ed
--- /dev/null
+++ b/nixos/modules/services/misc/pykms.nix
@@ -0,0 +1,92 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.pykms;
+  libDir = "/var/lib/pykms";
+
+in
+{
+  meta.maintainers = with lib.maintainers; [ peterhoeg ];
+
+  imports = [
+    (mkRemovedOptionModule [ "services" "pykms" "verbose" ] "Use services.pykms.logLevel instead")
+  ];
+
+  options = {
+    services.pykms = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the PyKMS service.";
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = "The IP address on which to listen.";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 1688;
+        description = "The port on which to listen.";
+      };
+
+      openFirewallPort = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether the listening port should be opened automatically.";
+      };
+
+      memoryLimit = mkOption {
+        type = types.str;
+        default = "64M";
+        description = "How much memory to use at most.";
+      };
+
+      logLevel = mkOption {
+        type = types.enum [ "CRITICAL" "ERROR" "WARNING" "INFO" "DEBUG" "MININFO" ];
+        default = "INFO";
+        description = "How much to log";
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        description = "Additional arguments";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewallPort [ cfg.port ];
+
+    systemd.services.pykms = {
+      description = "Python KMS";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      # python programs with DynamicUser = true require HOME to be set
+      environment.HOME = libDir;
+      serviceConfig = with pkgs; {
+        DynamicUser = true;
+        StateDirectory = baseNameOf libDir;
+        ExecStartPre = "${getBin pykms}/libexec/create_pykms_db.sh ${libDir}/clients.db";
+        ExecStart = lib.concatStringsSep " " ([
+          "${getBin pykms}/bin/server"
+          "--logfile=STDOUT"
+          "--loglevel=${cfg.logLevel}"
+          "--sqlite=${libDir}/clients.db"
+        ] ++ cfg.extraArgs ++ [
+          cfg.listenAddress
+          (toString cfg.port)
+        ]);
+        ProtectHome = "tmpfs";
+        WorkingDirectory = libDir;
+        SyslogIdentifier = "pykms";
+        Restart = "on-failure";
+        MemoryLimit = cfg.memoryLimit;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/radarr.nix b/nixos/modules/services/misc/radarr.nix
new file mode 100644
index 00000000000..74444e24043
--- /dev/null
+++ b/nixos/modules/services/misc/radarr.nix
@@ -0,0 +1,75 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.radarr;
+
+in
+{
+  options = {
+    services.radarr = {
+      enable = mkEnableOption "Radarr";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/radarr/.config/Radarr";
+        description = "The directory where Radarr stores its data files.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the Radarr web interface.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "radarr";
+        description = "User account under which Radarr runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "radarr";
+        description = "Group under which Radarr runs.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0700 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.radarr = {
+      description = "Radarr";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.radarr}/bin/Radarr -nobrowser -data='${cfg.dataDir}'";
+        Restart = "on-failure";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 7878 ];
+    };
+
+    users.users = mkIf (cfg.user == "radarr") {
+      radarr = {
+        group = cfg.group;
+        home = cfg.dataDir;
+        uid = config.ids.uids.radarr;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "radarr") {
+      radarr.gid = config.ids.gids.radarr;
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/redmine.nix b/nixos/modules/services/misc/redmine.nix
new file mode 100644
index 00000000000..696b8d1a25d
--- /dev/null
+++ b/nixos/modules/services/misc/redmine.nix
@@ -0,0 +1,384 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) mkBefore mkDefault mkEnableOption mkIf mkOption mkRemovedOptionModule types;
+  inherit (lib) concatStringsSep literalExpression mapAttrsToList;
+  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" ''
+    production:
+      adapter: ${cfg.database.type}
+      database: ${cfg.database.name}
+      host: ${if (cfg.database.type == "postgresql" && cfg.database.socket != null) then cfg.database.socket else cfg.database.host}
+      port: ${toString cfg.database.port}
+      username: ${cfg.database.user}
+      password: #dbpass#
+      ${optionalString (cfg.database.type == "mysql2" && cfg.database.socket != null) "socket: ${cfg.database.socket}"}
+  '';
+
+  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}";
+      nativeBuildInputs = [ pkgs.unzip ];
+      buildCommand = ''
+        mkdir -p $out
+        cd $out
+        unpackFile ${source}
+      '';
+  });
+
+  mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql2";
+  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";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.redmine;
+        defaultText = literalExpression "pkgs.redmine";
+        description = "Which Redmine package to use.";
+        example = literalExpression "pkgs.redmine.override { ruby = pkgs.ruby_2_7; }";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "redmine";
+        description = "User under which Redmine is ran.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "redmine";
+        description = "Group under which Redmine is ran.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 3000;
+        description = "Port on which Redmine is ran.";
+      };
+
+      stateDir = mkOption {
+        type = types.str;
+        default = "/var/lib/redmine";
+        description = "The state directory, logs and plugins are stored here.";
+      };
+
+      settings = mkOption {
+        type = format.type;
+        default = {};
+        description = ''
+          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 = literalExpression ''
+          {
+            email_delivery = {
+              delivery_method = "smtp";
+              smtp_settings = {
+                address = "mail.example.com";
+                port = 25;
+              };
+            };
+          }
+        '';
+      };
+
+      extraEnv = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration in additional_environment.rb.
+
+          See <link xlink:href="https://svn.redmine.org/redmine/trunk/config/additional_environment.rb.example"/>
+          for details.
+        '';
+        example = ''
+          config.logger.level = Logger::DEBUG
+        '';
+      };
+
+      themes = mkOption {
+        type = types.attrsOf types.path;
+        default = {};
+        description = "Set of themes.";
+        example = literalExpression ''
+          {
+            dkuk-redmine_alex_skin = builtins.fetchurl {
+              url = "https://bitbucket.org/dkuk/redmine_alex_skin/get/1842ef675ef3.zip";
+              sha256 = "0hrin9lzyi50k4w2bd2b30vrf1i4fi1c0gyas5801wn8i7kpm9yl";
+            };
+          }
+        '';
+      };
+
+      plugins = mkOption {
+        type = types.attrsOf types.path;
+        default = {};
+        description = "Set of plugins.";
+        example = literalExpression ''
+          {
+            redmine_env_auth = builtins.fetchurl {
+              url = "https://github.com/Intera/redmine_env_auth/archive/0.6.zip";
+              sha256 = "0yyr1yjd8gvvh832wdc8m3xfnhhxzk2pk3gm2psg5w9jdvd6skak";
+            };
+          }
+        '';
+      };
+
+      database = {
+        type = mkOption {
+          type = types.enum [ "mysql2" "postgresql" ];
+          example = "postgresql";
+          default = "mysql2";
+          description = "Database engine to use.";
+        };
+
+        host = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = "Database host address.";
+        };
+
+        port = mkOption {
+          type = types.int;
+          default = if cfg.database.type == "postgresql" then 5432 else 3306;
+          defaultText = literalExpression "3306";
+          description = "Database host port.";
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "redmine";
+          description = "Database name.";
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "redmine";
+          description = "Database user.";
+        };
+
+        passwordFile = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          example = "/run/keys/redmine-dbpassword";
+          description = ''
+            A file containing the password corresponding to
+            <option>database.user</option>.
+          '';
+        };
+
+        socket = mkOption {
+          type = types.nullOr types.path;
+          default =
+            if mysqlLocal then "/run/mysqld/mysqld.sock"
+            else if pgsqlLocal then "/run/postgresql"
+            else null;
+          defaultText = literalExpression "/run/mysqld/mysqld.sock";
+          example = "/run/mysqld/mysqld.sock";
+          description = "Path to the unix socket file to use for authentication.";
+        };
+
+        createLocally = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Create the database and database user locally.";
+        };
+      };
+    };
+  };
+
+  # implementation
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { 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";
+      }
+      { assertion = cfg.database.createLocally -> cfg.database.socket != null;
+        message = "services.redmine.database.socket must be set if services.redmine.database.createLocally is set to true";
+      }
+      { assertion = cfg.database.createLocally -> cfg.database.host == "localhost";
+        message = "services.redmine.database.host must be set to localhost if services.redmine.database.createLocally is set to true";
+      }
+    ];
+
+    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;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.database.user;
+          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    services.postgresql = mkIf pgsqlLocal {
+      enable = true;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.database.user;
+          ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    # create symlinks for the basic directory layout the redmine package expects
+    systemd.tmpfiles.rules = [
+      "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/cache' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/config' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/files' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/plugins' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/public' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/public/plugin_assets' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/public/themes' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/tmp' 0750 ${cfg.user} ${cfg.group} - -"
+
+      "d /run/redmine - - - - -"
+      "d /run/redmine/public - - - - -"
+      "L+ /run/redmine/config - - - - ${cfg.stateDir}/config"
+      "L+ /run/redmine/files - - - - ${cfg.stateDir}/files"
+      "L+ /run/redmine/log - - - - ${cfg.stateDir}/log"
+      "L+ /run/redmine/plugins - - - - ${cfg.stateDir}/plugins"
+      "L+ /run/redmine/public/plugin_assets - - - - ${cfg.stateDir}/public/plugin_assets"
+      "L+ /run/redmine/public/themes - - - - ${cfg.stateDir}/public/themes"
+      "L+ /run/redmine/tmp - - - - ${cfg.stateDir}/tmp"
+    ];
+
+    systemd.services.redmine = {
+      after = [ "network.target" ] ++ optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
+      wantedBy = [ "multi-user.target" ];
+      environment.RAILS_ENV = "production";
+      environment.RAILS_CACHE = "${cfg.stateDir}/cache";
+      environment.REDMINE_LANG = "en";
+      environment.SCHEMA = "${cfg.stateDir}/cache/schema.db";
+      path = with pkgs; [
+        imagemagick
+        breezy
+        cvs
+        darcs
+        git
+        mercurial
+        subversion
+      ];
+      preStart = ''
+        rm -rf "${cfg.stateDir}/plugins/"*
+        rm -rf "${cfg.stateDir}/public/themes/"*
+
+        # start with a fresh config directory
+        # the config directory is copied instead of linked as some mutable data is stored in there
+        find "${cfg.stateDir}/config" ! -name "secret_token.rb" -type f -exec rm -f {} +
+        cp -r ${cfg.package}/share/redmine/config.dist/* "${cfg.stateDir}/config/"
+
+        chmod -R u+w "${cfg.stateDir}/config"
+
+        # link in the application configuration
+        ln -fs ${configurationYml} "${cfg.stateDir}/config/configuration.yml"
+
+        # link in the additional environment configuration
+        ln -fs ${additionalEnvironment} "${cfg.stateDir}/config/additional_environment.rb"
+
+
+        # link in all user specified themes
+        for theme in ${concatStringsSep " " (mapAttrsToList unpackTheme cfg.themes)}; do
+          ln -fs $theme/* "${cfg.stateDir}/public/themes"
+        done
+
+        # link in redmine provided themes
+        ln -sf ${cfg.package}/share/redmine/public/themes.dist/* "${cfg.stateDir}/public/themes/"
+
+
+        # link in all user specified plugins
+        for plugin in ${concatStringsSep " " (mapAttrsToList unpackPlugin cfg.plugins)}; do
+          ln -fs $plugin/* "${cfg.stateDir}/plugins/''${plugin##*-redmine-plugin-}"
+        done
+
+
+        # handle database.passwordFile & permissions
+        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"
+
+
+        # generate a secret token if required
+        if ! test -e "${cfg.stateDir}/config/initializers/secret_token.rb"; then
+          ${bundle} exec rake generate_secret_token
+          chmod 440 "${cfg.stateDir}/config/initializers/secret_token.rb"
+        fi
+
+        # execute redmine required commands prior to starting the application
+        ${bundle} exec rake db:migrate
+        ${bundle} exec rake redmine:plugins:migrate
+        ${bundle} exec rake redmine:load_default_data
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        TimeoutSec = "300";
+        WorkingDirectory = "${cfg.package}/share/redmine";
+        ExecStart="${bundle} exec rails server webrick -e production -p ${toString cfg.port} -P '${cfg.stateDir}/redmine.pid'";
+      };
+
+    };
+
+    users.users = optionalAttrs (cfg.user == "redmine") {
+      redmine = {
+        group = cfg.group;
+        home = cfg.stateDir;
+        uid = config.ids.uids.redmine;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "redmine") {
+      redmine.gid = config.ids.gids.redmine;
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/ripple-data-api.nix b/nixos/modules/services/misc/ripple-data-api.nix
new file mode 100644
index 00000000000..93eba98b7d3
--- /dev/null
+++ b/nixos/modules/services/misc/ripple-data-api.nix
@@ -0,0 +1,195 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.rippleDataApi;
+
+  deployment_env_config = builtins.toJSON {
+    production = {
+      port = toString cfg.port;
+      maxSockets = 150;
+      batchSize = 100;
+      startIndex = 32570;
+      rippleds = cfg.rippleds;
+      redis = {
+        enable = cfg.redis.enable;
+        host = cfg.redis.host;
+        port = cfg.redis.port;
+        options.auth_pass = null;
+      };
+    };
+  };
+
+  db_config = builtins.toJSON {
+    production = {
+      username = optional (cfg.couchdb.pass != "") cfg.couchdb.user;
+      password = optional (cfg.couchdb.pass != "") cfg.couchdb.pass;
+      host = cfg.couchdb.host;
+      port = cfg.couchdb.port;
+      database = cfg.couchdb.db;
+      protocol = "http";
+    };
+  };
+
+in {
+  options = {
+    services.rippleDataApi = {
+      enable = mkEnableOption "ripple data api";
+
+      port = mkOption {
+        description = "Ripple data api port";
+        default = 5993;
+        type = types.int;
+      };
+
+      importMode = mkOption {
+        description = "Ripple data api import mode.";
+        default = "liveOnly";
+        type = types.enum ["live" "liveOnly"];
+      };
+
+      minLedger = mkOption {
+        description = "Ripple data api minimal ledger to fetch.";
+        default = null;
+        type = types.nullOr types.int;
+      };
+
+      maxLedger = mkOption {
+        description = "Ripple data api maximal ledger to fetch.";
+        default = null;
+        type = types.nullOr types.int;
+      };
+
+      redis = {
+        enable = mkOption {
+          description = "Whether to enable caching of ripple data to redis.";
+          default = true;
+          type = types.bool;
+        };
+
+        host = mkOption {
+          description = "Ripple data api redis host.";
+          default = "localhost";
+          type = types.str;
+        };
+
+        port = mkOption {
+          description = "Ripple data api redis port.";
+          default = 5984;
+          type = types.int;
+        };
+      };
+
+      couchdb = {
+        host = mkOption {
+          description = "Ripple data api couchdb host.";
+          default = "localhost";
+          type = types.str;
+        };
+
+        port = mkOption {
+          description = "Ripple data api couchdb port.";
+          default = 5984;
+          type = types.int;
+        };
+
+        db = mkOption {
+          description = "Ripple data api couchdb database.";
+          default = "rippled";
+          type = types.str;
+        };
+
+        user = mkOption {
+          description = "Ripple data api couchdb username.";
+          default = "rippled";
+          type = types.str;
+        };
+
+        pass = mkOption {
+          description = "Ripple data api couchdb password.";
+          default = "";
+          type = types.str;
+        };
+
+        create = mkOption {
+          description = "Whether to create couchdb database needed by ripple data api.";
+          type = types.bool;
+          default = true;
+        };
+      };
+
+      rippleds = mkOption {
+        description = "List of rippleds to be used by ripple data api.";
+        default = [
+          "http://s_east.ripple.com:51234"
+          "http://s_west.ripple.com:51234"
+        ];
+        type = types.listOf types.str;
+      };
+    };
+  };
+
+  config = mkIf (cfg.enable) {
+    services.couchdb.enable = mkDefault true;
+    services.couchdb.bindAddress = mkDefault "0.0.0.0";
+    services.redis.enable = mkDefault true;
+
+    systemd.services.ripple-data-api = {
+      after = [ "couchdb.service" "redis.service" "ripple-data-api-importer.service" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment = {
+        NODE_ENV = "production";
+        DEPLOYMENT_ENVS_CONFIG = pkgs.writeText "deployment.environment.json" deployment_env_config;
+        DB_CONFIG = pkgs.writeText "db.config.json" db_config;
+      };
+
+      serviceConfig = {
+        ExecStart = "${pkgs.ripple-data-api}/bin/api";
+        Restart = "always";
+        User = "ripple-data-api";
+      };
+    };
+
+    systemd.services.ripple-data-importer = {
+      after = [ "couchdb.service" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.curl ];
+
+      environment = {
+        NODE_ENV = "production";
+        DEPLOYMENT_ENVS_CONFIG = pkgs.writeText "deployment.environment.json" deployment_env_config;
+        DB_CONFIG = pkgs.writeText "db.config.json" db_config;
+        LOG_FILE = "/dev/null";
+      };
+
+      serviceConfig = let
+        importMode =
+          if cfg.minLedger != null && cfg.maxLedger != null then
+            "${toString cfg.minLedger} ${toString cfg.maxLedger}"
+          else
+            cfg.importMode;
+      in {
+        ExecStart = "${pkgs.ripple-data-api}/bin/importer ${importMode} debug";
+        Restart = "always";
+        User = "ripple-data-api";
+      };
+
+      preStart = mkMerge [
+        (mkIf (cfg.couchdb.create) ''
+          HOST="http://${optionalString (cfg.couchdb.pass != "") "${cfg.couchdb.user}:${cfg.couchdb.pass}@"}${cfg.couchdb.host}:${toString cfg.couchdb.port}"
+          curl -X PUT $HOST/${cfg.couchdb.db} || true
+        '')
+        "${pkgs.ripple-data-api}/bin/update-views"
+      ];
+    };
+
+    users.users.ripple-data-api =
+      { description = "Ripple data api user";
+        isSystemUser = true;
+        group = "ripple-data-api";
+      };
+    users.groups.ripple-data-api = {};
+  };
+}
diff --git a/nixos/modules/services/misc/rippled.nix b/nixos/modules/services/misc/rippled.nix
new file mode 100644
index 00000000000..f6ec0677774
--- /dev/null
+++ b/nixos/modules/services/misc/rippled.nix
@@ -0,0 +1,438 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.rippled;
+  opt = options.services.rippled;
+
+  b2i = val: if val then "1" else "0";
+
+  dbCfg = db: ''
+    type=${db.type}
+    path=${db.path}
+    ${optionalString (db.compression != null) ("compression=${b2i db.compression}") }
+    ${optionalString (db.onlineDelete != null) ("online_delete=${toString db.onlineDelete}")}
+    ${optionalString (db.advisoryDelete != null) ("advisory_delete=${b2i db.advisoryDelete}")}
+    ${db.extraOpts}
+  '';
+
+  rippledCfg = ''
+    [server]
+    ${concatMapStringsSep "\n" (n: "port_${n}") (attrNames cfg.ports)}
+
+    ${concatMapStrings (p: ''
+    [port_${p.name}]
+    ip=${p.ip}
+    port=${toString p.port}
+    protocol=${concatStringsSep "," p.protocol}
+    ${optionalString (p.user != "") "user=${p.user}"}
+    ${optionalString (p.password != "") "user=${p.password}"}
+    admin=${concatStringsSep "," p.admin}
+    ${optionalString (p.ssl.key != null) "ssl_key=${p.ssl.key}"}
+    ${optionalString (p.ssl.cert != null) "ssl_cert=${p.ssl.cert}"}
+    ${optionalString (p.ssl.chain != null) "ssl_chain=${p.ssl.chain}"}
+    '') (attrValues cfg.ports)}
+
+    [database_path]
+    ${cfg.databasePath}
+
+    [node_db]
+    ${dbCfg cfg.nodeDb}
+
+    ${optionalString (cfg.tempDb != null) ''
+    [temp_db]
+    ${dbCfg cfg.tempDb}''}
+
+    ${optionalString (cfg.importDb != null) ''
+    [import_db]
+    ${dbCfg cfg.importDb}''}
+
+    [ips]
+    ${concatStringsSep "\n" cfg.ips}
+
+    [ips_fixed]
+    ${concatStringsSep "\n" cfg.ipsFixed}
+
+    [validators]
+    ${concatStringsSep "\n" cfg.validators}
+
+    [node_size]
+    ${cfg.nodeSize}
+
+    [ledger_history]
+    ${toString cfg.ledgerHistory}
+
+    [fetch_depth]
+    ${toString cfg.fetchDepth}
+
+    [validation_quorum]
+    ${toString cfg.validationQuorum}
+
+    [sntp_servers]
+    ${concatStringsSep "\n" cfg.sntpServers}
+
+    ${optionalString cfg.statsd.enable ''
+    [insight]
+    server=statsd
+    address=${cfg.statsd.address}
+    prefix=${cfg.statsd.prefix}
+    ''}
+
+    [rpc_startup]
+    { "command": "log_level", "severity": "${cfg.logLevel}" }
+  '' + cfg.extraConfig;
+
+  portOptions = { name, ...}: {
+    options = {
+      name = mkOption {
+        internal = true;
+        default = name;
+      };
+
+      ip = mkOption {
+        default = "127.0.0.1";
+        description = "Ip where rippled listens.";
+        type = types.str;
+      };
+
+      port = mkOption {
+        description = "Port where rippled listens.";
+        type = types.int;
+      };
+
+      protocol = mkOption {
+        description = "Protocols expose by rippled.";
+        type = types.listOf (types.enum ["http" "https" "ws" "wss" "peer"]);
+      };
+
+      user = mkOption {
+        description = "When set, these credentials will be required on HTTP/S requests.";
+        type = types.str;
+        default = "";
+      };
+
+      password = mkOption {
+        description = "When set, these credentials will be required on HTTP/S requests.";
+        type = types.str;
+        default = "";
+      };
+
+      admin = mkOption {
+        description = "A comma-separated list of admin IP addresses.";
+        type = types.listOf types.str;
+        default = ["127.0.0.1"];
+      };
+
+      ssl = {
+        key = mkOption {
+          description = ''
+            Specifies the filename holding the SSL key in PEM format.
+          '';
+          default = null;
+          type = types.nullOr types.path;
+        };
+
+        cert = mkOption {
+          description = ''
+            Specifies the path to the SSL certificate file in PEM format.
+            This is not needed if the chain includes it.
+          '';
+          default = null;
+          type = types.nullOr types.path;
+        };
+
+        chain = mkOption {
+          description = ''
+            If you need a certificate chain, specify the path to the
+            certificate chain here. The chain may include the end certificate.
+          '';
+          default = null;
+          type = types.nullOr types.path;
+        };
+      };
+    };
+  };
+
+  dbOptions = {
+    options = {
+      type = mkOption {
+        description = "Rippled database type.";
+        type = types.enum ["rocksdb" "nudb"];
+        default = "rocksdb";
+      };
+
+      path = mkOption {
+        description = "Location to store the database.";
+        type = types.path;
+        default = cfg.databasePath;
+        defaultText = literalExpression "config.${opt.databasePath}";
+      };
+
+      compression = mkOption {
+        description = "Whether to enable snappy compression.";
+        type = types.nullOr types.bool;
+        default = null;
+      };
+
+      onlineDelete = mkOption {
+        description = "Enable automatic purging of older ledger information.";
+        type = types.nullOr (types.addCheck types.int (v: v > 256));
+        default = cfg.ledgerHistory;
+        defaultText = literalExpression "config.${opt.ledgerHistory}";
+      };
+
+      advisoryDelete = mkOption {
+        description = ''
+          If set, then require administrative RPC call "can_delete"
+          to enable online deletion of ledger records.
+        '';
+        type = types.nullOr types.bool;
+        default = null;
+      };
+
+      extraOpts = mkOption {
+        description = "Extra database options.";
+        type = types.lines;
+        default = "";
+      };
+    };
+  };
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+    services.rippled = {
+      enable = mkEnableOption "rippled";
+
+      package = mkOption {
+        description = "Which rippled package to use.";
+        type = types.package;
+        default = pkgs.rippled;
+        defaultText = literalExpression "pkgs.rippled";
+      };
+
+      ports = mkOption {
+        description = "Ports exposed by rippled";
+        type = with types; attrsOf (submodule portOptions);
+        default = {
+          rpc = {
+            port = 5005;
+            admin = ["127.0.0.1"];
+            protocol = ["http"];
+          };
+
+          peer = {
+            port = 51235;
+            ip = "0.0.0.0";
+            protocol = ["peer"];
+          };
+
+          ws_public = {
+            port = 5006;
+            ip = "0.0.0.0";
+            protocol = ["ws" "wss"];
+          };
+        };
+      };
+
+      nodeDb = mkOption {
+        description = "Rippled main database options.";
+        type = with types; nullOr (submodule dbOptions);
+        default = {
+          type = "rocksdb";
+          extraOpts = ''
+            open_files=2000
+            filter_bits=12
+            cache_mb=256
+            file_size_pb=8
+            file_size_mult=2;
+          '';
+        };
+      };
+
+      tempDb = mkOption {
+        description = "Rippled temporary database options.";
+        type = with types; nullOr (submodule dbOptions);
+        default = null;
+      };
+
+      importDb = mkOption {
+        description = "Settings for performing a one-time import.";
+        type = with types; nullOr (submodule dbOptions);
+        default = null;
+      };
+
+      nodeSize = mkOption {
+        description = ''
+          Rippled size of the node you are running.
+          "tiny", "small", "medium", "large", and "huge"
+        '';
+        type = types.enum ["tiny" "small" "medium" "large" "huge"];
+        default = "small";
+      };
+
+      ips = mkOption {
+        description = ''
+          List of hostnames or ips where the Ripple protocol is served.
+          For a starter list, you can either copy entries from:
+          https://ripple.com/ripple.txt or if you prefer you can let it
+           default to r.ripple.com 51235
+
+          A port may optionally be specified after adding a space to the
+          address. By convention, if known, IPs are listed in from most
+          to least trusted.
+        '';
+        type = types.listOf types.str;
+        default = ["r.ripple.com 51235"];
+      };
+
+      ipsFixed = mkOption {
+        description = ''
+          List of IP addresses or hostnames to which rippled should always
+          attempt to maintain peer connections with. This is useful for
+          manually forming private networks, for example to configure a
+          validation server that connects to the Ripple network through a
+          public-facing server, or for building a set of cluster peers.
+
+          A port may optionally be specified after adding a space to the address
+        '';
+        type = types.listOf types.str;
+        default = [];
+      };
+
+      validators = mkOption {
+        description = ''
+          List of nodes to always accept as validators. Nodes are specified by domain
+          or public key.
+        '';
+        type = types.listOf types.str;
+        default = [
+          "n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7  RL1"
+          "n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj  RL2"
+          "n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C  RL3"
+          "n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS  RL4"
+          "n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA  RL5"
+        ];
+      };
+
+      databasePath = mkOption {
+        description = ''
+          Path to the ripple database.
+        '';
+        type = types.path;
+        default = "/var/lib/rippled";
+      };
+
+      validationQuorum = mkOption {
+        description = ''
+          The minimum number of trusted validations a ledger must have before
+          the server considers it fully validated.
+        '';
+        type = types.int;
+        default = 3;
+      };
+
+      ledgerHistory = mkOption {
+        description = ''
+          The number of past ledgers to acquire on server startup and the minimum
+          to maintain while running.
+        '';
+        type = types.either types.int (types.enum ["full"]);
+        default = 1296000; # 1 month
+      };
+
+      fetchDepth = mkOption {
+        description = ''
+          The number of past ledgers to serve to other peers that request historical
+          ledger data (or "full" for no limit).
+        '';
+        type = types.either types.int (types.enum ["full"]);
+        default = "full";
+      };
+
+      sntpServers = mkOption {
+        description = ''
+          IP address or domain of NTP servers to use for time synchronization.;
+        '';
+        type = types.listOf types.str;
+        default = [
+          "time.windows.com"
+          "time.apple.com"
+          "time.nist.gov"
+          "pool.ntp.org"
+        ];
+      };
+
+      logLevel = mkOption {
+        description = "Logging verbosity.";
+        type = types.enum ["debug" "error" "info"];
+        default = "error";
+      };
+
+      statsd = {
+        enable = mkEnableOption "statsd monitoring for rippled";
+
+        address = mkOption {
+          description = "The UDP address and port of the listening StatsD server.";
+          default = "127.0.0.1:8125";
+          type = types.str;
+        };
+
+        prefix = mkOption {
+          description = "A string prepended to each collected metric.";
+          default = "";
+          type = types.str;
+        };
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Extra lines to be added verbatim to the rippled.cfg configuration file.
+        '';
+      };
+
+      config = mkOption {
+        internal = true;
+        default = pkgs.writeText "rippled.conf" rippledCfg;
+        defaultText = literalDocBook "generated config file";
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.rippled = {
+        description = "Ripple server user";
+        isSystemUser = true;
+        group = "rippled";
+        home = cfg.databasePath;
+        createHome = true;
+      };
+    users.groups.rippled = {};
+
+    systemd.services.rippled = {
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/rippled --fg --conf ${cfg.config}";
+        User = "rippled";
+        Restart = "on-failure";
+        LimitNOFILE=10000;
+      };
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+  };
+}
diff --git a/nixos/modules/services/misc/rmfakecloud.nix b/nixos/modules/services/misc/rmfakecloud.nix
new file mode 100644
index 00000000000..fe522653c21
--- /dev/null
+++ b/nixos/modules/services/misc/rmfakecloud.nix
@@ -0,0 +1,147 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.rmfakecloud;
+  serviceDataDir = "/var/lib/rmfakecloud";
+
+in {
+  options = {
+    services.rmfakecloud = {
+      enable = mkEnableOption "rmfakecloud remarkable self-hosted cloud";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.rmfakecloud;
+        defaultText = literalExpression "pkgs.rmfakecloud";
+        description = ''
+          rmfakecloud package to use.
+
+          The default does not include the web user interface.
+        '';
+      };
+
+      storageUrl = mkOption {
+        type = types.str;
+        example = "https://local.appspot.com";
+        description = ''
+          URL used by the tablet to access the rmfakecloud service.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 3000;
+        description = ''
+          Listening port number.
+        '';
+      };
+
+      logLevel = mkOption {
+        type = types.enum [ "info" "debug" "warn" "error" ];
+        default = "info";
+        description = ''
+          Logging level.
+        '';
+      };
+
+      extraSettings = mkOption {
+        type = with types; attrsOf str;
+        default = { };
+        example = { DATADIR = "/custom/path/for/rmfakecloud/data"; };
+        description = ''
+          Extra settings in the form of a set of key-value pairs.
+          For tokens and secrets, use `environmentFile` instead.
+
+          Available settings are listed on
+          https://ddvk.github.io/rmfakecloud/install/configuration/.
+        '';
+      };
+
+      environmentFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        example = "/etc/secrets/rmfakecloud.env";
+        description = ''
+          Path to an environment file loaded for the rmfakecloud service.
+
+          This can be used to securely store tokens and secrets outside of the
+          world-readable Nix store. Since this file is read by systemd, it may
+          have permission 0400 and be owned by root.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.rmfakecloud = {
+      description = "rmfakecloud remarkable self-hosted cloud";
+
+      environment = {
+        STORAGE_URL = cfg.storageUrl;
+        PORT = toString cfg.port;
+        LOGLEVEL = cfg.logLevel;
+      } // cfg.extraSettings;
+
+      preStart = ''
+        # Generate the secret key used to sign client session tokens.
+        # Replacing it invalidates the previously established sessions.
+        if [ -z "$JWT_SECRET_KEY" ] && [ ! -f jwt_secret_key ]; then
+          (umask 077; touch jwt_secret_key)
+          cat /dev/urandom | tr -cd '[:alnum:]' | head -c 48 >> jwt_secret_key
+        fi
+      '';
+
+      script = ''
+        if [ -z "$JWT_SECRET_KEY" ]; then
+          export JWT_SECRET_KEY="$(cat jwt_secret_key)"
+        fi
+
+        ${cfg.package}/bin/rmfakecloud
+      '';
+
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+
+        EnvironmentFile =
+          mkIf (cfg.environmentFile != null) cfg.environmentFile;
+
+        AmbientCapabilities =
+          mkIf (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
+
+        DynamicUser = true;
+        PrivateDevices = true;
+        ProtectHome = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        CapabilityBoundingSet = [ "" ];
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        ProtectClock = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        WorkingDirectory = serviceDataDir;
+        StateDirectory = baseNameOf serviceDataDir;
+        UMask = 0027;
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ pacien ];
+}
diff --git a/nixos/modules/services/misc/safeeyes.nix b/nixos/modules/services/misc/safeeyes.nix
new file mode 100644
index 00000000000..638218d8bb0
--- /dev/null
+++ b/nixos/modules/services/misc/safeeyes.nix
@@ -0,0 +1,51 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.safeeyes;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.safeeyes = {
+
+      enable = mkEnableOption "the safeeyes OSGi service";
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.safeeyes ];
+
+    systemd.user.services.safeeyes = {
+      description = "Safeeyes";
+
+      wantedBy = [ "graphical-session.target" ];
+      partOf   = [ "graphical-session.target" ];
+
+      path = [ pkgs.alsa-utils ];
+
+      startLimitIntervalSec = 350;
+      startLimitBurst = 10;
+      serviceConfig = {
+        ExecStart = ''
+          ${pkgs.safeeyes}/bin/safeeyes
+        '';
+        Restart = "on-failure";
+        RestartSec = 3;
+      };
+    };
+
+  };
+}
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/serviio.nix b/nixos/modules/services/misc/serviio.nix
new file mode 100644
index 00000000000..0ead6a81691
--- /dev/null
+++ b/nixos/modules/services/misc/serviio.nix
@@ -0,0 +1,87 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.serviio;
+
+  serviioStart = pkgs.writeScript "serviio.sh" ''
+    #!${pkgs.bash}/bin/sh
+
+    SERVIIO_HOME=${pkgs.serviio}
+
+    # Setup the classpath
+    SERVIIO_CLASS_PATH="$SERVIIO_HOME/lib/*:$SERVIIO_HOME/config"
+
+    # Setup Serviio specific properties
+    JAVA_OPTS="-Djava.net.preferIPv4Stack=true -Djava.awt.headless=true -Dorg.restlet.engine.loggerFacadeClass=org.restlet.ext.slf4j.Slf4jLoggerFacade
+               -Dderby.system.home=${cfg.dataDir}/library -Dserviio.home=${cfg.dataDir} -Dffmpeg.location=${pkgs.ffmpeg}/bin/ffmpeg -Ddcraw.location=${pkgs.dcraw}/bin/dcraw"
+
+    # Execute the JVM in the foreground
+    exec ${pkgs.jre}/bin/java -Xmx512M -Xms20M -XX:+UseG1GC -XX:GCTimeRatio=1 -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=20 $JAVA_OPTS -classpath "$SERVIIO_CLASS_PATH" org.serviio.MediaServer "$@"
+  '';
+
+in {
+
+  ###### interface
+  options = {
+    services.serviio = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the Serviio Media Server.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/serviio";
+        description = ''
+          The directory where serviio stores its state, data, etc.
+        '';
+      };
+
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.serviio = {
+      description = "Serviio Media Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.serviio ];
+      serviceConfig = {
+        User = "serviio";
+        Group = "serviio";
+        ExecStart = "${serviioStart}";
+        ExecStop = "${serviioStart} -stop";
+      };
+    };
+
+    users.users.serviio =
+      { group = "serviio";
+        home = cfg.dataDir;
+        description = "Serviio Media Server User";
+        createHome = true;
+        isSystemUser = true;
+      };
+
+    users.groups.serviio = { };
+
+    networking.firewall = {
+      allowedTCPPorts = [
+        8895  # serve UPnP responses
+        23423 # console
+        23424 # mediabrowser
+      ];
+      allowedUDPPorts = [
+        1900 # UPnP service discovey
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sickbeard.nix b/nixos/modules/services/misc/sickbeard.nix
new file mode 100644
index 00000000000..a3db9928634
--- /dev/null
+++ b/nixos/modules/services/misc/sickbeard.nix
@@ -0,0 +1,95 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+
+  name = "sickbeard";
+
+  cfg = config.services.sickbeard;
+  opt = options.services.sickbeard;
+  sickbeard = cfg.package;
+
+in
+{
+
+  ###### interface
+
+  options = {
+    services.sickbeard = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the sickbeard server.";
+      };
+      package = mkOption {
+        type = types.package;
+        default = pkgs.sickbeard;
+        defaultText = literalExpression "pkgs.sickbeard";
+        example = literalExpression "pkgs.sickrage";
+        description =''
+          Enable <literal>pkgs.sickrage</literal> or <literal>pkgs.sickgear</literal>
+          as an alternative to SickBeard
+        '';
+      };
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/${name}";
+        description = "Path where to store data files.";
+      };
+      configFile = mkOption {
+        type = types.path;
+        default = "${cfg.dataDir}/config.ini";
+        defaultText = literalExpression ''"''${config.${opt.dataDir}}/config.ini"'';
+        description = "Path to config file.";
+      };
+      port = mkOption {
+        type = types.ints.u16;
+        default = 8081;
+        description = "Port to bind to.";
+      };
+      user = mkOption {
+        type = types.str;
+        default = name;
+        description = "User to run the service as";
+      };
+      group = mkOption {
+        type = types.str;
+        default = name;
+        description = "Group to run the service as";
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users = optionalAttrs (cfg.user == name) {
+      ${name} = {
+        uid = config.ids.uids.sickbeard;
+        group = cfg.group;
+        description = "sickbeard user";
+        home = cfg.dataDir;
+        createHome = true;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == name) {
+      ${name}.gid = config.ids.gids.sickbeard;
+    };
+
+    systemd.services.sickbeard = {
+      description = "Sickbeard Server";
+      wantedBy    = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${sickbeard}/bin/${sickbeard.pname} --datadir ${cfg.dataDir} --config ${cfg.configFile} --port ${toString cfg.port}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/signald.nix b/nixos/modules/services/misc/signald.nix
new file mode 100644
index 00000000000..4cd34e4326d
--- /dev/null
+++ b/nixos/modules/services/misc/signald.nix
@@ -0,0 +1,105 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.signald;
+  dataDir = "/var/lib/signald";
+  defaultUser = "signald";
+in
+{
+  options.services.signald = {
+    enable = mkEnableOption "the signald service";
+
+    user = mkOption {
+      type = types.str;
+      default = defaultUser;
+      description = "User under which signald runs.";
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = defaultUser;
+      description = "Group under which signald runs.";
+    };
+
+    socketPath = mkOption {
+      type = types.str;
+      default = "/run/signald/signald.sock";
+      description = "Path to the signald socket";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users = optionalAttrs (cfg.user == defaultUser) {
+      ${defaultUser} = {
+        group = cfg.group;
+        isSystemUser = true;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == defaultUser) {
+      ${defaultUser} = { };
+    };
+
+    systemd.services.signald = {
+      description = "A daemon for interacting with the Signal Private Messenger";
+      wants = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.signald}/bin/signald -d ${dataDir} -s ${cfg.socketPath}";
+        Restart = "on-failure";
+        StateDirectory = "signald";
+        RuntimeDirectory = "signald";
+        StateDirectoryMode = "0750";
+        RuntimeDirectoryMode = "0750";
+
+        BindReadOnlyPaths = [
+          "/nix/store"
+          "-/etc/resolv.conf"
+          "-/etc/nsswitch.conf"
+          "-/etc/hosts"
+          "-/etc/localtime"
+        ];
+        CapabilityBoundingSet = "";
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        # Use a static user so other applications can access the files
+        #DynamicUser = true;
+        LockPersonality = true;
+        # Needed for java
+        #MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        # Needs network access
+        #PrivateNetwork = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        # Would re-mount paths ignored by temporary root
+        #ProtectSystem = "strict";
+        ProtectControlGroups = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
+        TemporaryFileSystem = "/:ro";
+        # Does not work well with the temporary root
+        #UMask = "0066";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/siproxd.nix b/nixos/modules/services/misc/siproxd.nix
new file mode 100644
index 00000000000..20fe0793b84
--- /dev/null
+++ b/nixos/modules/services/misc/siproxd.nix
@@ -0,0 +1,179 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.siproxd;
+
+  conf = ''
+    daemonize = 0
+    rtp_proxy_enable = 1
+    user = siproxd
+    if_inbound  = ${cfg.ifInbound}
+    if_outbound = ${cfg.ifOutbound}
+    sip_listen_port = ${toString cfg.sipListenPort}
+    rtp_port_low    = ${toString cfg.rtpPortLow}
+    rtp_port_high   = ${toString cfg.rtpPortHigh}
+    rtp_dscp        = ${toString cfg.rtpDscp}
+    sip_dscp        = ${toString cfg.sipDscp}
+    ${optionalString (cfg.hostsAllowReg != []) "hosts_allow_reg = ${concatStringsSep "," cfg.hostsAllowReg}"}
+    ${optionalString (cfg.hostsAllowSip != []) "hosts_allow_sip = ${concatStringsSep "," cfg.hostsAllowSip}"}
+    ${optionalString (cfg.hostsDenySip != []) "hosts_deny_sip  = ${concatStringsSep "," cfg.hostsDenySip}"}
+    ${if (cfg.passwordFile != "") then "proxy_auth_pwfile = ${cfg.passwordFile}" else ""}
+    ${cfg.extraConfig}
+  '';
+
+  confFile = builtins.toFile "siproxd.conf" conf;
+
+in
+{
+  ##### interface
+
+  options = {
+
+    services.siproxd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the Siproxd SIP
+          proxy/masquerading daemon.
+        '';
+      };
+
+      ifInbound = mkOption {
+        type = types.str;
+        example = "eth0";
+        description = "Local network interface";
+      };
+
+      ifOutbound = mkOption {
+        type = types.str;
+        example = "ppp0";
+        description = "Public network interface";
+      };
+
+      hostsAllowReg = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "192.168.1.0/24" "192.168.2.0/24" ];
+        description = ''
+          Acess control list for incoming SIP registrations.
+        '';
+      };
+
+      hostsAllowSip = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "123.45.0.0/16" "123.46.0.0/16" ];
+        description = ''
+          Acess control list for incoming SIP traffic.
+        '';
+      };
+
+      hostsDenySip = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "10.0.0.0/8" "11.0.0.0/8" ];
+        description = ''
+          Acess control list for denying incoming
+          SIP registrations and traffic.
+        '';
+      };
+
+      sipListenPort = mkOption {
+        type = types.int;
+        default = 5060;
+        description = ''
+          Port to listen for incoming SIP messages.
+        '';
+      };
+
+      rtpPortLow = mkOption {
+        type = types.int;
+        default = 7070;
+        description = ''
+         Bottom of UDP port range for incoming and outgoing RTP traffic
+        '';
+      };
+
+      rtpPortHigh = mkOption {
+        type = types.int;
+        default = 7089;
+        description = ''
+         Top of UDP port range for incoming and outgoing RTP traffic
+        '';
+      };
+
+      rtpTimeout = mkOption {
+        type = types.int;
+        default = 300;
+        description = ''
+          Timeout for an RTP stream. If for the specified
+          number of seconds no data is relayed on an active
+          stream, it is considered dead and will be killed.
+        '';
+      };
+
+      rtpDscp = mkOption {
+        type = types.int;
+        default = 46;
+        description = ''
+          DSCP (differentiated services) value to be assigned
+          to RTP packets. Allows QOS aware routers to handle
+          different types traffic with different priorities.
+        '';
+      };
+
+      sipDscp = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          DSCP (differentiated services) value to be assigned
+          to SIP packets. Allows QOS aware routers to handle
+          different types traffic with different priorities.
+        '';
+      };
+
+      passwordFile = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Path to per-user password file.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration to add to siproxd configuration.
+        '';
+      };
+
+    };
+
+  };
+
+  ##### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.siproxyd = {
+      uid = config.ids.uids.siproxd;
+    };
+
+    systemd.services.siproxd = {
+      description = "SIP proxy/masquerading daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.siproxd}/sbin/siproxd -c ${confFile}";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/snapper.nix b/nixos/modules/services/misc/snapper.nix
new file mode 100644
index 00000000000..3c3f6c4d641
--- /dev/null
+++ b/nixos/modules/services/misc/snapper.nix
@@ -0,0 +1,187 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.snapper;
+in
+
+{
+  options.services.snapper = {
+
+    snapshotRootOnBoot = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to snapshot root on boot
+      '';
+    };
+
+    snapshotInterval = mkOption {
+      type = types.str;
+      default = "hourly";
+      description = ''
+        Snapshot interval.
+
+        The format is described in
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry>.
+      '';
+    };
+
+    cleanupInterval = mkOption {
+      type = types.str;
+      default = "1d";
+      description = ''
+        Cleanup interval.
+
+        The format is described in
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry>.
+      '';
+    };
+
+    filters = mkOption {
+      type = types.nullOr types.lines;
+      default = null;
+      description = ''
+        Global display difference filter. See man:snapper(8) for more details.
+      '';
+    };
+
+    configs = mkOption {
+      default = { };
+      example = literalExpression ''
+        {
+          home = {
+            subvolume = "/home";
+            extraConfig = '''
+              ALLOW_USERS="alice"
+              TIMELINE_CREATE=yes
+              TIMELINE_CLEANUP=yes
+            ''';
+          };
+        }
+      '';
+
+      description = ''
+        Subvolume configuration
+      '';
+
+      type = types.attrsOf (types.submodule {
+        options = {
+          subvolume = mkOption {
+            type = types.path;
+            description = ''
+              Path of the subvolume or mount point.
+              This path is a subvolume and has to contain a subvolume named
+              .snapshots.
+              See also man:snapper(8) section PERMISSIONS.
+            '';
+          };
+
+          fstype = mkOption {
+            type = types.enum [ "btrfs" ];
+            default = "btrfs";
+            description = ''
+              Filesystem type. Only btrfs is stable and tested.
+            '';
+          };
+
+          extraConfig = mkOption {
+            type = types.lines;
+            default = "";
+            description = ''
+              Additional configuration next to SUBVOLUME and FSTYPE.
+              See man:snapper-configs(5).
+            '';
+          };
+        };
+      });
+    };
+  };
+
+  config = mkIf (cfg.configs != {}) (let
+    documentation = [ "man:snapper(8)" "man:snapper-configs(5)" ];
+  in {
+
+    environment = {
+
+      systemPackages = [ pkgs.snapper ];
+
+      # Note: snapper/config-templates/default is only needed for create-config
+      #       which is not the NixOS way to configure.
+      etc = {
+
+        "sysconfig/snapper".text = ''
+          SNAPPER_CONFIGS="${lib.concatStringsSep " " (builtins.attrNames cfg.configs)}"
+        '';
+
+      }
+      // (mapAttrs' (name: subvolume: nameValuePair "snapper/configs/${name}" ({
+        text = ''
+          ${subvolume.extraConfig}
+          FSTYPE="${subvolume.fstype}"
+          SUBVOLUME="${subvolume.subvolume}"
+        '';
+      })) cfg.configs)
+      // (lib.optionalAttrs (cfg.filters != null) {
+        "snapper/filters/default.txt".text = cfg.filters;
+      });
+
+    };
+
+    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";
+        CapabilityBoundingSet = "CAP_DAC_OVERRIDE CAP_FOWNER CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_SYS_ADMIN CAP_SYS_MODULE CAP_IPC_LOCK CAP_SYS_NICE";
+        LockPersonality = true;
+        NoNewPrivileges = false;
+        PrivateNetwork = true;
+        ProtectHostname = true;
+        RestrictAddressFamilies = "AF_UNIX";
+        RestrictRealtime = true;
+      };
+    };
+
+    systemd.services.snapper-timeline = {
+      description = "Timeline of Snapper Snapshots";
+      inherit documentation;
+      requires = [ "local-fs.target" ];
+      serviceConfig.ExecStart = "${pkgs.snapper}/lib/snapper/systemd-helper --timeline";
+      startAt = cfg.snapshotInterval;
+    };
+
+    systemd.services.snapper-cleanup = {
+      description = "Cleanup of Snapper Snapshots";
+      inherit documentation;
+      serviceConfig.ExecStart = "${pkgs.snapper}/lib/snapper/systemd-helper --cleanup";
+    };
+
+    systemd.timers.snapper-cleanup = {
+      description = "Cleanup of Snapper Snapshots";
+      inherit documentation;
+      wantedBy = [ "timers.target" ];
+      requires = [ "local-fs.target" ];
+      timerConfig.OnBootSec = "10m";
+      timerConfig.OnUnitActiveSec = cfg.cleanupInterval;
+    };
+
+    systemd.services.snapper-boot = lib.optionalAttrs cfg.snapshotRootOnBoot {
+      description = "Take snapper snapshot of root on boot";
+      inherit documentation;
+      serviceConfig.ExecStart = "${pkgs.snapper}/bin/snapper --config root create --cleanup-algorithm number --description boot";
+      serviceConfig.type = "oneshot";
+      requires = [ "local-fs.target" ];
+      wantedBy = [ "multi-user.target" ];
+      unitConfig.ConditionPathExists = "/etc/snapper/configs/root";
+    };
+
+  });
+}
diff --git a/nixos/modules/services/misc/sonarr.nix b/nixos/modules/services/misc/sonarr.nix
new file mode 100644
index 00000000000..77c7f0582d0
--- /dev/null
+++ b/nixos/modules/services/misc/sonarr.nix
@@ -0,0 +1,76 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.sonarr;
+in
+{
+  options = {
+    services.sonarr = {
+      enable = mkEnableOption "Sonarr";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/sonarr/.config/NzbDrone";
+        description = "The directory where Sonarr stores its data files.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports in the firewall for the Sonarr web interface
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "sonarr";
+        description = "User account under which Sonaar runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "sonarr";
+        description = "Group under which Sonaar runs.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0700 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.sonarr = {
+      description = "Sonarr";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.sonarr}/bin/NzbDrone -nobrowser -data='${cfg.dataDir}'";
+        Restart = "on-failure";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 8989 ];
+    };
+
+    users.users = mkIf (cfg.user == "sonarr") {
+      sonarr = {
+        group = cfg.group;
+        home = cfg.dataDir;
+        uid = config.ids.uids.sonarr;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "sonarr") {
+      sonarr.gid = config.ids.gids.sonarr;
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/builds.nix b/nixos/modules/services/misc/sourcehut/builds.nix
new file mode 100644
index 00000000000..685a132d350
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/builds.nix
@@ -0,0 +1,236 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.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";
+      defaultText = literalExpression ''"''${config.${opt.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.literalExpression ''(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.runCommand "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.runCommand "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..21551d7d5f0
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/default.nix
@@ -0,0 +1,1386 @@
+{ config, pkgs, lib, ... }:
+with lib;
+let
+  inherit (config.services) nginx postfix postgresql redis;
+  inherit (config.users) users groups;
+  cfg = config.services.sourcehut;
+  domain = cfg.settings."sr.ht".global-domain;
+  settingsFormat = pkgs.formats.ini {
+    listToValue = concatMapStringsSep "," (generators.mkValueStringDefault {});
+    mkKeyValue = k: v:
+      if v == null then ""
+      else generators.mkKeyValueDefault {
+        mkValueString = v:
+          if v == true then "yes"
+          else if v == false then "no"
+          else generators.mkValueStringDefault {} v;
+      } "=" k v;
+  };
+  configIniOfService = srv: settingsFormat.generate "sourcehut-${srv}-config.ini"
+    # Each service needs access to only a subset of sections (and secrets).
+    (filterAttrs (k: v: v != null)
+    (mapAttrs (section: v:
+      let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht(::.*)?$" section; in
+      if srvMatch == null # Include sections shared by all services
+      || head srvMatch == srv # Include sections for the service being configured
+      then v
+      # Enable Web links and integrations between services.
+      else if tail srvMatch == [ null ] && elem (head srvMatch) cfg.services
+      then {
+        inherit (v) origin;
+        # mansrht crashes without it
+        oauth-client-id = v.oauth-client-id or null;
+      }
+      # Drop sub-sections of other services
+      else null)
+    (recursiveUpdate cfg.settings {
+      # Those paths are mounted using BindPaths= or BindReadOnlyPaths=
+      # for services needing access to them.
+      "builds.sr.ht::worker".buildlogs = "/var/log/sourcehut/buildsrht-worker";
+      "git.sr.ht".post-update-script = "/usr/bin/gitsrht-update-hook";
+      "git.sr.ht".repos = "/var/lib/sourcehut/gitsrht/repos";
+      "hg.sr.ht".changegroup-script = "/usr/bin/hgsrht-hook-changegroup";
+      "hg.sr.ht".repos = "/var/lib/sourcehut/hgsrht/repos";
+      # Making this a per service option despite being in a global section,
+      # so that it uses the redis-server used by the service.
+      "sr.ht".redis-host = cfg.${srv}.redis.host;
+    })));
+  commonServiceSettings = srv: {
+    origin = mkOption {
+      description = "URL ${srv}.sr.ht is being served at (protocol://domain)";
+      type = types.str;
+      default = "https://${srv}.${domain}";
+      defaultText = "https://${srv}.example.com";
+    };
+    debug-host = mkOption {
+      description = "Address to bind the debug server to.";
+      type = with types; nullOr str;
+      default = null;
+    };
+    debug-port = mkOption {
+      description = "Port to bind the debug server to.";
+      type = with types; nullOr str;
+      default = null;
+    };
+    connection-string = mkOption {
+      description = "SQLAlchemy connection string for the database.";
+      type = types.str;
+      default = "postgresql:///localhost?user=${srv}srht&host=/run/postgresql";
+    };
+    migrate-on-upgrade = mkEnableOption "automatic migrations on package upgrade" // { default = true; };
+    oauth-client-id = mkOption {
+      description = "${srv}.sr.ht's OAuth client id for meta.sr.ht.";
+      type = types.str;
+    };
+    oauth-client-secret = mkOption {
+      description = "${srv}.sr.ht's OAuth client secret for meta.sr.ht.";
+      type = types.path;
+      apply = s: "<" + toString s;
+    };
+  };
+
+  # Specialized python containing all the modules
+  python = pkgs.sourcehut.python.withPackages (ps: with ps; [
+    gunicorn
+    eventlet
+    # For monitoring Celery: sudo -u listssrht celery --app listssrht.process -b redis+socket:///run/redis-sourcehut/redis.sock?virtual_host=5 flower
+    flower
+    # Sourcehut services
+    srht
+    buildsrht
+    dispatchsrht
+    gitsrht
+    hgsrht
+    hubsrht
+    listssrht
+    mansrht
+    metasrht
+    # Not a python package
+    #pagessrht
+    pastesrht
+    todosrht
+  ]);
+  mkOptionNullOrStr = description: mkOption {
+    inherit description;
+    type = with types; nullOr str;
+    default = null;
+  };
+in
+{
+  options.services.sourcehut = {
+    enable = mkEnableOption ''
+      sourcehut - git hosting, continuous integration, mailing list, ticket tracking,
+      task dispatching, wiki and account management services
+    '';
+
+    services = mkOption {
+      type = with types; listOf (enum
+        [ "builds" "dispatch" "git" "hg" "hub" "lists" "man" "meta" "pages" "paste" "todo" ]);
+      defaultText = "locally enabled services";
+      description = ''
+        Services that may be displayed as links in the title bar of the Web interface.
+      '';
+    };
+
+    listenAddress = mkOption {
+      type = types.str;
+      default = "localhost";
+      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.
+      '';
+    };
+
+    minio = {
+      enable = mkEnableOption ''local minio integration'';
+    };
+
+    nginx = {
+      enable = mkEnableOption ''local nginx integration'';
+      virtualHost = mkOption {
+        type = types.attrs;
+        default = {};
+        description = "Virtual-host configuration merged with all Sourcehut's virtual-hosts.";
+      };
+    };
+
+    postfix = {
+      enable = mkEnableOption ''local postfix integration'';
+    };
+
+    postgresql = {
+      enable = mkEnableOption ''local postgresql integration'';
+    };
+
+    redis = {
+      enable = mkEnableOption ''local redis integration in a dedicated redis-server'';
+    };
+
+    settings = mkOption {
+      type = lib.types.submodule {
+        freeformType = settingsFormat.type;
+        options."sr.ht" = {
+          global-domain = mkOption {
+            description = "Global domain name.";
+            type = types.str;
+            example = "example.com";
+          };
+          environment = mkOption {
+            description = "Values other than \"production\" adds a banner to each page.";
+            type = types.enum [ "development" "production" ];
+            default = "development";
+          };
+          network-key = mkOption {
+            description = ''
+              An absolute file path (which should be outside the Nix-store)
+              to a secret key to encrypt internal messages with. Use <code>srht-keygen network</code> to
+              generate this key. It must be consistent between all services and nodes.
+            '';
+            type = types.path;
+            apply = s: "<" + toString s;
+          };
+          owner-email = mkOption {
+            description = "Owner's email.";
+            type = types.str;
+            default = "contact@example.com";
+          };
+          owner-name = mkOption {
+            description = "Owner's name.";
+            type = types.str;
+            default = "John Doe";
+          };
+          site-blurb = mkOption {
+            description = "Blurb for your site.";
+            type = types.str;
+            default = "the hacker's forge";
+          };
+          site-info = mkOption {
+            description = "The top-level info page for your site.";
+            type = types.str;
+            default = "https://sourcehut.org";
+          };
+          service-key = mkOption {
+            description = ''
+              An absolute file path (which should be outside the Nix-store)
+              to a key used for encrypting session cookies. Use <code>srht-keygen service</code> to
+              generate the service key. This must be shared between each node of the same
+              service (e.g. git1.sr.ht and git2.sr.ht), but different services may use
+              different keys. If you configure all of your services with the same
+              config.ini, you may use the same service-key for all of them.
+            '';
+            type = types.path;
+            apply = s: "<" + toString s;
+          };
+          site-name = mkOption {
+            description = "The name of your network of sr.ht-based sites.";
+            type = types.str;
+            default = "sourcehut";
+          };
+          source-url = mkOption {
+            description = "The source code for your fork of sr.ht.";
+            type = types.str;
+            default = "https://git.sr.ht/~sircmpwn/srht";
+          };
+        };
+        options.mail = {
+          smtp-host = mkOptionNullOrStr "Outgoing SMTP host.";
+          smtp-port = mkOption {
+            description = "Outgoing SMTP port.";
+            type = with types; nullOr port;
+            default = null;
+          };
+          smtp-user = mkOptionNullOrStr "Outgoing SMTP user.";
+          smtp-password = mkOptionNullOrStr "Outgoing SMTP password.";
+          smtp-from = mkOptionNullOrStr "Outgoing SMTP FROM.";
+          error-to = mkOptionNullOrStr "Address receiving application exceptions";
+          error-from = mkOptionNullOrStr "Address sending application exceptions";
+          pgp-privkey = mkOptionNullOrStr ''
+            An absolute file path (which should be outside the Nix-store)
+            to an OpenPGP private key.
+
+            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 <code>gpg --edit-key [key-id]</code>,
+            then use the <code>passwd</code> command and do not enter a new password.
+          '';
+          pgp-pubkey = mkOptionNullOrStr "OpenPGP public key.";
+          pgp-key-id = mkOptionNullOrStr "OpenPGP key identifier.";
+        };
+        options.objects = {
+          s3-upstream = mkOption {
+            description = "Configure the S3-compatible object storage service.";
+            type = with types; nullOr str;
+            default = null;
+          };
+          s3-access-key = mkOption {
+            description = "Access key to the S3-compatible object storage service";
+            type = with types; nullOr str;
+            default = null;
+          };
+          s3-secret-key = mkOption {
+            description = ''
+              An absolute file path (which should be outside the Nix-store)
+              to the secret key of the S3-compatible object storage service.
+            '';
+            type = with types; nullOr path;
+            default = null;
+            apply = mapNullable (s: "<" + toString s);
+          };
+        };
+        options.webhooks = {
+          private-key = mkOption {
+            description = ''
+              An absolute file path (which should be outside the Nix-store)
+              to a base64-encoded Ed25519 key for signing webhook payloads.
+              This should be consistent for all *.sr.ht sites,
+              as this key will be used to verify signatures
+              from other sites in your network.
+              Use the <code>srht-keygen webhook</code> command to generate a key.
+            '';
+            type = types.path;
+            apply = s: "<" + toString s;
+          };
+        };
+
+        options."dispatch.sr.ht" = commonServiceSettings "dispatch" // {
+        };
+        options."dispatch.sr.ht::github" = {
+          oauth-client-id = mkOptionNullOrStr "OAuth client id.";
+          oauth-client-secret = mkOptionNullOrStr "OAuth client secret.";
+        };
+        options."dispatch.sr.ht::gitlab" = {
+          enabled = mkEnableOption "GitLab integration";
+          canonical-upstream = mkOption {
+            type = types.str;
+            description = "Canonical upstream.";
+            default = "gitlab.com";
+          };
+          repo-cache = mkOption {
+            type = types.str;
+            description = "Repository cache directory.";
+            default = "./repo-cache";
+          };
+          "gitlab.com" = mkOption {
+            type = with types; nullOr str;
+            description = "GitLab id and secret.";
+            default = null;
+            example = "GitLab:application id:secret";
+          };
+        };
+
+        options."builds.sr.ht" = commonServiceSettings "builds" // {
+          allow-free = mkEnableOption "nonpaying users to submit builds";
+          redis = mkOption {
+            description = "The Redis connection used for the Celery worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-buildsrht/redis.sock?virtual_host=2";
+          };
+          shell = mkOption {
+            description = ''
+              Scripts used to launch on SSH connection.
+              <literal>/usr/bin/master-shell</literal> on master,
+              <literal>/usr/bin/runner-shell</literal> on runner.
+              If master and worker are on the same system
+              set to <literal>/usr/bin/runner-shell</literal>.
+            '';
+            type = types.enum ["/usr/bin/master-shell" "/usr/bin/runner-shell"];
+            default = "/usr/bin/master-shell";
+          };
+        };
+        options."builds.sr.ht::worker" = {
+          bind-address = mkOption {
+            description = ''
+              HTTP bind address for serving local build information/monitoring.
+            '';
+            type = types.str;
+            default = "localhost:8080";
+          };
+          buildlogs = mkOption {
+            description = "Path to write build logs.";
+            type = types.str;
+            default = "/var/log/sourcehut/buildsrht-worker";
+          };
+          name = mkOption {
+            description = ''
+              Listening address and listening port
+              of the build runner (with HTTP port if not 80).
+            '';
+            type = types.str;
+            default = "localhost:5020";
+          };
+          timeout = mkOption {
+            description = ''
+              Max build duration.
+              See <link xlink:href="https://golang.org/pkg/time/#ParseDuration"/>.
+            '';
+            type = types.str;
+            default = "3m";
+          };
+        };
+
+        options."git.sr.ht" = commonServiceSettings "git" // {
+          outgoing-domain = mkOption {
+            description = "Outgoing domain.";
+            type = types.str;
+            default = "https://git.localhost.localdomain";
+          };
+          post-update-script = mkOption {
+            description = ''
+              A post-update script which is installed in every git repo.
+              This setting is propagated to newer and existing repositories.
+            '';
+            type = types.path;
+            default = "${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
+            defaultText = "\${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
+          };
+          repos = mkOption {
+            description = ''
+              Path to git repositories on disk.
+              If changing the default, you must ensure that
+              the gitsrht's user as read and write access to it.
+            '';
+            type = types.str;
+            default = "/var/lib/sourcehut/gitsrht/repos";
+          };
+          webhooks = mkOption {
+            description = "The Redis connection used for the webhooks worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-gitsrht/redis.sock?virtual_host=1";
+          };
+        };
+        options."git.sr.ht::api" = {
+          internal-ipnet = mkOption {
+            description = ''
+              Set of IP subnets which are permitted to utilize internal API
+              authentication. This should be limited to the subnets
+              from which your *.sr.ht services are running.
+              See <xref linkend="opt-services.sourcehut.listenAddress"/>.
+            '';
+            type = with types; listOf str;
+            default = [ "127.0.0.0/8" "::1/128" ];
+          };
+        };
+
+        options."hg.sr.ht" = commonServiceSettings "hg" // {
+          changegroup-script = mkOption {
+            description = ''
+              A changegroup script which is installed in every mercurial repo.
+              This setting is propagated to newer and existing repositories.
+            '';
+            type = types.str;
+            default = "${cfg.python}/bin/hgsrht-hook-changegroup";
+            defaultText = "\${cfg.python}/bin/hgsrht-hook-changegroup";
+          };
+          repos = mkOption {
+            description = ''
+              Path to mercurial repositories on disk.
+              If changing the default, you must ensure that
+              the hgsrht's user as read and write access to it.
+            '';
+            type = types.str;
+            default = "/var/lib/sourcehut/hgsrht/repos";
+          };
+          srhtext = mkOptionNullOrStr ''
+            Path to the srht mercurial extension
+            (defaults to where the hgsrht code is)
+          '';
+          clone_bundle_threshold = mkOption {
+            description = ".hg/store size (in MB) past which the nightly job generates clone bundles.";
+            type = types.ints.unsigned;
+            default = 50;
+          };
+          hg_ssh = mkOption {
+            description = "Path to hg-ssh (if not in $PATH).";
+            type = types.str;
+            default = "${pkgs.mercurial}/bin/hg-ssh";
+            defaultText = "\${pkgs.mercurial}/bin/hg-ssh";
+          };
+          webhooks = mkOption {
+            description = "The Redis connection used for the webhooks worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-hgsrht/redis.sock?virtual_host=1";
+          };
+        };
+
+        options."hub.sr.ht" = commonServiceSettings "hub" // {
+        };
+
+        options."lists.sr.ht" = commonServiceSettings "lists" // {
+          allow-new-lists = mkEnableOption "Allow creation of new lists.";
+          notify-from = mkOption {
+            description = "Outgoing email for notifications generated by users.";
+            type = types.str;
+            default = "lists-notify@localhost.localdomain";
+          };
+          posting-domain = mkOption {
+            description = "Posting domain.";
+            type = types.str;
+            default = "lists.localhost.localdomain";
+          };
+          redis = mkOption {
+            description = "The Redis connection used for the Celery worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-listssrht/redis.sock?virtual_host=2";
+          };
+          webhooks = mkOption {
+            description = "The Redis connection used for the webhooks worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-listssrht/redis.sock?virtual_host=1";
+          };
+        };
+        options."lists.sr.ht::worker" = {
+          reject-mimetypes = mkOption {
+            description = ''
+              Comma-delimited list of Content-Types to reject. Messages with Content-Types
+              included in this list are rejected. Multipart messages are always supported,
+              and each part is checked against this list.
+
+              Uses fnmatch for wildcard expansion.
+            '';
+            type = with types; listOf str;
+            default = ["text/html"];
+          };
+          reject-url = mkOption {
+            description = "Reject URL.";
+            type = types.str;
+            default = "https://man.sr.ht/lists.sr.ht/etiquette.md";
+          };
+          sock = mkOption {
+            description = ''
+              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.
+            '';
+            type = types.str;
+            default = "/tmp/lists.sr.ht-lmtp.sock";
+          };
+          sock-group = mkOption {
+            description = ''
+              The lmtp daemon will make the unix socket group-read/write
+              for users in this group.
+            '';
+            type = types.str;
+            default = "postfix";
+          };
+        };
+
+        options."man.sr.ht" = commonServiceSettings "man" // {
+        };
+
+        options."meta.sr.ht" =
+          removeAttrs (commonServiceSettings "meta")
+            ["oauth-client-id" "oauth-client-secret"] // {
+          api-origin = mkOption {
+            description = "Origin URL for API, 100 more than web.";
+            type = types.str;
+            default = "http://${cfg.listenAddress}:${toString (cfg.meta.port + 100)}";
+            defaultText = ''http://<xref linkend="opt-services.sourcehut.listenAddress"/>:''${toString (<xref linkend="opt-services.sourcehut.meta.port"/> + 100)}'';
+          };
+          webhooks = mkOption {
+            description = "The Redis connection used for the webhooks worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-metasrht/redis.sock?virtual_host=1";
+          };
+          welcome-emails = mkEnableOption "sending stock sourcehut welcome emails after signup";
+        };
+        options."meta.sr.ht::api" = {
+          internal-ipnet = mkOption {
+            description = ''
+              Set of IP subnets which are permitted to utilize internal API
+              authentication. This should be limited to the subnets
+              from which your *.sr.ht services are running.
+              See <xref linkend="opt-services.sourcehut.listenAddress"/>.
+            '';
+            type = with types; listOf str;
+            default = [ "127.0.0.0/8" "::1/128" ];
+          };
+        };
+        options."meta.sr.ht::aliases" = mkOption {
+          description = "Aliases for the client IDs of commonly used OAuth clients.";
+          type = with types; attrsOf int;
+          default = {};
+          example = { "git.sr.ht" = 12345; };
+        };
+        options."meta.sr.ht::billing" = {
+          enabled = mkEnableOption "the billing system";
+          stripe-public-key = mkOptionNullOrStr "Public key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys";
+          stripe-secret-key = mkOptionNullOrStr ''
+            An absolute file path (which should be outside the Nix-store)
+            to a secret key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys
+          '' // {
+            apply = mapNullable (s: "<" + toString s);
+          };
+        };
+        options."meta.sr.ht::settings" = {
+          registration = mkEnableOption "public registration";
+          onboarding-redirect = mkOption {
+            description = "Where to redirect new users upon registration.";
+            type = types.str;
+            default = "https://meta.localhost.localdomain";
+          };
+          user-invites = mkOption {
+            description = ''
+              How many invites each user is issued upon registration
+              (only applicable if open registration is disabled).
+            '';
+            type = types.ints.unsigned;
+            default = 5;
+          };
+        };
+
+        options."pages.sr.ht" = commonServiceSettings "pages" // {
+          gemini-certs = mkOption {
+            description = ''
+              An absolute file path (which should be outside the Nix-store)
+              to Gemini certificates.
+            '';
+            type = with types; nullOr path;
+            default = null;
+          };
+          max-site-size = mkOption {
+            description = "Maximum size of any given site (post-gunzip), in MiB.";
+            type = types.int;
+            default = 1024;
+          };
+          user-domain = mkOption {
+            description = ''
+              Configures the user domain, if enabled.
+              All users are given &lt;username&gt;.this.domain.
+            '';
+            type = with types; nullOr str;
+            default = null;
+          };
+        };
+        options."pages.sr.ht::api" = {
+          internal-ipnet = mkOption {
+            description = ''
+              Set of IP subnets which are permitted to utilize internal API
+              authentication. This should be limited to the subnets
+              from which your *.sr.ht services are running.
+              See <xref linkend="opt-services.sourcehut.listenAddress"/>.
+            '';
+            type = with types; listOf str;
+            default = [ "127.0.0.0/8" "::1/128" ];
+          };
+        };
+
+        options."paste.sr.ht" = commonServiceSettings "paste" // {
+        };
+
+        options."todo.sr.ht" = commonServiceSettings "todo" // {
+          notify-from = mkOption {
+            description = "Outgoing email for notifications generated by users.";
+            type = types.str;
+            default = "todo-notify@localhost.localdomain";
+          };
+          webhooks = mkOption {
+            description = "The Redis connection used for the webhooks worker.";
+            type = types.str;
+            default = "redis+socket:///run/redis-sourcehut-todosrht/redis.sock?virtual_host=1";
+          };
+        };
+        options."todo.sr.ht::mail" = {
+          posting-domain = mkOption {
+            description = "Posting domain.";
+            type = types.str;
+            default = "todo.localhost.localdomain";
+          };
+          sock = mkOption {
+            description = ''
+              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.
+            '';
+            type = types.str;
+            default = "/tmp/todo.sr.ht-lmtp.sock";
+          };
+          sock-group = mkOption {
+            description = ''
+              The lmtp daemon will make the unix socket group-read/write
+              for users in this group.
+            '';
+            type = types.str;
+            default = "postfix";
+          };
+        };
+      };
+      default = { };
+      description = ''
+        The configuration for the sourcehut network.
+      '';
+    };
+
+    builds = {
+      enableWorker = mkEnableOption ''
+        worker for builds.sr.ht
+
+        <warning><para>
+        For smaller deployments, job runners can be installed alongside the master server
+        but even if you only build your own software, integration with other services
+        may cause you to run untrusted builds
+        (e.g. automatic testing of patches via listssrht).
+        See <link xlink:href="https://man.sr.ht/builds.sr.ht/configuration.md#security-model"/>.
+        </para></warning>
+      '';
+
+      images = mkOption {
+        type = with types; attrsOf (attrsOf (attrsOf package));
+        default = { };
+        example = lib.literalExpression ''(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 = (import ("''${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") {
+              pkgs = (import pkgs_unstable {});
+            });
+          in
+          {
+            nixos.unstable.x86_64 = image_from_nixpkgs;
+          }
+        )'';
+        description = ''
+          Images for builds.sr.ht. Each package should be distro.release.arch and point to a /nix/store/package/root.img.qcow2.
+        '';
+      };
+    };
+
+    git = {
+      package = mkOption {
+        type = types.package;
+        default = pkgs.git;
+        defaultText = literalExpression "pkgs.git";
+        example = literalExpression "pkgs.gitFull";
+        description = ''
+          Git package for git.sr.ht. This can help silence collisions.
+        '';
+      };
+      fcgiwrap.preforkProcess = mkOption {
+        description = "Number of fcgiwrap processes to prefork.";
+        type = types.int;
+        default = 4;
+      };
+    };
+
+    hg = {
+      package = mkOption {
+        type = types.package;
+        default = pkgs.mercurial;
+        defaultText = literalExpression "pkgs.mercurial";
+        description = ''
+          Mercurial package for hg.sr.ht. This can help silence collisions.
+        '';
+      };
+      cloneBundles = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Generate clonebundles (which require more disk space but dramatically speed up cloning large repositories).
+        '';
+      };
+    };
+
+    lists = {
+      process = {
+        extraArgs = mkOption {
+          type = with types; listOf str;
+          default = [ "--loglevel DEBUG" "--pool eventlet" "--without-heartbeat" ];
+          description = "Extra arguments passed to the Celery responsible for processing mails.";
+        };
+        celeryConfig = mkOption {
+          type = types.lines;
+          default = "";
+          description = "Content of the <literal>celeryconfig.py</literal> used by the Celery of <literal>listssrht-process</literal>.";
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable (mkMerge [
+    {
+      environment.systemPackages = [ pkgs.sourcehut.coresrht ];
+
+      services.sourcehut.settings = {
+        "git.sr.ht".outgoing-domain = mkDefault "https://git.${domain}";
+        "lists.sr.ht".notify-from = mkDefault "lists-notify@${domain}";
+        "lists.sr.ht".posting-domain = mkDefault "lists.${domain}";
+        "meta.sr.ht::settings".onboarding-redirect = mkDefault "https://meta.${domain}";
+        "todo.sr.ht".notify-from = mkDefault "todo-notify@${domain}";
+        "todo.sr.ht::mail".posting-domain = mkDefault "todo.${domain}";
+      };
+    }
+    (mkIf cfg.postgresql.enable {
+      assertions = [
+        { assertion = postgresql.enable;
+          message = "postgresql must be enabled and configured";
+        }
+      ];
+    })
+    (mkIf cfg.postfix.enable {
+      assertions = [
+        { assertion = postfix.enable;
+          message = "postfix must be enabled and configured";
+        }
+      ];
+      # Needed for sharing the LMTP sockets with JoinsNamespaceOf=
+      systemd.services.postfix.serviceConfig.PrivateTmp = true;
+    })
+    (mkIf cfg.redis.enable {
+      services.redis.vmOverCommit = mkDefault true;
+    })
+    (mkIf cfg.nginx.enable {
+      assertions = [
+        { assertion = nginx.enable;
+          message = "nginx must be enabled and configured";
+        }
+      ];
+      # For proxyPass= in virtual-hosts for Sourcehut services.
+      services.nginx.recommendedProxySettings = mkDefault true;
+    })
+    (mkIf (cfg.builds.enable || cfg.git.enable || cfg.hg.enable) {
+      services.openssh = {
+        # Note that sshd will continue to honor AuthorizedKeysFile.
+        # Note that you may want automatically rotate
+        # or link to /dev/null the following log files:
+        # - /var/log/gitsrht-dispatch
+        # - /var/log/{build,git,hg}srht-keys
+        # - /var/log/{git,hg}srht-shell
+        # - /var/log/gitsrht-update-hook
+        authorizedKeysCommand = ''/etc/ssh/sourcehut/subdir/srht-dispatch "%u" "%h" "%t" "%k"'';
+        # srht-dispatch will setuid/setgid according to [git.sr.ht::dispatch]
+        authorizedKeysCommandUser = "root";
+        extraConfig = ''
+          PermitUserEnvironment SRHT_*
+        '';
+      };
+      environment.etc."ssh/sourcehut/config.ini".source =
+        settingsFormat.generate "sourcehut-dispatch-config.ini"
+          (filterAttrs (k: v: k == "git.sr.ht::dispatch")
+          cfg.settings);
+      environment.etc."ssh/sourcehut/subdir/srht-dispatch" = {
+        # sshd_config(5): The program must be owned by root, not writable by group or others
+        mode = "0755";
+        source = pkgs.writeShellScript "srht-dispatch" ''
+          set -e
+          cd /etc/ssh/sourcehut/subdir
+          ${cfg.python}/bin/gitsrht-dispatch "$@"
+        '';
+      };
+      systemd.services.sshd = {
+        #path = optional cfg.git.enable [ cfg.git.package ];
+        serviceConfig = {
+          BindReadOnlyPaths =
+            # Note that those /usr/bin/* paths are hardcoded in multiple places in *.sr.ht,
+            # for instance to get the user from the [git.sr.ht::dispatch] settings.
+            # *srht-keys needs to:
+            # - access a redis-server in [sr.ht] redis-host,
+            # - access the PostgreSQL server in [*.sr.ht] connection-string,
+            # - query metasrht-api (through the HTTP API).
+            # Using this has the side effect of creating empty files in /usr/bin/
+            optionals cfg.builds.enable [
+              "${pkgs.writeShellScript "buildsrht-keys-wrapper" ''
+                set -e
+                cd /run/sourcehut/buildsrht/subdir
+                set -x
+                exec -a "$0" ${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys "$@"
+              ''}:/usr/bin/buildsrht-keys"
+              "${pkgs.sourcehut.buildsrht}/bin/master-shell:/usr/bin/master-shell"
+              "${pkgs.sourcehut.buildsrht}/bin/runner-shell:/usr/bin/runner-shell"
+            ] ++
+            optionals cfg.git.enable [
+              # /path/to/gitsrht-keys calls /path/to/gitsrht-shell,
+              # or [git.sr.ht] shell= if set.
+              "${pkgs.writeShellScript "gitsrht-keys-wrapper" ''
+                set -e
+                cd /run/sourcehut/gitsrht/subdir
+                set -x
+                exec -a "$0" ${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys "$@"
+              ''}:/usr/bin/gitsrht-keys"
+              "${pkgs.writeShellScript "gitsrht-shell-wrapper" ''
+                set -e
+                cd /run/sourcehut/gitsrht/subdir
+                set -x
+                exec -a "$0" ${pkgs.sourcehut.gitsrht}/bin/gitsrht-shell "$@"
+              ''}:/usr/bin/gitsrht-shell"
+              "${pkgs.writeShellScript "gitsrht-update-hook" ''
+                set -e
+                test -e "''${PWD%/*}"/config.ini ||
+                # Git hooks are run relative to their repository's directory,
+                # but gitsrht-update-hook looks up ../config.ini
+                ln -s /run/sourcehut/gitsrht/config.ini "''${PWD%/*}"/config.ini
+                # hooks/post-update calls /usr/bin/gitsrht-update-hook as hooks/stage-3
+                # but this wrapper being a bash script, it overrides $0 with /usr/bin/gitsrht-update-hook
+                # hence this hack to put hooks/stage-3 back into gitsrht-update-hook's $0
+                if test "''${STAGE3:+set}"
+                then
+                  set -x
+                  exec -a hooks/stage-3 ${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook "$@"
+                else
+                  export STAGE3=set
+                  set -x
+                  exec -a "$0" ${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook "$@"
+                fi
+              ''}:/usr/bin/gitsrht-update-hook"
+            ] ++
+            optionals cfg.hg.enable [
+              # /path/to/hgsrht-keys calls /path/to/hgsrht-shell,
+              # or [hg.sr.ht] shell= if set.
+              "${pkgs.writeShellScript "hgsrht-keys-wrapper" ''
+                set -e
+                cd /run/sourcehut/hgsrht/subdir
+                set -x
+                exec -a "$0" ${pkgs.sourcehut.hgsrht}/bin/hgsrht-keys "$@"
+              ''}:/usr/bin/hgsrht-keys"
+              "${pkgs.writeShellScript "hgsrht-shell-wrapper" ''
+                set -e
+                cd /run/sourcehut/hgsrht/subdir
+                set -x
+                exec -a "$0" ${pkgs.sourcehut.hgsrht}/bin/hgsrht-shell "$@"
+              ''}:/usr/bin/hgsrht-shell"
+              # Mercurial's changegroup hooks are run relative to their repository's directory,
+              # but hgsrht-hook-changegroup looks up ./config.ini
+              "${pkgs.writeShellScript "hgsrht-hook-changegroup" ''
+                set -e
+                test -e "''$PWD"/config.ini ||
+                ln -s /run/sourcehut/hgsrht/config.ini "''$PWD"/config.ini
+                set -x
+                exec -a "$0" ${cfg.python}/bin/hgsrht-hook-changegroup "$@"
+              ''}:/usr/bin/hgsrht-hook-changegroup"
+            ];
+        };
+      };
+    })
+  ]);
+
+  imports = [
+
+    (import ./service.nix "builds" {
+      inherit configIniOfService;
+      srvsrht = "buildsrht";
+      port = 5002;
+      # TODO: a celery worker on the master and worker are apparently needed
+      extraServices.buildsrht-worker = let
+        qemuPackage = pkgs.qemu_kvm;
+        serviceName = "buildsrht-worker";
+        statePath = "/var/lib/sourcehut/${serviceName}";
+        in mkIf cfg.builds.enableWorker {
+        path = [ pkgs.openssh pkgs.docker ];
+        preStart = ''
+          set -x
+          if test -z "$(docker images -q qemu:latest 2>/dev/null)" \
+          || test "$(cat ${statePath}/docker-image-qemu)" != "${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
+            echo '${qemuPackage.version}' >${statePath}/docker-image-qemu
+          fi
+        '';
+        serviceConfig = {
+          ExecStart = "${pkgs.sourcehut.buildsrht}/bin/builds.sr.ht-worker";
+          BindPaths = [ cfg.settings."builds.sr.ht::worker".buildlogs ];
+          LogsDirectory = [ "sourcehut/${serviceName}" ];
+          RuntimeDirectory = [ "sourcehut/${serviceName}/subdir" ];
+          StateDirectory = [ "sourcehut/${serviceName}" ];
+          TimeoutStartSec = "1800s";
+          # builds.sr.ht-worker looks up ../config.ini
+          WorkingDirectory = "-"+"/run/sourcehut/${serviceName}/subdir";
+        };
+      };
+      extraConfig = let
+        image_dirs = flatten (
+          mapAttrsToList (distro: revs:
+            mapAttrsToList (rev: archs:
+              mapAttrsToList (arch: image:
+                pkgs.runCommand "buildsrht-images" { } ''
+                  mkdir -p $out/${distro}/${rev}/${arch}
+                  ln -s ${image}/*.qcow2 $out/${distro}/${rev}/${arch}/root.img.qcow2
+                ''
+              ) archs
+            ) revs
+          ) cfg.builds.images
+        );
+        image_dir_pre = pkgs.symlinkJoin {
+          name = "builds.sr.ht-worker-images-pre";
+          paths = image_dirs;
+            # FIXME: not working, apparently because ubuntu/latest is a broken link
+            # ++ [ "${pkgs.sourcehut.buildsrht}/lib/images" ];
+        };
+        image_dir = pkgs.runCommand "builds.sr.ht-worker-images" { } ''
+          mkdir -p $out/images
+          cp -Lr ${image_dir_pre}/* $out/images
+        '';
+        in mkMerge [
+        {
+          users.users.${cfg.builds.user}.shell = pkgs.bash;
+
+          virtualisation.docker.enable = true;
+
+          services.sourcehut.settings = mkMerge [
+            { # Note that git.sr.ht::dispatch is not a typo,
+              # gitsrht-dispatch always use this section
+              "git.sr.ht::dispatch"."/usr/bin/buildsrht-keys" =
+                mkDefault "${cfg.builds.user}:${cfg.builds.group}";
+            }
+            (mkIf cfg.builds.enableWorker {
+              "builds.sr.ht::worker".shell = "/usr/bin/runner-shell";
+              "builds.sr.ht::worker".images = mkDefault "${image_dir}/images";
+              "builds.sr.ht::worker".controlcmd = mkDefault "${image_dir}/images/control";
+            })
+          ];
+        }
+        (mkIf cfg.builds.enableWorker {
+          users.groups = {
+            docker.members = [ cfg.builds.user ];
+          };
+        })
+        (mkIf (cfg.builds.enableWorker && cfg.nginx.enable) {
+          # Allow nginx access to buildlogs
+          users.users.${nginx.user}.extraGroups = [ cfg.builds.group ];
+          systemd.services.nginx = {
+            serviceConfig.BindReadOnlyPaths = [ cfg.settings."builds.sr.ht::worker".buildlogs ];
+          };
+          services.nginx.virtualHosts."logs.${domain}" = mkMerge [ {
+            /* FIXME: is a listen needed?
+            listen = with builtins;
+              # FIXME: not compatible with IPv6
+              let address = split ":" cfg.settings."builds.sr.ht::worker".name; in
+              [{ addr = elemAt address 0; port = lib.toInt (elemAt address 2); }];
+            */
+            locations."/logs/".alias = cfg.settings."builds.sr.ht::worker".buildlogs + "/";
+          } cfg.nginx.virtualHost ];
+        })
+      ];
+    })
+
+    (import ./service.nix "dispatch" {
+      inherit configIniOfService;
+      port = 5005;
+    })
+
+    (import ./service.nix "git" (let
+      baseService = {
+        path = [ cfg.git.package ];
+        serviceConfig.BindPaths = [ "${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos" ];
+      };
+      in {
+      inherit configIniOfService;
+      mainService = mkMerge [ baseService {
+        serviceConfig.StateDirectory = [ "sourcehut/gitsrht" "sourcehut/gitsrht/repos" ];
+        preStart = mkIf (!versionAtLeast config.system.stateVersion "22.05") (mkBefore ''
+          # Fix Git hooks of repositories pre-dating https://github.com/NixOS/nixpkgs/pull/133984
+          (
+          set +f
+          shopt -s nullglob
+          for h in /var/lib/sourcehut/gitsrht/repos/~*/*/hooks/{pre-receive,update,post-update}
+          do ln -fnsv /usr/bin/gitsrht-update-hook "$h"; done
+          )
+        '');
+      } ];
+      port = 5001;
+      webhooks = true;
+      extraTimers.gitsrht-periodic = {
+        service = baseService;
+        timerConfig.OnCalendar = ["*:0/20"];
+      };
+      extraConfig = mkMerge [
+        {
+          # 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...
+          users.users.${cfg.git.user}.shell = pkgs.bash;
+          services.sourcehut.settings = {
+            "git.sr.ht::dispatch"."/usr/bin/gitsrht-keys" =
+              mkDefault "${cfg.git.user}:${cfg.git.group}";
+          };
+          systemd.services.sshd = baseService;
+        }
+        (mkIf cfg.nginx.enable {
+          services.nginx.virtualHosts."git.${domain}" = {
+            locations."/authorize" = {
+              proxyPass = "http://${cfg.listenAddress}:${toString cfg.git.port}";
+              extraConfig = ''
+                proxy_pass_request_body off;
+                proxy_set_header Content-Length "";
+                proxy_set_header X-Original-URI $request_uri;
+              '';
+            };
+            locations."~ ^/([^/]+)/([^/]+)/(HEAD|info/refs|objects/info/.*|git-upload-pack).*$" = {
+              root = "/var/lib/sourcehut/gitsrht/repos";
+              fastcgiParams = {
+                GIT_HTTP_EXPORT_ALL = "";
+                GIT_PROJECT_ROOT = "$document_root";
+                PATH_INFO = "$uri";
+                SCRIPT_FILENAME = "${cfg.git.package}/bin/git-http-backend";
+              };
+              extraConfig = ''
+                auth_request /authorize;
+                fastcgi_read_timeout 500s;
+                fastcgi_pass unix:/run/gitsrht-fcgiwrap.sock;
+                gzip off;
+              '';
+            };
+          };
+          systemd.sockets.gitsrht-fcgiwrap = {
+            before = [ "nginx.service" ];
+            wantedBy = [ "sockets.target" "gitsrht.service" ];
+            # This path remains accessible to nginx.service, which has no RootDirectory=
+            socketConfig.ListenStream = "/run/gitsrht-fcgiwrap.sock";
+            socketConfig.SocketUser = nginx.user;
+            socketConfig.SocketMode = "600";
+          };
+        })
+      ];
+      extraServices.gitsrht-fcgiwrap = mkIf cfg.nginx.enable {
+        serviceConfig = {
+          # Socket is passed by gitsrht-fcgiwrap.socket
+          ExecStart = "${pkgs.fcgiwrap}/sbin/fcgiwrap -c ${toString cfg.git.fcgiwrap.preforkProcess}";
+          # No need for config.ini
+          ExecStartPre = mkForce [];
+          User = null;
+          DynamicUser = true;
+          BindReadOnlyPaths = [ "${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos" ];
+          IPAddressDeny = "any";
+          InaccessiblePaths = [ "-+/run/postgresql" "-+/run/redis-sourcehut" ];
+          PrivateNetwork = true;
+          RestrictAddressFamilies = mkForce [ "none" ];
+          SystemCallFilter = mkForce [
+            "@system-service"
+            "~@aio" "~@keyring" "~@memlock" "~@privileged" "~@resources" "~@setuid"
+            # @timer is needed for alarm()
+          ];
+        };
+      };
+    }))
+
+    (import ./service.nix "hg" (let
+      baseService = {
+        path = [ cfg.hg.package ];
+        serviceConfig.BindPaths = [ "${cfg.settings."hg.sr.ht".repos}:/var/lib/sourcehut/hgsrht/repos" ];
+      };
+      in {
+      inherit configIniOfService;
+      mainService = mkMerge [ baseService {
+        serviceConfig.StateDirectory = [ "sourcehut/hgsrht" "sourcehut/hgsrht/repos" ];
+      } ];
+      port = 5010;
+      webhooks = true;
+      extraTimers.hgsrht-periodic = {
+        service = baseService;
+        timerConfig.OnCalendar = ["*:0/20"];
+      };
+      extraTimers.hgsrht-clonebundles = mkIf cfg.hg.cloneBundles {
+        service = baseService;
+        timerConfig.OnCalendar = ["daily"];
+        timerConfig.AccuracySec = "1h";
+      };
+      extraConfig = mkMerge [
+        {
+          users.users.${cfg.hg.user}.shell = pkgs.bash;
+          services.sourcehut.settings = {
+            # Note that git.sr.ht::dispatch is not a typo,
+            # gitsrht-dispatch always uses this section.
+            "git.sr.ht::dispatch"."/usr/bin/hgsrht-keys" =
+              mkDefault "${cfg.hg.user}:${cfg.hg.group}";
+          };
+          systemd.services.sshd = baseService;
+        }
+        (mkIf cfg.nginx.enable {
+          # Allow nginx access to repositories
+          users.users.${nginx.user}.extraGroups = [ cfg.hg.group ];
+          services.nginx.virtualHosts."hg.${domain}" = {
+            locations."/authorize" = {
+              proxyPass = "http://${cfg.listenAddress}:${toString cfg.hg.port}";
+              extraConfig = ''
+                proxy_pass_request_body off;
+                proxy_set_header Content-Length "";
+                proxy_set_header X-Original-URI $request_uri;
+              '';
+            };
+            # Let clients reach pull bundles. We don't really need to lock this down even for
+            # private repos because the bundles are named after the revision hashes...
+            # so someone would need to know or guess a SHA value to download anything.
+            # TODO: proxyPass to an hg serve service?
+            locations."~ ^/[~^][a-z0-9_]+/[a-zA-Z0-9_.-]+/\\.hg/bundles/.*$" = {
+              root = "/var/lib/nginx/hgsrht/repos";
+              extraConfig = ''
+                auth_request /authorize;
+                gzip off;
+              '';
+            };
+          };
+          systemd.services.nginx = {
+            serviceConfig.BindReadOnlyPaths = [ "${cfg.settings."hg.sr.ht".repos}:/var/lib/nginx/hgsrht/repos" ];
+          };
+        })
+      ];
+    }))
+
+    (import ./service.nix "hub" {
+      inherit configIniOfService;
+      port = 5014;
+      extraConfig = {
+        services.nginx = mkIf cfg.nginx.enable {
+          virtualHosts."hub.${domain}" = mkMerge [ {
+            serverAliases = [ domain ];
+          } cfg.nginx.virtualHost ];
+        };
+      };
+    })
+
+    (import ./service.nix "lists" (let
+      srvsrht = "listssrht";
+      in {
+      inherit configIniOfService;
+      port = 5006;
+      webhooks = true;
+      # Receive the mail from Postfix and enqueue them into Redis and PostgreSQL
+      extraServices.listssrht-lmtp = {
+        wants = [ "postfix.service" ];
+        unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
+        serviceConfig.ExecStart = "${cfg.python}/bin/listssrht-lmtp";
+        # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
+        serviceConfig.PrivateUsers = mkForce false;
+      };
+      # Dequeue the mails from Redis and dispatch them
+      extraServices.listssrht-process = {
+        serviceConfig = {
+          preStart = ''
+            cp ${pkgs.writeText "${srvsrht}-webhooks-celeryconfig.py" cfg.lists.process.celeryConfig} \
+               /run/sourcehut/${srvsrht}-webhooks/celeryconfig.py
+          '';
+          ExecStart = "${cfg.python}/bin/celery --app listssrht.process worker --hostname listssrht-process@%%h " + concatStringsSep " " cfg.lists.process.extraArgs;
+          # Avoid crashing: os.getloadavg()
+          ProcSubset = mkForce "all";
+        };
+      };
+      extraConfig = mkIf cfg.postfix.enable {
+        users.groups.${postfix.group}.members = [ cfg.lists.user ];
+        services.sourcehut.settings."lists.sr.ht::mail".sock-group = postfix.group;
+        services.postfix = {
+          destination = [ "lists.${domain}" ];
+          # FIXME: an accurate recipient list should be queried
+          # from the lists.sr.ht PostgreSQL database to avoid backscattering.
+          # But usernames are unfortunately not in that database but in meta.sr.ht.
+          # Note that two syntaxes are allowed:
+          # - ~username/list-name@lists.${domain}
+          # - u.username.list-name@lists.${domain}
+          localRecipients = [ "@lists.${domain}" ];
+          transport = ''
+            lists.${domain} lmtp:unix:${cfg.settings."lists.sr.ht::worker".sock}
+          '';
+        };
+      };
+    }))
+
+    (import ./service.nix "man" {
+      inherit configIniOfService;
+      port = 5004;
+    })
+
+    (import ./service.nix "meta" {
+      inherit configIniOfService;
+      port = 5000;
+      webhooks = true;
+      extraServices.metasrht-api = {
+        serviceConfig.Restart = "always";
+        serviceConfig.RestartSec = "2s";
+        preStart = "set -x\n" + concatStringsSep "\n\n" (attrValues (mapAttrs (k: s:
+          let srvMatch = builtins.match "^([a-z]*)\\.sr\\.ht$" k;
+              srv = head srvMatch;
+          in
+          # Configure client(s) as "preauthorized"
+          optionalString (srvMatch != null && cfg.${srv}.enable && ((s.oauth-client-id or null) != null)) ''
+            # Configure ${srv}'s OAuth client as "preauthorized"
+            ${postgresql.package}/bin/psql '${cfg.settings."meta.sr.ht".connection-string}' \
+              -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${s.oauth-client-id}'"
+          ''
+          ) cfg.settings));
+        serviceConfig.ExecStart = "${pkgs.sourcehut.metasrht}/bin/metasrht-api -b ${cfg.listenAddress}:${toString (cfg.meta.port + 100)}";
+      };
+      extraTimers.metasrht-daily.timerConfig = {
+        OnCalendar = ["daily"];
+        AccuracySec = "1h";
+      };
+      extraConfig = mkMerge [
+        {
+          assertions = [
+            { assertion = let s = cfg.settings."meta.sr.ht::billing"; in
+                          s.enabled == "yes" -> (s.stripe-public-key != null && s.stripe-secret-key != null);
+              message = "If meta.sr.ht::billing is enabled, the keys must be defined.";
+            }
+          ];
+          environment.systemPackages = optional cfg.meta.enable
+            (pkgs.writeShellScriptBin "metasrht-manageuser" ''
+              set -eux
+              if test "$(${pkgs.coreutils}/bin/id -n -u)" != '${cfg.meta.user}'
+              then exec sudo -u '${cfg.meta.user}' "$0" "$@"
+              else
+                # In order to load config.ini
+                if cd /run/sourcehut/metasrht
+                then exec ${cfg.python}/bin/metasrht-manageuser "$@"
+                else cat <<EOF
+                  Please run: sudo systemctl start metasrht
+              EOF
+                  exit 1
+                fi
+              fi
+            '');
+        }
+        (mkIf cfg.nginx.enable {
+          services.nginx.virtualHosts."meta.${domain}" = {
+            locations."/query" = {
+              proxyPass = cfg.settings."meta.sr.ht".api-origin;
+              extraConfig = ''
+                if ($request_method = 'OPTIONS') {
+                  add_header 'Access-Control-Allow-Origin' '*';
+                  add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
+                  add_header 'Access-Control-Allow-Headers' 'User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
+                  add_header 'Access-Control-Max-Age' 1728000;
+                  add_header 'Content-Type' 'text/plain; charset=utf-8';
+                  add_header 'Content-Length' 0;
+                  return 204;
+                }
+
+                add_header 'Access-Control-Allow-Origin' '*';
+                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
+                add_header 'Access-Control-Allow-Headers' 'User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
+                add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
+              '';
+            };
+          };
+        })
+      ];
+    })
+
+    (import ./service.nix "pages" {
+      inherit configIniOfService;
+      port = 5112;
+      mainService = let
+        srvsrht = "pagessrht";
+        version = pkgs.sourcehut.${srvsrht}.version;
+        stateDir = "/var/lib/sourcehut/${srvsrht}";
+        iniKey = "pages.sr.ht";
+        in {
+        preStart = mkBefore ''
+          set -x
+          # Use the /run/sourcehut/${srvsrht}/config.ini
+          # installed by a previous ExecStartPre= in baseService
+          cd /run/sourcehut/${srvsrht}
+
+          if test ! -e ${stateDir}/db; then
+            ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f ${pkgs.sourcehut.pagessrht}/share/sql/schema.sql
+            echo ${version} >${stateDir}/db
+          fi
+
+          ${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
+            # Just try all the migrations because they're not linked to the version
+            for sql in ${pkgs.sourcehut.pagessrht}/share/sql/migrations/*.sql; do
+              ${postgresql.package}/bin/psql '${cfg.settings.${iniKey}.connection-string}' -f "$sql" || true
+            done
+          ''}
+
+          # Disable webhook
+          touch ${stateDir}/webhook
+        '';
+        serviceConfig = {
+          ExecStart = mkForce "${pkgs.sourcehut.pagessrht}/bin/pages.sr.ht -b ${cfg.listenAddress}:${toString cfg.pages.port}";
+        };
+      };
+    })
+
+    (import ./service.nix "paste" {
+      inherit configIniOfService;
+      port = 5011;
+    })
+
+    (import ./service.nix "todo" {
+      inherit configIniOfService;
+      port = 5003;
+      webhooks = true;
+      extraServices.todosrht-lmtp = {
+        wants = [ "postfix.service" ];
+        unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
+        serviceConfig.ExecStart = "${cfg.python}/bin/todosrht-lmtp";
+        # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
+        serviceConfig.PrivateUsers = mkForce false;
+      };
+      extraConfig = mkIf cfg.postfix.enable {
+        users.groups.${postfix.group}.members = [ cfg.todo.user ];
+        services.sourcehut.settings."todo.sr.ht::mail".sock-group = postfix.group;
+        services.postfix = {
+          destination = [ "todo.${domain}" ];
+          # FIXME: an accurate recipient list should be queried
+          # from the todo.sr.ht PostgreSQL database to avoid backscattering.
+          # But usernames are unfortunately not in that database but in meta.sr.ht.
+          # Note that two syntaxes are allowed:
+          # - ~username/tracker-name@todo.${domain}
+          # - u.username.tracker-name@todo.${domain}
+          localRecipients = [ "@todo.${domain}" ];
+          transport = ''
+            todo.${domain} lmtp:unix:${cfg.settings."todo.sr.ht::mail".sock}
+          '';
+        };
+      };
+    })
+
+    (mkRenamedOptionModule [ "services" "sourcehut" "originBase" ]
+                           [ "services" "sourcehut" "settings" "sr.ht" "global-domain" ])
+    (mkRenamedOptionModule [ "services" "sourcehut" "address" ]
+                           [ "services" "sourcehut" "listenAddress" ])
+
+  ];
+
+  meta.doc = ./sourcehut.xml;
+  meta.maintainers = with maintainers; [ julm 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..292a51d3e1c
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/dispatch.nix
@@ -0,0 +1,127 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.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";
+      defaultText = literalExpression ''"''${config.${opt.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..ff110905d18
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/git.nix
@@ -0,0 +1,217 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.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";
+      defaultText = literalExpression ''"''${config.${opt.statePath}}/gitsrht"'';
+      description = ''
+        State path for git.sr.ht.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.git;
+      defaultText = literalExpression "pkgs.git";
+      example = literalExpression "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 ${config.services.nginx.package}/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..6ba1df8b6dd
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/hg.nix
@@ -0,0 +1,175 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.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";
+      defaultText = literalExpression ''"''${config.${opt.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..7d137a76505
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/hub.nix
@@ -0,0 +1,120 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.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";
+      defaultText = literalExpression ''"''${config.${opt.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..76f155caa05
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/lists.nix
@@ -0,0 +1,187 @@
+# Email setup is fairly involved, useful references:
+# https://drewdevault.com/2018/08/05/Local-mail-server.html
+
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.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";
+      defaultText = literalExpression ''"''${config.${opt.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..8ca271c32ee
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/man.nix
@@ -0,0 +1,124 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.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";
+      defaultText = literalExpression ''"''${config.${opt.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..33e4f2332b5
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/meta.nix
@@ -0,0 +1,213 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.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";
+      defaultText = literalExpression ''"''${config.${opt.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..b481ebaf891
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/paste.nix
@@ -0,0 +1,135 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.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";
+      defaultText = literalExpression ''"''${config.${opt.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..f1706ad0a6a
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/service.nix
@@ -0,0 +1,375 @@
+srv:
+{ configIniOfService
+, srvsrht ? "${srv}srht" # Because "buildsrht" does not follow that pattern (missing an "s").
+, iniKey ? "${srv}.sr.ht"
+, webhooks ? false
+, extraTimers ? {}
+, mainService ? {}
+, extraServices ? {}
+, extraConfig ? {}
+, port
+}:
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  inherit (config.services) postgresql;
+  redis = config.services.redis.servers."sourcehut-${srvsrht}";
+  inherit (config.users) users;
+  cfg = config.services.sourcehut;
+  configIni = configIniOfService srv;
+  srvCfg = cfg.${srv};
+  baseService = serviceName: { allowStripe ? false }: extraService: let
+    runDir = "/run/sourcehut/${serviceName}";
+    rootDir = "/run/sourcehut/chroots/${serviceName}";
+    in
+    mkMerge [ extraService {
+    after = [ "network.target" ] ++
+      optional cfg.postgresql.enable "postgresql.service" ++
+      optional cfg.redis.enable "redis-sourcehut-${srvsrht}.service";
+    requires =
+      optional cfg.postgresql.enable "postgresql.service" ++
+      optional cfg.redis.enable "redis-sourcehut-${srvsrht}.service";
+    path = [ pkgs.gawk ];
+    environment.HOME = runDir;
+    serviceConfig = {
+      User = mkDefault srvCfg.user;
+      Group = mkDefault srvCfg.group;
+      RuntimeDirectory = [
+        "sourcehut/${serviceName}"
+        # Used by *srht-keys which reads ../config.ini
+        "sourcehut/${serviceName}/subdir"
+        "sourcehut/chroots/${serviceName}"
+      ];
+      RuntimeDirectoryMode = "2750";
+      # No need for the chroot path once inside the chroot
+      InaccessiblePaths = [ "-+${rootDir}" ];
+      # g+rx is for group members (eg. fcgiwrap or nginx)
+      # to read Git/Mercurial repositories, buildlogs, etc.
+      # o+x is for intermediate directories created by BindPaths= and like,
+      # as they're owned by root:root.
+      UMask = "0026";
+      RootDirectory = rootDir;
+      RootDirectoryStartOnly = true;
+      PrivateTmp = true;
+      MountAPIVFS = true;
+      # config.ini is looked up in there, before /etc/srht/config.ini
+      # Note that it fails to be set in ExecStartPre=
+      WorkingDirectory = mkDefault ("-"+runDir);
+      BindReadOnlyPaths = [
+        builtins.storeDir
+        "/etc"
+        "/run/booted-system"
+        "/run/current-system"
+        "/run/systemd"
+        ] ++
+        optional cfg.postgresql.enable "/run/postgresql" ++
+        optional cfg.redis.enable "/run/redis-sourcehut-${srvsrht}";
+      # LoadCredential= are unfortunately not available in ExecStartPre=
+      # Hence this one is run as root (the +) with RootDirectoryStartOnly=
+      # to reach credentials wherever they are.
+      # Note that each systemd service gets its own ${runDir}/config.ini file.
+      ExecStartPre = mkBefore [("+"+pkgs.writeShellScript "${serviceName}-credentials" ''
+        set -x
+        # Replace values begining with a '<' by the content of the file whose name is after.
+        gawk '{ if (match($0,/^([^=]+=)<(.+)/,m)) { getline f < m[2]; print m[1] f } else print $0 }' ${configIni} |
+        ${optionalString (!allowStripe) "gawk '!/^stripe-secret-key=/' |"}
+        install -o ${srvCfg.user} -g root -m 400 /dev/stdin ${runDir}/config.ini
+      '')];
+      # The following options are only for optimizing:
+      # systemd-analyze security
+      AmbientCapabilities = "";
+      CapabilityBoundingSet = "";
+      # ProtectClock= adds DeviceAllow=char-rtc r
+      DeviceAllow = "";
+      LockPersonality = true;
+      MemoryDenyWriteExecute = true;
+      NoNewPrivileges = true;
+      PrivateDevices = true;
+      PrivateMounts = true;
+      PrivateNetwork = mkDefault false;
+      PrivateUsers = true;
+      ProcSubset = "pid";
+      ProtectClock = true;
+      ProtectControlGroups = true;
+      ProtectHome = true;
+      ProtectHostname = true;
+      ProtectKernelLogs = true;
+      ProtectKernelModules = true;
+      ProtectKernelTunables = true;
+      ProtectProc = "invisible";
+      ProtectSystem = "strict";
+      RemoveIPC = true;
+      RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+      RestrictNamespaces = true;
+      RestrictRealtime = true;
+      RestrictSUIDSGID = true;
+      #SocketBindAllow = [ "tcp:${toString srvCfg.port}" "tcp:${toString srvCfg.prometheusPort}" ];
+      #SocketBindDeny = "any";
+      SystemCallFilter = [
+        "@system-service"
+        "~@aio" "~@keyring" "~@memlock" "~@privileged" "~@resources" "~@timer"
+        "@chown" "@setuid"
+      ];
+      SystemCallArchitectures = "native";
+    };
+  } ];
+in
+{
+  options.services.sourcehut.${srv} = {
+    enable = mkEnableOption "${srv} service";
+
+    user = mkOption {
+      type = types.str;
+      default = srvsrht;
+      description = ''
+        User for ${srv}.sr.ht.
+      '';
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = srvsrht;
+      description = ''
+        Group for ${srv}.sr.ht.
+        Membership grants access to the Git/Mercurial repositories by default,
+        but not to the config.ini file (where secrets are).
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = port;
+      description = ''
+        Port on which the "${srv}" backend should listen.
+      '';
+    };
+
+    redis = {
+      host = mkOption {
+        type = types.str;
+        default = "unix:/run/redis-sourcehut-${srvsrht}/redis.sock?db=0";
+        example = "redis://shared.wireguard:6379/0";
+        description = ''
+          The redis host URL. This is used for caching and temporary storage, and must
+          be shared between nodes (e.g. git1.sr.ht and git2.sr.ht), but need not be
+          shared between services. It may be shared between services, however, with no
+          ill effect, if this better suits your infrastructure.
+        '';
+      };
+    };
+
+    postgresql = {
+      database = mkOption {
+        type = types.str;
+        default = "${srv}.sr.ht";
+        description = ''
+          PostgreSQL database name for the ${srv}.sr.ht service,
+          used if <xref linkend="opt-services.sourcehut.postgresql.enable"/> is <literal>true</literal>.
+        '';
+      };
+    };
+
+    gunicorn = {
+      extraArgs = mkOption {
+        type = with types; listOf str;
+        default = ["--timeout 120" "--workers 1" "--log-level=info"];
+        description = "Extra arguments passed to Gunicorn.";
+      };
+    };
+  } // optionalAttrs webhooks {
+    webhooks = {
+      extraArgs = mkOption {
+        type = with types; listOf str;
+        default = ["--loglevel DEBUG" "--pool eventlet" "--without-heartbeat"];
+        description = "Extra arguments passed to the Celery responsible for webhooks.";
+      };
+      celeryConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Content of the <literal>celeryconfig.py</literal> used by the Celery responsible for webhooks.";
+      };
+    };
+  };
+
+  config = lib.mkIf (cfg.enable && srvCfg.enable) (mkMerge [ extraConfig {
+    users = {
+      users = {
+        "${srvCfg.user}" = {
+          isSystemUser = true;
+          group = mkDefault srvCfg.group;
+          description = mkDefault "sourcehut user for ${srv}.sr.ht";
+        };
+      };
+      groups = {
+        "${srvCfg.group}" = { };
+      } // optionalAttrs (cfg.postgresql.enable
+        && hasSuffix "0" (postgresql.settings.unix_socket_permissions or "")) {
+        "postgres".members = [ srvCfg.user ];
+      } // optionalAttrs (cfg.redis.enable
+        && hasSuffix "0" (redis.settings.unixsocketperm or "")) {
+        "redis-sourcehut-${srvsrht}".members = [ srvCfg.user ];
+      };
+    };
+
+    services.nginx = mkIf cfg.nginx.enable {
+      virtualHosts."${srv}.${cfg.settings."sr.ht".global-domain}" = mkMerge [ {
+        forceSSL = mkDefault true;
+        locations."/".proxyPass = "http://${cfg.listenAddress}:${toString srvCfg.port}";
+        locations."/static" = {
+          root = "${pkgs.sourcehut.${srvsrht}}/${pkgs.sourcehut.python.sitePackages}/${srvsrht}";
+          extraConfig = mkDefault ''
+            expires 30d;
+          '';
+        };
+      } cfg.nginx.virtualHost ];
+    };
+
+    services.postgresql = mkIf cfg.postgresql.enable {
+      authentication = ''
+        local ${srvCfg.postgresql.database} ${srvCfg.user} trust
+      '';
+      ensureDatabases = [ srvCfg.postgresql.database ];
+      ensureUsers = map (name: {
+          inherit name;
+          ensurePermissions = { "DATABASE \"${srvCfg.postgresql.database}\"" = "ALL PRIVILEGES"; };
+        }) [srvCfg.user];
+    };
+
+    services.sourcehut.services = mkDefault (filter (s: cfg.${s}.enable)
+      [ "builds" "dispatch" "git" "hg" "hub" "lists" "man" "meta" "pages" "paste" "todo" ]);
+
+    services.sourcehut.settings = mkMerge [
+      {
+        "${srv}.sr.ht".origin = mkDefault "https://${srv}.${cfg.settings."sr.ht".global-domain}";
+      }
+
+      (mkIf cfg.postgresql.enable {
+        "${srv}.sr.ht".connection-string = mkDefault "postgresql:///${srvCfg.postgresql.database}?user=${srvCfg.user}&host=/run/postgresql";
+      })
+    ];
+
+    services.redis.servers."sourcehut-${srvsrht}" = mkIf cfg.redis.enable {
+      enable = true;
+      databases = 3;
+      syslog = true;
+      # TODO: set a more informed value
+      save = mkDefault [ [1800 10] [300 100] ];
+      settings = {
+        # TODO: set a more informed value
+        maxmemory = "128MB";
+        maxmemory-policy = "volatile-ttl";
+      };
+    };
+
+    systemd.services = mkMerge [
+      {
+        "${srvsrht}" = baseService srvsrht { allowStripe = srv == "meta"; } (mkMerge [
+        {
+          description = "sourcehut ${srv}.sr.ht website service";
+          before = optional cfg.nginx.enable "nginx.service";
+          wants = optional cfg.nginx.enable "nginx.service";
+          wantedBy = [ "multi-user.target" ];
+          path = optional cfg.postgresql.enable postgresql.package;
+          # Beware: change in credentials' content will not trigger restart.
+          restartTriggers = [ configIni ];
+          serviceConfig = {
+            Type = "simple";
+            Restart = mkDefault "always";
+            #RestartSec = mkDefault "2min";
+            StateDirectory = [ "sourcehut/${srvsrht}" ];
+            StateDirectoryMode = "2750";
+            ExecStart = "${cfg.python}/bin/gunicorn ${srvsrht}.app:app --name ${srvsrht} --bind ${cfg.listenAddress}:${toString srvCfg.port} " + concatStringsSep " " srvCfg.gunicorn.extraArgs;
+          };
+          preStart = let
+            version = pkgs.sourcehut.${srvsrht}.version;
+            stateDir = "/var/lib/sourcehut/${srvsrht}";
+            in mkBefore ''
+            set -x
+            # Use the /run/sourcehut/${srvsrht}/config.ini
+            # installed by a previous ExecStartPre= in baseService
+            cd /run/sourcehut/${srvsrht}
+
+            if test ! -e ${stateDir}/db; then
+              # Setup the initial database.
+              # Note that it stamps the alembic head afterward
+              ${cfg.python}/bin/${srvsrht}-initdb
+              echo ${version} >${stateDir}/db
+            fi
+
+            ${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
+              if [ "$(cat ${stateDir}/db)" != "${version}" ]; then
+                # Manage schema migrations using alembic
+                ${cfg.python}/bin/${srvsrht}-migrate -a upgrade head
+                echo ${version} >${stateDir}/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 ${stateDir}/webhook; then
+              # Update ${iniKey}'s users' profile copy to the latest
+              ${cfg.python}/bin/srht-update-profiles ${iniKey}
+              touch ${stateDir}/webhook
+            fi
+          '';
+        } mainService ]);
+      }
+
+      (mkIf webhooks {
+        "${srvsrht}-webhooks" = baseService "${srvsrht}-webhooks" {}
+          {
+            description = "sourcehut ${srv}.sr.ht webhooks service";
+            after = [ "${srvsrht}.service" ];
+            wantedBy = [ "${srvsrht}.service" ];
+            partOf = [ "${srvsrht}.service" ];
+            preStart = ''
+              cp ${pkgs.writeText "${srvsrht}-webhooks-celeryconfig.py" srvCfg.webhooks.celeryConfig} \
+                 /run/sourcehut/${srvsrht}-webhooks/celeryconfig.py
+            '';
+            serviceConfig = {
+              Type = "simple";
+              Restart = "always";
+              ExecStart = "${cfg.python}/bin/celery --app ${srvsrht}.webhooks worker --hostname ${srvsrht}-webhooks@%%h " + concatStringsSep " " srvCfg.webhooks.extraArgs;
+              # Avoid crashing: os.getloadavg()
+              ProcSubset = mkForce "all";
+            };
+          };
+      })
+
+      (mapAttrs (timerName: timer: (baseService timerName {} (mkMerge [
+        {
+          description = "sourcehut ${timerName} service";
+          after = [ "network.target" "${srvsrht}.service" ];
+          serviceConfig = {
+            Type = "oneshot";
+            ExecStart = "${cfg.python}/bin/${timerName}";
+          };
+        }
+        (timer.service or {})
+      ]))) extraTimers)
+
+      (mapAttrs (serviceName: extraService: baseService serviceName {} (mkMerge [
+        {
+          description = "sourcehut ${serviceName} service";
+          # So that extraServices have the PostgreSQL database initialized.
+          after = [ "${srvsrht}.service" ];
+          wantedBy = [ "${srvsrht}.service" ];
+          partOf = [ "${srvsrht}.service" ];
+          serviceConfig = {
+            Type = "simple";
+            Restart = mkDefault "always";
+          };
+        }
+        extraService
+      ])) extraServices)
+    ];
+
+    systemd.timers = mapAttrs (timerName: timer:
+      {
+        description = "sourcehut timer for ${timerName}";
+        wantedBy = [ "timers.target" ];
+        inherit (timer) timerConfig;
+      }) extraTimers;
+  } ]);
+}
diff --git a/nixos/modules/services/misc/sourcehut/sourcehut.xml b/nixos/modules/services/misc/sourcehut/sourcehut.xml
new file mode 100644
index 00000000000..41094f65a94
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/sourcehut.xml
@@ -0,0 +1,119 @@
+<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.
+   This NixOS module also provides basic configuration integrating Sourcehut into locally running
+   <literal><link linkend="opt-services.nginx.enable">services.nginx</link></literal>,
+   <literal><link linkend="opt-services.redis.servers">services.redis.servers.sourcehut</link></literal>,
+   <literal><link linkend="opt-services.postfix.enable">services.postfix</link></literal>
+   and
+   <literal><link linkend="opt-services.postgresql.enable">services.postgresql</link></literal> services.
+  </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.git.enable">git.enable</link> = true;
+    <link linkend="opt-services.sourcehut.man.enable">man.enable</link> = true;
+    <link linkend="opt-services.sourcehut.meta.enable">meta.enable</link> = true;
+    <link linkend="opt-services.sourcehut.nginx.enable">nginx.enable</link> = true;
+    <link linkend="opt-services.sourcehut.postfix.enable">postfix.enable</link> = true;
+    <link linkend="opt-services.sourcehut.postgresql.enable">postgresql.enable</link> = true;
+    <link linkend="opt-services.sourcehut.redis.enable">redis.enable</link> = true;
+    <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 = "/run/keys/path/to/network-key";
+          service-key = "/run/keys/path/to/service-key";
+        };
+        webhooks.private-key= "/run/keys/path/to/webhook-key";
+    };
+  };
+
+  <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..262fa48f59d
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/todo.nix
@@ -0,0 +1,163 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  opt = options.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";
+      defaultText = literalExpression ''"''${config.${opt.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/spice-vdagentd.nix b/nixos/modules/services/misc/spice-vdagentd.nix
new file mode 100644
index 00000000000..2dd9fcf68ab
--- /dev/null
+++ b/nixos/modules/services/misc/spice-vdagentd.nix
@@ -0,0 +1,30 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.spice-vdagentd;
+in
+{
+  options = {
+    services.spice-vdagentd = {
+      enable = mkEnableOption "Spice guest vdagent daemon";
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.spice-vdagent ];
+
+    systemd.services.spice-vdagentd = {
+      description = "spice-vdagent daemon";
+      wantedBy = [ "graphical.target" ];
+      preStart = ''
+        mkdir -p "/run/spice-vdagentd/"
+      '';
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${pkgs.spice-vdagent}/bin/spice-vdagentd";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/ssm-agent.nix b/nixos/modules/services/misc/ssm-agent.nix
new file mode 100644
index 00000000000..4ae596ade17
--- /dev/null
+++ b/nixos/modules/services/misc/ssm-agent.nix
@@ -0,0 +1,73 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.ssm-agent;
+
+  # The SSM agent doesn't pay attention to our /etc/os-release yet, and the lsb-release tool
+  # in nixpkgs doesn't seem to work properly on NixOS, so let's just fake the two fields SSM
+  # looks for. See https://github.com/aws/amazon-ssm-agent/issues/38 for upstream fix.
+  fake-lsb-release = pkgs.writeScriptBin "lsb_release" ''
+    #!${pkgs.runtimeShell}
+
+    case "$1" in
+      -i) echo "nixos";;
+      -r) echo "${config.system.nixos.version}";;
+    esac
+  '';
+in {
+  options.services.ssm-agent = {
+    enable = mkEnableOption "AWS SSM agent";
+
+    package = mkOption {
+      type = types.path;
+      description = "The SSM agent package to use";
+      default = pkgs.ssm-agent.override { overrideEtc = false; };
+      defaultText = literalExpression "pkgs.ssm-agent.override { overrideEtc = false; }";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.ssm-agent = {
+      inherit (cfg.package.meta) description;
+      after    = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      path = [ fake-lsb-release pkgs.coreutils ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/amazon-ssm-agent";
+        KillMode = "process";
+        # 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
new file mode 100644
index 00000000000..386281e2b7c
--- /dev/null
+++ b/nixos/modules/services/misc/sssd.nix
@@ -0,0 +1,97 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.sssd;
+  nscd = config.services.nscd;
+in {
+  options = {
+    services.sssd = {
+      enable = mkEnableOption "the System Security Services Daemon";
+
+      config = mkOption {
+        type = types.lines;
+        description = "Contents of <filename>sssd.conf</filename>.";
+        default = ''
+          [sssd]
+          config_file_version = 2
+          services = nss, pam
+          domains = shadowutils
+
+          [nss]
+
+          [pam]
+
+          [domain/shadowutils]
+          id_provider = proxy
+          proxy_lib_name = files
+          auth_provider = proxy
+          proxy_pam_target = sssd-shadowutils
+          proxy_fast_alias = True
+        '';
+      };
+
+      sshAuthorizedKeysIntegration = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to make sshd look up authorized keys from SSS.
+          For this to work, the <literal>ssh</literal> SSS service must be enabled in the sssd configuration.
+        '';
+      };
+    };
+  };
+  config = mkMerge [
+    (mkIf cfg.enable {
+      systemd.services.sssd = {
+        description = "System Security Services Daemon";
+        wantedBy    = [ "multi-user.target" ];
+        before = [ "systemd-user-sessions.service" "nss-user-lookup.target" ];
+        after = [ "network-online.target" "nscd.service" ];
+        requires = [ "network-online.target" "nscd.service" ];
+        wants = [ "nss-user-lookup.target" ];
+        restartTriggers = [
+          config.environment.etc."nscd.conf".source
+          config.environment.etc."sssd/sssd.conf".source
+        ];
+        script = ''
+          export LDB_MODULES_PATH+="''${LDB_MODULES_PATH+:}${pkgs.ldb}/modules/ldb:${pkgs.sssd}/modules/ldb"
+          mkdir -p /var/lib/sss/{pubconf,db,mc,pipes,gpo_cache,secrets} /var/lib/sss/pipes/private /var/lib/sss/pubconf/krb5.include.d
+          ${pkgs.sssd}/bin/sssd -D
+        '';
+        serviceConfig = {
+          Type = "forking";
+          PIDFile = "/run/sssd.pid";
+        };
+      };
+
+      environment.etc."sssd/sssd.conf" = {
+        text = cfg.config;
+        mode = "0400";
+      };
+
+      system.nssModules = [ pkgs.sssd ];
+      system.nssDatabases = {
+        group = [ "sss" ];
+        passwd = [ "sss" ];
+        services = [ "sss" ];
+        shadow = [ "sss" ];
+      };
+      services.dbus.packages = [ pkgs.sssd ];
+    })
+
+    (mkIf cfg.sshAuthorizedKeysIntegration {
+    # Ugly: sshd refuses to start if a store path is given because /nix/store is group-writable.
+    # So indirect by a symlink.
+    environment.etc."ssh/authorized_keys_command" = {
+      mode = "0755";
+      text = ''
+        #!/bin/sh
+        exec ${pkgs.sssd}/bin/sss_ssh_authorizedkeys "$@"
+      '';
+    };
+    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
new file mode 100644
index 00000000000..2dda8970dd3
--- /dev/null
+++ b/nixos/modules/services/misc/subsonic.nix
@@ -0,0 +1,169 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.subsonic;
+  opt = options.services.subsonic;
+in {
+  options = {
+    services.subsonic = {
+      enable = mkEnableOption "Subsonic daemon";
+
+      home = mkOption {
+        type = types.path;
+        default = "/var/lib/subsonic";
+        description = ''
+          The directory where Subsonic will create files.
+          Make sure it is writable.
+        '';
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = ''
+          The host name or IP address on which to bind Subsonic.
+          Only relevant if you have multiple network interfaces and want
+          to make Subsonic available on only one of them. The default value
+          will bind Subsonic to all available network interfaces.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 4040;
+        description = ''
+          The port on which Subsonic will listen for
+          incoming HTTP traffic. Set to 0 to disable.
+        '';
+      };
+
+      httpsPort = mkOption {
+        type = types.port;
+        default = 0;
+        description = ''
+          The port on which Subsonic will listen for
+          incoming HTTPS traffic. Set to 0 to disable.
+        '';
+      };
+
+      contextPath = mkOption {
+        type = types.path;
+        default = "/";
+        description = ''
+          The context path, i.e., the last part of the Subsonic
+          URL. Typically '/' or '/subsonic'. Default '/'
+        '';
+      };
+
+      maxMemory = mkOption {
+        type = types.int;
+        default = 100;
+        description = ''
+          The memory limit (max Java heap size) in megabytes.
+          Default: 100
+        '';
+      };
+
+      defaultMusicFolder = mkOption {
+        type = types.path;
+        default = "/var/music";
+        description = ''
+          Configure Subsonic to use this folder for music.  This option
+          only has effect the first time Subsonic is started.
+        '';
+      };
+
+      defaultPodcastFolder = mkOption {
+        type = types.path;
+        default = "/var/music/Podcast";
+        description = ''
+          Configure Subsonic to use this folder for Podcasts.  This option
+          only has effect the first time Subsonic is started.
+        '';
+      };
+
+      defaultPlaylistFolder = mkOption {
+        type = types.path;
+        default = "/var/playlists";
+        description = ''
+          Configure Subsonic to use this folder for playlists.  This option
+          only has effect the first time Subsonic is started.
+        '';
+      };
+
+      transcoders = mkOption {
+        type = types.listOf types.path;
+        default = [ "${pkgs.ffmpeg.bin}/bin/ffmpeg" ];
+        defaultText = literalExpression ''[ "''${pkgs.ffmpeg.bin}/bin/ffmpeg" ]'';
+        description = ''
+          List of paths to transcoder executables that should be accessible
+          from Subsonic. Symlinks will be created to each executable inside
+          ''${config.${opt.home}}/transcoders.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.subsonic = {
+      description = "Personal media streamer";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      script = ''
+        ${pkgs.jre8}/bin/java -Xmx${toString cfg.maxMemory}m \
+          -Dsubsonic.home=${cfg.home} \
+          -Dsubsonic.host=${cfg.listenAddress} \
+          -Dsubsonic.port=${toString cfg.port} \
+          -Dsubsonic.httpsPort=${toString cfg.httpsPort} \
+          -Dsubsonic.contextPath=${cfg.contextPath} \
+          -Dsubsonic.defaultMusicFolder=${cfg.defaultMusicFolder} \
+          -Dsubsonic.defaultPodcastFolder=${cfg.defaultPodcastFolder} \
+          -Dsubsonic.defaultPlaylistFolder=${cfg.defaultPlaylistFolder} \
+          -Djava.awt.headless=true \
+          -verbose:gc \
+          -jar ${pkgs.subsonic}/subsonic-booter-jar-with-dependencies.jar
+      '';
+
+      preStart = ''
+        # Formerly this module set cfg.home to /var/subsonic. Try to move
+        # /var/subsonic to cfg.home.
+        oldHome="/var/subsonic"
+        if [ "${cfg.home}" != "$oldHome" ] &&
+                ! [ -e "${cfg.home}" ] &&
+                [ -d "$oldHome" ] &&
+                [ $(${pkgs.coreutils}/bin/stat -c %u "$oldHome") -eq \
+                    ${toString config.users.users.subsonic.uid} ]; then
+            logger Moving "$oldHome" to "${cfg.home}"
+            ${pkgs.coreutils}/bin/mv -T "$oldHome" "${cfg.home}"
+        fi
+
+        # Install transcoders.
+        ${pkgs.coreutils}/bin/rm -rf ${cfg.home}/transcode ; \
+        ${pkgs.coreutils}/bin/mkdir -p ${cfg.home}/transcode ; \
+        ${pkgs.bash}/bin/bash -c ' \
+          for exe in "$@"; do \
+            ${pkgs.coreutils}/bin/ln -sf "$exe" ${cfg.home}/transcode; \
+          done' IGNORED_FIRST_ARG ${toString cfg.transcoders}
+      '';
+      serviceConfig = {
+        # Needed for Subsonic to find subsonic.war.
+        WorkingDirectory = "${pkgs.subsonic}";
+        Restart = "always";
+        User = "subsonic";
+        UMask = "0022";
+      };
+    };
+
+    users.users.subsonic = {
+      description = "Subsonic daemon user";
+      home = cfg.home;
+      createHome = true;
+      group = "subsonic";
+      uid = config.ids.uids.subsonic;
+    };
+
+    users.groups.subsonic.gid = config.ids.gids.subsonic;
+  };
+}
diff --git a/nixos/modules/services/misc/sundtek.nix b/nixos/modules/services/misc/sundtek.nix
new file mode 100644
index 00000000000..e3234518c94
--- /dev/null
+++ b/nixos/modules/services/misc/sundtek.nix
@@ -0,0 +1,33 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.sundtek;
+
+in
+{
+  options.services.sundtek = {
+    enable = mkEnableOption "Sundtek driver";
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.sundtek ];
+
+    systemd.services.sundtek = {
+      description = "Sundtek driver";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = ''
+          ${pkgs.sundtek}/bin/mediasrv -d -v -p ${pkgs.sundtek}/bin ;\
+          ${pkgs.sundtek}/bin/mediaclient --start --wait-for-devices
+          '';
+        ExecStop = "${pkgs.sundtek}/bin/mediaclient --shutdown";
+        RemainAfterExit = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/svnserve.nix b/nixos/modules/services/misc/svnserve.nix
new file mode 100644
index 00000000000..5fa262ca3b9
--- /dev/null
+++ b/nixos/modules/services/misc/svnserve.nix
@@ -0,0 +1,46 @@
+# SVN server
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.svnserve;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.svnserve = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable svnserve to serve Subversion repositories through the SVN protocol.";
+      };
+
+      svnBaseDir = mkOption {
+        type = types.str;
+        default = "/repos";
+        description = "Base directory from which Subversion repositories are accessed.";
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.svnserve = {
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      preStart = "mkdir -p ${cfg.svnBaseDir}";
+      script = "${pkgs.subversion.out}/bin/svnserve -r ${cfg.svnBaseDir} -d --foreground --pid-file=/run/svnserve.pid";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/synergy.nix b/nixos/modules/services/misc/synergy.nix
new file mode 100644
index 00000000000..d6cd5d7f0d6
--- /dev/null
+++ b/nixos/modules/services/misc/synergy.nix
@@ -0,0 +1,149 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfgC = config.services.synergy.client;
+  cfgS = config.services.synergy.server;
+
+in
+
+{
+  ###### interface
+
+  options = {
+
+    services.synergy = {
+
+      # !!! All these option descriptions needs to be cleaned up.
+
+      client = {
+        enable = mkEnableOption "the Synergy client (receive keyboard and mouse events from a Synergy server)";
+
+        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
+            port overrides the default port, 24800.
+          '';
+        };
+        autoStart = mkOption {
+          default = true;
+          type = types.bool;
+          description = "Whether the Synergy client should be started automatically.";
+        };
+      };
+
+      server = {
+        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
+            this screen in the configuration.
+          '';
+        };
+        address = mkOption {
+          type = types.str;
+          default = "";
+          description = "Address on which to listen for clients.";
+        };
+        autoStart = mkOption {
+          default = true;
+          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.";
+          };
+        };
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkMerge [
+    (mkIf cfgC.enable {
+      systemd.user.services.synergy-client = {
+        after = [ "network.target" "graphical-session.target" ];
+        description = "Synergy client";
+        wantedBy = optional cfgC.autoStart "graphical-session.target";
+        path = [ pkgs.synergy ];
+        serviceConfig.ExecStart = ''${pkgs.synergy}/bin/synergyc -f ${optionalString (cfgC.screenName != "") "-n ${cfgC.screenName}"} ${cfgC.serverAddress}'';
+        serviceConfig.Restart = "on-failure";
+      };
+    })
+    (mkIf cfgS.enable {
+      systemd.user.services.synergy-server = {
+        after = [ "network.target" "graphical-session.target" ];
+        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}"}${optionalString cfgS.tls.enable " --enable-crypto"}${optionalString (cfgS.tls.cert != null) (" --tls-cert=${cfgS.tls.cert}")}'';
+        serviceConfig.Restart = "on-failure";
+      };
+    })
+  ];
+
+}
+
+/* SYNERGY SERVER example configuration file
+section: screens
+  laptop:
+  dm:
+  win:
+end
+section: aliases
+    laptop:
+      192.168.5.5
+    dm:
+      192.168.5.78
+    win:
+      192.168.5.54
+end
+section: links
+   laptop:
+       left = dm
+   dm:
+       right = laptop
+       left = win
+  win:
+      right = dm
+end
+*/
diff --git a/nixos/modules/services/misc/sysprof.nix b/nixos/modules/services/misc/sysprof.nix
new file mode 100644
index 00000000000..ab91a8b586a
--- /dev/null
+++ b/nixos/modules/services/misc/sysprof.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, ... }:
+
+{
+  options = {
+    services.sysprof = {
+      enable = lib.mkEnableOption "sysprof profiling daemon";
+    };
+  };
+
+  config = lib.mkIf config.services.sysprof.enable {
+    environment.systemPackages = [ pkgs.sysprof ];
+
+    services.dbus.packages = [ pkgs.sysprof ];
+
+    systemd.packages = [ pkgs.sysprof ];
+  };
+
+  meta.maintainers = pkgs.sysprof.meta.maintainers;
+}
diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix
new file mode 100644
index 00000000000..ff63c41e193
--- /dev/null
+++ b/nixos/modules/services/misc/taskserver/default.nix
@@ -0,0 +1,569 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.taskserver;
+
+  taskd = "${pkgs.taskserver}/bin/taskd";
+
+  mkManualPkiOption = desc: mkOption {
+    type = types.nullOr types.path;
+    default = null;
+    description = desc + ''
+      <note><para>
+      Setting this option will prevent automatic CA creation and handling.
+      </para></note>
+    '';
+  };
+
+  manualPkiOptions = {
+    ca.cert = mkManualPkiOption ''
+      Fully qualified path to the CA certificate.
+    '';
+
+    server.cert = mkManualPkiOption ''
+      Fully qualified path to the server certificate.
+    '';
+
+    server.crl = mkManualPkiOption ''
+      Fully qualified path to the server certificate revocation list.
+    '';
+
+    server.key = mkManualPkiOption ''
+      Fully qualified path to the server key.
+    '';
+  };
+
+  mkAutoDesc = preamble: ''
+    ${preamble}
+
+    <note><para>
+    This option is for the automatically handled CA and will be ignored if any
+    of the <option>services.taskserver.pki.manual.*</option> options are set.
+    </para></note>
+  '';
+
+  mkExpireOption = desc: mkOption {
+    type = types.nullOr types.int;
+    default = null;
+    example = 365;
+    apply = val: if val == null then -1 else val;
+    description = mkAutoDesc ''
+      The expiration time of ${desc} in days or <literal>null</literal> for no
+      expiration time.
+    '';
+  };
+
+  autoPkiOptions = {
+    bits = mkOption {
+      type = types.int;
+      default = 4096;
+      example = 2048;
+      description = mkAutoDesc "The bit size for generated keys.";
+    };
+
+    expiration = {
+      ca = mkExpireOption "the CA certificate";
+      server = mkExpireOption "the server certificate";
+      client = mkExpireOption "client certificates";
+      crl = mkExpireOption "the certificate revocation list (CRL)";
+    };
+  };
+
+  needToCreateCA = let
+    notFound = path: let
+      dotted = concatStringsSep "." path;
+    in throw "Can't find option definitions for path `${dotted}'.";
+    findPkiDefinitions = path: attrs: let
+      mkSublist = key: val: let
+        newPath = path ++ singleton key;
+      in if isOption val
+         then attrByPath newPath (notFound newPath) cfg.pki.manual
+         else findPkiDefinitions newPath val;
+    in flatten (mapAttrsToList mkSublist attrs);
+  in all (x: x == null) (findPkiDefinitions [] manualPkiOptions);
+
+  orgOptions = { ... }: {
+    options.users = mkOption {
+      type = types.uniq (types.listOf types.str);
+      default = [];
+      example = [ "alice" "bob" ];
+      description = ''
+        A list of user names that belong to the organization.
+      '';
+    };
+
+    options.groups = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "workers" "slackers" ];
+      description = ''
+        A list of group names that belong to the organization.
+      '';
+    };
+  };
+
+  certtool = "${pkgs.gnutls.bin}/bin/certtool";
+
+  nixos-taskserver = with pkgs.python2.pkgs; buildPythonApplication {
+    name = "nixos-taskserver";
+
+    src = pkgs.runCommand "nixos-taskserver-src" { preferLocalBuild = true; } ''
+      mkdir -p "$out"
+      cat "${pkgs.substituteAll {
+        src = ./helper-tool.py;
+        inherit taskd certtool;
+        inherit (cfg) dataDir user group fqdn;
+        certBits = cfg.pki.auto.bits;
+        clientExpiration = cfg.pki.auto.expiration.client;
+        crlExpiration = cfg.pki.auto.expiration.crl;
+        isAutoConfig = if needToCreateCA then "True" else "False";
+      }}" > "$out/main.py"
+      cat > "$out/setup.py" <<EOF
+      from setuptools import setup
+      setup(name="nixos-taskserver",
+            py_modules=["main"],
+            install_requires=["Click"],
+            entry_points="[console_scripts]\\nnixos-taskserver=main:cli")
+      EOF
+    '';
+
+    propagatedBuildInputs = [ click ];
+  };
+
+in {
+  options = {
+    services.taskserver = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = let
+          url = "https://nixos.org/manual/nixos/stable/index.html#module-services-taskserver";
+        in ''
+          Whether to enable the Taskwarrior server.
+
+          More instructions about NixOS in conjuction with Taskserver can be
+          found <link xlink:href="${url}">in the NixOS manual</link>.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "taskd";
+        description = "User for Taskserver.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "taskd";
+        description = "Group for Taskserver.";
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/taskserver";
+        description = "Data directory for Taskserver.";
+      };
+
+      ciphers = mkOption {
+        type = types.nullOr (types.separatedString ":");
+        default = null;
+        example = "NORMAL:-VERS-SSL3.0";
+        description = let
+          url = "https://gnutls.org/manual/html_node/Priority-Strings.html";
+        in ''
+          List of GnuTLS ciphers to use. See the GnuTLS documentation about
+          priority strings at <link xlink:href="${url}"/> for full details.
+        '';
+      };
+
+      organisations = mkOption {
+        type = types.attrsOf (types.submodule orgOptions);
+        default = {};
+        example.myShinyOrganisation.users = [ "alice" "bob" ];
+        example.myShinyOrganisation.groups = [ "staff" "outsiders" ];
+        example.yetAnotherOrganisation.users = [ "foo" "bar" ];
+        description = ''
+          An attribute set where the keys name the organisation and the values
+          are a set of lists of <option>users</option> and
+          <option>groups</option>.
+        '';
+      };
+
+      confirmation = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Determines whether certain commands are confirmed.
+        '';
+      };
+
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Logs debugging information.
+        '';
+      };
+
+      extensions = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          Fully qualified path of the Taskserver extension scripts.
+          Currently there are none.
+        '';
+      };
+
+      ipLog = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Logs the IP addresses of incoming requests.
+        '';
+      };
+
+      queueSize = mkOption {
+        type = types.int;
+        default = 10;
+        description = ''
+          Size of the connection backlog, see <citerefentry>
+            <refentrytitle>listen</refentrytitle>
+            <manvolnum>2</manvolnum>
+          </citerefentry>.
+        '';
+      };
+
+      requestLimit = mkOption {
+        type = types.int;
+        default = 1048576;
+        description = ''
+          Size limit of incoming requests, in bytes.
+        '';
+      };
+
+      allowedClientIDs = mkOption {
+        type = with types; either str (listOf str);
+        default = [];
+        example = [ "[Tt]ask [2-9]+" ];
+        description = ''
+          A list of regular expressions that are matched against the reported
+          client id (such as <literal>task 2.3.0</literal>).
+
+          The values <literal>all</literal> or <literal>none</literal> have
+          special meaning. Overidden by any entry in the option
+          <option>services.taskserver.disallowedClientIDs</option>.
+        '';
+      };
+
+      disallowedClientIDs = mkOption {
+        type = with types; either str (listOf str);
+        default = [];
+        example = [ "[Tt]ask [2-9]+" ];
+        description = ''
+          A list of regular expressions that are matched against the reported
+          client id (such as <literal>task 2.3.0</literal>).
+
+          The values <literal>all</literal> or <literal>none</literal> have
+          special meaning. Any entry here overrides those in
+          <option>services.taskserver.allowedClientIDs</option>.
+        '';
+      };
+
+      listenHost = mkOption {
+        type = types.str;
+        default = "localhost";
+        example = "::";
+        description = ''
+          The address (IPv4, IPv6 or DNS) to listen on.
+
+          If the value is something else than <literal>localhost</literal> the
+          port defined by <option>listenPort</option> is automatically added to
+          <option>networking.firewall.allowedTCPPorts</option>.
+        '';
+      };
+
+      listenPort = mkOption {
+        type = types.int;
+        default = 53589;
+        description = ''
+          Port number of the Taskserver.
+        '';
+      };
+
+      fqdn = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = ''
+          The fully qualified domain name of this server, which is also used
+          as the common name in the certificates.
+        '';
+      };
+
+      trust = mkOption {
+        type = types.enum [ "allow all" "strict" ];
+        default = "strict";
+        description = ''
+          Determines how client certificates are validated.
+
+          The value <literal>allow all</literal> performs no client
+          certificate validation. This is not recommended. The value
+          <literal>strict</literal> causes the client certificate to be
+          validated against a CA.
+        '';
+      };
+
+      pki.manual = manualPkiOptions;
+      pki.auto = autoPkiOptions;
+
+      config = mkOption {
+        type = types.attrs;
+        example.client.cert = "/tmp/debugging.cert";
+        description = ''
+          Configuration options to pass to Taskserver.
+
+          The options here are the same as described in <citerefentry>
+            <refentrytitle>taskdrc</refentrytitle>
+            <manvolnum>5</manvolnum>
+          </citerefentry>, but with one difference:
+
+          The <literal>server</literal> option is
+          <literal>server.listen</literal> here, because the
+          <literal>server</literal> option would collide with other options
+          like <literal>server.cert</literal> and we would run in a type error
+          (attribute set versus string).
+
+          Nix types like integers or booleans are automatically converted to
+          the right values Taskserver would expect.
+        '';
+        apply = let
+          mkKey = path: if path == ["server" "listen"] then "server"
+                        else concatStringsSep "." path;
+          recurse = path: attrs: let
+            mapper = name: val: let
+              newPath = path ++ [ name ];
+              scalar = if val == true then "true"
+                       else if val == false then "false"
+                       else toString val;
+            in if isAttrs val then recurse newPath val
+               else [ "${mkKey newPath}=${scalar}" ];
+          in concatLists (mapAttrsToList mapper attrs);
+        in recurse [];
+      };
+    };
+  };
+
+  imports = [
+    (mkRemovedOptionModule ["services" "taskserver" "extraConfig"] ''
+      This option was removed in favor of `services.taskserver.config` with
+      different semantics (it's now a list of attributes instead of lines).
+
+      Please look up the documentation of `services.taskserver.config' to get
+      more information about the new way to pass additional configuration
+      options.
+    '')
+  ];
+
+  config = mkMerge [
+    (mkIf cfg.enable {
+      environment.systemPackages = [ nixos-taskserver ];
+
+      users.users = optionalAttrs (cfg.user == "taskd") {
+        taskd = {
+          uid = config.ids.uids.taskd;
+          description = "Taskserver user";
+          group = cfg.group;
+        };
+      };
+
+      users.groups = optionalAttrs (cfg.group == "taskd") {
+        taskd.gid = config.ids.gids.taskd;
+      };
+
+      services.taskserver.config = {
+        # systemd related
+        daemon = false;
+        log = "-";
+
+        # logging
+        debug = cfg.debug;
+        ip.log = cfg.ipLog;
+
+        # general
+        ciphers = cfg.ciphers;
+        confirmation = cfg.confirmation;
+        extensions = cfg.extensions;
+        queue.size = cfg.queueSize;
+        request.limit = cfg.requestLimit;
+
+        # client
+        client.allow = cfg.allowedClientIDs;
+        client.deny = cfg.disallowedClientIDs;
+
+        # server
+        trust = cfg.trust;
+        server = {
+          listen = "${cfg.listenHost}:${toString cfg.listenPort}";
+        } // (if needToCreateCA then {
+          cert = "${cfg.dataDir}/keys/server.cert";
+          key = "${cfg.dataDir}/keys/server.key";
+          crl = "${cfg.dataDir}/keys/server.crl";
+        } else {
+          cert = "${cfg.pki.manual.server.cert}";
+          key = "${cfg.pki.manual.server.key}";
+          ${mapNullable (_: "crl") cfg.pki.manual.server.crl} = "${cfg.pki.manual.server.crl}";
+        });
+
+        ca.cert = if needToCreateCA then "${cfg.dataDir}/keys/ca.cert"
+                  else "${cfg.pki.manual.ca.cert}";
+      };
+
+      systemd.services.taskserver-init = {
+        wantedBy = [ "taskserver.service" ];
+        before = [ "taskserver.service" ];
+        description = "Initialize Taskserver Data Directory";
+
+        preStart = ''
+          mkdir -m 0770 -p "${cfg.dataDir}"
+          chown "${cfg.user}:${cfg.group}" "${cfg.dataDir}"
+        '';
+
+        script = ''
+          ${taskd} init
+          touch "${cfg.dataDir}/.is_initialized"
+        '';
+
+        environment.TASKDDATA = cfg.dataDir;
+
+        unitConfig.ConditionPathExists = "!${cfg.dataDir}/.is_initialized";
+
+        serviceConfig.Type = "oneshot";
+        serviceConfig.User = cfg.user;
+        serviceConfig.Group = cfg.group;
+        serviceConfig.PermissionsStartOnly = true;
+        serviceConfig.PrivateNetwork = true;
+        serviceConfig.PrivateDevices = true;
+        serviceConfig.PrivateTmp = true;
+      };
+
+      systemd.services.taskserver = {
+        description = "Taskwarrior Server";
+
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+
+        environment.TASKDDATA = cfg.dataDir;
+
+        preStart = let
+          jsonOrgs = builtins.toJSON cfg.organisations;
+          jsonFile = pkgs.writeText "orgs.json" jsonOrgs;
+          helperTool = "${nixos-taskserver}/bin/nixos-taskserver";
+        in "${helperTool} process-json '${jsonFile}'";
+
+        serviceConfig = {
+          ExecStart = let
+            mkCfgFlag = flag: escapeShellArg "--${flag}";
+            cfgFlags = concatMapStringsSep " " mkCfgFlag cfg.config;
+          in "@${taskd} taskd server ${cfgFlags}";
+          ExecReload = "${pkgs.coreutils}/bin/kill -USR1 $MAINPID";
+          Restart = "on-failure";
+          PermissionsStartOnly = true;
+          PrivateTmp = true;
+          PrivateDevices = true;
+          User = cfg.user;
+          Group = cfg.group;
+        };
+      };
+    })
+    (mkIf (cfg.enable && needToCreateCA) {
+      systemd.services.taskserver-ca = {
+        wantedBy = [ "taskserver.service" ];
+        after = [ "taskserver-init.service" ];
+        before = [ "taskserver.service" ];
+        description = "Initialize CA for TaskServer";
+        serviceConfig.Type = "oneshot";
+        serviceConfig.UMask = "0077";
+        serviceConfig.PrivateNetwork = true;
+        serviceConfig.PrivateTmp = true;
+
+        script = ''
+          silent_certtool() {
+            if ! output="$("${certtool}" "$@" 2>&1)"; then
+              echo "GNUTLS certtool invocation failed with output:" >&2
+              echo "$output" >&2
+            fi
+          }
+
+          mkdir -m 0700 -p "${cfg.dataDir}/keys"
+          chown root:root "${cfg.dataDir}/keys"
+
+          if [ ! -e "${cfg.dataDir}/keys/ca.key" ]; then
+            silent_certtool -p \
+              --bits ${toString cfg.pki.auto.bits} \
+              --outfile "${cfg.dataDir}/keys/ca.key"
+            silent_certtool -s \
+              --template "${pkgs.writeText "taskserver-ca.template" ''
+                cn = ${cfg.fqdn}
+                expiration_days = ${toString cfg.pki.auto.expiration.ca}
+                cert_signing_key
+                ca
+              ''}" \
+              --load-privkey "${cfg.dataDir}/keys/ca.key" \
+              --outfile "${cfg.dataDir}/keys/ca.cert"
+
+            chgrp "${cfg.group}" "${cfg.dataDir}/keys/ca.cert"
+            chmod g+r "${cfg.dataDir}/keys/ca.cert"
+          fi
+
+          if [ ! -e "${cfg.dataDir}/keys/server.key" ]; then
+            silent_certtool -p \
+              --bits ${toString cfg.pki.auto.bits} \
+              --outfile "${cfg.dataDir}/keys/server.key"
+
+            silent_certtool -c \
+              --template "${pkgs.writeText "taskserver-cert.template" ''
+                cn = ${cfg.fqdn}
+                expiration_days = ${toString cfg.pki.auto.expiration.server}
+                tls_www_server
+                encryption_key
+                signing_key
+              ''}" \
+              --load-ca-privkey "${cfg.dataDir}/keys/ca.key" \
+              --load-ca-certificate "${cfg.dataDir}/keys/ca.cert" \
+              --load-privkey "${cfg.dataDir}/keys/server.key" \
+              --outfile "${cfg.dataDir}/keys/server.cert"
+
+            chgrp "${cfg.group}" \
+              "${cfg.dataDir}/keys/server.key" \
+              "${cfg.dataDir}/keys/server.cert"
+
+            chmod g+r \
+              "${cfg.dataDir}/keys/server.key" \
+              "${cfg.dataDir}/keys/server.cert"
+          fi
+
+          if [ ! -e "${cfg.dataDir}/keys/server.crl" ]; then
+            silent_certtool --generate-crl \
+              --template "${pkgs.writeText "taskserver-crl.template" ''
+                expiration_days = ${toString cfg.pki.auto.expiration.crl}
+              ''}" \
+              --load-ca-privkey "${cfg.dataDir}/keys/ca.key" \
+              --load-ca-certificate "${cfg.dataDir}/keys/ca.cert" \
+              --outfile "${cfg.dataDir}/keys/server.crl"
+
+            chgrp "${cfg.group}" "${cfg.dataDir}/keys/server.crl"
+            chmod g+r "${cfg.dataDir}/keys/server.crl"
+          fi
+
+          chmod go+x "${cfg.dataDir}/keys"
+        '';
+      };
+    })
+    (mkIf (cfg.enable && cfg.listenHost != "localhost") {
+      networking.firewall.allowedTCPPorts = [ cfg.listenPort ];
+    })
+  ];
+
+  meta.doc = ./doc.xml;
+}
diff --git a/nixos/modules/services/misc/taskserver/doc.xml b/nixos/modules/services/misc/taskserver/doc.xml
new file mode 100644
index 00000000000..f6ead7c3785
--- /dev/null
+++ b/nixos/modules/services/misc/taskserver/doc.xml
@@ -0,0 +1,135 @@
+<chapter xmlns="http://docbook.org/ns/docbook"
+    xmlns:xlink="http://www.w3.org/1999/xlink"
+    version="5.0"
+    xml:id="module-services-taskserver">
+ <title>Taskserver</title>
+ <para>
+  Taskserver is the server component of
+  <link xlink:href="https://taskwarrior.org/">Taskwarrior</link>, a free and
+  open source todo list application.
+ </para>
+ <para>
+  <emphasis>Upstream documentation:</emphasis>
+  <link xlink:href="https://taskwarrior.org/docs/#taskd"/>
+ </para>
+ <section xml:id="module-services-taskserver-configuration">
+  <title>Configuration</title>
+
+  <para>
+   Taskserver does all of its authentication via TLS using client certificates,
+   so you either need to roll your own CA or purchase a certificate from a
+   known CA, which allows creation of client certificates. These certificates
+   are usually advertised as <quote>server certificates</quote>.
+  </para>
+
+  <para>
+   So in order to make it easier to handle your own CA, there is a helper tool
+   called <command>nixos-taskserver</command> which manages the custom CA along
+   with Taskserver organisations, users and groups.
+  </para>
+
+  <para>
+   While the client certificates in Taskserver only authenticate whether a user
+   is allowed to connect, every user has its own UUID which identifies it as an
+   entity.
+  </para>
+
+  <para>
+   With <command>nixos-taskserver</command> the client certificate is created
+   along with the UUID of the user, so it handles all of the credentials needed
+   in order to setup the Taskwarrior client to work with a Taskserver.
+  </para>
+ </section>
+ <section xml:id="module-services-taskserver-nixos-taskserver-tool">
+  <title>The nixos-taskserver tool</title>
+
+  <para>
+   Because Taskserver by default only provides scripts to setup users
+   imperatively, the <command>nixos-taskserver</command> tool is used for
+   addition and deletion of organisations along with users and groups defined
+   by <xref linkend="opt-services.taskserver.organisations"/> and as well for
+   imperative set up.
+  </para>
+
+  <para>
+   The tool is designed to not interfere if the command is used to manually set
+   up some organisations, users or groups.
+  </para>
+
+  <para>
+   For example if you add a new organisation using <command>nixos-taskserver
+   org add foo</command>, the organisation is not modified and deleted no
+   matter what you define in
+   <option>services.taskserver.organisations</option>, even if you're adding
+   the same organisation in that option.
+  </para>
+
+  <para>
+   The tool is modelled to imitate the official <command>taskd</command>
+   command, documentation for each subcommand can be shown by using the
+   <option>--help</option> switch.
+  </para>
+ </section>
+ <section xml:id="module-services-taskserver-declarative-ca-management">
+  <title>Declarative/automatic CA management</title>
+
+  <para>
+   Everything is done according to what you specify in the module options,
+   however in order to set up a Taskwarrior client for synchronisation with a
+   Taskserver instance, you have to transfer the keys and certificates to the
+   client machine.
+  </para>
+
+  <para>
+   This is done using <command>nixos-taskserver user export $orgname
+   $username</command> which is printing a shell script fragment to stdout
+   which can either be used verbatim or adjusted to import the user on the
+   client machine.
+  </para>
+
+  <para>
+   For example, let's say you have the following configuration:
+<screen>
+{
+  <xref linkend="opt-services.taskserver.enable"/> = true;
+  <xref linkend="opt-services.taskserver.fqdn"/> = "server";
+  <xref linkend="opt-services.taskserver.listenHost"/> = "::";
+  <link linkend="opt-services.taskserver.organisations._name_.users">services.taskserver.organisations.my-company.users</link> = [ "alice" ];
+}
+</screen>
+   This creates an organisation called <literal>my-company</literal> with the
+   user <literal>alice</literal>.
+  </para>
+
+  <para>
+   Now in order to import the <literal>alice</literal> user to another machine
+   <literal>alicebox</literal>, all we need to do is something like this:
+<screen>
+<prompt>$ </prompt>ssh server nixos-taskserver user export my-company alice | sh
+</screen>
+   Of course, if no SSH daemon is available on the server you can also copy
+   &amp; paste it directly into a shell.
+  </para>
+
+  <para>
+   After this step the user should be set up and you can start synchronising
+   your tasks for the first time with <command>task sync init</command> on
+   <literal>alicebox</literal>.
+  </para>
+
+  <para>
+   Subsequent synchronisation requests merely require the command <command>task
+   sync</command> after that stage.
+  </para>
+ </section>
+ <section xml:id="module-services-taskserver-manual-ca-management">
+  <title>Manual CA management</title>
+
+  <para>
+   If you set any options within
+   <link linkend="opt-services.taskserver.pki.manual.ca.cert">service.taskserver.pki.manual</link>.*,
+   <command>nixos-taskserver</command> won't issue certificates, but you can
+   still use it for adding or removing user accounts.
+  </para>
+ </section>
+</chapter>
diff --git a/nixos/modules/services/misc/taskserver/helper-tool.py b/nixos/modules/services/misc/taskserver/helper-tool.py
new file mode 100644
index 00000000000..22a3d8d5311
--- /dev/null
+++ b/nixos/modules/services/misc/taskserver/helper-tool.py
@@ -0,0 +1,688 @@
+import grp
+import json
+import pwd
+import os
+import re
+import string
+import subprocess
+import sys
+
+from contextlib import contextmanager
+from shutil import rmtree
+from tempfile import NamedTemporaryFile
+
+import click
+
+IS_AUTO_CONFIG = @isAutoConfig@ # NOQA
+CERTTOOL_COMMAND = "@certtool@"
+CERT_BITS = "@certBits@"
+CLIENT_EXPIRATION = "@clientExpiration@"
+CRL_EXPIRATION = "@crlExpiration@"
+
+TASKD_COMMAND = "@taskd@"
+TASKD_DATA_DIR = "@dataDir@"
+TASKD_USER = "@user@"
+TASKD_GROUP = "@group@"
+FQDN = "@fqdn@"
+
+CA_KEY = os.path.join(TASKD_DATA_DIR, "keys", "ca.key")
+CA_CERT = os.path.join(TASKD_DATA_DIR, "keys", "ca.cert")
+CRL_FILE = os.path.join(TASKD_DATA_DIR, "keys", "server.crl")
+
+RE_CONFIGUSER = re.compile(r'^\s*user\s*=(.*)$')
+RE_USERKEY = re.compile(r'New user key: (.+)$', re.MULTILINE)
+
+
+def lazyprop(fun):
+    """
+    Decorator which only evaluates the specified function when accessed.
+    """
+    name = '_lazy_' + fun.__name__
+
+    @property
+    def _lazy(self):
+        val = getattr(self, name, None)
+        if val is None:
+            val = fun(self)
+            setattr(self, name, val)
+        return val
+
+    return _lazy
+
+
+class TaskdError(OSError):
+    pass
+
+
+def run_as_taskd_user():
+    uid = pwd.getpwnam(TASKD_USER).pw_uid
+    gid = grp.getgrnam(TASKD_GROUP).gr_gid
+    os.setgid(gid)
+    os.setuid(uid)
+
+
+def taskd_cmd(cmd, *args, **kwargs):
+    """
+    Invoke taskd with the specified command with the privileges of the 'taskd'
+    user and 'taskd' group.
+
+    If 'capture_stdout' is passed as a keyword argument with the value True,
+    the return value are the contents the command printed to stdout.
+    """
+    capture_stdout = kwargs.pop("capture_stdout", False)
+    fun = subprocess.check_output if capture_stdout else subprocess.check_call
+    return fun(
+        [TASKD_COMMAND, cmd, "--data", TASKD_DATA_DIR] + list(args),
+        preexec_fn=run_as_taskd_user,
+        **kwargs
+    )
+
+
+def certtool_cmd(*args, **kwargs):
+    """
+    Invoke certtool from GNUTLS and return the output of the command.
+
+    The provided arguments are added to the certtool command and keyword
+    arguments are added to subprocess.check_output().
+
+    Note that this will suppress all output of certtool and it will only be
+    printed whenever there is an unsuccessful return code.
+    """
+    return subprocess.check_output(
+        [CERTTOOL_COMMAND] + list(args),
+        preexec_fn=lambda: os.umask(0077),
+        stderr=subprocess.STDOUT,
+        **kwargs
+    )
+
+
+def label(msg):
+    if sys.stdout.isatty() or sys.stderr.isatty():
+        sys.stderr.write(msg + "\n")
+
+
+def mkpath(*args):
+    return os.path.join(TASKD_DATA_DIR, "orgs", *args)
+
+
+def mark_imperative(*path):
+    """
+    Mark the specified path as being imperatively managed by creating an empty
+    file called ".imperative", so that it doesn't interfere with the
+    declarative configuration.
+    """
+    open(os.path.join(mkpath(*path), ".imperative"), 'a').close()
+
+
+def is_imperative(*path):
+    """
+    Check whether the given path is marked as imperative, see mark_imperative()
+    for more information.
+    """
+    full_path = []
+    for component in path:
+        full_path.append(component)
+        if os.path.exists(os.path.join(mkpath(*full_path), ".imperative")):
+            return True
+    return False
+
+
+def fetch_username(org, key):
+    for line in open(mkpath(org, "users", key, "config"), "r"):
+        match = RE_CONFIGUSER.match(line)
+        if match is None:
+            continue
+        return match.group(1).strip()
+    return None
+
+
+@contextmanager
+def create_template(contents):
+    """
+    Generate a temporary file with the specified contents as a list of strings
+    and yield its path as the context.
+    """
+    template = NamedTemporaryFile(mode="w", prefix="certtool-template")
+    template.writelines(map(lambda l: l + "\n", contents))
+    template.flush()
+    yield template.name
+    template.close()
+
+
+def generate_key(org, user):
+    if not IS_AUTO_CONFIG:
+        msg = "Automatic PKI handling is disabled, you need to " \
+              "manually issue a client certificate for user {}.\n"
+        sys.stderr.write(msg.format(user))
+        return
+
+    basedir = os.path.join(TASKD_DATA_DIR, "keys", org, user)
+    if os.path.exists(basedir):
+        raise OSError("Keyfile directory for {} already exists.".format(user))
+
+    privkey = os.path.join(basedir, "private.key")
+    pubcert = os.path.join(basedir, "public.cert")
+
+    try:
+        os.makedirs(basedir, mode=0700)
+
+        certtool_cmd("-p", "--bits", CERT_BITS, "--outfile", privkey)
+
+        template_data = [
+            "organization = {0}".format(org),
+            "cn = {}".format(FQDN),
+            "expiration_days = {}".format(CLIENT_EXPIRATION),
+            "tls_www_client",
+            "encryption_key",
+            "signing_key"
+        ]
+
+        with create_template(template_data) as template:
+            certtool_cmd(
+                "-c",
+                "--load-privkey", privkey,
+                "--load-ca-privkey", CA_KEY,
+                "--load-ca-certificate", CA_CERT,
+                "--template", template,
+                "--outfile", pubcert
+            )
+    except:
+        rmtree(basedir)
+        raise
+
+
+def revoke_key(org, user):
+    basedir = os.path.join(TASKD_DATA_DIR, "keys", org, user)
+    if not os.path.exists(basedir):
+        raise OSError("Keyfile directory for {} doesn't exist.".format(user))
+
+    pubcert = os.path.join(basedir, "public.cert")
+
+    expiration = "expiration_days = {}".format(CRL_EXPIRATION)
+
+    with create_template([expiration]) as template:
+        oldcrl = NamedTemporaryFile(mode="wb", prefix="old-crl")
+        oldcrl.write(open(CRL_FILE, "rb").read())
+        oldcrl.flush()
+        certtool_cmd(
+            "--generate-crl",
+            "--load-crl", oldcrl.name,
+            "--load-ca-privkey", CA_KEY,
+            "--load-ca-certificate", CA_CERT,
+            "--load-certificate", pubcert,
+            "--template", template,
+            "--outfile", CRL_FILE
+        )
+        oldcrl.close()
+    rmtree(basedir)
+
+
+def is_key_line(line, match):
+    return line.startswith("---") and line.lstrip("- ").startswith(match)
+
+
+def getkey(*args):
+    path = os.path.join(TASKD_DATA_DIR, "keys", *args)
+    buf = []
+    for line in open(path, "r"):
+        if len(buf) == 0:
+            if is_key_line(line, "BEGIN"):
+                buf.append(line)
+            continue
+
+        buf.append(line)
+
+        if is_key_line(line, "END"):
+            return ''.join(buf)
+    raise IOError("Unable to get key from {}.".format(path))
+
+
+def mktaskkey(cfg, path, keydata):
+    heredoc = 'cat > "{}" <<EOF\n{}EOF'.format(path, keydata)
+    cmd = 'task config taskd.{} -- "{}"'.format(cfg, path)
+    return heredoc + "\n" + cmd
+
+
+class User(object):
+    def __init__(self, org, name, key):
+        self.__org = org
+        self.name = name
+        self.key = key
+
+    def export(self):
+        credentials = '/'.join([self.__org, self.name, self.key])
+        allow_unquoted = string.ascii_letters + string.digits + "/-_."
+        if not all((c in allow_unquoted) for c in credentials):
+            credentials = "'" + credentials.replace("'", r"'\''") + "'"
+
+        script = []
+
+        if IS_AUTO_CONFIG:
+            pubcert = getkey(self.__org, self.name, "public.cert")
+            privkey = getkey(self.__org, self.name, "private.key")
+            cacert = getkey("ca.cert")
+
+            keydir = "${TASKDATA:-$HOME/.task}/keys"
+
+            script += [
+                "umask 0077",
+                'mkdir -p "{}"'.format(keydir),
+                mktaskkey("certificate", os.path.join(keydir, "public.cert"),
+                          pubcert),
+                mktaskkey("key", os.path.join(keydir, "private.key"), privkey),
+                mktaskkey("ca", os.path.join(keydir, "ca.cert"), cacert)
+            ]
+
+        script.append(
+            "task config taskd.credentials -- {}".format(credentials)
+        )
+
+        return "\n".join(script) + "\n"
+
+
+class Group(object):
+    def __init__(self, org, name):
+        self.__org = org
+        self.name = name
+
+
+class Organisation(object):
+    def __init__(self, name, ignore_imperative):
+        self.name = name
+        self.ignore_imperative = ignore_imperative
+
+    def add_user(self, name):
+        """
+        Create a new user along with a certificate and key.
+
+        Returns a 'User' object or None if the user already exists.
+        """
+        if self.ignore_imperative and is_imperative(self.name):
+            return None
+        if name not in self.users.keys():
+            output = taskd_cmd("add", "user", self.name, name,
+                               capture_stdout=True)
+            key = RE_USERKEY.search(output)
+            if key is None:
+                msg = "Unable to find key while creating user {}."
+                raise TaskdError(msg.format(name))
+
+            generate_key(self.name, name)
+            newuser = User(self.name, name, key.group(1))
+            self._lazy_users[name] = newuser
+            return newuser
+        return None
+
+    def del_user(self, name):
+        """
+        Delete a user and revoke its keys.
+        """
+        if name in self.users.keys():
+            user = self.get_user(name)
+            if self.ignore_imperative and \
+               is_imperative(self.name, "users", user.key):
+                return
+
+            # Work around https://bug.tasktools.org/browse/TD-40:
+            rmtree(mkpath(self.name, "users", user.key))
+
+            revoke_key(self.name, name)
+            del self._lazy_users[name]
+
+    def add_group(self, name):
+        """
+        Create a new group.
+
+        Returns a 'Group' object or None if the group already exists.
+        """
+        if self.ignore_imperative and is_imperative(self.name):
+            return None
+        if name not in self.groups.keys():
+            taskd_cmd("add", "group", self.name, name)
+            newgroup = Group(self.name, name)
+            self._lazy_groups[name] = newgroup
+            return newgroup
+        return None
+
+    def del_group(self, name):
+        """
+        Delete a group.
+        """
+        if name in self.users.keys():
+            if self.ignore_imperative and \
+               is_imperative(self.name, "groups", name):
+                return
+            taskd_cmd("remove", "group", self.name, name)
+            del self._lazy_groups[name]
+
+    def get_user(self, name):
+        return self.users.get(name)
+
+    @lazyprop
+    def users(self):
+        result = {}
+        for key in os.listdir(mkpath(self.name, "users")):
+            user = fetch_username(self.name, key)
+            if user is not None:
+                result[user] = User(self.name, user, key)
+        return result
+
+    def get_group(self, name):
+        return self.groups.get(name)
+
+    @lazyprop
+    def groups(self):
+        result = {}
+        for group in os.listdir(mkpath(self.name, "groups")):
+            result[group] = Group(self.name, group)
+        return result
+
+
+class Manager(object):
+    def __init__(self, ignore_imperative=False):
+        """
+        Instantiates an organisations manager.
+
+        If ignore_imperative is True, all actions that modify data are checked
+        whether they're created imperatively and if so, they will result in no
+        operation.
+        """
+        self.ignore_imperative = ignore_imperative
+
+    def add_org(self, name):
+        """
+        Create a new organisation.
+
+        Returns an 'Organisation' object or None if the organisation already
+        exists.
+        """
+        if name not in self.orgs.keys():
+            taskd_cmd("add", "org", name)
+            neworg = Organisation(name, self.ignore_imperative)
+            self._lazy_orgs[name] = neworg
+            return neworg
+        return None
+
+    def del_org(self, name):
+        """
+        Delete and revoke keys of an organisation with all its users and
+        groups.
+        """
+        org = self.get_org(name)
+        if org is not None:
+            if self.ignore_imperative and is_imperative(name):
+                return
+            for user in org.users.keys():
+                org.del_user(user)
+            for group in org.groups.keys():
+                org.del_group(group)
+            taskd_cmd("remove", "org", name)
+            del self._lazy_orgs[name]
+
+    def get_org(self, name):
+        return self.orgs.get(name)
+
+    @lazyprop
+    def orgs(self):
+        result = {}
+        for org in os.listdir(mkpath()):
+            result[org] = Organisation(org, self.ignore_imperative)
+        return result
+
+
+class OrganisationType(click.ParamType):
+    name = 'organisation'
+
+    def convert(self, value, param, ctx):
+        org = Manager().get_org(value)
+        if org is None:
+            self.fail("Organisation {} does not exist.".format(value))
+        return org
+
+ORGANISATION = OrganisationType()
+
+
+@click.group()
+@click.pass_context
+def cli(ctx):
+    """
+    Manage Taskserver users and certificates
+    """
+    if not IS_AUTO_CONFIG:
+        return
+    for path in (CA_KEY, CA_CERT, CRL_FILE):
+        if not os.path.exists(path):
+            msg = "CA setup not done or incomplete, missing file {}."
+            ctx.fail(msg.format(path))
+
+
+@cli.group("org")
+def org_cli():
+    """
+    Manage organisations
+    """
+    pass
+
+
+@cli.group("user")
+def user_cli():
+    """
+    Manage users
+    """
+    pass
+
+
+@cli.group("group")
+def group_cli():
+    """
+    Manage groups
+    """
+    pass
+
+
+@user_cli.command("list")
+@click.argument("organisation", type=ORGANISATION)
+def list_users(organisation):
+    """
+    List all users belonging to the specified organisation.
+    """
+    label("The following users exists for {}:".format(organisation.name))
+    for user in organisation.users.values():
+        sys.stdout.write(user.name + "\n")
+
+
+@group_cli.command("list")
+@click.argument("organisation", type=ORGANISATION)
+def list_groups(organisation):
+    """
+    List all users belonging to the specified organisation.
+    """
+    label("The following users exists for {}:".format(organisation.name))
+    for group in organisation.groups.values():
+        sys.stdout.write(group.name + "\n")
+
+
+@org_cli.command("list")
+def list_orgs():
+    """
+    List available organisations
+    """
+    label("The following organisations exist:")
+    for org in Manager().orgs:
+        sys.stdout.write(org.name + "\n")
+
+
+@user_cli.command("getkey")
+@click.argument("organisation", type=ORGANISATION)
+@click.argument("user")
+def get_uuid(organisation, user):
+    """
+    Get the UUID of the specified user belonging to the specified organisation.
+    """
+    userobj = organisation.get_user(user)
+    if userobj is None:
+        msg = "User {} doesn't exist in organisation {}."
+        sys.exit(msg.format(userobj.name, organisation.name))
+
+    label("User {} has the following UUID:".format(userobj.name))
+    sys.stdout.write(user.key + "\n")
+
+
+@user_cli.command("export")
+@click.argument("organisation", type=ORGANISATION)
+@click.argument("user")
+def export_user(organisation, user):
+    """
+    Export user of the specified organisation as a series of shell commands
+    that can be used on the client side to easily import the certificates.
+
+    Note that the private key will be exported as well, so use this with care!
+    """
+    userobj = organisation.get_user(user)
+    if userobj is None:
+        msg = "User {} doesn't exist in organisation {}."
+        sys.exit(msg.format(user, organisation.name))
+
+    sys.stdout.write(userobj.export())
+
+
+@org_cli.command("add")
+@click.argument("name")
+def add_org(name):
+    """
+    Create an organisation with the specified name.
+    """
+    if os.path.exists(mkpath(name)):
+        msg = "Organisation with name {} already exists."
+        sys.exit(msg.format(name))
+
+    taskd_cmd("add", "org", name)
+    mark_imperative(name)
+
+
+@org_cli.command("remove")
+@click.argument("name")
+def del_org(name):
+    """
+    Delete the organisation with the specified name.
+
+    All of the users and groups will be deleted as well and client certificates
+    will be revoked.
+    """
+    Manager().del_org(name)
+    msg = ("Organisation {} deleted. Be sure to restart the Taskserver"
+           " using 'systemctl restart taskserver.service' in order for"
+           " the certificate revocation to apply.")
+    click.echo(msg.format(name), err=True)
+
+
+@user_cli.command("add")
+@click.argument("organisation", type=ORGANISATION)
+@click.argument("user")
+def add_user(organisation, user):
+    """
+    Create a user for the given organisation along with a client certificate
+    and print the key of the new user.
+
+    The client certificate along with it's public key can be shown via the
+    'user export' subcommand.
+    """
+    userobj = organisation.add_user(user)
+    if userobj is None:
+        msg = "User {} already exists in organisation {}."
+        sys.exit(msg.format(user, organisation))
+    else:
+        mark_imperative(organisation.name, "users", userobj.key)
+
+
+@user_cli.command("remove")
+@click.argument("organisation", type=ORGANISATION)
+@click.argument("user")
+def del_user(organisation, user):
+    """
+    Delete a user from the given organisation.
+
+    This will also revoke the client certificate of the given user.
+    """
+    organisation.del_user(user)
+    msg = ("User {} deleted. Be sure to restart the Taskserver using"
+           " 'systemctl restart taskserver.service' in order for the"
+           " certificate revocation to apply.")
+    click.echo(msg.format(user), err=True)
+
+
+@group_cli.command("add")
+@click.argument("organisation", type=ORGANISATION)
+@click.argument("group")
+def add_group(organisation, group):
+    """
+    Create a group for the given organisation.
+    """
+    groupobj = organisation.add_group(group)
+    if groupobj is None:
+        msg = "Group {} already exists in organisation {}."
+        sys.exit(msg.format(group, organisation))
+    else:
+        mark_imperative(organisation.name, "groups", groupobj.name)
+
+
+@group_cli.command("remove")
+@click.argument("organisation", type=ORGANISATION)
+@click.argument("group")
+def del_group(organisation, group):
+    """
+    Delete a group from the given organisation.
+    """
+    organisation.del_group(group)
+    click("Group {} deleted.".format(group), err=True)
+
+
+def add_or_delete(old, new, add_fun, del_fun):
+    """
+    Given an 'old' and 'new' list, figure out the intersections and invoke
+    'add_fun' against every element that is not in the 'old' list and 'del_fun'
+    against every element that is not in the 'new' list.
+
+    Returns a tuple where the first element is the list of elements that were
+    added and the second element consisting of elements that were deleted.
+    """
+    old_set = set(old)
+    new_set = set(new)
+    to_delete = old_set - new_set
+    to_add = new_set - old_set
+    for elem in to_delete:
+        del_fun(elem)
+    for elem in to_add:
+        add_fun(elem)
+    return to_add, to_delete
+
+
+@cli.command("process-json")
+@click.argument('json-file', type=click.File('rb'))
+def process_json(json_file):
+    """
+    Create and delete users, groups and organisations based on a JSON file.
+
+    The structure of this file is exactly the same as the
+    'services.taskserver.organisations' option of the NixOS module and is used
+    for declaratively adding and deleting users.
+
+    Hence this subcommand is not recommended outside of the scope of the NixOS
+    module.
+    """
+    data = json.load(json_file)
+
+    mgr = Manager(ignore_imperative=True)
+    add_or_delete(mgr.orgs.keys(), data.keys(), mgr.add_org, mgr.del_org)
+
+    for org in mgr.orgs.values():
+        if is_imperative(org.name):
+            continue
+        add_or_delete(org.users.keys(), data[org.name]['users'],
+                      org.add_user, org.del_user)
+        add_or_delete(org.groups.keys(), data[org.name]['groups'],
+                      org.add_group, org.del_group)
+
+
+if __name__ == '__main__':
+    cli()
diff --git a/nixos/modules/services/misc/tautulli.nix b/nixos/modules/services/misc/tautulli.nix
new file mode 100644
index 00000000000..9a972b29122
--- /dev/null
+++ b/nixos/modules/services/misc/tautulli.nix
@@ -0,0 +1,81 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.tautulli;
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "plexpy" ] [ "services" "tautulli" ])
+  ];
+
+  options = {
+    services.tautulli = {
+      enable = mkEnableOption "Tautulli Plex Monitor";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/plexpy";
+        description = "The directory where Tautulli stores its data files.";
+      };
+
+      configFile = mkOption {
+        type = types.str;
+        default = "/var/lib/plexpy/config.ini";
+        description = "The location of Tautulli's config file.";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 8181;
+        description = "TCP port where Tautulli listens.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "plexpy";
+        description = "User account under which Tautulli runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "nogroup";
+        description = "Group under which Tautulli runs.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.tautulli;
+        defaultText = literalExpression "pkgs.tautulli";
+        description = ''
+          The Tautulli package to use.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' - ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.tautulli = {
+      description = "Tautulli Plex Monitor";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        GuessMainPID = "false";
+        ExecStart = "${cfg.package}/bin/tautulli --datadir ${cfg.dataDir} --config ${cfg.configFile} --port ${toString cfg.port} --pidfile ${cfg.dataDir}/tautulli.pid --nolaunch";
+        Restart = "on-failure";
+      };
+    };
+
+    users.users = mkIf (cfg.user == "plexpy") {
+      plexpy = { group = cfg.group; uid = config.ids.uids.plexpy; };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/tiddlywiki.nix b/nixos/modules/services/misc/tiddlywiki.nix
new file mode 100644
index 00000000000..2adc08f6cfe
--- /dev/null
+++ b/nixos/modules/services/misc/tiddlywiki.nix
@@ -0,0 +1,52 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.tiddlywiki;
+  listenParams = concatStrings (mapAttrsToList (n: v: " '${n}=${toString v}' ") cfg.listenOptions);
+  exe = "${pkgs.nodePackages.tiddlywiki}/lib/node_modules/.bin/tiddlywiki";
+  name = "tiddlywiki";
+  dataDir = "/var/lib/" + name;
+
+in {
+
+  options.services.tiddlywiki = {
+
+    enable = mkEnableOption "TiddlyWiki nodejs server";
+
+    listenOptions = mkOption {
+      type = types.attrs;
+      default = {};
+      example = {
+        credentials = "../credentials.csv";
+        readers="(authenticated)";
+        port = 3456;
+      };
+      description = ''
+        Parameters passed to <literal>--listen</literal> command.
+        Refer to <link xlink:href="https://tiddlywiki.com/#WebServer"/>
+        for details on supported values.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd = {
+      services.tiddlywiki = {
+        description = "TiddlyWiki nodejs server";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          Type = "simple";
+          Restart = "on-failure";
+          DynamicUser = true;
+          StateDirectory = name;
+          ExecStartPre = "-${exe} ${dataDir} --init server";
+          ExecStart = "${exe} ${dataDir} --listen ${listenParams}";
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/tp-auto-kbbl.nix b/nixos/modules/services/misc/tp-auto-kbbl.nix
new file mode 100644
index 00000000000..59018f7f81f
--- /dev/null
+++ b/nixos/modules/services/misc/tp-auto-kbbl.nix
@@ -0,0 +1,58 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.tp-auto-kbbl;
+
+in {
+  meta.maintainers = with maintainers; [ sebtm ];
+
+  options = {
+    services.tp-auto-kbbl = {
+      enable = mkEnableOption "Auto toggle keyboard back-lighting on Thinkpads (and maybe other laptops) for Linux";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.tp-auto-kbbl;
+        defaultText = literalExpression "pkgs.tp-auto-kbbl";
+        description = "Package providing <command>tp-auto-kbbl</command>.";
+      };
+
+      arguments = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        description = ''
+          List of arguments appended to <literal>./tp-auto-kbbl --device [device] [arguments]</literal>
+        '';
+      };
+
+      device = mkOption {
+        type = types.str;
+        default = "/dev/input/event0";
+        description = "Device watched for activities.";
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services.tp-auto-kbbl = {
+      serviceConfig = {
+        ExecStart = concatStringsSep " "
+          ([ "${cfg.package}/bin/tp-auto-kbbl" "--device ${cfg.device}" ] ++ cfg.arguments);
+        Restart = "always";
+        Type = "simple";
+      };
+
+      unitConfig = {
+        Description = "Auto toggle keyboard backlight";
+        Documentation = "https://github.com/saibotd/tp-auto-kbbl";
+        After = [ "dbus.service" ];
+      };
+
+      wantedBy = [ "multi-user.target" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/tzupdate.nix b/nixos/modules/services/misc/tzupdate.nix
new file mode 100644
index 00000000000..eac1e1112a5
--- /dev/null
+++ b/nixos/modules/services/misc/tzupdate.nix
@@ -0,0 +1,45 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.tzupdate;
+in {
+  options.services.tzupdate = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable the tzupdate timezone updating service. This provides
+        a one-shot service which can be activated with systemctl to
+        update the timezone.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # We need to have imperative time zone management for this to work.
+    # This will give users an error if they have set an explicit time
+    # zone, which is better than silently overriding it.
+    time.timeZone = null;
+
+    # We provide a one-shot service which can be manually run. We could
+    # provide a service that runs on startup, but it's tricky to get
+    # a service to run after you have *internet* access.
+    systemd.services.tzupdate = {
+      description = "tzupdate timezone update service";
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+
+      serviceConfig = {
+        Type = "oneshot";
+        # We could link directly into pkgs.tzdata, but at least timedatectl seems
+        # to expect the symlink to point directly to a file in etc.
+        # Setting the "debian timezone file" to point at /dev/null stops it doing anything.
+        ExecStart = "${pkgs.tzupdate}/bin/tzupdate -z /etc/zoneinfo -d /dev/null";
+      };
+    };
+  };
+
+  meta.maintainers = [ maintainers.michaelpj ];
+}
diff --git a/nixos/modules/services/misc/uhub.nix b/nixos/modules/services/misc/uhub.nix
new file mode 100644
index 00000000000..0d0a8c2a4cb
--- /dev/null
+++ b/nixos/modules/services/misc/uhub.nix
@@ -0,0 +1,112 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  settingsFormat = {
+    type = with lib.types; attrsOf (oneOf [ bool int str ]);
+    generate = name: attrs:
+      pkgs.writeText name (lib.strings.concatStringsSep "\n"
+        (lib.attrsets.mapAttrsToList
+          (key: value: "${key}=${builtins.toJSON value}") attrs));
+  };
+in {
+  options = {
+
+    services.uhub = mkOption {
+      default = { };
+      description = "Uhub ADC hub instances";
+      type = types.attrsOf (types.submodule {
+        options = {
+
+          enable = mkEnableOption "hub instance" // { default = true; };
+
+          enableTLS = mkOption {
+            type = types.bool;
+            default = false;
+            description = "Whether to enable TLS support.";
+          };
+
+          settings = mkOption {
+            inherit (settingsFormat) type;
+            description = ''
+              Configuration of uhub.
+              See https://www.uhub.org/doc/config.php for a list of options.
+            '';
+            default = { };
+            example = {
+              server_bind_addr = "any";
+              server_port = 1511;
+              hub_name = "My Public Hub";
+              hub_description = "Yet another ADC hub";
+              max_users = 150;
+            };
+          };
+
+          plugins = mkOption {
+            description = "Uhub plugin configuration.";
+            type = with types;
+              listOf (submodule {
+                options = {
+                  plugin = mkOption {
+                    type = path;
+                    example = literalExpression
+                      "$${pkgs.uhub}/plugins/mod_auth_sqlite.so";
+                    description = "Path to plugin file.";
+                  };
+                  settings = mkOption {
+                    description = "Settings specific to this plugin.";
+                    type = with types; attrsOf str;
+                    example = { file = "/etc/uhub/users.db"; };
+                  };
+                };
+              });
+            default = [ ];
+          };
+
+        };
+      });
+    };
+
+  };
+
+  config = let
+    hubs = lib.attrsets.filterAttrs (_: cfg: cfg.enable) config.services.uhub;
+  in {
+
+    environment.etc = lib.attrsets.mapAttrs' (name: cfg:
+      let
+        settings' = cfg.settings // {
+          tls_enable = cfg.enableTLS;
+          file_plugins = pkgs.writeText "uhub-plugins.conf"
+            (lib.strings.concatStringsSep "\n" (map ({ plugin, settings }:
+              "plugin ${plugin} ${
+                toString
+                (lib.attrsets.mapAttrsToList (key: value: ''"${key}=${value}"'')
+                  settings)
+              }") cfg.plugins));
+        };
+      in {
+        name = "uhub/${name}.conf";
+        value.source = settingsFormat.generate "uhub-${name}.conf" settings';
+      }) hubs;
+
+    systemd.services = lib.attrsets.mapAttrs' (name: cfg: {
+      name = "uhub-${name}";
+      value = let pkg = pkgs.uhub.override { tlsSupport = cfg.enableTLS; };
+      in {
+        description = "high performance peer-to-peer hub for the ADC network";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        reloadIfChanged = true;
+        serviceConfig = {
+          Type = "notify";
+          ExecStart = "${pkg}/bin/uhub -c /etc/uhub/${name}.conf -L";
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+          DynamicUser = true;
+        };
+      };
+    }) hubs;
+  };
+
+}
diff --git a/nixos/modules/services/misc/weechat.nix b/nixos/modules/services/misc/weechat.nix
new file mode 100644
index 00000000000..7a4c4dca2ac
--- /dev/null
+++ b/nixos/modules/services/misc/weechat.nix
@@ -0,0 +1,63 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.weechat;
+in
+
+{
+  options.services.weechat = {
+    enable = mkEnableOption "weechat";
+    root = mkOption {
+      description = "Weechat state directory.";
+      type = types.str;
+      default = "/var/lib/weechat";
+    };
+    sessionName = mkOption {
+      description = "Name of the `screen' session for weechat.";
+      default = "weechat-screen";
+      type = types.str;
+    };
+    binary = mkOption {
+      type = types.path;
+      description = "Binary to execute.";
+      default = "${pkgs.weechat}/bin/weechat";
+      defaultText = literalExpression ''"''${pkgs.weechat}/bin/weechat"'';
+      example = literalExpression ''"''${pkgs.weechat}/bin/weechat-headless"'';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users = {
+      groups.weechat = {};
+      users.weechat = {
+        createHome = true;
+        group = "weechat";
+        home = cfg.root;
+        isSystemUser = true;
+      };
+    };
+
+    systemd.services.weechat = {
+      environment.WEECHAT_HOME = cfg.root;
+      serviceConfig = {
+        User = "weechat";
+        Group = "weechat";
+        RemainAfterExit = "yes";
+      };
+      script = "exec ${config.security.wrapperDir}/screen -Dm -S ${cfg.sessionName} ${cfg.binary}";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network.target" ];
+    };
+
+    security.wrappers.screen =
+      { setuid = true;
+        owner = "root";
+        group = "root";
+        source = "${pkgs.screen}/bin/screen";
+      };
+  };
+
+  meta.doc = ./weechat.xml;
+}
diff --git a/nixos/modules/services/misc/weechat.xml b/nixos/modules/services/misc/weechat.xml
new file mode 100644
index 00000000000..7255edfb9da
--- /dev/null
+++ b/nixos/modules/services/misc/weechat.xml
@@ -0,0 +1,66 @@
+<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-weechat">
+ <title>WeeChat</title>
+ <para>
+  <link xlink:href="https://weechat.org/">WeeChat</link> is a fast and
+  extensible IRC client.
+ </para>
+ <section xml:id="module-services-weechat-basic-usage">
+  <title>Basic Usage</title>
+
+  <para>
+   By default, the module creates a
+   <literal><link xlink:href="https://www.freedesktop.org/wiki/Software/systemd/">systemd</link></literal>
+   unit which runs the chat client in a detached
+   <literal><link xlink:href="https://www.gnu.org/software/screen/">screen</link></literal>
+   session.
+  </para>
+
+  <para>
+   This can be done by enabling the <literal>weechat</literal> service:
+<programlisting>
+{ ... }:
+
+{
+  <link linkend="opt-services.weechat.enable">services.weechat.enable</link> = true;
+}
+</programlisting>
+  </para>
+
+  <para>
+   The service is managed by a dedicated user named <literal>weechat</literal>
+   in the state directory <literal>/var/lib/weechat</literal>.
+  </para>
+ </section>
+ <section xml:id="module-services-weechat-reattach">
+  <title>Re-attaching to WeeChat</title>
+
+  <para>
+   WeeChat runs in a screen session owned by a dedicated user. To explicitly
+   allow your another user to attach to this session, the
+   <literal>screenrc</literal> needs to be tweaked by adding
+   <link xlink:href="https://www.gnu.org/software/screen/manual/html_node/Multiuser.html#Multiuser">multiuser</link>
+   support:
+<programlisting>
+{
+  <link linkend="opt-programs.screen.screenrc">programs.screen.screenrc</link> = ''
+    multiuser on
+    acladd normal_user
+  '';
+}
+</programlisting>
+   Now, the session can be re-attached like this:
+<programlisting>
+screen -x weechat/weechat-screen
+</programlisting>
+  </para>
+
+  <para>
+   <emphasis>The session name can be changed using
+   <link linkend="opt-services.weechat.sessionName">services.weechat.sessionName.</link></emphasis>
+  </para>
+ </section>
+</chapter>
diff --git a/nixos/modules/services/misc/xmr-stak.nix b/nixos/modules/services/misc/xmr-stak.nix
new file mode 100644
index 00000000000..9256e9ae01c
--- /dev/null
+++ b/nixos/modules/services/misc/xmr-stak.nix
@@ -0,0 +1,93 @@
+{ lib, config, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xmr-stak;
+
+  pkg = pkgs.xmr-stak.override {
+    inherit (cfg) openclSupport cudaSupport;
+  };
+
+in
+
+{
+  options = {
+    services.xmr-stak = {
+      enable = mkEnableOption "xmr-stak miner";
+      openclSupport = mkEnableOption "support for OpenCL (AMD/ATI graphics cards)";
+      cudaSupport = mkEnableOption "support for CUDA (NVidia graphics cards)";
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "--noCPU" "--currency monero" ];
+        description = "List of parameters to pass to xmr-stak.";
+      };
+
+      configFiles = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        example = literalExpression ''
+          {
+            "config.txt" = '''
+              "verbose_level" : 4,
+              "h_print_time" : 60,
+              "tls_secure_algo" : true,
+            ''';
+            "pools.txt" = '''
+              "currency" : "monero7",
+              "pool_list" :
+              [ { "pool_address" : "pool.supportxmr.com:443",
+                  "wallet_address" : "my-wallet-address",
+                  "rig_id" : "",
+                  "pool_password" : "nixos",
+                  "use_nicehash" : false,
+                  "use_tls" : true,
+                  "tls_fingerprint" : "",
+                  "pool_weight" : 23
+                },
+              ],
+            ''';
+          }
+        '';
+        description = ''
+          Content of config files like config.txt, pools.txt or cpu.txt.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.xmr-stak = {
+      wantedBy = [ "multi-user.target" ];
+      bindsTo = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+      environment = mkIf cfg.cudaSupport {
+        LD_LIBRARY_PATH = "${pkgs.linuxPackages_latest.nvidia_x11}/lib";
+      };
+
+      preStart = concatStrings (flip mapAttrsToList cfg.configFiles (fn: content: ''
+        ln -sf '${pkgs.writeText "xmr-stak-${fn}" content}' '${fn}'
+      ''));
+
+      serviceConfig = let rootRequired = cfg.openclSupport || cfg.cudaSupport; in {
+        ExecStart = "${pkg}/bin/xmr-stak ${concatStringsSep " " cfg.extraArgs}";
+        # xmr-stak generates cpu and/or gpu configuration files
+        WorkingDirectory = "/tmp";
+        PrivateTmp = true;
+        DynamicUser = !rootRequired;
+        LimitMEMLOCK = toString (1024*1024);
+      };
+    };
+  };
+
+  imports = [
+    (mkRemovedOptionModule ["services" "xmr-stak" "configText"] ''
+      This option was removed in favour of `services.xmr-stak.configFiles`
+      because the new config file `pools.txt` was introduced. You are
+      now able to define all other config files like cpu.txt or amd.txt.
+    '')
+  ];
+}
diff --git a/nixos/modules/services/misc/xmrig.nix b/nixos/modules/services/misc/xmrig.nix
new file mode 100644
index 00000000000..c5c3803920c
--- /dev/null
+++ b/nixos/modules/services/misc/xmrig.nix
@@ -0,0 +1,76 @@
+{ config, pkgs, lib, ... }:
+
+
+let
+  cfg = config.services.xmrig;
+
+  json = pkgs.formats.json { };
+  configFile = json.generate "config.json" cfg.settings;
+in
+
+with lib;
+
+{
+  options = {
+    services.xmrig = {
+      enable = mkEnableOption "XMRig Mining Software";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.xmrig;
+        defaultText = literalExpression "pkgs.xmrig";
+        example = literalExpression "pkgs.xmrig-mo";
+        description = "XMRig package to use.";
+      };
+
+      settings = mkOption {
+        default = { };
+        type = json.type;
+        example = literalExpression ''
+          {
+            autosave = true;
+            cpu = true;
+            opencl = false;
+            cuda = false;
+            pools = [
+              {
+                url = "pool.supportxmr.com:443";
+                user = "your-wallet";
+                keepalive = true;
+                tls = true;
+              }
+            ]
+          }
+        '';
+        description = ''
+          XMRig configuration. Refer to
+          <link xlink:href="https://xmrig.com/docs/miner/config"/>
+          for details on supported values.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    boot.kernelModules = [ "msr" ];
+
+    systemd.services.xmrig = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      description = "XMRig Mining Software Service";
+      serviceConfig = {
+        ExecStartPre = "${cfg.package}/bin/xmrig --config=${configFile} --dry-run";
+        ExecStart = "${cfg.package}/bin/xmrig --config=${configFile}";
+        # https://xmrig.com/docs/miner/randomx-optimization-guide/msr
+        # If you use recent XMRig with root privileges (Linux) or admin
+        # privileges (Windows) the miner configure all MSR registers
+        # automatically.
+        DynamicUser = lib.mkDefault false;
+      };
+    };
+  };
+
+  meta = with lib; {
+    maintainers = with maintainers; [ ratsclub ];
+  };
+}
diff --git a/nixos/modules/services/misc/zoneminder.nix b/nixos/modules/services/misc/zoneminder.nix
new file mode 100644
index 00000000000..a557e742b7c
--- /dev/null
+++ b/nixos/modules/services/misc/zoneminder.nix
@@ -0,0 +1,370 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.zoneminder;
+  fpm = config.services.phpfpm.pools.zoneminder;
+  pkg = pkgs.zoneminder;
+
+  dirName = pkg.dirName;
+
+  user = "zoneminder";
+  group = {
+    nginx = config.services.nginx.group;
+    none  = user;
+  }.${cfg.webserver};
+
+  useNginx = cfg.webserver == "nginx";
+
+  defaultDir = "/var/lib/${user}";
+  home = if useCustomDir then cfg.storageDir else defaultDir;
+
+  useCustomDir = cfg.storageDir != null;
+
+  zms = "/cgi-bin/zms";
+
+  dirs = dirList: [ dirName ] ++ map (e: "${dirName}/${e}") dirList;
+
+  cacheDirs = [ "swap" ];
+  libDirs   = [ "events" "exports" "images" "sounds" ];
+
+  dirStanzas = baseDir:
+    lib.concatStringsSep "\n" (map (e:
+      "ZM_DIR_${lib.toUpper e}=${baseDir}/${e}"
+      ) libDirs);
+
+  defaultsFile = pkgs.writeText "60-defaults.conf" ''
+    # 01-system-paths.conf
+    ${dirStanzas home}
+    ZM_PATH_ARP=${lib.getBin pkgs.nettools}/bin/arp
+    ZM_PATH_LOGS=/var/log/${dirName}
+    ZM_PATH_MAP=/dev/shm
+    ZM_PATH_SOCKS=/run/${dirName}
+    ZM_PATH_SWAP=/var/cache/${dirName}/swap
+    ZM_PATH_ZMS=${zms}
+
+    # 02-multiserver.conf
+    ZM_SERVER_HOST=
+
+    # Database
+    ZM_DB_TYPE=mysql
+    ZM_DB_HOST=${cfg.database.host}
+    ZM_DB_NAME=${cfg.database.name}
+    ZM_DB_USER=${cfg.database.username}
+    ZM_DB_PASS=${cfg.database.password}
+
+    # Web
+    ZM_WEB_USER=${user}
+    ZM_WEB_GROUP=${group}
+  '';
+
+  configFile = pkgs.writeText "80-nixos.conf" ''
+    # You can override defaults here
+
+    ${cfg.extraConfig}
+  '';
+
+in {
+  options = {
+    services.zoneminder = with lib; {
+      enable = lib.mkEnableOption ''
+        ZoneMinder
+        </para><para>
+        If you intend to run the database locally, you should set
+        `config.services.zoneminder.database.createLocally` to true. Otherwise,
+        when set to `false` (the default), you will have to create the database
+        and database user as well as populate the database yourself.
+        Additionally, you will need to run `zmupdate.pl` yourself when
+        upgrading to a newer version.
+      '';
+
+      webserver = mkOption {
+        type = types.enum [ "nginx" "none" ];
+        default = "nginx";
+        description = ''
+          The webserver to configure for the PHP frontend.
+          </para>
+          <para>
+
+          Set it to `none` if you want to configure it yourself. PRs are welcome
+          for support for other web servers.
+        '';
+      };
+
+      hostname = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = ''
+          The hostname on which to listen.
+        '';
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 8095;
+        description = ''
+          The port on which to listen.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open the firewall port(s).
+        '';
+      };
+
+      database = {
+        createLocally = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Create the database and database user locally.
+          '';
+        };
+
+        host = mkOption {
+          type = types.str;
+          default = "localhost";
+          description = ''
+            Hostname hosting the database.
+          '';
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "zm";
+          description = ''
+            Name of database.
+          '';
+        };
+
+        username = mkOption {
+          type = types.str;
+          default = "zmuser";
+          description = ''
+            Username for accessing the database.
+          '';
+        };
+
+        password = mkOption {
+          type = types.str;
+          default = "zmpass";
+          description = ''
+            Username for accessing the database.
+            Not used if <literal>createLocally</literal> is set.
+          '';
+        };
+      };
+
+      cameras = mkOption {
+        type = types.int;
+        default = 1;
+        description = ''
+          Set this to the number of cameras you expect to support.
+        '';
+      };
+
+      storageDir = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/storage/tank";
+        description = ''
+          ZoneMinder can generate quite a lot of data, so in case you don't want
+          to use the default ${defaultDir}, you can override the path here.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Additional configuration added verbatim to the configuration file.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    assertions = [
+      { assertion = cfg.database.createLocally -> cfg.database.username == user;
+        message = "services.zoneminder.database.username must be set to ${user} if services.zoneminder.database.createLocally is set true";
+      }
+    ];
+
+    environment.etc = {
+      "zoneminder/60-defaults.conf".source = defaultsFile;
+      "zoneminder/80-nixos.conf".source    = configFile;
+    };
+
+    networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [
+      cfg.port
+      6802 # zmtrigger
+    ];
+
+    services = {
+      fcgiwrap = lib.mkIf useNginx {
+        enable = true;
+        preforkProcesses = cfg.cameras;
+        inherit user group;
+      };
+
+      mysql = lib.mkIf cfg.database.createLocally {
+        enable = true;
+        package = lib.mkDefault pkgs.mariadb;
+        ensureDatabases = [ cfg.database.name ];
+        ensureUsers = [{
+          name = cfg.database.username;
+          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+        }];
+      };
+
+      nginx = lib.mkIf useNginx {
+        enable = true;
+        virtualHosts = {
+          ${cfg.hostname} = {
+            default = true;
+            root = "${pkg}/share/zoneminder/www";
+            listen = [ { addr = "0.0.0.0"; inherit (cfg) port; } ];
+            extraConfig = let
+              fcgi = config.services.fcgiwrap;
+            in ''
+              index index.php;
+
+              location / {
+                try_files $uri $uri/ /index.php?$args =404;
+
+                rewrite ^/skins/.*/css/fonts/(.*)$ /fonts/$1 permanent;
+
+                location ~ /api/(css|img|ico) {
+                  rewrite ^/api(.+)$ /api/app/webroot/$1 break;
+                  try_files $uri $uri/ =404;
+                }
+
+                location ~ \.(gif|ico|jpg|jpeg|png)$ {
+                  access_log off;
+                  expires 30d;
+                }
+
+                location /api {
+                  rewrite ^/api(.+)$ /api/app/webroot/index.php?p=$1 last;
+                }
+
+                location /cgi-bin {
+                  gzip off;
+
+                  include ${config.services.nginx.package}/conf/fastcgi_params;
+                  fastcgi_param SCRIPT_FILENAME ${pkg}/libexec/zoneminder/${zms};
+                  fastcgi_param HTTP_PROXY "";
+                  fastcgi_intercept_errors on;
+
+                  fastcgi_pass ${fcgi.socketType}:${fcgi.socketAddress};
+                }
+
+                location /cache/ {
+                  alias /var/cache/${dirName}/;
+                }
+
+                location ~ \.php$ {
+                  try_files $uri =404;
+                  fastcgi_index index.php;
+
+                  include ${config.services.nginx.package}/conf/fastcgi_params;
+                  fastcgi_param SCRIPT_FILENAME $request_filename;
+                  fastcgi_param HTTP_PROXY "";
+
+                  fastcgi_pass unix:${fpm.socket};
+                }
+              }
+            '';
+          };
+        };
+      };
+
+      phpfpm = lib.mkIf useNginx {
+        pools.zoneminder = {
+          inherit user group;
+          phpPackage = pkgs.php.withExtensions ({ enabled, all }: enabled ++ [ all.apcu ]);
+          phpOptions = ''
+            date.timezone = "${config.time.timeZone}"
+          '';
+          settings = lib.mapAttrs (name: lib.mkDefault) {
+            "listen.owner" = user;
+            "listen.group" = group;
+            "listen.mode" = "0660";
+
+            "pm" = "dynamic";
+            "pm.start_servers" = 1;
+            "pm.min_spare_servers" = 1;
+            "pm.max_spare_servers" = 2;
+            "pm.max_requests" = 500;
+            "pm.max_children" = 5;
+            "pm.status_path" = "/$pool-status";
+            "ping.path" = "/$pool-ping";
+          };
+        };
+      };
+    };
+
+    systemd.services = {
+      zoneminder = with pkgs; {
+        inherit (zoneminder.meta) description;
+        documentation = [ "https://zoneminder.readthedocs.org/en/latest/" ];
+        path = [
+          coreutils
+          procps
+          psmisc
+        ];
+        after = [ "nginx.service" ] ++ lib.optional cfg.database.createLocally "mysql.service";
+        wantedBy = [ "multi-user.target" ];
+        restartTriggers = [ defaultsFile configFile ];
+        preStart = lib.optionalString useCustomDir ''
+          install -dm775 -o ${user} -g ${group} ${cfg.storageDir}/{${lib.concatStringsSep "," libDirs}}
+        '' + lib.optionalString cfg.database.createLocally ''
+          if ! test -e "/var/lib/${dirName}/db-created"; then
+            ${config.services.mysql.package}/bin/mysql < ${pkg}/share/zoneminder/db/zm_create.sql
+            touch "/var/lib/${dirName}/db-created"
+          fi
+
+          ${zoneminder}/bin/zmupdate.pl -nointeractive
+        '';
+        serviceConfig = {
+          User = user;
+          Group = group;
+          SupplementaryGroups = [ "video" ];
+          ExecStart  = "${zoneminder}/bin/zmpkg.pl start";
+          ExecStop   = "${zoneminder}/bin/zmpkg.pl stop";
+          ExecReload = "${zoneminder}/bin/zmpkg.pl restart";
+          PIDFile = "/run/${dirName}/zm.pid";
+          Type = "forking";
+          Restart = "on-failure";
+          RestartSec = "10s";
+          CacheDirectory = dirs cacheDirs;
+          RuntimeDirectory = dirName;
+          ReadWriteDirectories = lib.mkIf useCustomDir [ cfg.storageDir ];
+          StateDirectory = dirs (if useCustomDir then [] else libDirs);
+          LogsDirectory = dirName;
+          PrivateTmp = true;
+          ProtectSystem = "strict";
+          ProtectKernelTunables = true;
+          SystemCallArchitectures = "native";
+          NoNewPrivileges = true;
+        };
+      };
+    };
+
+    users.groups.${user} = {
+      gid = config.ids.gids.zoneminder;
+    };
+
+    users.users.${user} = {
+      uid = config.ids.uids.zoneminder;
+      group = user;
+      inherit home;
+      inherit (pkgs.zoneminder.meta) description;
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ ];
+}
diff --git a/nixos/modules/services/misc/zookeeper.nix b/nixos/modules/services/misc/zookeeper.nix
new file mode 100644
index 00000000000..3809a93a61e
--- /dev/null
+++ b/nixos/modules/services/misc/zookeeper.nix
@@ -0,0 +1,158 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.zookeeper;
+
+  zookeeperConfig = ''
+    dataDir=${cfg.dataDir}
+    clientPort=${toString cfg.port}
+    autopurge.purgeInterval=${toString cfg.purgeInterval}
+    ${cfg.extraConf}
+    ${cfg.servers}
+  '';
+
+  configDir = pkgs.buildEnv {
+    name = "zookeeper-conf";
+    paths = [
+      (pkgs.writeTextDir "zoo.cfg" zookeeperConfig)
+      (pkgs.writeTextDir "log4j.properties" cfg.logging)
+    ];
+  };
+
+in {
+
+  options.services.zookeeper = {
+    enable = mkOption {
+      description = "Whether to enable Zookeeper.";
+      default = false;
+      type = types.bool;
+    };
+
+    port = mkOption {
+      description = "Zookeeper Client port.";
+      default = 2181;
+      type = types.int;
+    };
+
+    id = mkOption {
+      description = "Zookeeper ID.";
+      default = 0;
+      type = types.int;
+    };
+
+    purgeInterval = mkOption {
+      description = ''
+        The time interval in hours for which the purge task has to be triggered. Set to a positive integer (1 and above) to enable the auto purging.
+      '';
+      default = 1;
+      type = types.int;
+    };
+
+    extraConf = mkOption {
+      description = "Extra configuration for Zookeeper.";
+      type = types.lines;
+      default = ''
+        initLimit=5
+        syncLimit=2
+        tickTime=2000
+      '';
+    };
+
+    servers = mkOption {
+      description = "All Zookeeper Servers.";
+      default = "";
+      type = types.lines;
+      example = ''
+        server.0=host0:2888:3888
+        server.1=host1:2888:3888
+        server.2=host2:2888:3888
+      '';
+    };
+
+    logging = mkOption {
+      description = "Zookeeper logging configuration.";
+      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
+      '';
+      type = types.lines;
+    };
+
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/zookeeper";
+      description = ''
+        Data directory for Zookeeper
+      '';
+    };
+
+    extraCmdLineOptions = mkOption {
+      description = "Extra command line options for the Zookeeper launcher.";
+      default = [ "-Dcom.sun.management.jmxremote" "-Dcom.sun.management.jmxremote.local.only=true" ];
+      type = types.listOf types.str;
+      example = [ "-Djava.net.preferIPv4Stack=true" "-Dcom.sun.management.jmxremote" "-Dcom.sun.management.jmxremote.local.only=true" ];
+    };
+
+    preferIPv4 = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Add the -Djava.net.preferIPv4Stack=true flag to the Zookeeper server.
+      '';
+    };
+
+    package = mkOption {
+      description = "The zookeeper package to use";
+      default = pkgs.zookeeper;
+      defaultText = literalExpression "pkgs.zookeeper";
+      type = types.package;
+    };
+
+  };
+
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [cfg.package];
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0700 zookeeper - - -"
+      "Z '${cfg.dataDir}' 0700 zookeeper - - -"
+    ];
+
+    systemd.services.zookeeper = {
+      description = "Zookeeper Daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${pkgs.jre}/bin/java \
+            -cp "${cfg.package}/lib/*:${configDir}" \
+            ${escapeShellArgs cfg.extraCmdLineOptions} \
+            -Dzookeeper.datadir.autocreate=false \
+            ${optionalString cfg.preferIPv4 "-Djava.net.preferIPv4Stack=true"} \
+            org.apache.zookeeper.server.quorum.QuorumPeerMain \
+            ${configDir}/zoo.cfg
+        '';
+        User = "zookeeper";
+      };
+      preStart = ''
+        echo "${toString cfg.id}" > ${cfg.dataDir}/myid
+        mkdir -p ${cfg.dataDir}/version-2
+      '';
+    };
+
+    users.users.zookeeper = {
+      isSystemUser = true;
+      group = "zookeeper";
+      description = "Zookeeper daemon user";
+      home = cfg.dataDir;
+    };
+    users.groups.zookeeper = {};
+  };
+}