about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/release-notes/rl-2305.section.md2
-rw-r--r--nixos/doc/manual/release-notes/rl-2405.section.md10
-rw-r--r--nixos/lib/testing/meta.nix2
-rw-r--r--nixos/modules/hardware/cpu/amd-ryzen-smu.nix26
-rw-r--r--nixos/modules/module-list.nix5
-rw-r--r--nixos/modules/programs/fzf.nix52
-rw-r--r--nixos/modules/programs/kubeswitch.nix56
-rw-r--r--nixos/modules/programs/lazygit.nix37
-rw-r--r--nixos/modules/programs/ryzen-monitor-ng.nix35
-rw-r--r--nixos/modules/services/backup/restic-rest-server.nix37
-rw-r--r--nixos/modules/services/databases/redis.nix5
-rw-r--r--nixos/modules/services/display-managers/default.nix3
-rw-r--r--nixos/modules/services/misc/paperless.nix35
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.nix1
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/nats.nix34
-rw-r--r--nixos/modules/services/networking/cloudflared.nix2
-rw-r--r--nixos/modules/services/networking/i2p.nix5
-rw-r--r--nixos/modules/services/web-apps/movim.nix711
-rw-r--r--nixos/modules/services/x11/desktop-managers/budgie.nix4
-rw-r--r--nixos/modules/services/x11/desktop-managers/cinnamon.nix10
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.nix5
-rw-r--r--nixos/tests/all-tests.nix2
-rw-r--r--nixos/tests/budgie.nix4
-rw-r--r--nixos/tests/cinnamon.nix4
-rw-r--r--nixos/tests/pantheon.nix7
-rw-r--r--nixos/tests/redis.nix117
-rw-r--r--nixos/tests/restic-rest-server.nix122
-rw-r--r--nixos/tests/unifi.nix4
-rw-r--r--nixos/tests/web-apps/movim/default.nix8
-rw-r--r--nixos/tests/web-apps/movim/standard.nix102
30 files changed, 1333 insertions, 114 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md
index 031442940b9e3..ce874a6e0b2d6 100644
--- a/nixos/doc/manual/release-notes/rl-2305.section.md
+++ b/nixos/doc/manual/release-notes/rl-2305.section.md
@@ -79,7 +79,7 @@ In addition to numerous new and updated packages, this release has the following
 
 - [frigate](https://frigate.video), an open source NVR built around real-time AI object detection. Available as [services.frigate](#opt-services.frigate.enable).
 
-- [fzf](https://github.com/junegunn/fzf), a command line fuzzyfinder. Available as [programs.fzf](#opt-programs.fzf.enable).
+- [fzf](https://github.com/junegunn/fzf), a command line fuzzyfinder. Available as [programs.fzf](#opt-programs.fzf.fuzzyCompletion).
 
 - [gemstash](https://github.com/rubygems/gemstash), a RubyGems.org cache and private gem server. Available as [services.gemstash](#opt-services.gemstash.enable).
 
diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md
index d36318c0ba706..8ee89e357cac5 100644
--- a/nixos/doc/manual/release-notes/rl-2405.section.md
+++ b/nixos/doc/manual/release-notes/rl-2405.section.md
@@ -90,6 +90,10 @@ Use `services.pipewire.extraConfig` or `services.pipewire.configPackages` for Pi
 
 - [maubot](https://github.com/maubot/maubot), a plugin-based Matrix bot framework. Available as [services.maubot](#opt-services.maubot.enable).
 
+- [ryzen-monitor-ng](https://github.com/mann1x/ryzen_monitor_ng), a desktop AMD CPU power monitor and controller, similar to Ryzen Master but for Linux. Available as [programs.ryzen-monitor-ng](#opt-programs.ryzen-monitor-ng.enable)
+
+- [ryzen-smu](https://gitlab.com/leogx9r/ryzen_smu), Linux kernel driver to expose the SMU (System Management Unit) for certain AMD Ryzen Processors. Includes the userspace program `monitor_cpu`. Available at [hardward.cpu.amd.ryzen-smu](#opt-hardware.cpu.amd.ryzen-smu.enable)
+
 - systemd's gateway, upload, and remote services, which provides ways of sending journals across the network. Enable using [services.journald.gateway](#opt-services.journald.gateway.enable), [services.journald.upload](#opt-services.journald.upload.enable), and [services.journald.remote](#opt-services.journald.remote.enable).
 
 - [GNS3](https://www.gns3.com/), a network software emulator. Available as [services.gns3-server](#opt-services.gns3-server.enable).
@@ -163,6 +167,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
 
 - [Uni-Sync](https://github.com/EightB1ts/uni-sync), a synchronization tool for Lian Li Uni Controllers. Available as [hardware.uni-sync](#opt-hardware.uni-sync.enable)
 
+- [prometheus-nats-exporter](https://github.com/nats-io/prometheus-nats-exporter), a Prometheus exporter for NATS. Available as [services.prometheus.exporters.nats](#opt-services.prometheus.exporters.nats.enable).
+
 ## Backward Incompatibilities {#sec-release-24.05-incompatibilities}
 
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
@@ -211,6 +217,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
 
 - `services.neo4j.allowUpgrade` was removed and no longer has any effect. Neo4j 5 supports automatic rolling upgrades.
 
+- `unifiLTS`, `unifi5` and `unifi6` have been removed, as they require MongoDB versions which are end-of-life. All these versions can be upgraded to `unifi7` directly.
+
 - `nitter` requires a `guest_accounts.jsonl` to be provided as a path or loaded into the default location at `/var/lib/nitter/guest_accounts.jsonl`. See [Guest Account Branch Deployment](https://github.com/zedeus/nitter/wiki/Guest-Account-Branch-Deployment) for details.
 
 - `boot.supportedFilesystems` and `boot.initrd.supportedFilesystems` are now attribute sets instead of lists. Assignment from lists as done previously is still supported, but checking whether a filesystem is enabled must now by done using `supportedFilesystems.fs or false` instead of using `lib.elem "fs" supportedFilesystems` as was done previously.
@@ -322,8 +330,6 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
 
 - `xxd` has been moved from `vim` default output to its own output to reduce closure size. The canonical way to reference it across all platforms is `unixtools.xxd`.
 
-- `programs.fzf.keybindings` and `programs.fzf.fuzzyCompletion` got replaced by `programs.fzf.enable` as shell-completion is included in the fzf-binary now there is no easy option to load completion and keybindings separately. Please consult fzf-documentation on how to configure/disable certain keybindings.
-
 - The `stalwart-mail` package has been updated to v0.5.3, which includes [breaking changes](https://github.com/stalwartlabs/mail-server/blob/v0.5.3/UPGRADING.md).
 
 - `services.zope2` has been removed as `zope2` is unmaintained and was relying on Python2.
diff --git a/nixos/lib/testing/meta.nix b/nixos/lib/testing/meta.nix
index 529fe714fcf6c..bdf313e5b119f 100644
--- a/nixos/lib/testing/meta.nix
+++ b/nixos/lib/testing/meta.nix
@@ -36,7 +36,7 @@ in
           };
           platforms = lib.mkOption {
             type = types.listOf types.raw;
-            default = lib.platforms.linux;
+            default = lib.platforms.linux ++ lib.platforms.darwin;
             description = ''
               Sets the [`meta.platforms`](https://nixos.org/manual/nixpkgs/stable/#var-meta-platforms) attribute on the [{option}`test`](#test-opt-test) derivation.
             '';
diff --git a/nixos/modules/hardware/cpu/amd-ryzen-smu.nix b/nixos/modules/hardware/cpu/amd-ryzen-smu.nix
new file mode 100644
index 0000000000000..b1a5895aaa24a
--- /dev/null
+++ b/nixos/modules/hardware/cpu/amd-ryzen-smu.nix
@@ -0,0 +1,26 @@
+{ config
+, lib
+, ...
+}:
+let
+  inherit (lib) mkEnableOption mkIf;
+  cfg = config.hardware.cpu.amd.ryzen-smu;
+  ryzen-smu = config.boot.kernelPackages.ryzen-smu;
+in
+{
+  options.hardware.cpu.amd.ryzen-smu = {
+    enable = mkEnableOption ''
+        ryzen_smu, a linux kernel driver that exposes access to the SMU (System Management Unit) for certain AMD Ryzen Processors.
+
+        WARNING: Damage cause by use of your AMD processor outside of official AMD specifications or outside of factory settings are not covered under any AMD product warranty and may not be covered by your board or system manufacturer's warranty
+      '';
+  };
+
+  config = mkIf cfg.enable {
+    boot.kernelModules = [ "ryzen-smu" ];
+    boot.extraModulePackages = [ ryzen-smu ];
+    environment.systemPackages = [ ryzen-smu ];
+  };
+
+  meta.maintainers = with lib.maintainers; [ Cryolitia phdyellow ];
+}
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 9fc036f9213a5..786f838bc6c6f 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -54,6 +54,7 @@
   ./hardware/corectrl.nix
   ./hardware/cpu/amd-microcode.nix
   ./hardware/cpu/amd-sev.nix
+  ./hardware/cpu/amd-ryzen-smu.nix
   ./hardware/cpu/intel-microcode.nix
   ./hardware/cpu/intel-sgx.nix
   ./hardware/cpu/x86-msr.nix
@@ -212,6 +213,8 @@
   ./programs/kbdlight.nix
   ./programs/kclock.nix
   ./programs/kdeconnect.nix
+  ./programs/lazygit.nix
+  ./programs/kubeswitch.nix
   ./programs/less.nix
   ./programs/liboping.nix
   ./programs/light.nix
@@ -251,6 +254,7 @@
   ./programs/regreet.nix
   ./programs/rog-control-center.nix
   ./programs/rust-motd.nix
+  ./programs/ryzen-monitor-ng.nix
   ./programs/screen.nix
   ./programs/seahorse.nix
   ./programs/sedutil.nix
@@ -1363,6 +1367,7 @@
   ./services/web-apps/miniflux.nix
   ./services/web-apps/monica.nix
   ./services/web-apps/moodle.nix
+  ./services/web-apps/movim.nix
   ./services/web-apps/netbox.nix
   ./services/web-apps/nextcloud.nix
   ./services/web-apps/nextcloud-notify_push.nix
diff --git a/nixos/modules/programs/fzf.nix b/nixos/modules/programs/fzf.nix
index 05e39c43c11b8..acc23d75df7b6 100644
--- a/nixos/modules/programs/fzf.nix
+++ b/nixos/modules/programs/fzf.nix
@@ -1,46 +1,38 @@
 { pkgs, config, lib, ... }:
 
-with lib;
-
 let
   cfg = config.programs.fzf;
-
 in
 {
-  imports = [
-    (lib.mkRemovedOptionModule [ "programs" "fzf" "keybindings" ] ''
-      Use "programs.fzf.enable" instead, due to fzf upstream-change it's not possible to load shell-completion and keybindings separately.
-      If you want to change/disable certain keybindings please check the fzf-documentation.
-    '')
-    (lib.mkRemovedOptionModule [ "programs" "fzf" "fuzzyCompletion" ] ''
-      Use "programs.fzf.enable" instead, due to fzf upstream-change it's not possible to load shell-completion and keybindings separately.
-      If you want to change/disable certain keybindings please check the fzf-documentation.
-    '')
-  ];
-
   options = {
-    programs.fzf.enable = mkEnableOption (mdDoc "fuzzy completion with fzf and keybindings");
+    programs.fzf = {
+      fuzzyCompletion = lib.mkEnableOption (lib.mdDoc "fuzzy completion with fzf");
+      keybindings = lib.mkEnableOption (lib.mdDoc "fzf keybindings");
+    };
   };
 
-  config = mkIf cfg.enable {
-    environment.systemPackages = [ pkgs.fzf ];
+  config = lib.mkIf (cfg.keybindings || cfg.fuzzyCompletion) {
+    environment.systemPackages = lib.mkIf (cfg.keybindings || cfg.fuzzyCompletion) [ pkgs.fzf ];
 
-    programs.bash.interactiveShellInit = ''
-      eval "$(${getExe pkgs.fzf} --bash)"
-    '';
-
-    programs.fish.interactiveShellInit = ''
-      ${getExe pkgs.fzf} --fish | source
-    '';
-
-    programs.zsh = {
-      interactiveShellInit = optionalString (!config.programs.zsh.ohMyZsh.enable) ''
-        eval "$(${getExe pkgs.fzf} --zsh)"
+    programs = {
+      bash.interactiveShellInit = lib.optionalString cfg.fuzzyCompletion ''
+        source ${pkgs.fzf}/share/fzf/completion.bash
+      '' + lib.optionalString cfg.keybindings ''
+        source ${pkgs.fzf}/share/fzf/key-bindings.bash
       '';
 
-      ohMyZsh.plugins = mkIf (config.programs.zsh.ohMyZsh.enable) [ "fzf" ];
+      zsh = {
+        interactiveShellInit = lib.optionalString (!config.programs.zsh.ohMyZsh.enable)
+        (lib.optionalString cfg.fuzzyCompletion ''
+          source ${pkgs.fzf}/share/fzf/completion.zsh
+        '' + lib.optionalString cfg.keybindings ''
+          source ${pkgs.fzf}/share/fzf/key-bindings.zsh
+        '');
+
+        ohMyZsh.plugins = lib.mkIf config.programs.zsh.ohMyZsh.enable [ "fzf" ];
+      };
     };
   };
 
-  meta.maintainers = with maintainers; [ laalsaas ];
+  meta.maintainers = with lib.maintainers; [ laalsaas ];
 }
diff --git a/nixos/modules/programs/kubeswitch.nix b/nixos/modules/programs/kubeswitch.nix
new file mode 100644
index 0000000000000..ba2d25fbeb455
--- /dev/null
+++ b/nixos/modules/programs/kubeswitch.nix
@@ -0,0 +1,56 @@
+{
+  config,
+  pkgs,
+  lib,
+  ...
+}:
+let
+  cfg = config.programs.kubeswitch;
+in
+{
+  options = {
+    programs.kubeswitch = {
+      enable = lib.mkEnableOption (lib.mdDoc "kubeswitch");
+
+      commandName = lib.mkOption {
+        type = lib.types.str;
+        default = "kswitch";
+        description = "The name of the command to use";
+      };
+
+      package = lib.mkOption {
+        type = lib.types.package;
+        default = pkgs.kubeswitch;
+        defaultText = lib.literalExpression "pkgs.kubeswitch";
+        description = "The package to install for kubeswitch";
+      };
+    };
+  };
+
+  config =
+    let
+      shell_files = pkgs.stdenv.mkDerivation rec {
+        name = "kubeswitch-shell-files";
+        phases = [ "installPhase" ];
+        installPhase = ''
+          mkdir -p $out/share
+          for shell in bash zsh; do
+            ${cfg.package}/bin/switcher init $shell | sed 's/switch(/${cfg.commandName}(/' > $out/share/${cfg.commandName}_init.$shell
+            ${cfg.package}/bin/switcher --cmd ${cfg.commandName} completion $shell > $out/share/${cfg.commandName}_completion.$shell
+          done
+        '';
+      };
+    in
+    lib.mkIf cfg.enable {
+      environment.systemPackages = [ cfg.package ];
+
+      programs.bash.interactiveShellInit = ''
+        source ${shell_files}/share/${cfg.commandName}_init.bash
+        source ${shell_files}/share/${cfg.commandName}_completion.bash
+      '';
+      programs.zsh.interactiveShellInit = ''
+        source ${shell_files}/share/${cfg.commandName}_init.zsh
+        source ${shell_files}/share/${cfg.commandName}_completion.zsh
+      '';
+    };
+}
diff --git a/nixos/modules/programs/lazygit.nix b/nixos/modules/programs/lazygit.nix
new file mode 100644
index 0000000000000..3e36a0e0c4a8f
--- /dev/null
+++ b/nixos/modules/programs/lazygit.nix
@@ -0,0 +1,37 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.programs.lazygit;
+
+  settingsFormat = pkgs.formats.yaml { };
+in
+{
+  options.programs.lazygit = {
+    enable = lib.mkEnableOption "lazygit, a simple terminal UI for git commands";
+
+    package = lib.mkPackageOption pkgs "lazygit" { };
+
+    settings = lib.mkOption {
+      inherit (settingsFormat) type;
+      default = { };
+      description = ''
+        Lazygit configuration.
+
+        See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md for documentation.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment = {
+      systemPackages = [ cfg.package ];
+      etc = lib.mkIf (cfg.settings != { }) {
+        "xdg/lazygit/config.yml".source = settingsFormat.generate "lazygit-config.yml" cfg.settings;
+      };
+    };
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ linsui ];
+  };
+}
diff --git a/nixos/modules/programs/ryzen-monitor-ng.nix b/nixos/modules/programs/ryzen-monitor-ng.nix
new file mode 100644
index 0000000000000..cb0c391ce6b15
--- /dev/null
+++ b/nixos/modules/programs/ryzen-monitor-ng.nix
@@ -0,0 +1,35 @@
+{ pkgs
+, config
+, lib
+, ...
+}:
+let
+  inherit (lib) mkEnableOption mkPackageOption mkIf;
+  cfg = config.programs.ryzen-monitor-ng;
+in
+{
+  options = {
+    programs.ryzen-monitor-ng = {
+      enable =  mkEnableOption ''
+        ryzen_monitor_ng, a userspace application for setting and getting Ryzen SMU (System Management Unit) parameters via the ryzen_smu kernel driver.
+
+        Monitor power information of Ryzen processors via the PM table of the SMU.
+
+        SMU Set and Get for many parameters and CO counts.
+
+        https://github.com/mann1x/ryzen_monitor_ng
+
+        WARNING: Damage cause by use of your AMD processor outside of official AMD specifications or outside of factory settings are not covered under any AMD product warranty and may not be covered by your board or system manufacturer's warranty
+      '';
+
+      package = mkPackageOption pkgs "ryzen-monitor-ng" {};
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    hardware.cpu.amd.ryzen-smu.enable = true;
+  };
+
+  meta.maintainers = with lib.maintainers; [ Cryolitia phdyellow ];
+}
diff --git a/nixos/modules/services/backup/restic-rest-server.nix b/nixos/modules/services/backup/restic-rest-server.nix
index 105a05caf3048..c9d5a37116a13 100644
--- a/nixos/modules/services/backup/restic-rest-server.nix
+++ b/nixos/modules/services/backup/restic-rest-server.nix
@@ -12,7 +12,7 @@ in
     enable = mkEnableOption (lib.mdDoc "Restic REST Server");
 
     listenAddress = mkOption {
-      default = ":8000";
+      default = "8000";
       example = "127.0.0.1:8080";
       type = types.str;
       description = lib.mdDoc "Listen on a specific IP address and port.";
@@ -61,14 +61,19 @@ in
   };
 
   config = mkIf cfg.enable {
+    assertions = [{
+      assertion = lib.substring 0 1 cfg.listenAddress != ":";
+      message = "The restic-rest-server now uses systemd socket activation, which expects only the Port number: services.restic.server.listenAddress = \"${lib.substring 1 6 cfg.listenAddress}\";";
+    }];
+
     systemd.services.restic-rest-server = {
       description = "Restic REST Server";
-      after = [ "network.target" ];
+      after = [ "network.target" "restic-rest-server.socket" ];
+      requires = [ "restic-rest-server.socket" ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
         ExecStart = ''
           ${cfg.package}/bin/rest-server \
-          --listen ${cfg.listenAddress} \
           --path ${cfg.dataDir} \
           ${optionalString cfg.appendOnly "--append-only"} \
           ${optionalString cfg.privateRepos "--private-repos"} \
@@ -80,16 +85,40 @@ in
         Group = "restic";
 
         # Security hardening
-        ReadWritePaths = [ cfg.dataDir ];
+        CapabilityBoundingSet = "";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateNetwork = true;
         PrivateTmp = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectProc = "invisible";
         ProtectSystem = "strict";
         ProtectKernelTunables = true;
         ProtectKernelModules = true;
         ProtectControlGroups = true;
         PrivateDevices = true;
+        ReadWritePaths = [ cfg.dataDir ];
+        RemoveIPC = true;
+        RestrictAddressFamilies = "none";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "@system-service";
+        UMask = 027;
       };
     };
 
+    systemd.sockets.restic-rest-server = {
+      listenStreams = [ cfg.listenAddress ];
+      wantedBy = [ "sockets.target" ];
+    };
+
     systemd.tmpfiles.rules = mkIf cfg.privateRepos [
         "f ${cfg.dataDir}/.htpasswd 0700 restic restic -"
     ];
diff --git a/nixos/modules/services/databases/redis.nix b/nixos/modules/services/databases/redis.nix
index 2e644895a2602..fe2d75fc53a96 100644
--- a/nixos/modules/services/databases/redis.nix
+++ b/nixos/modules/services/databases/redis.nix
@@ -338,7 +338,7 @@ in {
       after = [ "network.target" ];
 
       serviceConfig = {
-        ExecStart = "${cfg.package}/bin/redis-server /var/lib/${redisName name}/redis.conf ${escapeShellArgs conf.extraParams}";
+        ExecStart = "${cfg.package}/bin/${cfg.package.serverBin or "redis-server"} /var/lib/${redisName name}/redis.conf ${escapeShellArgs conf.extraParams}";
         ExecStartPre = "+"+pkgs.writeShellScript "${redisName name}-prep-conf" (let
           redisConfVar = "/var/lib/${redisName name}/redis.conf";
           redisConfRun = "/run/${redisName name}/nixos.conf";
@@ -391,7 +391,8 @@ in {
         RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
         RestrictNamespaces = true;
         LockPersonality = true;
-        MemoryDenyWriteExecute = true;
+        # we need to disable MemoryDenyWriteExecute for keydb
+        MemoryDenyWriteExecute = cfg.package.pname != "keydb";
         RestrictRealtime = true;
         RestrictSUIDSGID = true;
         PrivateMounts = true;
diff --git a/nixos/modules/services/display-managers/default.nix b/nixos/modules/services/display-managers/default.nix
index 7f5db9fbb509b..7e808d0d63833 100644
--- a/nixos/modules/services/display-managers/default.nix
+++ b/nixos/modules/services/display-managers/default.nix
@@ -173,11 +173,14 @@ in
   imports = [
     (lib.mkRenamedOptionModule [ "services" "xserver" "displayManager" "autoLogin" ] [ "services" "displayManager" "autoLogin" ])
     (lib.mkRenamedOptionModule [ "services" "xserver" "displayManager" "defaultSession" ] [ "services" "displayManager" "defaultSession" ])
+    (lib.mkRenamedOptionModule [ "services" "xserver" "displayManager" "hiddenUsers" ] [ "services" "displayManager" "hiddenUsers" ])
     (lib.mkRenamedOptionModule [ "services" "xserver" "displayManager" "job" "environment" ] [ "services" "displayManager" "environment" ])
     (lib.mkRenamedOptionModule [ "services" "xserver" "displayManager" "job" "execCmd" ] [ "services" "displayManager" "execCmd" ])
     (lib.mkRenamedOptionModule [ "services" "xserver" "displayManager" "job" "logToFile" ] [ "services" "displayManager" "logToFile" ])
     (lib.mkRenamedOptionModule [ "services" "xserver" "displayManager" "job" "logToJournal" ] [ "services" "displayManager" "logToJournal" ])
     (lib.mkRenamedOptionModule [ "services" "xserver" "displayManager" "job" "preStart" ] [ "services" "displayManager" "preStart" ])
+    (lib.mkRenamedOptionModule [ "services" "xserver" "displayManager" "sessionData" ] [ "services" "displayManager" "sessionData" ])
+    (lib.mkRenamedOptionModule [ "services" "xserver" "displayManager" "sessionPackages" ] [ "services" "displayManager" "sessionPackages" ])
   ];
 
   config = lib.mkIf cfg.enable {
diff --git a/nixos/modules/services/misc/paperless.nix b/nixos/modules/services/misc/paperless.nix
index 9301d1f687254..9a81fdde62af8 100644
--- a/nixos/modules/services/misc/paperless.nix
+++ b/nixos/modules/services/misc/paperless.nix
@@ -220,15 +220,16 @@ in
   config = mkIf cfg.enable {
     services.redis.servers.paperless.enable = mkIf enableRedis 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.tmpfiles.settings."10-paperless" = let
+      defaultRule = {
+        inherit (cfg) user;
+        inherit (config.users.users.${cfg.user}) group;
+      };
+    in {
+      "${cfg.dataDir}".d = defaultRule;
+      "${cfg.mediaDir}".d = defaultRule;
+      "${cfg.consumptionDir}".d = if cfg.consumptionDirIsPublic then { mode = "777"; } else defaultRule;
+    };
 
     systemd.services.paperless-scheduler = {
       description = "Paperless Celery Beat";
@@ -238,6 +239,7 @@ in
         User = cfg.user;
         ExecStart = "${pkg}/bin/celery --app paperless beat --loglevel INFO";
         Restart = "on-failure";
+        LoadCredential = lib.optionalString (cfg.passwordFile != null) "PAPERLESS_ADMIN_PASSWORD:${cfg.passwordFile}";
       };
       environment = env;
 
@@ -270,7 +272,7 @@ in
       ''
       + optionalString (cfg.passwordFile != null) ''
         export PAPERLESS_ADMIN_USER="''${PAPERLESS_ADMIN_USER:-admin}"
-        export PAPERLESS_ADMIN_PASSWORD=$(cat "${cfg.dataDir}/superuser-password")
+        export PAPERLESS_ADMIN_PASSWORD=$(cat $CREDENTIALS_DIRECTORY/PAPERLESS_ADMIN_PASSWORD)
         superuserState="$PAPERLESS_ADMIN_USER:$PAPERLESS_ADMIN_PASSWORD"
         superuserStateFile="${cfg.dataDir}/superuser-state"
 
@@ -298,19 +300,6 @@ in
       environment = env;
     };
 
-    # Reading the user-provided password file requires root access
-    systemd.services.paperless-copy-password = mkIf (cfg.passwordFile != null) {
-      requiredBy = [ "paperless-scheduler.service" ];
-      before = [ "paperless-scheduler.service" ];
-      serviceConfig = {
-        ExecStart = ''
-          ${pkgs.coreutils}/bin/install --mode 600 --owner '${cfg.user}' --compare \
-            '${cfg.passwordFile}' '${cfg.dataDir}/superuser-password'
-        '';
-        Type = "oneshot";
-      };
-    };
-
     systemd.services.paperless-consumer = {
       description = "Paperless document consumer";
       # Bind to `paperless-scheduler` so that the consumer never runs
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix
index 640c6c339cf62..0331a07b5109d 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters.nix
@@ -55,6 +55,7 @@ let
     "modemmanager"
     "mongodb"
     "mysqld"
+    "nats"
     "nextcloud"
     "nginx"
     "nginxlog"
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/nats.nix b/nixos/modules/services/monitoring/prometheus/exporters/nats.nix
new file mode 100644
index 0000000000000..83e60426f5ed2
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/nats.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, options, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.prometheus.exporters.nats;
+
+in
+{
+  port = 7777;
+
+  extraOpts = {
+    url = mkOption {
+      type = types.str;
+      default = "http://127.0.0.1:8222";
+      description = ''
+        NATS monitor endpoint to query.
+      '';
+    };
+  };
+
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-nats-exporter}/bin/prometheus-nats-exporter \
+          -addr ${cfg.listenAddress} \
+          -port ${toString cfg.port} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags} \
+          ${cfg.url}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/cloudflared.nix b/nixos/modules/services/networking/cloudflared.nix
index b9556bfa60d06..76db339a1831c 100644
--- a/nixos/modules/services/networking/cloudflared.nix
+++ b/nixos/modules/services/networking/cloudflared.nix
@@ -11,7 +11,7 @@ let
       default = null;
       example = "30s";
       description = lib.mdDoc ''
-        Timeout for establishing a new TCP connection to your origin server. This excludes the time taken to establish TLS, which is controlled by [https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/local-management/ingress/#tlstimeout](tlsTimeout).
+        Timeout for establishing a new TCP connection to your origin server. This excludes the time taken to establish TLS, which is controlled by [tlsTimeout](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/local-management/ingress/#tlstimeout).
       '';
     };
 
diff --git a/nixos/modules/services/networking/i2p.nix b/nixos/modules/services/networking/i2p.nix
index c5c7a955cbd4f..5c6c08831a43d 100644
--- a/nixos/modules/services/networking/i2p.nix
+++ b/nixos/modules/services/networking/i2p.nix
@@ -5,7 +5,8 @@ with lib;
 let
   cfg = config.services.i2p;
   homeDir = "/var/lib/i2p";
-in {
+in
+{
   ###### interface
   options.services.i2p.enable = mkEnableOption (lib.mdDoc "I2P router");
 
@@ -27,7 +28,7 @@ in {
         User = "i2p";
         WorkingDirectory = homeDir;
         Restart = "on-abort";
-        ExecStart = "${pkgs.i2p}/bin/i2prouter-plain";
+        ExecStart = "${pkgs.i2p}/bin/i2prouter";
       };
     };
   };
diff --git a/nixos/modules/services/web-apps/movim.nix b/nixos/modules/services/web-apps/movim.nix
new file mode 100644
index 0000000000000..bb88a185b4618
--- /dev/null
+++ b/nixos/modules/services/web-apps/movim.nix
@@ -0,0 +1,711 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib)
+    filterAttrsRecursive
+    generators
+    literalExpression
+    mkDefault
+    mkIf
+    mkOption
+    mkEnableOption
+    mkPackageOption
+    mkMerge
+    pipe
+    types
+    ;
+
+  cfg = config.services.movim;
+
+  defaultPHPCfg = {
+    "output_buffering" = 0;
+    "error_reporting" = "E_ALL & ~E_DEPRECATED & ~E_STRICT";
+    "opcache.enable_cli" = 1;
+    "opcache.interned_strings_buffer" = 8;
+    "opcache.max_accelerated_files" = 6144;
+    "opcache.memory_consumption" = 128;
+    "opcache.revalidate_freq" = 2;
+    "opcache.fast_shutdown" = 1;
+  };
+
+  phpCfg = generators.toKeyValue
+    { mkKeyValue = generators.mkKeyValueDefault { } " = "; }
+    (defaultPHPCfg // cfg.phpCfg);
+
+  podConfigFlags =
+    let
+      bevalue = a: lib.escapeShellArg (generators.mkValueStringDefault { } a);
+    in
+    lib.concatStringsSep " "
+      (lib.attrsets.foldlAttrs
+        (acc: k: v: acc ++ lib.optional (v != null) "--${k}=${bevalue v}")
+        [ ]
+        cfg.podConfig);
+
+  package =
+    let
+      p = cfg.package.override
+        ({
+          inherit phpCfg;
+          withPgsql = cfg.database.type == "pgsql";
+          withMysql = cfg.database.type == "mysql";
+          inherit (cfg) minifyStaticFiles;
+        } // lib.optionalAttrs (lib.isAttrs cfg.minifyStaticFiles) (with cfg.minifyStaticFiles; {
+          esbuild = esbuild.package;
+          lightningcss = lightningcss.package;
+          scour = scour.package;
+        }));
+    in
+    p.overrideAttrs (finalAttrs: prevAttrs:
+      let
+        appDir = "$out/share/php/${finalAttrs.pname}";
+
+        stateDirectories = ''
+          # Symlinking in our state directories
+          rm -rf $out/.env $out/cache ${appDir}/public/cache
+          ln -s ${cfg.dataDir}/.env ${appDir}/.env
+          ln -s ${cfg.dataDir}/public/cache ${appDir}/public/cache
+          ln -s ${cfg.logDir} ${appDir}/log
+          ln -s ${cfg.runtimeDir}/cache ${appDir}/cache
+        '';
+
+        exposeComposer = ''
+          # Expose PHP Composer for scripts
+          mkdir -p $out/bin
+          echo "#!${lib.getExe pkgs.dash}" > $out/bin/movim-composer
+          echo "${finalAttrs.php.packages.composer}/bin/composer --working-dir="${appDir}" \"\$@\"" >> $out/bin/movim-composer
+          chmod +x $out/bin/movim-composer
+        '';
+
+        podConfigInputDisableReplace = lib.optionalString (podConfigFlags != "")
+          (lib.concatStringsSep "\n"
+            (lib.attrsets.foldlAttrs
+              (acc: k: v:
+                acc ++ lib.optional (v != null)
+                  # Disable all Admin panel options that were set in the
+                  # `cfg.podConfig` to prevent confusing situtions where the
+                  # values are rewritten on server reboot
+                  ''
+                    substituteInPlace ${appDir}/app/widgets/AdminMain/adminmain.tpl \
+                      --replace-warn 'name="${k}"' 'name="${k}" disabled'
+                  '')
+              [ ]
+              cfg.podConfig));
+
+        precompressStaticFilesJobs =
+          let
+            inherit (cfg.precompressStaticFiles) brotli gzip;
+
+            findTextFileNames = lib.concatStringsSep " -o "
+              (builtins.map (n: ''-iname "*.${n}"'')
+                [ "css" "ini" "js" "json" "manifest" "mjs" "svg" "webmanifest" ]);
+          in
+          lib.concatStringsSep "\n" [
+            (lib.optionalString brotli.enable ''
+              echo -n "Precompressing static files with Brotli …"
+              find ${appDir}/public -type f ${findTextFileNames} \
+                | ${lib.getExe pkgs.parallel} ${lib.escapeShellArgs [
+                    "--will-cite"
+                    "-j $NIX_BUILD_CORES"
+                    "${lib.getExe brotli.package} --keep --quality=${builtins.toString brotli.compressionLevel} --output={}.br {}"
+                   ]}
+              echo " done."
+            '')
+            (lib.optionalString gzip.enable ''
+              echo -n "Precompressing static files with Gzip …"
+              find ${appDir}/public -type f ${findTextFileNames} \
+                | ${lib.getExe pkgs.parallel} ${lib.escapeShellArgs [
+                    "--will-cite"
+                    "-j $NIX_BUILD_CORES"
+                    "${lib.getExe gzip.package} -c -${builtins.toString gzip.compressionLevel} {} > {}.gz"
+                   ]}
+              echo " done."
+            '')
+          ];
+      in
+      {
+        postInstall = lib.concatStringsSep "\n\n" [
+          prevAttrs.postInstall
+          stateDirectories
+          exposeComposer
+          podConfigInputDisableReplace
+          precompressStaticFilesJobs
+        ];
+      });
+
+  configFile = pipe cfg.settings [
+    (filterAttrsRecursive (_: v: v != null))
+    (generators.toKeyValue { })
+    (pkgs.writeText "movim-env")
+  ];
+
+  pool = "movim";
+  fpm = config.services.phpfpm.pools.${pool};
+  phpExecutionUnit = "phpfpm-${pool}";
+
+  dbService = {
+    "postgresql" = "postgresql.service";
+    "mysql" = "mysql.service";
+  }.${cfg.database.type};
+in
+{
+  options.services = {
+    movim = {
+      enable = mkEnableOption "a Movim instance";
+      package = mkPackageOption pkgs "movim" { };
+      phpPackage = mkPackageOption pkgs "php" { };
+
+      phpCfg = mkOption {
+        type = with types; attrsOf (oneOf [ int str bool ]);
+        defaultText = literalExpression (generators.toPretty { } defaultPHPCfg);
+        default = { };
+        description = "Extra PHP INI options such as `memory_limit`, `max_execution_time`, etc.";
+      };
+
+      user = mkOption {
+        type = types.nonEmptyStr;
+        default = "movim";
+        description = "User running Movim service";
+      };
+
+      group = mkOption {
+        type = types.nonEmptyStr;
+        default = "movim";
+        description = "Group running Movim service";
+      };
+
+      dataDir = mkOption {
+        type = types.nonEmptyStr;
+        default = "/var/lib/movim";
+        description = "State directory of the `movim` user which holds the application’s state & data.";
+      };
+
+      logDir = mkOption {
+        type = types.nonEmptyStr;
+        default = "/var/log/movim";
+        description = "Log directory of the `movim` user which holds the application’s logs.";
+      };
+
+      runtimeDir = mkOption {
+        type = types.nonEmptyStr;
+        default = "/run/movim";
+        description = "Runtime directory of the `movim` user which holds the application’s caches & temporary files.";
+      };
+
+      domain = mkOption {
+        type = types.nonEmptyStr;
+        description = "Fully-qualified domain name (FQDN) for the Movim instance.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 8080;
+        description = "Movim daemon port.";
+      };
+
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Debugging logs.";
+      };
+
+      verbose = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Verbose logs.";
+      };
+
+      minifyStaticFiles = mkOption {
+        type = with types; either bool (submodule {
+          options = {
+            script = mkOption {
+              type = types.submodule {
+                options = {
+                  enable = mkEnableOption "Script minification";
+                  package = mkPackageOption pkgs "esbuild" { };
+                  target = mkOption {
+                    type = with types; nullOr nonEmptyStr;
+                    default = null;
+                  };
+                };
+              };
+            };
+            style = mkOption {
+              type = types.submodule {
+                options = {
+                  enable = mkEnableOption "Script minification";
+                  package = mkPackageOption pkgs "lightningcss" { };
+                  target = mkOption {
+                    type = with types; nullOr nonEmptyStr;
+                    default = null;
+                  };
+                };
+              };
+            };
+            svg = mkOption {
+              type = types.submodule {
+                options = {
+                  enable = mkEnableOption "SVG minification";
+                  package = mkPackageOption pkgs "scour" { };
+                };
+              };
+            };
+          };
+        });
+        default = true;
+        description = "Do minification on public static files";
+      };
+
+      precompressStaticFiles = mkOption {
+        type = with types; submodule {
+          options = {
+            brotli = {
+              enable = mkEnableOption "Brotli precompression";
+              package = mkPackageOption pkgs "brotli" { };
+              compressionLevel = mkOption {
+                type = types.ints.between 0 11;
+                default = 11;
+                description = "Brotli compression level";
+              };
+            };
+            gzip = {
+              enable = mkEnableOption "Gzip precompression";
+              package = mkPackageOption pkgs "gzip" { };
+              compressionLevel = mkOption {
+                type = types.ints.between 1 9;
+                default = 9;
+                description = "Gzip compression level";
+              };
+            };
+          };
+        };
+        default = {
+          brotli.enable = true;
+          gzip.enable = false;
+        };
+        description = "Aggressively precompress static files";
+      };
+
+      podConfig = mkOption {
+        type = types.submodule {
+          options = {
+            info = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = "Content of the info box on the login page";
+            };
+
+            description = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = "General description of the instance";
+            };
+
+            timezone = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = "The server timezone";
+            };
+
+            restrictsuggestions = mkOption {
+              type = with types; nullOr bool;
+              default = null;
+              description = "Only suggest chatrooms, Communities and other contents that are available on the user XMPP server and related services";
+            };
+
+            chatonly = mkOption {
+              type = with types; nullOr bool;
+              default = null;
+              description = "Disable all the social feature (Communities, Blog…) and keep only the chat ones";
+            };
+
+            disableregistration = mkOption {
+              type = with types; nullOr bool;
+              default = null;
+              description = "Remove the XMPP registration flow and buttons from the interface";
+            };
+
+            loglevel = mkOption {
+              type = with types; nullOr (ints.between 0 3);
+              default = null;
+              description = "The server loglevel";
+            };
+
+            locale = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = "The server main locale";
+            };
+
+            xmppdomain = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = "The default XMPP server domain";
+            };
+
+            xmppdescription = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = "The default XMPP server description";
+            };
+
+            xmppwhitelist = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = "The allowlisted XMPP servers";
+            };
+          };
+        };
+        default = { };
+        description = ''
+          Pod configuration (values from `php daemon.php config --help`).
+          Note that these values will now be disabled in the admin panel.
+        '';
+      };
+
+      settings = mkOption {
+        type = with types; attrsOf (nullOr (oneOf [ int str bool ]));
+        default = { };
+        description = ".env settings for Movim. Secrets should use `secretFile` option instead. `null`s will be culled.";
+      };
+
+      secretFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        description = "The secret file to be sourced for the .env settings.";
+      };
+
+      database = {
+        type = mkOption {
+          type = types.enum [ "mysql" "postgresql" ];
+          example = "mysql";
+          default = "postgresql";
+          description = "Database engine to use.";
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "movim";
+          description = "Database name.";
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "movim";
+          description = "Database username.";
+        };
+
+        createLocally = mkOption {
+          type = types.bool;
+          default = true;
+          description = "local database using UNIX socket authentication";
+        };
+      };
+
+      nginx = mkOption {
+        type = with types; nullOr (submodule
+          (import ../web-servers/nginx/vhost-options.nix {
+            inherit config lib;
+          }));
+        default = null;
+        example = lib.literalExpression /* nginx */ ''
+          {
+            serverAliases = [
+              "pics.''${config.networking.domain}"
+            ];
+            enableACME = true;
+            forceHttps = true;
+          }
+        '';
+        description = ''
+          With this option, you can customize an nginx virtual host which already has sensible defaults for Movim.
+          Set to `{ }` if you do not need any customization to the virtual host.
+          If enabled, then by default, the {option}`serverName` is `''${domain}`,
+          If this is set to null (the default), no nginx virtualHost will be configured.
+        '';
+      };
+
+      poolConfig = mkOption {
+        type = with types; attrsOf (oneOf [ int str bool ]);
+        default = { };
+        description = "Options for Movim’s PHP-FPM pool.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    users = {
+      users = {
+        movim = mkIf (cfg.user == "movim") {
+          isSystemUser = true;
+          group = cfg.group;
+        };
+        "${config.services.nginx.user}".extraGroups = [ cfg.group ];
+      };
+      groups = {
+        ${cfg.group} = { };
+      };
+    };
+
+    services = {
+      movim = {
+        settings = mkMerge [
+          {
+            DAEMON_URL = "//${cfg.domain}";
+            DAEMON_PORT = cfg.port;
+            DAEMON_INTERFACE = "127.0.0.1";
+            DAEMON_DEBUG = cfg.debug;
+            DAEMON_VERBOSE = cfg.verbose;
+          }
+          (mkIf cfg.database.createLocally {
+            DB_DRIVER = {
+              "postgresql" = "pgsql";
+              "mysql" = "mysql";
+            }.${cfg.database.type};
+            DB_HOST = "localhost";
+            DB_PORT = config.services.${cfg.database.type}.settings.port;
+            DB_DATABASE = cfg.database.name;
+            DB_USERNAME = cfg.database.user;
+            DB_PASSWORD = "";
+          })
+        ];
+
+        poolConfig = lib.mapAttrs' (n: v: lib.nameValuePair n (lib.mkDefault v)) {
+          "pm" = "dynamic";
+          "php_admin_value[error_log]" = "stderr";
+          "php_admin_flag[log_errors]" = true;
+          "catch_workers_output" = true;
+          "pm.max_children" = 32;
+          "pm.start_servers" = 2;
+          "pm.min_spare_servers" = 2;
+          "pm.max_spare_servers" = 8;
+          "pm.max_requests" = 500;
+        };
+      };
+
+      nginx = mkIf (cfg.nginx != null) {
+        enable = true;
+        recommendedOptimisation = true;
+        recommendedGzipSettings = true;
+        recommendedBrotliSettings = true;
+        recommendedProxySettings = true;
+        # TODO: recommended cache options already in Nginx⁇
+        appendHttpConfig = /* nginx */ ''
+          fastcgi_cache_path /tmp/nginx_cache levels=1:2 keys_zone=nginx_cache:100m inactive=60m;
+          fastcgi_cache_key "$scheme$request_method$host$request_uri";
+        '';
+        virtualHosts."${cfg.domain}" = mkMerge [
+          cfg.nginx
+          {
+            root = lib.mkForce "${package}/share/php/movim/public";
+            locations = {
+              "/favicon.ico" = {
+                priority = 100;
+                extraConfig = /* nginx */ ''
+                  access_log off;
+                  log_not_found off;
+                '';
+              };
+              "/robots.txt" = {
+                priority = 100;
+                extraConfig = /* nginx */ ''
+                  access_log off;
+                  log_not_found off;
+                '';
+              };
+              "~ /\\.(?!well-known).*" = {
+                priority = 210;
+                extraConfig = /* nginx */ ''
+                  deny all;
+                '';
+              };
+              # Ask nginx to cache every URL starting with "/picture"
+              "/picture" = {
+                priority = 400;
+                tryFiles = "$uri $uri/ /index.php$is_args$args";
+                extraConfig = /* nginx */ ''
+                  set $no_cache 0; # Enable cache only there
+                '';
+              };
+              "/" = {
+                priority = 490;
+                tryFiles = "$uri $uri/ /index.php$is_args$args";
+                extraConfig = /* nginx */ ''
+                  # https://github.com/movim/movim/issues/314
+                  add_header Content-Security-Policy "default-src 'self'; img-src 'self' aesgcm: https:; media-src 'self' aesgcm: https:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';";
+                  set $no_cache 1;
+                '';
+              };
+              "~ \\.php$" = {
+                priority = 500;
+                tryFiles = "$uri =404";
+                extraConfig = /* nginx */ ''
+                  include ${config.services.nginx.package}/conf/fastcgi.conf;
+                  add_header X-Cache $upstream_cache_status;
+                  fastcgi_ignore_headers "Cache-Control" "Expires" "Set-Cookie";
+                  fastcgi_cache nginx_cache;
+                  fastcgi_cache_valid any 7d;
+                  fastcgi_cache_bypass $no_cache;
+                  fastcgi_no_cache $no_cache;
+                  fastcgi_split_path_info ^(.+\.php)(/.+)$;
+                  fastcgi_index index.php;
+                  fastcgi_pass unix:${fpm.socket};
+                '';
+              };
+              "/ws/" = {
+                priority = 900;
+                proxyPass = "http://${cfg.settings.DAEMON_INTERFACE}:${builtins.toString cfg.port}/";
+                proxyWebsockets = true;
+                recommendedProxySettings = true;
+                extraConfig = /* nginx */ ''
+                  proxy_set_header X-Forwarded-Proto $scheme;
+                  proxy_redirect off;
+                '';
+              };
+            };
+            extraConfig = /* ngnix */ ''
+              index index.php;
+            '';
+          }
+        ];
+      };
+
+      mysql = mkIf (cfg.database.createLocally && cfg.database.type == "mysql") {
+        enable = mkDefault true;
+        package = mkDefault pkgs.mariadb;
+        ensureDatabases = [ cfg.database.name ];
+        ensureUsers = [{
+          name = cfg.user;
+          ensureDBOwnership = true;
+        }];
+      };
+
+      postgresql = mkIf (cfg.database.createLocally && cfg.database.type == "postgresql") {
+        enable = mkDefault true;
+        ensureDatabases = [ cfg.database.name ];
+        ensureUsers = [{
+          name = cfg.user;
+          ensureDBOwnership = true;
+        }];
+        authentication = ''
+          host ${cfg.database.name} ${cfg.database.user} localhost trust
+        '';
+      };
+
+      phpfpm.pools.${pool} =
+        let
+          socketOwner =
+            if (cfg.nginx != null)
+            then config.services.nginx.user
+            else cfg.user;
+        in
+        {
+          phpPackage = package.php;
+          user = cfg.user;
+          group = cfg.group;
+
+          phpOptions = ''
+            error_log = 'stderr'
+            log_errors = on
+          '';
+
+          settings = {
+            "listen.owner" = socketOwner;
+            "listen.group" = cfg.group;
+            "listen.mode" = "0660";
+            "catch_workers_output" = true;
+          } // cfg.poolConfig;
+        };
+    };
+
+    systemd = {
+      services.movim-data-setup = {
+        description = "Movim setup: .env file, databases init, cache reload";
+        wantedBy = [ "multi-user.target" ];
+        requiredBy = [ "${phpExecutionUnit}.service" ];
+        before = [ "${phpExecutionUnit}.service" ];
+        after = lib.optional cfg.database.createLocally dbService;
+        requires = lib.optional cfg.database.createLocally dbService;
+
+        serviceConfig = {
+          Type = "oneshot";
+          User = cfg.user;
+          Group = cfg.group;
+          UMask = "077";
+        } // lib.optionalAttrs (cfg.secretFile != null) {
+          LoadCredential = "env-secrets:${cfg.secretFile}";
+        };
+
+        script = ''
+          # Env vars
+          rm -f ${cfg.dataDir}/.env
+          cp --no-preserve=all ${configFile} ${cfg.dataDir}/.env
+          echo -e '\n' >> ${cfg.dataDir}/.env
+          if [[ -f "$CREDENTIALS_DIRECTORY/env-secrets"  ]]; then
+            cat "$CREDENTIALS_DIRECTORY/env-secrets" >> ${cfg.dataDir}/.env
+            echo -e '\n' >> ${cfg.dataDir}/.env
+          fi
+
+          # Caches, logs
+          mkdir -p ${cfg.dataDir}/public/cache ${cfg.logDir} ${cfg.runtimeDir}/cache
+          chmod -R ug+rw ${cfg.dataDir}/public/cache
+          chmod -R ug+rw ${cfg.logDir}
+          chmod -R ug+rwx ${cfg.runtimeDir}/cache
+
+          # Migrations
+          MOVIM_VERSION="${package.version}"
+          if [[ ! -f "${cfg.dataDir}/.migration-version" ]] || [[ "$MOVIM_VERSION" != "$(<${cfg.dataDir}/.migration-version)" ]]; then
+            ${package}/bin/movim-composer movim:migrate && echo $MOVIM_VERSION > ${cfg.dataDir}/.migration-version
+          fi
+        ''
+        + lib.optionalString (podConfigFlags != "") (
+          let
+            flags = lib.concatStringsSep " "
+              ([ "--no-interaction" ]
+                ++ lib.optional cfg.debug "-vvv"
+                ++ lib.optional (!cfg.debug && cfg.verbose) "-v");
+          in
+          ''
+            ${lib.getExe package} config ${podConfigFlags}
+          ''
+        );
+      };
+
+      services.movim = {
+        description = "Movim daemon";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "movim-data-setup.service" ];
+        requires = [ "movim-data-setup.service" ]
+          ++ lib.optional cfg.database.createLocally dbService;
+        environment = {
+          PUBLIC_URL = "//${cfg.domain}";
+          WS_PORT = builtins.toString cfg.port;
+        };
+
+        serviceConfig = {
+          User = cfg.user;
+          Group = cfg.group;
+          WorkingDirectory = "${package}/share/php/movim";
+          ExecStart = "${lib.getExe package} start";
+        };
+      };
+
+      services.${phpExecutionUnit} = {
+        after = [ "movim-data-setup.service" ];
+        requires = [ "movim-data-setup.service" ]
+          ++ lib.optional cfg.database.createLocally dbService;
+      };
+
+      tmpfiles.settings."10-movim" = with cfg; {
+        "${dataDir}".d = { inherit user group; mode = "0710"; };
+        "${dataDir}/public".d = { inherit user group; mode = "0750"; };
+        "${dataDir}/public/cache".d = { inherit user group; mode = "0750"; };
+        "${runtimeDir}".d = { inherit user group; mode = "0700"; };
+        "${runtimeDir}/cache".d = { inherit user group; mode = "0700"; };
+        "${logDir}".d = { inherit user group; mode = "0700"; };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/x11/desktop-managers/budgie.nix b/nixos/modules/services/x11/desktop-managers/budgie.nix
index a911db725c014..d6e6bb2fa14e8 100644
--- a/nixos/modules/services/x11/desktop-managers/budgie.nix
+++ b/nixos/modules/services/x11/desktop-managers/budgie.nix
@@ -43,6 +43,8 @@ let
   budgie-control-center = pkgs.budgie.budgie-control-center.override {
     enableSshSocket = config.services.openssh.startWhenNeeded;
   };
+
+  notExcluded = pkg: (!(lib.elem pkg config.environment.budgie.excludePackages));
 in {
   meta.maintainers = lib.teams.budgie.members;
 
@@ -160,7 +162,7 @@ in {
       ++ cfg.sessionPath;
 
     # Both budgie-desktop-view and nemo defaults to this emulator.
-    programs.gnome-terminal.enable = mkDefault true;
+    programs.gnome-terminal.enable = mkDefault (notExcluded pkgs.gnome.gnome-terminal);
 
     # Fonts.
     fonts.packages = [
diff --git a/nixos/modules/services/x11/desktop-managers/cinnamon.nix b/nixos/modules/services/x11/desktop-managers/cinnamon.nix
index 935f173a9d81c..6983b51376fd4 100644
--- a/nixos/modules/services/x11/desktop-managers/cinnamon.nix
+++ b/nixos/modules/services/x11/desktop-managers/cinnamon.nix
@@ -95,7 +95,7 @@ in
       '';
 
       # Default services
-      services.blueman.enable = mkDefault true;
+      services.blueman.enable = mkDefault (notExcluded pkgs.blueman);
       hardware.bluetooth.enable = mkDefault true;
       hardware.pulseaudio.enable = mkDefault true;
       security.polkit.enable = true;
@@ -228,10 +228,10 @@ in
     })
 
     (mkIf serviceCfg.apps.enable {
-      programs.geary.enable = mkDefault true;
-      programs.gnome-disks.enable = mkDefault true;
-      programs.gnome-terminal.enable = mkDefault true;
-      programs.file-roller.enable = mkDefault true;
+      programs.geary.enable = mkDefault (notExcluded pkgs.gnome.geary);
+      programs.gnome-disks.enable = mkDefault (notExcluded pkgs.gnome.gnome-disk-utility);
+      programs.gnome-terminal.enable = mkDefault (notExcluded pkgs.gnome.gnome-terminal);
+      programs.file-roller.enable = mkDefault (notExcluded pkgs.gnome.file-roller);
 
       environment.systemPackages = with pkgs // pkgs.gnome // pkgs.cinnamon; utils.removePackagesByName [
         # cinnamon team apps
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.nix b/nixos/modules/services/x11/desktop-managers/pantheon.nix
index 695d81f666a10..2115f8f0ab235 100644
--- a/nixos/modules/services/x11/desktop-managers/pantheon.nix
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.nix
@@ -12,6 +12,7 @@ let
     extraGSettingsOverrides = cfg.extraGSettingsOverrides;
   };
 
+  notExcluded = pkg: (!(lib.elem pkg config.environment.pantheon.excludePackages));
 in
 
 {
@@ -288,8 +289,8 @@ in
     })
 
     (mkIf serviceCfg.apps.enable {
-      programs.evince.enable = mkDefault true;
-      programs.file-roller.enable = mkDefault true;
+      programs.evince.enable = mkDefault (notExcluded pkgs.gnome.evince);
+      programs.file-roller.enable = mkDefault (notExcluded pkgs.gnome.file-roller);
 
       environment.systemPackages = utils.removePackagesByName ([
         pkgs.gnome.gnome-font-viewer
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 6f78d68730c91..909eea38b35e0 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -558,6 +558,7 @@ in {
   morty = handleTest ./morty.nix {};
   mosquitto = handleTest ./mosquitto.nix {};
   moosefs = handleTest ./moosefs.nix {};
+  movim = discoverTests (import ./web-apps/movim { inherit handleTestOn; });
   mpd = handleTest ./mpd.nix {};
   mpv = handleTest ./mpv.nix {};
   mtp = handleTest ./mtp.nix {};
@@ -777,6 +778,7 @@ in {
   redis = handleTest ./redis.nix {};
   redmine = handleTest ./redmine.nix {};
   restartByActivationScript = handleTest ./restart-by-activation-script.nix {};
+  restic-rest-server = handleTest ./restic-rest-server.nix {};
   restic = handleTest ./restic.nix {};
   retroarch = handleTest ./retroarch.nix {};
   rkvm = handleTest ./rkvm {};
diff --git a/nixos/tests/budgie.nix b/nixos/tests/budgie.nix
index 5228e869b056a..203e718c8c6d9 100644
--- a/nixos/tests/budgie.nix
+++ b/nixos/tests/budgie.nix
@@ -18,6 +18,10 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
       };
     };
 
+    # We don't ship gnome-text-editor in Budgie module, we add this line mainly
+    # to catch eval issues related to this option.
+    environment.budgie.excludePackages = [ pkgs.gnome-text-editor ];
+
     services.xserver.desktopManager.budgie = {
       enable = true;
       extraPlugins = [
diff --git a/nixos/tests/cinnamon.nix b/nixos/tests/cinnamon.nix
index eab907d0b712c..694308152149b 100644
--- a/nixos/tests/cinnamon.nix
+++ b/nixos/tests/cinnamon.nix
@@ -8,6 +8,10 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     services.xserver.enable = true;
     services.xserver.desktopManager.cinnamon.enable = true;
 
+    # We don't ship gnome-text-editor in Cinnamon module, we add this line mainly
+    # to catch eval issues related to this option.
+    environment.cinnamon.excludePackages = [ pkgs.gnome-text-editor ];
+
     # For the sessionPath subtest.
     services.xserver.desktopManager.cinnamon.sessionPath = [ pkgs.gnome.gpaste ];
   };
diff --git a/nixos/tests/pantheon.nix b/nixos/tests/pantheon.nix
index f8de2eb8061d1..d2a4a009af53d 100644
--- a/nixos/tests/pantheon.nix
+++ b/nixos/tests/pantheon.nix
@@ -13,6 +13,13 @@ import ./make-test-python.nix ({ pkgs, lib, ...} :
     services.xserver.enable = true;
     services.xserver.desktopManager.pantheon.enable = true;
 
+    # We ship pantheon.appcenter by default when this is enabled.
+    services.flatpak.enable = true;
+
+    # We don't ship gnome-text-editor in Pantheon module, we add this line mainly
+    # to catch eval issues related to this option.
+    environment.pantheon.excludePackages = [ pkgs.gnome-text-editor ];
+
     environment.systemPackages = [ pkgs.xdotool ];
   };
 
diff --git a/nixos/tests/redis.nix b/nixos/tests/redis.nix
index 94b50d07be6dc..6c84701c9c0a9 100644
--- a/nixos/tests/redis.nix
+++ b/nixos/tests/redis.nix
@@ -1,44 +1,87 @@
-import ./make-test-python.nix ({ pkgs, lib, ... }:
 {
-  name = "redis";
-  meta.maintainers = with lib.maintainers; [ flokli ];
-
-  nodes = {
-    machine =
-      { pkgs, lib, ... }:
-
-      {
-        services.redis.servers."".enable = true;
-        services.redis.servers."test".enable = true;
-
-        users.users = lib.listToAttrs (map (suffix: lib.nameValuePair "member${suffix}" {
-          createHome = false;
-          description = "A member of the redis${suffix} group";
-          isNormalUser = true;
-          extraGroups = [ "redis${suffix}" ];
-        }) ["" "-test"]);
-      };
+  system ? builtins.currentSystem,
+  config ? { },
+  pkgs ? import ../../.. { inherit system config; },
+
+  lib ? pkgs.lib,
+}:
+let
+  makeTest = import ./make-test-python.nix;
+  mkTestName =
+    pkg: "${pkg.pname}_${builtins.replaceStrings [ "." ] [ "" ] (lib.versions.majorMinor pkg.version)}";
+  redisPackages = {
+    inherit (pkgs) redis keydb;
   };
+  makeRedisTest =
+    {
+      package,
+      name ? mkTestName package,
+    }:
+    makeTest {
+      inherit name;
+      meta.maintainers = [
+        lib.maintainers.flokli
+        lib.teams.helsinki-systems.members
+      ];
+
+      nodes = {
+        machine =
+          { lib, ... }:
+
+          {
+            services = {
+              redis = {
+                inherit package;
+                servers."".enable = true;
+                servers."test".enable = true;
+              };
+            };
+
+            users.users = lib.listToAttrs (
+              map
+                (
+                  suffix:
+                  lib.nameValuePair "member${suffix}" {
+                    createHome = false;
+                    description = "A member of the redis${suffix} group";
+                    isNormalUser = true;
+                    extraGroups = [ "redis${suffix}" ];
+                  }
+                )
+                [
+                  ""
+                  "-test"
+                ]
+            );
+          };
+      };
 
-  testScript = { nodes, ... }: let
-    inherit (nodes.machine.config.services) redis;
-    in ''
-    start_all()
-    machine.wait_for_unit("redis")
-    machine.wait_for_unit("redis-test")
+      testScript =
+        { nodes, ... }:
+        let
+          inherit (nodes.machine.services) redis;
+        in
+        ''
+          start_all()
+          machine.wait_for_unit("redis")
+          machine.wait_for_unit("redis-test")
 
-    # The unnamed Redis server still opens a port for backward-compatibility
-    machine.wait_for_open_port(6379)
+          # The unnamed Redis server still opens a port for backward-compatibility
+          machine.wait_for_open_port(6379)
 
-    machine.wait_for_file("${redis.servers."".unixSocket}")
-    machine.wait_for_file("${redis.servers."test".unixSocket}")
+          machine.wait_for_file("${redis.servers."".unixSocket}")
+          machine.wait_for_file("${redis.servers."test".unixSocket}")
 
-    # The unix socket is accessible to the redis group
-    machine.succeed('su member -c "redis-cli ping | grep PONG"')
-    machine.succeed('su member-test -c "redis-cli ping | grep PONG"')
+          # The unix socket is accessible to the redis group
+          machine.succeed('su member -c "${pkgs.redis}/bin/redis-cli ping | grep PONG"')
+          machine.succeed('su member-test -c "${pkgs.redis}/bin/redis-cli ping | grep PONG"')
 
-    machine.succeed("redis-cli ping | grep PONG")
-    machine.succeed("redis-cli -s ${redis.servers."".unixSocket} ping | grep PONG")
-    machine.succeed("redis-cli -s ${redis.servers."test".unixSocket} ping | grep PONG")
-  '';
-})
+          machine.succeed("${pkgs.redis}/bin/redis-cli ping | grep PONG")
+          machine.succeed("${pkgs.redis}/bin/redis-cli -s ${redis.servers."".unixSocket} ping | grep PONG")
+          machine.succeed("${pkgs.redis}/bin/redis-cli -s ${
+            redis.servers."test".unixSocket
+          } ping | grep PONG")
+        '';
+    };
+in
+lib.mapAttrs (_: package: makeRedisTest { inherit package; }) redisPackages
diff --git a/nixos/tests/restic-rest-server.nix b/nixos/tests/restic-rest-server.nix
new file mode 100644
index 0000000000000..1d38ddbe513c9
--- /dev/null
+++ b/nixos/tests/restic-rest-server.nix
@@ -0,0 +1,122 @@
+import ./make-test-python.nix (
+  { pkgs, ... }:
+
+  let
+    remoteRepository = "rest:http://restic_rest_server:8001/";
+
+    backupPrepareCommand = ''
+      touch /root/backupPrepareCommand
+      test ! -e /root/backupCleanupCommand
+    '';
+
+    backupCleanupCommand = ''
+      rm /root/backupPrepareCommand
+      touch /root/backupCleanupCommand
+    '';
+
+    testDir = pkgs.stdenvNoCC.mkDerivation {
+      name = "test-files-to-backup";
+      unpackPhase = "true";
+      installPhase = ''
+        mkdir $out
+        echo some_file > $out/some_file
+        echo some_other_file > $out/some_other_file
+        mkdir $out/a_dir
+        echo a_file > $out/a_dir/a_file
+      '';
+    };
+
+    passwordFile = "${pkgs.writeText "password" "correcthorsebatterystaple"}";
+    paths = [ "/opt" ];
+    exclude = [ "/opt/excluded_file_*" ];
+    pruneOpts = [
+      "--keep-daily 2"
+      "--keep-weekly 1"
+      "--keep-monthly 1"
+      "--keep-yearly 99"
+    ];
+  in
+  {
+    name = "restic-rest-server";
+
+    nodes = {
+      restic_rest_server = {
+        services.restic.server = {
+          enable = true;
+          extraFlags = [ "--no-auth" ];
+          listenAddress = "8001";
+        };
+        networking.firewall.allowedTCPPorts = [ 8001 ];
+      };
+      server = {
+        services.restic.backups = {
+          remotebackup = {
+            inherit passwordFile paths exclude pruneOpts backupPrepareCommand backupCleanupCommand;
+            repository = remoteRepository;
+            initialize = true;
+            timerConfig = null; # has no effect here, just checking that it doesn't break the service
+          };
+          remoteprune = {
+            inherit passwordFile;
+            repository = remoteRepository;
+            pruneOpts = [ "--keep-last 1" ];
+          };
+        };
+      };
+    };
+
+    testScript = ''
+      restic_rest_server.start()
+      server.start()
+      restic_rest_server.wait_for_unit("restic-rest-server.socket")
+      restic_rest_server.wait_for_open_port(8001)
+      server.wait_for_unit("dbus.socket")
+      server.fail(
+          "restic-remotebackup snapshots",
+      )
+      server.succeed(
+          # set up
+          "cp -rT ${testDir} /opt",
+          "touch /opt/excluded_file_1 /opt/excluded_file_2",
+
+          # test that remotebackup runs custom commands and produces a snapshot
+          "timedatectl set-time '2016-12-13 13:45'",
+          "systemctl start restic-backups-remotebackup.service",
+          "rm /root/backupCleanupCommand",
+          'restic-remotebackup snapshots --json | ${pkgs.jq}/bin/jq "length | . == 1"',
+
+          # test that restoring that snapshot produces the same directory
+          "mkdir /tmp/restore-1",
+          "restic-remotebackup restore latest -t /tmp/restore-1",
+          "diff -ru ${testDir} /tmp/restore-1/opt",
+
+          # test that we can create four snapshots in remotebackup and rclonebackup
+          "timedatectl set-time '2017-12-13 13:45'",
+          "systemctl start restic-backups-remotebackup.service",
+          "rm /root/backupCleanupCommand",
+
+          "timedatectl set-time '2018-12-13 13:45'",
+          "systemctl start restic-backups-remotebackup.service",
+          "rm /root/backupCleanupCommand",
+
+          "timedatectl set-time '2018-12-14 13:45'",
+          "systemctl start restic-backups-remotebackup.service",
+          "rm /root/backupCleanupCommand",
+
+          "timedatectl set-time '2018-12-15 13:45'",
+          "systemctl start restic-backups-remotebackup.service",
+          "rm /root/backupCleanupCommand",
+
+          "timedatectl set-time '2018-12-16 13:45'",
+          "systemctl start restic-backups-remotebackup.service",
+          "rm /root/backupCleanupCommand",
+
+          'restic-remotebackup snapshots --json | ${pkgs.jq}/bin/jq "length | . == 4"',
+
+          # test that remoteprune brings us back to 1 snapshot in remotebackup
+          "systemctl start restic-backups-remoteprune.service",
+          'restic-remotebackup snapshots --json | ${pkgs.jq}/bin/jq "length | . == 1"',
+      )
+    '';
+  }
+)
diff --git a/nixos/tests/unifi.nix b/nixos/tests/unifi.nix
index d371bafd69652..789b11b55985c 100644
--- a/nixos/tests/unifi.nix
+++ b/nixos/tests/unifi.nix
@@ -31,8 +31,6 @@ let
     '';
   };
 in with pkgs; {
-  unifiLTS = makeAppTest unifiLTS;
-  unifi5 = makeAppTest unifi5;
-  unifi6 = makeAppTest unifi6;
   unifi7 = makeAppTest unifi7;
+  unifi8 = makeAppTest unifi8;
 }
diff --git a/nixos/tests/web-apps/movim/default.nix b/nixos/tests/web-apps/movim/default.nix
new file mode 100644
index 0000000000000..5d6314e2b41be
--- /dev/null
+++ b/nixos/tests/web-apps/movim/default.nix
@@ -0,0 +1,8 @@
+{ system ? builtins.currentSystem, handleTestOn }:
+
+let
+  supportedSystems = [ "x86_64-linux" "i686-linux" ];
+in
+{
+  standard = handleTestOn supportedSystems ./standard.nix { inherit system; };
+}
diff --git a/nixos/tests/web-apps/movim/standard.nix b/nixos/tests/web-apps/movim/standard.nix
new file mode 100644
index 0000000000000..470d81d8f7229
--- /dev/null
+++ b/nixos/tests/web-apps/movim/standard.nix
@@ -0,0 +1,102 @@
+import ../../make-test-python.nix ({ lib, pkgs, ... }:
+
+let
+  movim = {
+    domain = "movim.local";
+    info = "No ToS in tests";
+    description = "NixOS testing server";
+  };
+  xmpp = {
+    domain = "xmpp.local";
+    admin = rec {
+      JID = "${username}@${xmpp.domain}";
+      username = "romeo";
+      password = "juliet";
+    };
+  };
+in
+{
+  name = "movim-standard";
+
+  meta = {
+    maintainers = with pkgs.lib.maintainers; [ toastal ];
+  };
+
+  nodes = {
+    server = { pkgs, ... }: {
+      services.movim = {
+        inherit (movim) domain;
+        enable = true;
+        verbose = true;
+        podConfig = {
+          inherit (movim) description info;
+          xmppdomain = xmpp.domain;
+        };
+        nginx = { };
+      };
+
+      services.prosody = {
+        enable = true;
+        xmppComplianceSuite = false;
+        disco_items = [
+          { url = "upload.${xmpp.domain}"; description = "File Uploads"; }
+        ];
+        virtualHosts."${xmpp.domain}" = {
+          inherit (xmpp) domain;
+          enabled = true;
+          extraConfig = ''
+            Component "pubsub.${xmpp.domain}" "pubsub"
+                pubsub_max_items = 10000
+                expose_publisher = true
+
+            Component "upload.${xmpp.domain}" "http_file_share"
+                http_external_url = "http://upload.${xmpp.domain}"
+                http_file_share_expires_after = 300 * 24 * 60 * 60
+                http_file_share_size_limit = 1024 * 1024 * 1024
+                http_file_share_daily_quota = 4 * 1024 * 1024 * 1024
+          '';
+        };
+        extraConfig = ''
+          pep_max_items = 10000
+
+          http_paths = {
+              file_share = "/";
+          }
+        '';
+      };
+
+      networking.extraHosts = ''
+        127.0.0.1 ${movim.domain}
+        127.0.0.1 ${xmpp.domain}
+      '';
+    };
+  };
+
+  testScript = /* python */ ''
+    server.wait_for_unit("phpfpm-movim.service")
+    server.wait_for_unit("nginx.service")
+    server.wait_for_open_port(80)
+
+    server.wait_for_unit("prosody.service")
+    server.succeed('prosodyctl status | grep "Prosody is running"')
+    server.succeed("prosodyctl register ${xmpp.admin.username} ${xmpp.domain} ${xmpp.admin.password}")
+
+    server.wait_for_unit("movim.service")
+
+    # Test unauthenticated
+    server.fail("curl -L --fail-with-body --max-redirs 0 http://${movim.domain}/chat")
+
+    # Test basic Websocket
+    server.succeed("echo \"\" | ${lib.getExe pkgs.websocat} 'ws://${movim.domain}/ws/?path=login&offset=0' --origin 'http://${movim.domain}'")
+
+    # Test login + create cookiejar
+    login_html = server.succeed("curl --fail-with-body -c /tmp/cookies http://${movim.domain}/login")
+    assert "${movim.description}" in login_html
+    assert "${movim.info}" in login_html
+
+    # Test authentication POST
+    server.succeed("curl --fail-with-body -b /tmp/cookies -X POST --data-urlencode 'username=${xmpp.admin.JID}' --data-urlencode 'password=${xmpp.admin.password}' http://${movim.domain}/login")
+
+    server.succeed("curl -L --fail-with-body --max-redirs 1 -b /tmp/cookies http://${movim.domain}/chat")
+  '';
+})