about summary refs log tree commit diff
path: root/nixos/modules/services/web-apps
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services/web-apps')
-rw-r--r--nixos/modules/services/web-apps/akkoma.nix17
-rw-r--r--nixos/modules/services/web-apps/c2fmzq-server.md42
-rw-r--r--nixos/modules/services/web-apps/c2fmzq-server.nix125
-rw-r--r--nixos/modules/services/web-apps/cloudlog.nix2
-rw-r--r--nixos/modules/services/web-apps/dex.nix7
-rw-r--r--nixos/modules/services/web-apps/hedgedoc.nix1226
-rw-r--r--nixos/modules/services/web-apps/hledger-web.nix2
-rw-r--r--nixos/modules/services/web-apps/isso.nix4
-rw-r--r--nixos/modules/services/web-apps/jitsi-meet.nix4
-rw-r--r--nixos/modules/services/web-apps/lanraragi.nix100
-rw-r--r--nixos/modules/services/web-apps/meme-bingo-web.nix4
-rw-r--r--nixos/modules/services/web-apps/microbin.nix93
-rw-r--r--nixos/modules/services/web-apps/nextcloud.md8
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix122
-rw-r--r--nixos/modules/services/web-apps/outline.nix50
-rw-r--r--nixos/modules/services/web-apps/peering-manager.nix137
-rw-r--r--nixos/modules/services/web-apps/phylactery.nix2
-rw-r--r--nixos/modules/services/web-apps/plausible.nix2
-rw-r--r--nixos/modules/services/web-apps/shiori.nix9
-rw-r--r--nixos/modules/services/web-apps/snipe-it.nix18
-rw-r--r--nixos/modules/services/web-apps/writefreely.nix2
-rw-r--r--nixos/modules/services/web-apps/zitadel.nix2
22 files changed, 815 insertions, 1163 deletions
diff --git a/nixos/modules/services/web-apps/akkoma.nix b/nixos/modules/services/web-apps/akkoma.nix
index 8d1775258612..eaee70c712bb 100644
--- a/nixos/modules/services/web-apps/akkoma.nix
+++ b/nixos/modules/services/web-apps/akkoma.nix
@@ -282,11 +282,11 @@ let
         AKKOMA_CONFIG_PATH="$RUNTIME_DIRECTORY/config.exs" \
         ERL_EPMD_ADDRESS="${cfg.dist.address}" \
         ERL_EPMD_PORT="${toString cfg.dist.epmdPort}" \
-        ERL_FLAGS="${concatStringsSep " " [
-          "-kernel inet_dist_use_interface '${erlAddr cfg.dist.address}'"
-          "-kernel inet_dist_listen_min ${toString cfg.dist.portMin}"
-          "-kernel inet_dist_listen_max ${toString cfg.dist.portMax}"
-        ]}" \
+        ERL_FLAGS=${lib.escapeShellArg (lib.escapeShellArgs ([
+          "-kernel" "inet_dist_use_interface" (erlAddr cfg.dist.address)
+          "-kernel" "inet_dist_listen_min" (toString cfg.dist.portMin)
+          "-kernel" "inet_dist_listen_max" (toString cfg.dist.portMax)
+        ] ++ cfg.dist.extraFlags))} \
         RELEASE_COOKIE="$(<"$RUNTIME_DIRECTORY/cookie")" \
         RELEASE_NAME="akkoma" \
           exec "${cfg.package}/bin/$(basename "$0")" "$@"
@@ -553,6 +553,13 @@ in {
           description = mdDoc "TCP port to bind Erlang Port Mapper Daemon to.";
         };
 
+        extraFlags = mkOption {
+          type = with types; listOf str;
+          default = [ ];
+          description = mdDoc "Extra flags to pass to Erlang";
+          example = [ "+sbwt" "none" "+sbwtdcpu" "none" "+sbwtdio" "none" ];
+        };
+
         portMin = mkOption {
           type = types.port;
           default = 49152;
diff --git a/nixos/modules/services/web-apps/c2fmzq-server.md b/nixos/modules/services/web-apps/c2fmzq-server.md
new file mode 100644
index 000000000000..236953bd4ff7
--- /dev/null
+++ b/nixos/modules/services/web-apps/c2fmzq-server.md
@@ -0,0 +1,42 @@
+# c2FmZQ {#module-services-c2fmzq}
+
+c2FmZQ is an application that can securely encrypt, store, and share files,
+including but not limited to pictures and videos.
+
+The service `c2fmzq-server` can be enabled by setting
+```
+{
+  services.c2fmzq-server.enable = true;
+}
+```
+This will spin up an instance of the server which is API-compatible with
+[Stingle Photos](https://stingle.org) and an experimental Progressive Web App
+(PWA) to interact with the storage via the browser.
+
+In principle the server can be exposed directly on a public interface and there
+are command line options to manage HTTPS certificates directly, but the module
+is designed to be served behind a reverse proxy or only accessed via localhost.
+
+```
+{
+  services.c2fmzq-server = {
+    enable = true;
+    bindIP = "127.0.0.1"; # default
+    port = 8080; # default
+  };
+
+  services.nginx = {
+    enable = true;
+    recommendedProxySettings = true;
+    virtualHosts."example.com" = {
+      enableACME = true;
+      forceSSL = true;
+      locations."/" = {
+        proxyPass = "http://127.0.0.1:8080";
+      };
+    };
+  };
+}
+```
+
+For more information, see <https://github.com/c2FmZQ/c2FmZQ/>.
diff --git a/nixos/modules/services/web-apps/c2fmzq-server.nix b/nixos/modules/services/web-apps/c2fmzq-server.nix
new file mode 100644
index 000000000000..2749c2a5a87a
--- /dev/null
+++ b/nixos/modules/services/web-apps/c2fmzq-server.nix
@@ -0,0 +1,125 @@
+{ lib, pkgs, config, ... }:
+
+let
+  inherit (lib) mkEnableOption mkPackageOption mkOption types;
+
+  cfg = config.services.c2fmzq-server;
+
+  argsFormat = {
+    type = with lib.types; nullOr (oneOf [ bool int str ]);
+    generate = lib.cli.toGNUCommandLineShell { };
+  };
+in {
+  options.services.c2fmzq-server = {
+    enable = mkEnableOption "c2fmzq-server";
+
+    bindIP = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      description = "The local address to use.";
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 8080;
+      description = "The local port to use.";
+    };
+
+    passphraseFile = mkOption {
+      type = types.str;
+      example = "/run/secrets/c2fmzq/pwfile";
+      description = "Path to file containing the database passphrase";
+    };
+
+    package = mkPackageOption pkgs "c2fmzq" { };
+
+    settings = mkOption {
+      type = types.submodule {
+        freeformType = argsFormat.type;
+
+        options = {
+          address = mkOption {
+            internal = true;
+            type = types.str;
+            default = "${cfg.bindIP}:${toString cfg.port}";
+          };
+
+          database = mkOption {
+            type = types.str;
+            default = "%S/c2fmzq-server/data";
+            description = "Path of the database";
+          };
+
+          verbose = mkOption {
+            type = types.ints.between 1 3;
+            default = 2;
+            description = "The level of logging verbosity: 1:Error 2:Info 3:Debug";
+          };
+        };
+      };
+      description = ''
+        Configuration for c2FmZQ-server passed as CLI arguments.
+        Run {command}`c2FmZQ-server help` for supported values.
+      '';
+      example = {
+        verbose = 3;
+        allow-new-accounts = true;
+        auto-approve-new-accounts = true;
+        encrypt-metadata = true;
+        enable-webapp = true;
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.c2fmzq-server = {
+      description = "c2FmZQ-server";
+      documentation = [ "https://github.com/c2FmZQ/c2FmZQ/blob/main/README.md" ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "network-online.target" ];
+
+      serviceConfig = {
+        ExecStart = "${lib.getExe cfg.package} ${argsFormat.generate cfg.settings}";
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+        DynamicUser = true;
+        Environment = "C2FMZQ_PASSPHRASE_FILE=%d/passphrase-file";
+        IPAccounting = true;
+        IPAddressAllow = cfg.bindIP;
+        IPAddressDeny = "any";
+        LoadCredential = "passphrase-file:${cfg.passphraseFile}";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateIPC = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SocketBindAllow = cfg.port;
+        SocketBindDeny = "any";
+        StateDirectory = "c2fmzq-server";
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged @obsolete" ];
+      };
+    };
+  };
+
+  meta = {
+    doc = ./c2fmzq-server.md;
+    maintainers = with lib.maintainers; [ hmenke ];
+  };
+}
diff --git a/nixos/modules/services/web-apps/cloudlog.nix b/nixos/modules/services/web-apps/cloudlog.nix
index da2cf93d7f1c..5519d6967a12 100644
--- a/nixos/modules/services/web-apps/cloudlog.nix
+++ b/nixos/modules/services/web-apps/cloudlog.nix
@@ -69,7 +69,7 @@ let
 in
 {
   options.services.cloudlog = with types; {
-    enable = mkEnableOption (mdDoc "Whether to enable Cloudlog");
+    enable = mkEnableOption (mdDoc "Cloudlog");
     dataDir = mkOption {
       type = str;
       default = "/var/lib/cloudlog";
diff --git a/nixos/modules/services/web-apps/dex.nix b/nixos/modules/services/web-apps/dex.nix
index bd041db007a1..0c4a71c6dfe4 100644
--- a/nixos/modules/services/web-apps/dex.nix
+++ b/nixos/modules/services/web-apps/dex.nix
@@ -108,8 +108,7 @@ in
         ProtectClock = true;
         ProtectHome = true;
         ProtectHostname = true;
-        # Would re-mount paths ignored by temporary root
-        #ProtectSystem = "strict";
+        ProtectSystem = "strict";
         ProtectControlGroups = true;
         ProtectKernelLogs = true;
         ProtectKernelModules = true;
@@ -121,9 +120,7 @@ in
         RestrictSUIDSGID = true;
         SystemCallArchitectures = "native";
         SystemCallFilter = [ "@system-service" "~@privileged @setuid @keyring" ];
-        TemporaryFileSystem = "/:ro";
-        # Does not work well with the temporary root
-        #UMask = "0066";
+        UMask = "0066";
       } // optionalAttrs (cfg.environmentFile != null) {
         EnvironmentFile = cfg.environmentFile;
       };
diff --git a/nixos/modules/services/web-apps/hedgedoc.nix b/nixos/modules/services/web-apps/hedgedoc.nix
index bfa5fd5aff25..1a66f077b09d 100644
--- a/nixos/modules/services/web-apps/hedgedoc.nix
+++ b/nixos/modules/services/web-apps/hedgedoc.nix
@@ -1,7 +1,7 @@
 { config, lib, pkgs, ... }:
 
 let
-  inherit (lib) literalExpression mdDoc mkEnableOption mkIf mkOption mkPackageOptionMD mkRenamedOptionModule types versionAtLeast;
+  inherit (lib) mkOption types mdDoc literalExpression;
 
   cfg = config.services.hedgedoc;
 
@@ -9,990 +9,189 @@ let
   # versionAtLeast statement remains set to 21.03 for backwards compatibility.
   # See https://github.com/NixOS/nixpkgs/pull/108899 and
   # https://github.com/NixOS/rfcs/blob/master/rfcs/0080-nixos-release-schedule.md.
-  name = if versionAtLeast config.system.stateVersion "21.03"
-    then "hedgedoc"
-    else "codimd";
+  name = if lib.versionAtLeast config.system.stateVersion "21.03" then
+    "hedgedoc"
+  else
+    "codimd";
 
-  settingsFormat = pkgs.formats.json {};
-
-  prettyJSON = conf:
-    pkgs.runCommandLocal "hedgedoc-config.json" {
-      nativeBuildInputs = [ pkgs.jq ];
-    } ''
-      jq '{production:del(.[]|nulls)|del(.[][]?|nulls)}' \
-        < ${settingsFormat.generate "hedgedoc-ugly.json" cfg.settings} \
-        > $out
-    '';
+  settingsFormat = pkgs.formats.json { };
 in
 {
+  meta.maintainers = with lib.maintainers; [ SuperSandro2000 h7x4 ];
+
   imports = [
-    (mkRenamedOptionModule [ "services" "codimd" ] [ "services" "hedgedoc" ])
-    (mkRenamedOptionModule
-      [ "services" "hedgedoc" "configuration" ] [ "services" "hedgedoc" "settings" ])
+    (lib.mkRenamedOptionModule [ "services" "codimd" ] [ "services" "hedgedoc" ])
+    (lib.mkRenamedOptionModule [ "services" "hedgedoc" "configuration" ] [ "services" "hedgedoc" "settings" ])
+    (lib.mkRenamedOptionModule [ "services" "hedgedoc" "groups" ] [ "users" "users" "hedgedoc" "extraGroups" ])
+    (lib.mkRemovedOptionModule [ "services" "hedgedoc" "workDir" ] ''
+      This option has been removed in favor of systemd managing the state directory.
+
+      If you have set this option without specifying `services.settings.uploadsDir`,
+      please move these files to `/var/lib/hedgedoc/uploads`, or set the option to point
+      at the correct location.
+    '')
   ];
 
   options.services.hedgedoc = {
-    package = mkPackageOptionMD pkgs "hedgedoc" { };
-    enable = mkEnableOption (lib.mdDoc "the HedgeDoc Markdown Editor");
+    package = lib.mkPackageOptionMD pkgs "hedgedoc" { };
+    enable = lib.mkEnableOption (mdDoc "the HedgeDoc Markdown Editor");
 
-    groups = mkOption {
-      type = types.listOf types.str;
-      default = [];
-      description = lib.mdDoc ''
-        Groups to which the service user should be added.
-      '';
-    };
-
-    workDir = mkOption {
-      type = types.path;
-      default = "/var/lib/${name}";
-      description = lib.mdDoc ''
-        Working directory for the HedgeDoc service.
-      '';
-    };
+    settings = mkOption {
+      type = types.submodule {
+        freeformType = settingsFormat.type;
+        options = {
+          domain = mkOption {
+            type = with types; nullOr str;
+            default = null;
+            example = "hedgedoc.org";
+            description = mdDoc ''
+              Domain to use for website.
 
-    settings = let options = {
-      debug = mkEnableOption (lib.mdDoc "debug mode");
-      domain = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        example = "hedgedoc.org";
-        description = lib.mdDoc ''
-          Domain name for the HedgeDoc instance.
-        '';
-      };
-      urlPath = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        example = "/url/path/to/hedgedoc";
-        description = lib.mdDoc ''
-          Path under which HedgeDoc is accessible.
-        '';
-      };
-      host = mkOption {
-        type = types.str;
-        default = "localhost";
-        description = lib.mdDoc ''
-          Address to listen on.
-        '';
-      };
-      port = mkOption {
-        type = types.port;
-        default = 3000;
-        example = 80;
-        description = lib.mdDoc ''
-          Port to listen on.
-        '';
-      };
-      path = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        example = "/run/hedgedoc.sock";
-        description = lib.mdDoc ''
-          Specify where a UNIX domain socket should be placed.
-        '';
-      };
-      allowOrigin = mkOption {
-        type = types.listOf types.str;
-        default = [];
-        example = [ "localhost" "hedgedoc.org" ];
-        description = lib.mdDoc ''
-          List of domains to whitelist.
-        '';
-      };
-      useSSL = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Enable to use SSL server. This will also enable
-          {option}`protocolUseSSL`.
-        '';
-      };
-      enableStatsApi = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Enables or disables the /status and /metrics endpoint.
-        '';
-      };
-      hsts = {
-        enable = mkOption {
-          type = types.bool;
-          default = true;
-          description = lib.mdDoc ''
-            Whether to enable HSTS if HTTPS is also enabled.
-          '';
-        };
-        maxAgeSeconds = mkOption {
-          type = types.int;
-          default = 31536000;
-          description = lib.mdDoc ''
-            Max duration for clients to keep the HSTS status.
-          '';
-        };
-        includeSubdomains = mkOption {
-          type = types.bool;
-          default = true;
-          description = lib.mdDoc ''
-            Whether to include subdomains in HSTS.
-          '';
-        };
-        preload = mkOption {
-          type = types.bool;
-          default = true;
-          description = lib.mdDoc ''
-            Whether to allow preloading of the site's HSTS status.
-          '';
-        };
-      };
-      csp = mkOption {
-        type = types.nullOr types.attrs;
-        default = null;
-        example = literalExpression ''
-          {
-            enable = true;
-            directives = {
-              scriptSrc = "trustworthy.scripts.example.com";
-            };
-            upgradeInsecureRequest = "auto";
-            addDefaults = true;
-          }
-        '';
-        description = lib.mdDoc ''
-          Specify the Content Security Policy which is passed to Helmet.
-          For configuration details see <https://helmetjs.github.io/docs/csp/>.
-        '';
-      };
-      protocolUseSSL = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Enable to use TLS for resource paths.
-          This only applies when {option}`domain` is set.
-        '';
-      };
-      urlAddPort = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Enable to add the port to callback URLs.
-          This only applies when {option}`domain` is set
-          and only for ports other than 80 and 443.
-        '';
-      };
-      useCDN = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Whether to use CDN resources or not.
-        '';
-      };
-      allowAnonymous = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          Whether to allow anonymous usage.
-        '';
-      };
-      allowAnonymousEdits = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Whether to allow guests to edit existing notes with the `freely` permission,
-          when {option}`allowAnonymous` is enabled.
-        '';
-      };
-      allowFreeURL = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Whether to allow note creation by accessing a nonexistent note URL.
-        '';
-      };
-      requireFreeURLAuthentication = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Whether to require authentication for FreeURL mode style note creation.
-        '';
-      };
-      defaultPermission = mkOption {
-        type = types.enum [ "freely" "editable" "limited" "locked" "private" ];
-        default = "editable";
-        description = lib.mdDoc ''
-          Default permissions for notes.
-          This only applies for signed-in users.
-        '';
-      };
-      dbURL = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        example = ''
-          postgres://user:pass@host:5432/dbname
-        '';
-        description = lib.mdDoc ''
-          Specify which database to use.
-          HedgeDoc supports mysql, postgres, sqlite and mssql.
-          See [
-          https://sequelize.readthedocs.io/en/v3/](https://sequelize.readthedocs.io/en/v3/) for more information.
-          Note: This option overrides {option}`db`.
-        '';
-      };
-      db = mkOption {
-        type = types.attrs;
-        default = {};
-        example = literalExpression ''
-          {
-            dialect = "sqlite";
-            storage = "/var/lib/${name}/db.${name}.sqlite";
-          }
-        '';
-        description = lib.mdDoc ''
-          Specify the configuration for sequelize.
-          HedgeDoc supports mysql, postgres, sqlite and mssql.
-          See [
-          https://sequelize.readthedocs.io/en/v3/](https://sequelize.readthedocs.io/en/v3/) for more information.
-          Note: This option overrides {option}`db`.
-        '';
-      };
-      sslKeyPath= mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        example = "/var/lib/hedgedoc/hedgedoc.key";
-        description = lib.mdDoc ''
-          Path to the SSL key. Needed when {option}`useSSL` is enabled.
-        '';
-      };
-      sslCertPath = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        example = "/var/lib/hedgedoc/hedgedoc.crt";
-        description = lib.mdDoc ''
-          Path to the SSL cert. Needed when {option}`useSSL` is enabled.
-        '';
-      };
-      sslCAPath = mkOption {
-        type = types.listOf types.str;
-        default = [];
-        example = [ "/var/lib/hedgedoc/ca.crt" ];
-        description = lib.mdDoc ''
-          SSL ca chain. Needed when {option}`useSSL` is enabled.
-        '';
-      };
-      dhParamPath = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        example = "/var/lib/hedgedoc/dhparam.pem";
-        description = lib.mdDoc ''
-          Path to the SSL dh params. Needed when {option}`useSSL` is enabled.
-        '';
-      };
-      tmpPath = mkOption {
-        type = types.str;
-        default = "/tmp";
-        description = lib.mdDoc ''
-          Path to the temp directory HedgeDoc should use.
-          Note that {option}`serviceConfig.PrivateTmp` is enabled for
-          the HedgeDoc systemd service by default.
-          (Non-canonical paths are relative to HedgeDoc's base directory)
-        '';
-      };
-      defaultNotePath = mkOption {
-        type = types.nullOr types.str;
-        default = "${cfg.package}/public/default.md";
-        defaultText = literalExpression "\"\${cfg.package}/public/default.md\"";
-        description = lib.mdDoc ''
-          Path to the default Note file.
-          (Non-canonical paths are relative to HedgeDoc's base directory)
-        '';
-      };
-      docsPath = mkOption {
-        type = types.nullOr types.str;
-        default = "${cfg.package}/public/docs";
-        defaultText = literalExpression "\"\${cfg.package}/public/docs\"";
-        description = lib.mdDoc ''
-          Path to the docs directory.
-          (Non-canonical paths are relative to HedgeDoc's base directory)
-        '';
-      };
-      indexPath = mkOption {
-        type = types.nullOr types.str;
-        default = "${cfg.package}/public/views/index.ejs";
-        defaultText = literalExpression "\"\${cfg.package}/public/views/index.ejs\"";
-        description = lib.mdDoc ''
-          Path to the index template file.
-          (Non-canonical paths are relative to HedgeDoc's base directory)
-        '';
-      };
-      hackmdPath = mkOption {
-        type = types.nullOr types.str;
-        default = "${cfg.package}/public/views/hackmd.ejs";
-        defaultText = literalExpression "\"\${cfg.package}/public/views/hackmd.ejs\"";
-        description = lib.mdDoc ''
-          Path to the hackmd template file.
-          (Non-canonical paths are relative to HedgeDoc's base directory)
-        '';
-      };
-      errorPath = mkOption {
-        type = types.nullOr types.str;
-        default = "${cfg.package}/public/views/error.ejs";
-        defaultText = literalExpression "\"\${cfg.package}/public/views/error.ejs\"";
-        description = lib.mdDoc ''
-          Path to the error template file.
-          (Non-canonical paths are relative to HedgeDoc's base directory)
-        '';
-      };
-      prettyPath = mkOption {
-        type = types.nullOr types.str;
-        default = "${cfg.package}/public/views/pretty.ejs";
-        defaultText = literalExpression "\"\${cfg.package}/public/views/pretty.ejs\"";
-        description = lib.mdDoc ''
-          Path to the pretty template file.
-          (Non-canonical paths are relative to HedgeDoc's base directory)
-        '';
-      };
-      slidePath = mkOption {
-        type = types.nullOr types.str;
-        default = "${cfg.package}/public/views/slide.hbs";
-        defaultText = literalExpression "\"\${cfg.package}/public/views/slide.hbs\"";
-        description = lib.mdDoc ''
-          Path to the slide template file.
-          (Non-canonical paths are relative to HedgeDoc's base directory)
-        '';
-      };
-      uploadsPath = mkOption {
-        type = types.str;
-        default = "${cfg.workDir}/uploads";
-        defaultText = literalExpression "\"\${cfg.workDir}/uploads\"";
-        description = lib.mdDoc ''
-          Path under which uploaded files are saved.
-        '';
-      };
-      sessionName = mkOption {
-        type = types.str;
-        default = "connect.sid";
-        description = lib.mdDoc ''
-          Specify the name of the session cookie.
-        '';
-      };
-      sessionSecret = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = lib.mdDoc ''
-          Specify the secret used to sign the session cookie.
-          If unset, one will be generated on startup.
-        '';
-      };
-      sessionLife = mkOption {
-        type = types.int;
-        default = 1209600000;
-        description = lib.mdDoc ''
-          Session life time in milliseconds.
-        '';
-      };
-      heartbeatInterval = mkOption {
-        type = types.int;
-        default = 5000;
-        description = lib.mdDoc ''
-          Specify the socket.io heartbeat interval.
-        '';
-      };
-      heartbeatTimeout = mkOption {
-        type = types.int;
-        default = 10000;
-        description = lib.mdDoc ''
-          Specify the socket.io heartbeat timeout.
-        '';
-      };
-      documentMaxLength = mkOption {
-        type = types.int;
-        default = 100000;
-        description = lib.mdDoc ''
-          Specify the maximum document length.
-        '';
-      };
-      email = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          Whether to enable email sign-in.
-        '';
-      };
-      allowEmailRegister = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          Whether to enable email registration.
-        '';
-      };
-      allowGravatar = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          Whether to use gravatar as profile picture source.
-        '';
-      };
-      imageUploadType = mkOption {
-        type = types.enum [ "imgur" "s3" "minio" "filesystem" ];
-        default = "filesystem";
-        description = lib.mdDoc ''
-          Specify where to upload images.
-        '';
-      };
-      minio = mkOption {
-        type = types.nullOr (types.submodule {
-          options = {
-            accessKey = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Minio access key.
-              '';
-            };
-            secretKey = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Minio secret key.
-              '';
-            };
-            endPoint = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Minio endpoint.
-              '';
-            };
-            port = mkOption {
-              type = types.port;
-              default = 9000;
-              description = lib.mdDoc ''
-                Minio listen port.
-              '';
-            };
-            secure = mkOption {
-              type = types.bool;
-              default = true;
-              description = lib.mdDoc ''
-                Whether to use HTTPS for Minio.
-              '';
-            };
+              This is useful if you are trying to run hedgedoc behind
+              a reverse proxy.
+            '';
           };
-        });
-        default = null;
-        description = lib.mdDoc "Configure the minio third-party integration.";
-      };
-      s3 = mkOption {
-        type = types.nullOr (types.submodule {
-          options = {
-            accessKeyId = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                AWS access key id.
-              '';
-            };
-            secretAccessKey = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                AWS access key.
-              '';
-            };
-            region = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                AWS S3 region.
-              '';
-            };
-          };
-        });
-        default = null;
-        description = lib.mdDoc "Configure the s3 third-party integration.";
-      };
-      s3bucket = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = lib.mdDoc ''
-          Specify the bucket name for upload types `s3` and `minio`.
-        '';
-      };
-      allowPDFExport = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          Whether to enable PDF exports.
-        '';
-      };
-      imgur.clientId = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = lib.mdDoc ''
-          Imgur API client ID.
-        '';
-      };
-      azure = mkOption {
-        type = types.nullOr (types.submodule {
-          options = {
-            connectionString = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Azure Blob Storage connection string.
-              '';
-            };
-            container = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Azure Blob Storage container name.
-                It will be created if non-existent.
-              '';
-            };
-          };
-        });
-        default = null;
-        description = lib.mdDoc "Configure the azure third-party integration.";
-      };
-      oauth2 = mkOption {
-        type = types.nullOr (types.submodule {
-          options = {
-            authorizationURL = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Specify the OAuth authorization URL.
-              '';
-            };
-            tokenURL = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Specify the OAuth token URL.
-              '';
-            };
-            baseURL = mkOption {
-              type = with types; nullOr str;
-              default = null;
-              description = lib.mdDoc ''
-                Specify the OAuth base URL.
-              '';
-            };
-            userProfileURL = mkOption {
-              type = with types; nullOr str;
-              default = null;
-              description = lib.mdDoc ''
-                Specify the OAuth userprofile URL.
-              '';
-            };
-            userProfileUsernameAttr = mkOption {
-              type = with types; nullOr str;
-              default = null;
-              description = lib.mdDoc ''
-                Specify the name of the attribute for the username from the claim.
-              '';
-            };
-            userProfileDisplayNameAttr = mkOption {
-              type = with types; nullOr str;
-              default = null;
-              description = lib.mdDoc ''
-                Specify the name of the attribute for the display name from the claim.
-              '';
-            };
-            userProfileEmailAttr = mkOption {
-              type = with types; nullOr str;
-              default = null;
-              description = lib.mdDoc ''
-                Specify the name of the attribute for the email from the claim.
-              '';
-            };
-            scope = mkOption {
-              type = with types; nullOr str;
-              default = null;
-              description = lib.mdDoc ''
-                Specify the OAuth scope.
-              '';
-            };
-            providerName = mkOption {
-              type = with types; nullOr str;
-              default = null;
-              description = lib.mdDoc ''
-                Specify the name to be displayed for this strategy.
-              '';
-            };
-            rolesClaim = mkOption {
-              type = with types; nullOr str;
-              default = null;
-              description = lib.mdDoc ''
-                Specify the role claim name.
-              '';
-            };
-            accessRole = mkOption {
-              type = with types; nullOr str;
-              default = null;
-              description = lib.mdDoc ''
-                Specify role which should be included in the ID token roles claim to grant access
-              '';
-            };
-            clientID = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Specify the OAuth client ID.
-              '';
-            };
-            clientSecret = mkOption {
-              type = with types; nullOr str;
-              default = null;
-              description = lib.mdDoc ''
-                Specify the OAuth client secret.
-              '';
-            };
+          urlPath = mkOption {
+            type = with types; nullOr str;
+            default = null;
+            example = "hedgedoc";
+            description = mdDoc ''
+              URL path for the website.
+
+              This is useful if you are hosting hedgedoc on a path like
+              `www.example.com/hedgedoc`
+            '';
           };
-        });
-        default = null;
-        description = lib.mdDoc "Configure the OAuth integration.";
-      };
-      facebook = mkOption {
-        type = types.nullOr (types.submodule {
-          options = {
-            clientID = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Facebook API client ID.
-              '';
-            };
-            clientSecret = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Facebook API client secret.
-              '';
-            };
+          host = mkOption {
+            type = with types; nullOr str;
+            default = "localhost";
+            description = mdDoc ''
+              Address to listen on.
+            '';
           };
-        });
-        default = null;
-        description = lib.mdDoc "Configure the facebook third-party integration";
-      };
-      twitter = mkOption {
-        type = types.nullOr (types.submodule {
-          options = {
-            consumerKey = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Twitter API consumer key.
-              '';
-            };
-            consumerSecret = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Twitter API consumer secret.
-              '';
-            };
+          port = mkOption {
+            type = types.port;
+            default = 3000;
+            example = 80;
+            description = mdDoc ''
+              Port to listen on.
+            '';
           };
-        });
-        default = null;
-        description = lib.mdDoc "Configure the Twitter third-party integration.";
-      };
-      github = mkOption {
-        type = types.nullOr (types.submodule {
-          options = {
-            clientID = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                GitHub API client ID.
-              '';
-            };
-            clientSecret = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Github API client secret.
-              '';
-            };
+          path = mkOption {
+            type = with types; nullOr path;
+            default = null;
+            example = "/run/hedgedoc/hedgedoc.sock";
+            description = mdDoc ''
+              Path to UNIX domain socket to listen on
+
+              ::: {.note}
+                If specified, {option}`host` and {option}`port` will be ignored.
+              :::
+            '';
           };
-        });
-        default = null;
-        description = lib.mdDoc "Configure the GitHub third-party integration.";
-      };
-      gitlab = mkOption {
-        type = types.nullOr (types.submodule {
-          options = {
-            baseURL = mkOption {
-              type = types.str;
-              default = "";
-              description = lib.mdDoc ''
-                GitLab API authentication endpoint.
-                Only needed for other endpoints than gitlab.com.
-              '';
-            };
-            clientID = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                GitLab API client ID.
-              '';
-            };
-            clientSecret = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                GitLab API client secret.
-              '';
-            };
-            scope = mkOption {
-              type = types.enum [ "api" "read_user" ];
-              default = "api";
-              description = lib.mdDoc ''
-                GitLab API requested scope.
-                GitLab snippet import/export requires api scope.
-              '';
-            };
+          protocolUseSSL = mkOption {
+            type = types.bool;
+            default = false;
+            example = true;
+            description = mdDoc ''
+              Use `https://` for all links.
+
+              This is useful if you are trying to run hedgedoc behind
+              a reverse proxy.
+
+              ::: {.note}
+                Only applied if {option}`domain` is set.
+              :::
+            '';
           };
-        });
-        default = null;
-        description = lib.mdDoc "Configure the GitLab third-party integration.";
-      };
-      mattermost = mkOption {
-        type = types.nullOr (types.submodule {
-          options = {
-            baseURL = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Mattermost authentication endpoint.
-              '';
-            };
-            clientID = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Mattermost API client ID.
-              '';
-            };
-            clientSecret = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Mattermost API client secret.
-              '';
-            };
+          allowOrigin = mkOption {
+            type = with types; listOf str;
+            default = with cfg.settings; [ host ] ++ lib.optionals (domain != null) [ domain ];
+            defaultText = literalExpression ''
+              with config.services.hedgedoc.settings; [ host ] ++ lib.optionals (domain != null) [ domain ]
+            '';
+            example = [ "localhost" "hedgedoc.org" ];
+            description = mdDoc ''
+              List of domains to whitelist.
+            '';
           };
-        });
-        default = null;
-        description = lib.mdDoc "Configure the Mattermost third-party integration.";
-      };
-      dropbox = mkOption {
-        type = types.nullOr (types.submodule {
-          options = {
-            clientID = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Dropbox API client ID.
-              '';
-            };
-            clientSecret = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Dropbox API client secret.
-              '';
-            };
-            appKey = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Dropbox app key.
-              '';
-            };
+          db = mkOption {
+            type = types.attrs;
+            default = {
+              dialect = "sqlite";
+              storage = "/var/lib/${name}/db.sqlite";
+            };
+            defaultText = literalExpression ''
+              {
+                dialect = "sqlite";
+                storage = "/var/lib/hedgedoc/db.sqlite";
+              }
+            '';
+            example = literalExpression ''
+              db = {
+                username = "hedgedoc";
+                database = "hedgedoc";
+                host = "localhost:5432";
+                # or via socket
+                # host = "/run/postgresql";
+                dialect = "postgresql";
+              };
+            '';
+            description = mdDoc ''
+              Specify the configuration for sequelize.
+              HedgeDoc supports `mysql`, `postgres`, `sqlite` and `mssql`.
+              See <https://sequelize.readthedocs.io/en/v3/>
+              for more information.
+
+              ::: {.note}
+                The relevant parts will be overriden if you set {option}`dbURL`.
+              :::
+            '';
           };
-        });
-        default = null;
-        description = lib.mdDoc "Configure the Dropbox third-party integration.";
-      };
-      google = mkOption {
-        type = types.nullOr (types.submodule {
-          options = {
-            clientID = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Google API client ID.
-              '';
-            };
-            clientSecret = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Google API client secret.
-              '';
-            };
+          useSSL = mkOption {
+            type = types.bool;
+            default = false;
+            description = mdDoc ''
+              Enable to use SSL server.
+
+              ::: {.note}
+                This will also enable {option}`protocolUseSSL`.
+
+                It will also require you to set the following:
+
+                - {option}`sslKeyPath`
+                - {option}`sslCertPath`
+                - {option}`sslCAPath`
+                - {option}`dhParamPath`
+              :::
+            '';
           };
-        });
-        default = null;
-        description = lib.mdDoc "Configure the Google third-party integration.";
-      };
-      ldap = mkOption {
-        type = types.nullOr (types.submodule {
-          options = {
-            providerName = mkOption {
-              type = types.str;
-              default = "";
-              description = lib.mdDoc ''
-                Optional name to be displayed at login form, indicating the LDAP provider.
-              '';
-            };
-            url = mkOption {
-              type = types.str;
-              example = "ldap://localhost";
-              description = lib.mdDoc ''
-                URL of LDAP server.
-              '';
-            };
-            bindDn = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Bind DN for LDAP access.
-              '';
-            };
-            bindCredentials = mkOption {
-              type = types.str;
-              description = lib.mdDoc ''
-                Bind credentials for LDAP access.
-              '';
-            };
-            searchBase = mkOption {
-              type = types.str;
-              example = "o=users,dc=example,dc=com";
-              description = lib.mdDoc ''
-                LDAP directory to begin search from.
-              '';
-            };
-            searchFilter = mkOption {
-              type = types.str;
-              example = "(uid={{username}})";
-              description = lib.mdDoc ''
-                LDAP filter to search with.
-              '';
-            };
-            searchAttributes = mkOption {
-              type = types.nullOr (types.listOf types.str);
-              default = null;
-              example = [ "displayName" "mail" ];
-              description = lib.mdDoc ''
-                LDAP attributes to search with.
-              '';
-            };
-            userNameField = mkOption {
-              type = types.str;
-              default = "";
-              description = lib.mdDoc ''
-                LDAP field which is used as the username on HedgeDoc.
-                By default {option}`useridField` is used.
-              '';
-            };
-            useridField = mkOption {
-              type = types.str;
-              example = "uid";
-              description = lib.mdDoc ''
-                LDAP field which is a unique identifier for users on HedgeDoc.
-              '';
-            };
-            tlsca = mkOption {
-              type = types.str;
-              default = "/etc/ssl/certs/ca-certificates.crt";
-              example = "server-cert.pem,root.pem";
-              description = lib.mdDoc ''
-                Root CA for LDAP TLS in PEM format.
-              '';
-            };
+          uploadsPath = mkOption {
+            type = types.path;
+            default = "/var/lib/${name}/uploads";
+            defaultText = "/var/lib/hedgedoc/uploads";
+            description = mdDoc ''
+              Directory for storing uploaded images.
+            '';
           };
-        });
-        default = null;
-        description = lib.mdDoc "Configure the LDAP integration.";
-      };
-      saml = mkOption {
-        type = types.nullOr (types.submodule {
-          options = {
-            idpSsoUrl = mkOption {
-              type = types.str;
-              example = "https://idp.example.com/sso";
-              description = lib.mdDoc ''
-                IdP authentication endpoint.
-              '';
-            };
-            idpCert = mkOption {
-              type = types.path;
-              example = "/path/to/cert.pem";
-              description = lib.mdDoc ''
-                Path to IdP certificate file in PEM format.
-              '';
-            };
-            issuer = mkOption {
-              type = types.str;
-              default = "";
-              description = lib.mdDoc ''
-                Optional identity of the service provider.
-                This defaults to the server URL.
-              '';
-            };
-            identifierFormat = mkOption {
-              type = types.str;
-              default = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress";
-              description = lib.mdDoc ''
-                Optional name identifier format.
-              '';
-            };
-            groupAttribute = mkOption {
-              type = types.str;
-              default = "";
-              example = "memberOf";
-              description = lib.mdDoc ''
-                Optional attribute name for group list.
-              '';
-            };
-            externalGroups = mkOption {
-              type = types.listOf types.str;
-              default = [];
-              example = [ "Temporary-staff" "External-users" ];
-              description = lib.mdDoc ''
-                Excluded group names.
-              '';
-            };
-            requiredGroups = mkOption {
-              type = types.listOf types.str;
-              default = [];
-              example = [ "Hedgedoc-Users" ];
-              description = lib.mdDoc ''
-                Required group names.
-              '';
-            };
-            providerName = mkOption {
-              type = types.str;
-              default = "";
-              example = "My institution";
-              description = lib.mdDoc ''
-                Optional name to be displayed at login form indicating the SAML provider.
-              '';
-            };
-            attribute = {
-              id = mkOption {
-                type = types.str;
-                default = "";
-                description = lib.mdDoc ''
-                  Attribute map for `id`.
-                  Defaults to `NameID` of SAML response.
-                '';
-              };
-              username = mkOption {
-                type = types.str;
-                default = "";
-                description = lib.mdDoc ''
-                  Attribute map for `username`.
-                  Defaults to `NameID` of SAML response.
-                '';
-              };
-              email = mkOption {
-                type = types.str;
-                default = "";
-                description = lib.mdDoc ''
-                  Attribute map for `email`.
-                  Defaults to `NameID` of SAML response if
-                  {option}`identifierFormat` has
-                  the default value.
-                '';
-              };
-            };
+
+          # Declared because we change the default to false.
+          allowGravatar = mkOption {
+            type = types.bool;
+            default = false;
+            example = true;
+            description = mdDoc ''
+              Whether to enable [Libravatar](https://wiki.libravatar.org/) as
+              profile picture source on your instance.
+
+              Despite the naming of the setting, Hedgedoc replaced Gravatar
+              with Libravatar in [CodiMD 1.4.0](https://hedgedoc.org/releases/1.4.0/)
+            '';
           };
-        });
-        default = null;
-        description = lib.mdDoc "Configure the SAML integration.";
-      };
-    }; in lib.mkOption {
-      type = lib.types.submodule {
-        freeformType = settingsFormat.type;
-        inherit options;
+        };
       };
-      description = lib.mdDoc ''
+
+      description = mdDoc ''
         HedgeDoc configuration, see
         <https://docs.hedgedoc.org/configuration/>
         for documentation.
@@ -1003,7 +202,7 @@ in
       type = with types; nullOr path;
       default = null;
       example = "/var/lib/hedgedoc/hedgedoc.env";
-      description = lib.mdDoc ''
+      description = mdDoc ''
         Environment file as defined in {manpage}`systemd.exec(5)`.
 
         Secrets may be passed to the service without adding them to the world-readable
@@ -1028,45 +227,94 @@ in
     };
   };
 
-  config = mkIf cfg.enable {
-    assertions = [
-      { assertion = cfg.settings.db == {} -> (
-          cfg.settings.dbURL != "" && cfg.settings.dbURL != null
-        );
-        message = "Database configuration for HedgeDoc missing."; }
-    ];
-    users.groups.${name} = {};
+  config = lib.mkIf cfg.enable {
+    users.groups.${name} = { };
     users.users.${name} = {
       description = "HedgeDoc service user";
       group = name;
-      extraGroups = cfg.groups;
-      home = cfg.workDir;
-      createHome = true;
       isSystemUser = true;
     };
 
+    services.hedgedoc.settings = {
+      defaultNotePath = lib.mkDefault "${cfg.package}/public/default.md";
+      docsPath = lib.mkDefault "${cfg.package}/public/docs";
+      viewPath = lib.mkDefault "${cfg.package}/public/views";
+    };
+
     systemd.services.hedgedoc = {
       description = "HedgeDoc Service";
+      documentation = [ "https://docs.hedgedoc.org/" ];
       wantedBy = [ "multi-user.target" ];
       after = [ "networking.target" ];
-      preStart = ''
-        ${pkgs.envsubst}/bin/envsubst \
-          -o ${cfg.workDir}/config.json \
-          -i ${prettyJSON cfg.settings}
-        mkdir -p ${cfg.settings.uploadsPath}
-      '';
+      preStart =
+        let
+          configFile = settingsFormat.generate "hedgedoc-config.json" {
+            production = cfg.settings;
+          };
+        in
+        ''
+          ${pkgs.envsubst}/bin/envsubst \
+            -o /run/${name}/config.json \
+            -i ${configFile}
+          ${pkgs.coreutils}/bin/mkdir -p ${cfg.settings.uploadsPath}
+        '';
       serviceConfig = {
-        WorkingDirectory = cfg.workDir;
-        StateDirectory = [ cfg.workDir cfg.settings.uploadsPath ];
-        ExecStart = "${lib.getExe cfg.package}";
-        EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
+        User = name;
+        Group = name;
+
+        Restart = "always";
+        ExecStart = "${cfg.package}/bin/hedgedoc";
+        RuntimeDirectory = [ name ];
+        StateDirectory = [ name ];
+        WorkingDirectory = "/run/${name}";
+        ReadWritePaths = [
+          "-${cfg.settings.uploadsPath}"
+        ] ++ lib.optionals (cfg.settings.db ? "storage") [ "-${cfg.settings.db.storage}" ];
+        EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
         Environment = [
-          "CMD_CONFIG_FILE=${cfg.workDir}/config.json"
+          "CMD_CONFIG_FILE=/run/${name}/config.json"
           "NODE_ENV=production"
         ];
-        Restart = "always";
-        User = name;
+
+        # Hardening
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+        LockPersonality = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
         PrivateTmp = true;
+        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_INET"
+          "AF_INET6"
+          # Required for connecting to database sockets,
+          # and listening to unix socket at `cfg.settings.path`
+          "AF_UNIX"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SocketBindAllow = lib.mkIf (cfg.settings.path == null) cfg.settings.port;
+        SocketBindDeny = "any";
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged @obsolete"
+          "@pkey"
+        ];
+        UMask = "0007";
       };
     };
   };
diff --git a/nixos/modules/services/web-apps/hledger-web.nix b/nixos/modules/services/web-apps/hledger-web.nix
index 0fc283ff5219..be8ecc645e59 100644
--- a/nixos/modules/services/web-apps/hledger-web.nix
+++ b/nixos/modules/services/web-apps/hledger-web.nix
@@ -7,7 +7,7 @@ in {
 
     enable = mkEnableOption (lib.mdDoc "hledger-web service");
 
-    serveApi = mkEnableOption (lib.mdDoc "Serve only the JSON web API, without the web UI");
+    serveApi = mkEnableOption (lib.mdDoc "serving only the JSON web API, without the web UI");
 
     host = mkOption {
       type = types.str;
diff --git a/nixos/modules/services/web-apps/isso.nix b/nixos/modules/services/web-apps/isso.nix
index 1a852ec352f2..6cb2d9ec785e 100644
--- a/nixos/modules/services/web-apps/isso.nix
+++ b/nixos/modules/services/web-apps/isso.nix
@@ -12,11 +12,11 @@ in {
   options = {
     services.isso = {
       enable = mkEnableOption (lib.mdDoc ''
-        A commenting server similar to Disqus.
+        isso, a commenting server similar to Disqus.
 
         Note: The application's author suppose to run isso behind a reverse proxy.
         The embedded solution offered by NixOS is also only suitable for small installations
-        below 20 requests per second.
+        below 20 requests per second
       '');
 
       settings = mkOption {
diff --git a/nixos/modules/services/web-apps/jitsi-meet.nix b/nixos/modules/services/web-apps/jitsi-meet.nix
index 3825b03c2449..21416be35877 100644
--- a/nixos/modules/services/web-apps/jitsi-meet.nix
+++ b/nixos/modules/services/web-apps/jitsi-meet.nix
@@ -105,9 +105,9 @@ in
         type = bool;
         default = true;
         description = lib.mdDoc ''
-          Whether to enable Jitsi Videobridge instance and configure it to connect to Prosody.
+          Jitsi Videobridge instance and configure it to connect to Prosody.
 
-          Additional configuration is possible with {option}`services.jitsi-videobridge`.
+          Additional configuration is possible with {option}`services.jitsi-videobridge`
         '';
       };
 
diff --git a/nixos/modules/services/web-apps/lanraragi.nix b/nixos/modules/services/web-apps/lanraragi.nix
new file mode 100644
index 000000000000..f1ab8b8b4eb4
--- /dev/null
+++ b/nixos/modules/services/web-apps/lanraragi.nix
@@ -0,0 +1,100 @@
+{ pkgs, lib, config, ... }:
+
+let
+  cfg = config.services.lanraragi;
+in
+{
+  meta.maintainers = with lib.maintainers; [ tomasajt ];
+
+  options.services = {
+    lanraragi = {
+      enable = lib.mkEnableOption (lib.mdDoc "LANraragi");
+      package = lib.mkPackageOptionMD pkgs "lanraragi" { };
+
+      port = lib.mkOption {
+        type = lib.types.port;
+        default = 3000;
+        description = lib.mdDoc "Port for LANraragi's web interface.";
+      };
+
+      passwordFile = lib.mkOption {
+        type = lib.types.nullOr lib.types.path;
+        default = null;
+        example = "/run/keys/lanraragi-password";
+        description = lib.mdDoc ''
+          A file containing the password for LANraragi's admin interface.
+        '';
+      };
+
+      redis = {
+        port = lib.mkOption {
+          type = lib.types.port;
+          default = 6379;
+          description = lib.mdDoc "Port for LANraragi's Redis server.";
+        };
+        passwordFile = lib.mkOption {
+          type = lib.types.nullOr lib.types.path;
+          default = null;
+          example = "/run/keys/redis-lanraragi-password";
+          description = lib.mdDoc ''
+            A file containing the password for LANraragi's Redis server.
+          '';
+        };
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.redis.servers.lanraragi = {
+      enable = true;
+      port = cfg.redis.port;
+      requirePassFile = cfg.redis.passwordFile;
+    };
+
+    systemd.services.lanraragi = {
+      description = "LANraragi main service";
+      after = [ "network.target" "redis-lanraragi.service" ];
+      requires = [ "redis-lanraragi.service" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = lib.getExe cfg.package;
+        DynamicUser = true;
+        StateDirectory = "lanraragi";
+        RuntimeDirectory = "lanraragi";
+        LogsDirectory = "lanraragi";
+        Restart = "on-failure";
+        WorkingDirectory = "/var/lib/lanraragi";
+      };
+      environment = {
+        "LRR_TEMP_DIRECTORY" = "/run/lanraragi";
+        "LRR_LOG_DIRECTORY" = "/var/log/lanraragi";
+        "LRR_NETWORK" = "http://*:${toString cfg.port}";
+        "HOME" = "/var/lib/lanraragi";
+      };
+      preStart = ''
+        REDIS_PASS=${lib.optionalString (cfg.redis.passwordFile != null) "$(head -n1 ${cfg.redis.passwordFile})"}
+        cat > lrr.conf <<EOF
+        {
+          redis_address => "127.0.0.1:${toString cfg.redis.port}",
+          redis_password => "$REDIS_PASS",
+          redis_database => "0",
+          redis_database_minion => "1",
+          redis_database_config => "2",
+          redis_database_search => "3",
+        }
+        EOF
+      '' + lib.optionalString (cfg.passwordFile != null) ''
+        PASS_HASH=$(
+          PASS=$(head -n1 ${cfg.passwordFile}) ${cfg.package.perlEnv}/bin/perl -I${cfg.package}/share/lanraragi/lib -e \
+            'use LANraragi::Controller::Config; print LANraragi::Controller::Config::make_password_hash($ENV{PASS})' \
+            2>/dev/null
+        )
+
+        ${lib.getExe pkgs.redis} -h 127.0.0.1 -p ${toString cfg.redis.port} -a "$REDIS_PASS" <<EOF
+          SELECT 2
+          HSET LRR_CONFIG password $PASS_HASH
+        EOF
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/meme-bingo-web.nix b/nixos/modules/services/web-apps/meme-bingo-web.nix
index cb864321ef27..652dc8840252 100644
--- a/nixos/modules/services/web-apps/meme-bingo-web.nix
+++ b/nixos/modules/services/web-apps/meme-bingo-web.nix
@@ -8,9 +8,9 @@ in {
   options = {
     services.meme-bingo-web = {
       enable = mkEnableOption (mdDoc ''
-        A web app for the meme bingo, rendered entirely on the web server and made interactive with forms.
+        a web app for the meme bingo, rendered entirely on the web server and made interactive with forms.
 
-        Note: The application's author suppose to run meme-bingo-web behind a reverse proxy for SSL and HTTP/3.
+        Note: The application's author suppose to run meme-bingo-web behind a reverse proxy for SSL and HTTP/3
       '');
 
       package = mkOption {
diff --git a/nixos/modules/services/web-apps/microbin.nix b/nixos/modules/services/web-apps/microbin.nix
new file mode 100644
index 000000000000..233bfac6e699
--- /dev/null
+++ b/nixos/modules/services/web-apps/microbin.nix
@@ -0,0 +1,93 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.microbin;
+in
+{
+  options.services.microbin = {
+    enable = lib.mkEnableOption (lib.mdDoc "MicroBin is a super tiny, feature rich, configurable paste bin web application");
+
+    package = lib.mkPackageOption pkgs "microbin" { };
+
+    settings = lib.mkOption {
+      type = lib.types.submodule { freeformType = with lib.types; attrsOf (oneOf [ bool int str ]); };
+      default = { };
+      example = {
+        MICROBIN_PORT = 8080;
+        MICROBIN_HIDE_LOGO = false;
+      };
+      description = lib.mdDoc ''
+        Additional configuration for MicroBin, see
+        <https://microbin.eu/docs/installation-and-configuration/configuration/>
+        for supported values.
+
+        For secrets use passwordFile option instead.
+      '';
+    };
+
+    dataDir = lib.mkOption {
+      type = lib.types.str;
+      default = "/var/lib/microbin";
+      description = lib.mdDoc "Default data folder for MicroBin.";
+    };
+
+    passwordFile = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      example = "/run/secrets/microbin.env";
+      description = lib.mdDoc ''
+        Path to file containing environment variables.
+        Useful for passing down secrets.
+        Variables that can be considered secrets are:
+         - MICROBIN_BASIC_AUTH_USERNAME
+         - MICROBIN_BASIC_AUTH_PASSWORD
+         - MICROBIN_ADMIN_USERNAME
+         - MICROBIN_ADMIN_PASSWORD
+         - MICROBIN_UPLOADER_PASSWORD
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.microbin.settings = with lib; {
+      MICROBIN_BIND = mkDefault "0.0.0.0";
+      MICROBIN_DISABLE_TELEMETRY = mkDefault true;
+      MICROBIN_LIST_SERVER = mkDefault false;
+      MICROBIN_PORT = mkDefault "8080";
+    };
+
+    systemd.services.microbin = {
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment = lib.mapAttrs (_: v: if lib.isBool v then lib.boolToString v else toString v) cfg.settings;
+      serviceConfig = {
+        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+        DevicePolicy = "closed";
+        DynamicUser = true;
+        EnvironmentFile = lib.optional (cfg.passwordFile != null) cfg.passwordFile;
+        ExecStart = "${cfg.package}/bin/microbin";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ReadWritePaths = cfg.dataDir;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        StateDirectory = "microbin";
+        SystemCallArchitectures = [ "native" ];
+        SystemCallFilter = [ "@system-service" ];
+        WorkingDirectory = cfg.dataDir;
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ surfaceflinger ];
+}
diff --git a/nixos/modules/services/web-apps/nextcloud.md b/nixos/modules/services/web-apps/nextcloud.md
index cbd7b5b3d066..a25bed30e47f 100644
--- a/nixos/modules/services/web-apps/nextcloud.md
+++ b/nixos/modules/services/web-apps/nextcloud.md
@@ -119,13 +119,7 @@ Auto updates for Nextcloud apps can be enabled using
   - **Server-side encryption.**
     Nextcloud supports [server-side encryption (SSE)](https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/encryption_configuration.html).
     This is not an end-to-end encryption, but can be used to encrypt files that will be persisted
-    to external storage such as S3. Please note that this won't work anymore when using OpenSSL 3
-    for PHP's openssl extension and **Nextcloud 25 or older** because this is implemented using the
-    legacy cipher RC4. For Nextcloud26 this isn't relevant anymore, because Nextcloud has an RC4 implementation
-    written in native PHP and thus doesn't need `ext-openssl` for that anymore.
-    If [](#opt-system.stateVersion) is *above* `22.05`,
-    this is disabled by default. To turn it on again and for further information please refer to
-    [](#opt-services.nextcloud.enableBrokenCiphersForSSE).
+    to external storage such as S3.
 
 ## Using an alternative webserver as reverse-proxy (e.g. `httpd`) {#module-services-nextcloud-httpd}
 
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index e0a7e7d4859c..f9713cac47e9 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -27,13 +27,7 @@ let
 
   phpPackage = cfg.phpPackage.buildEnv {
     extensions = { enabled, all }:
-      (with all;
-        # disable default openssl extension
-        (lib.filter (e: e.pname != "php-openssl") enabled)
-        # use OpenSSL 1.1 for RC4 Nextcloud encryption if user
-        # has acknowledged the brokenness of the ciphers (RC4).
-        # TODO: remove when https://github.com/nextcloud/server/issues/32003 is fixed.
-        ++ (if cfg.enableBrokenCiphersForSSE then [ cfg.phpPackage.extensions.openssl-legacy ] else [ cfg.phpPackage.extensions.openssl ])
+      (with all; enabled
         ++ optional cfg.enableImagemagick imagick
         # Optionally enabled depending on caching settings
         ++ optional cfg.caching.apcu apcu
@@ -66,6 +60,9 @@ let
   mysqlLocal = cfg.database.createLocally && cfg.config.dbtype == "mysql";
   pgsqlLocal = cfg.database.createLocally && cfg.config.dbtype == "pgsql";
 
+  # https://github.com/nextcloud/documentation/pull/11179
+  ocmProviderIsNotAStaticDirAnymore = versionAtLeast cfg.package.version "27.1.2";
+
 in {
 
   imports = [
@@ -87,6 +84,10 @@ in {
       Further details about this can be found in the `Nextcloud`-section of the NixOS-manual
       (which can be opened e.g. by running `nixos-help`).
     '')
+    (mkRemovedOptionModule [ "services" "nextcloud" "enableBrokenCiphersForSSE" ] ''
+      This option has no effect since there's no supported Nextcloud version packaged here
+      using OpenSSL for RC4 SSE.
+    '')
     (mkRemovedOptionModule [ "services" "nextcloud" "disableImagemagick" ] ''
       Use services.nextcloud.enableImagemagick instead.
     '')
@@ -95,39 +96,6 @@ in {
   options.services.nextcloud = {
     enable = mkEnableOption (lib.mdDoc "nextcloud");
 
-    enableBrokenCiphersForSSE = mkOption {
-      type = types.bool;
-      default = versionOlder stateVersion "22.11";
-      defaultText = literalExpression "versionOlder system.stateVersion \"22.11\"";
-      description = lib.mdDoc ''
-        This option enables using the OpenSSL PHP extension linked against OpenSSL 1.1
-        rather than latest OpenSSL (≥ 3), this is not recommended unless you need
-        it for server-side encryption (SSE). SSE uses the legacy RC4 cipher which is
-        considered broken for several years now. See also [RFC7465](https://datatracker.ietf.org/doc/html/rfc7465).
-
-        This cipher has been disabled in OpenSSL ≥ 3 and requires
-        a specific legacy profile to re-enable it.
-
-        If you deploy Nextcloud using OpenSSL ≥ 3 for PHP and have
-        server-side encryption configured, you will not be able to access
-        your files anymore. Enabling this option can restore access to your files.
-        Upon testing we didn't encounter any data corruption when turning
-        this on and off again, but this cannot be guaranteed for
-        each Nextcloud installation.
-
-        It is `true` by default for systems with a [](#opt-system.stateVersion) below
-        `22.11` to make sure that existing installations won't break on update. On newer
-        NixOS systems you have to explicitly enable it on your own.
-
-        Please note that this only provides additional value when using
-        external storage such as S3 since it's not an end-to-end encryption.
-        If this is not the case,
-        it is advised to [disable server-side encryption](https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/encryption_configuration.html#disabling-encryption) and set this to `false`.
-
-        In the future, Nextcloud may move to AES-256-GCM, by then,
-        this option will be removed.
-      '';
-    };
     hostName = mkOption {
       type = types.str;
       description = lib.mdDoc "FQDN for the nextcloud instance.";
@@ -225,7 +193,7 @@ in {
     package = mkOption {
       type = types.package;
       description = lib.mdDoc "Which package to use for the Nextcloud instance.";
-      relatedPackages = [ "nextcloud25" "nextcloud26" "nextcloud27" ];
+      relatedPackages = [ "nextcloud26" "nextcloud27" ];
     };
     phpPackage = mkOption {
       type = types.package;
@@ -740,28 +708,7 @@ in {
         '')
         ++ (optional (versionOlder cfg.package.version "25") (upgradeWarning 24 "22.11"))
         ++ (optional (versionOlder cfg.package.version "26") (upgradeWarning 25 "23.05"))
-        ++ (optional (versionOlder cfg.package.version "27") (upgradeWarning 26 "23.11"))
-        ++ (optional cfg.enableBrokenCiphersForSSE ''
-          You're using PHP's openssl extension built against OpenSSL 1.1 for Nextcloud.
-          This is only necessary if you're using Nextcloud's server-side encryption.
-          Please keep in mind that it's using the broken RC4 cipher.
-
-          If you don't use that feature, you can switch to OpenSSL 3 and get
-          rid of this warning by declaring
-
-            services.nextcloud.enableBrokenCiphersForSSE = false;
-
-          If you need to use server-side encryption you can ignore this warning.
-          Otherwise you'd have to disable server-side encryption first in order
-          to be able to safely disable this option and get rid of this warning.
-          See <https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/encryption_configuration.html#disabling-encryption> on how to achieve this.
-
-          For more context, here is the implementing pull request: https://github.com/NixOS/nixpkgs/pull/198470
-        '')
-        ++ (optional (cfg.enableBrokenCiphersForSSE && versionAtLeast cfg.package.version "26") ''
-          Nextcloud26 supports RC4 without requiring legacy OpenSSL, so
-          `services.nextcloud.enableBrokenCiphersForSSE` can be set to `false`.
-        '');
+        ++ (optional (versionOlder cfg.package.version "27") (upgradeWarning 26 "23.11"));
 
       services.nextcloud.package = with pkgs;
         mkDefault (
@@ -1136,10 +1083,6 @@ in {
               }
             '';
           };
-          "/" = {
-            priority = 900;
-            extraConfig = "rewrite ^ /index.php;";
-          };
           "~ ^/store-apps" = {
             priority = 201;
             extraConfig = "root ${cfg.home};";
@@ -1164,15 +1107,23 @@ in {
               try_files $uri $uri/ =404;
             '';
           };
-          "~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)".extraConfig = ''
-            return 404;
-          '';
-          "~ ^/(?:\\.(?!well-known)|autotest|occ|issue|indie|db_|console)".extraConfig = ''
-            return 404;
-          '';
-          "~ ^\\/(?:index|remote|public|cron|core\\/ajax\\/update|status|ocs\\/v[12]|updater\\/.+|oc[ms]-provider\\/.+|.+\\/richdocumentscode\\/proxy)\\.php(?:$|\\/)" = {
+          "~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)" = {
+            priority = 450;
+            extraConfig = ''
+              return 404;
+            '';
+          };
+          "~ ^/(?:\\.|autotest|occ|issue|indie|db_|console)" = {
+            priority = 450;
+            extraConfig = ''
+              return 404;
+            '';
+          };
+          "~ \\.php(?:$|/)" = {
             priority = 500;
             extraConfig = ''
+              # legacy support (i.e. static files and directories in cfg.package)
+              rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[s${optionalString (!ocmProviderIsNotAStaticDirAnymore) "m"}]-provider\/.+|.+\/richdocumentscode\/proxy) /index.php$request_uri;
               include ${config.services.nginx.package}/conf/fastcgi.conf;
               fastcgi_split_path_info ^(.+?\.php)(\\/.*)$;
               set $path_info $fastcgi_path_info;
@@ -1188,19 +1139,30 @@ in {
               fastcgi_read_timeout ${builtins.toString cfg.fastcgiTimeout}s;
             '';
           };
-          "~ \\.(?:css|js|woff2?|svg|gif|map)$".extraConfig = ''
+          "~ \\.(?:css|js|mjs|svg|gif|png|jpg|jpeg|ico|wasm|tflite|map|html|ttf|bcmap|mp4|webm)$".extraConfig = ''
             try_files $uri /index.php$request_uri;
             expires 6M;
             access_log off;
+            location ~ \.wasm$ {
+              default_type application/wasm;
+            }
           '';
-          "~ ^\\/(?:updater|ocs-provider|ocm-provider)(?:$|\\/)".extraConfig = ''
+          "~ ^\\/(?:updater|ocs-provider${optionalString (!ocmProviderIsNotAStaticDirAnymore) "|ocm-provider"})(?:$|\\/)".extraConfig = ''
             try_files $uri/ =404;
             index index.php;
           '';
-          "~ \\.(?:png|html|ttf|ico|jpg|jpeg|bcmap|mp4|webm)$".extraConfig = ''
-            try_files $uri /index.php$request_uri;
-            access_log off;
-          '';
+          "/remote" = {
+            priority = 1500;
+            extraConfig = ''
+              return 301 /remote.php$request_uri;
+            '';
+          };
+          "/" = {
+            priority = 1600;
+            extraConfig = ''
+              try_files $uri $uri/ /index.php$request_uri;
+            '';
+          };
         };
         extraConfig = ''
           index index.php index.html /index.php$request_uri;
diff --git a/nixos/modules/services/web-apps/outline.nix b/nixos/modules/services/web-apps/outline.nix
index 1d8298963e6d..0e3bd07c1fc1 100644
--- a/nixos/modules/services/web-apps/outline.nix
+++ b/nixos/modules/services/web-apps/outline.nix
@@ -117,13 +117,14 @@ in
     storage = lib.mkOption {
       description = lib.mdDoc ''
         To support uploading of images for avatars and document attachments an
-        s3-compatible storage must be provided. AWS S3 is recommended for
+        s3-compatible storage can be provided. AWS S3 is recommended for
         redundancy however if you want to keep all file storage local an
         alternative such as [minio](https://github.com/minio/minio)
         can be used.
+        Local filesystem storage can also be used.
 
-        A more detailed guide on setting up S3 is available
-        [here](https://wiki.generaloutline.com/share/125de1cc-9ff6-424b-8415-0d58c809a40f).
+        A more detailed guide on setting up storage is available
+        [here](https://docs.getoutline.com/s/hosting/doc/file-storage-N4M0T6Ypu7).
       '';
       example = lib.literalExpression ''
         {
@@ -136,6 +137,19 @@ in
       '';
       type = lib.types.submodule {
         options = {
+          storageType = lib.mkOption {
+            type = lib.types.enum [ "local" "s3" ];
+            description = lib.mdDoc "File storage type, it can be local or s3.";
+            default = "s3";
+          };
+          localRootDir = lib.mkOption {
+            type = lib.types.str;
+            description = lib.mdDoc ''
+              If `storageType` is `local`, this sets the parent directory
+              under which all attachments/images go.
+            '';
+            default = "/var/lib/outline/data";
+          };
           accessKey = lib.mkOption {
             type = lib.types.str;
             description = lib.mdDoc "S3 access key.";
@@ -557,7 +571,10 @@ in
     systemd.tmpfiles.rules = [
       "f ${cfg.secretKeyFile} 0600 ${cfg.user} ${cfg.group} -"
       "f ${cfg.utilsSecretFile} 0600 ${cfg.user} ${cfg.group} -"
-      "f ${cfg.storage.secretKeyFile} 0600 ${cfg.user} ${cfg.group} -"
+      (if (cfg.storage.storageType == "s3") then
+        "f ${cfg.storage.secretKeyFile} 0600 ${cfg.user} ${cfg.group} -"
+      else
+        "d ${cfg.storage.localRootDir} 0700 ${cfg.user} ${cfg.group} - -")
     ];
 
     services.postgresql = lib.mkIf (cfg.databaseUrl == "local") {
@@ -599,14 +616,6 @@ in
           URL = cfg.publicUrl;
           PORT = builtins.toString cfg.port;
 
-          AWS_ACCESS_KEY_ID = cfg.storage.accessKey;
-          AWS_REGION = cfg.storage.region;
-          AWS_S3_UPLOAD_BUCKET_URL = cfg.storage.uploadBucketUrl;
-          AWS_S3_UPLOAD_BUCKET_NAME = cfg.storage.uploadBucketName;
-          AWS_S3_UPLOAD_MAX_SIZE = builtins.toString cfg.storage.uploadMaxSize;
-          AWS_S3_FORCE_PATH_STYLE = builtins.toString cfg.storage.forcePathStyle;
-          AWS_S3_ACL = cfg.storage.acl;
-
           CDN_URL = cfg.cdnUrl;
           FORCE_HTTPS = builtins.toString cfg.forceHttps;
           ENABLE_UPDATES = builtins.toString cfg.enableUpdateCheck;
@@ -622,8 +631,21 @@ in
           RATE_LIMITER_ENABLED = builtins.toString cfg.rateLimiter.enable;
           RATE_LIMITER_REQUESTS = builtins.toString cfg.rateLimiter.requests;
           RATE_LIMITER_DURATION_WINDOW = builtins.toString cfg.rateLimiter.durationWindow;
+
+          FILE_STORAGE = cfg.storage.storageType;
+          FILE_STORAGE_UPLOAD_MAX_SIZE = builtins.toString cfg.storage.uploadMaxSize;
+          FILE_STORAGE_LOCAL_ROOT_DIR = cfg.storage.localRootDir;
         }
 
+        (lib.mkIf (cfg.storage.storageType == "s3") {
+          AWS_ACCESS_KEY_ID = cfg.storage.accessKey;
+          AWS_REGION = cfg.storage.region;
+          AWS_S3_UPLOAD_BUCKET_URL = cfg.storage.uploadBucketUrl;
+          AWS_S3_UPLOAD_BUCKET_NAME = cfg.storage.uploadBucketName;
+          AWS_S3_FORCE_PATH_STYLE = builtins.toString cfg.storage.forcePathStyle;
+          AWS_S3_ACL = cfg.storage.acl;
+        })
+
         (lib.mkIf (cfg.slackAuthentication != null) {
           SLACK_CLIENT_ID = cfg.slackAuthentication.clientId;
         })
@@ -676,7 +698,9 @@ in
       script = ''
         export SECRET_KEY="$(head -n1 ${lib.escapeShellArg cfg.secretKeyFile})"
         export UTILS_SECRET="$(head -n1 ${lib.escapeShellArg cfg.utilsSecretFile})"
-        export AWS_SECRET_ACCESS_KEY="$(head -n1 ${lib.escapeShellArg cfg.storage.secretKeyFile})"
+        ${lib.optionalString (cfg.storage.storageType == "s3") ''
+          export AWS_SECRET_ACCESS_KEY="$(head -n1 ${lib.escapeShellArg cfg.storage.secretKeyFile})"
+        ''}
         ${lib.optionalString (cfg.slackAuthentication != null) ''
           export SLACK_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.slackAuthentication.secretFile})"
         ''}
diff --git a/nixos/modules/services/web-apps/peering-manager.nix b/nixos/modules/services/web-apps/peering-manager.nix
index 55bf0da7b7b9..7012df6dffbf 100644
--- a/nixos/modules/services/web-apps/peering-manager.nix
+++ b/nixos/modules/services/web-apps/peering-manager.nix
@@ -35,7 +35,15 @@ in {
         Enable Peering Manager.
 
         This module requires a reverse proxy that serves `/static` separately.
-        See this [example](https://github.com/peering-manager-community/peering-manager/blob/develop/contrib/nginx.conf/) on how to configure this.
+        See this [example](https://github.com/peering-manager/contrib/blob/main/nginx.conf on how to configure this.
+      '';
+    };
+
+    enableScheduledTasks = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Set up [scheduled tasks](https://peering-manager.readthedocs.io/en/stable/setup/8-scheduled-tasks/)
       '';
     };
 
@@ -194,32 +202,30 @@ in {
     };
 
     systemd.services = let
-      defaultServiceConfig = {
-        WorkingDirectory = "/var/lib/peering-manager";
-        User = "peering-manager";
-        Group = "peering-manager";
-        StateDirectory = "peering-manager";
-        StateDirectoryMode = "0750";
-        Restart = "on-failure";
+      defaults = {
+        environment = {
+          PYTHONPATH = pkg.pythonPath;
+        };
+        serviceConfig = {
+          WorkingDirectory = "/var/lib/peering-manager";
+          User = "peering-manager";
+          Group = "peering-manager";
+          StateDirectory = "peering-manager";
+          StateDirectoryMode = "0750";
+          Restart = "on-failure";
+        };
       };
     in {
-      peering-manager-migration = {
+      peering-manager-migration = lib.recursiveUpdate defaults {
         description = "Peering Manager migrations";
         wantedBy = [ "peering-manager.target" ];
-
-        environment = {
-          PYTHONPATH = pkg.pythonPath;
-        };
-
-        serviceConfig = defaultServiceConfig // {
+        serviceConfig = {
           Type = "oneshot";
-          ExecStart = ''
-            ${pkg}/bin/peering-manager migrate
-          '';
+          ExecStart = "${pkg}/bin/peering-manager migrate";
         };
       };
 
-      peering-manager = {
+      peering-manager = lib.recursiveUpdate defaults {
         description = "Peering Manager WSGI Service";
         wantedBy = [ "peering-manager.target" ];
         after = [ "peering-manager-migration.service" ];
@@ -228,11 +234,7 @@ in {
           ${pkg}/bin/peering-manager remove_stale_contenttypes --no-input
         '';
 
-        environment = {
-          PYTHONPATH = pkg.pythonPath;
-        };
-
-        serviceConfig = defaultServiceConfig // {
+        serviceConfig = {
           ExecStart = ''
             ${pkg.python.pkgs.gunicorn}/bin/gunicorn peering_manager.wsgi \
               --bind ${cfg.listenAddress}:${toString cfg.port} \
@@ -241,45 +243,92 @@ in {
         };
       };
 
-      peering-manager-rq = {
+      peering-manager-rq = lib.recursiveUpdate defaults {
         description = "Peering Manager Request Queue Worker";
         wantedBy = [ "peering-manager.target" ];
         after = [ "peering-manager.service" ];
+        serviceConfig.ExecStart = "${pkg}/bin/peering-manager rqworker high default low";
+      };
 
-        environment = {
-          PYTHONPATH = pkg.pythonPath;
+      peering-manager-housekeeping = lib.recursiveUpdate defaults {
+        description = "Peering Manager housekeeping job";
+        after = [ "peering-manager.service" ];
+        serviceConfig = {
+          Type = "oneshot";
+          ExecStart = "${pkg}/bin/peering-manager housekeeping";
         };
+      };
 
-        serviceConfig = defaultServiceConfig // {
-          ExecStart = ''
-            ${pkg}/bin/peering-manager rqworker high default low
-          '';
+      peering-manager-peeringdb-sync = lib.recursiveUpdate defaults {
+        description = "PeeringDB sync";
+        after = [ "peering-manager.service" ];
+        serviceConfig = {
+          Type = "oneshot";
+          ExecStart = "${pkg}/bin/peering-manager peeringdb_sync";
         };
       };
 
-      peering-manager-housekeeping = {
-        description = "Peering Manager housekeeping job";
+      peering-manager-prefix-fetch = lib.recursiveUpdate defaults {
+        description = "Fetch IRR AS-SET prefixes";
         after = [ "peering-manager.service" ];
+        serviceConfig = {
+          Type = "oneshot";
+          ExecStart = "${pkg}/bin/peering-manager grab_prefixes";
+        };
+      };
 
-        environment = {
-          PYTHONPATH = pkg.pythonPath;
+      peering-manager-configuration-deployment = lib.recursiveUpdate defaults {
+        description = "Push configuration to routers";
+        after = [ "peering-manager.service" ];
+        serviceConfig = {
+          Type = "oneshot";
+          ExecStart = "${pkg}/bin/peering-manager configure_routers";
         };
+      };
 
-        serviceConfig = defaultServiceConfig // {
+      peering-manager-session-poll = lib.recursiveUpdate defaults {
+        description = "Poll peering sessions from routers";
+        after = [ "peering-manager.service" ];
+        serviceConfig = {
           Type = "oneshot";
-          ExecStart = ''
-            ${pkg}/bin/peering-manager housekeeping
-          '';
+          ExecStart = "${pkg}/bin/peering-manager poll_bgp_sessions --all";
         };
       };
     };
 
-    systemd.timers.peering-manager-housekeeping = {
-      description = "Run Peering Manager housekeeping job";
-      wantedBy = [ "timers.target" ];
+    systemd.timers = {
+      peering-manager-housekeeping = {
+        description = "Run Peering Manager housekeeping job";
+        wantedBy = [ "timers.target" ];
+        timerConfig.OnCalendar = "daily";
+      };
+
+      peering-manager-peeringdb-sync = {
+        enable = lib.mkDefault cfg.enableScheduledTasks;
+        description = "Sync PeeringDB at 2:30";
+        wantedBy = [ "timers.target" ];
+        timerConfig.OnCalendar = "02:30:00";
+      };
+
+      peering-manager-prefix-fetch = {
+        enable = lib.mkDefault cfg.enableScheduledTasks;
+        description = "Fetch IRR AS-SET prefixes at 4:30";
+        wantedBy = [ "timers.target" ];
+        timerConfig.OnCalendar = "04:30:00";
+      };
+
+      peering-manager-configuration-deployment = {
+        enable = lib.mkDefault cfg.enableScheduledTasks;
+        description = "Push router configuration every hour 5 minutes before full hour";
+        wantedBy = [ "timers.target" ];
+        timerConfig.OnCalendar = "*:55:00";
+      };
 
-      timerConfig = {
-        OnCalendar = "daily";
+      peering-manager-session-poll = {
+        enable = lib.mkDefault cfg.enableScheduledTasks;
+        description = "Poll peering sessions from routers every hour";
+        wantedBy = [ "timers.target" ];
+        timerConfig.OnCalendar = "*:00:00";
       };
     };
 
diff --git a/nixos/modules/services/web-apps/phylactery.nix b/nixos/modules/services/web-apps/phylactery.nix
index 4801bd203b48..723b38ee75d9 100644
--- a/nixos/modules/services/web-apps/phylactery.nix
+++ b/nixos/modules/services/web-apps/phylactery.nix
@@ -4,7 +4,7 @@ with lib;
 let cfg = config.services.phylactery;
 in {
   options.services.phylactery = {
-    enable = mkEnableOption (lib.mdDoc "Whether to enable Phylactery server");
+    enable = mkEnableOption (lib.mdDoc "Phylactery server");
 
     host = mkOption {
       type = types.str;
diff --git a/nixos/modules/services/web-apps/plausible.nix b/nixos/modules/services/web-apps/plausible.nix
index 84914b7ad11c..576b54a7edf2 100644
--- a/nixos/modules/services/web-apps/plausible.nix
+++ b/nixos/modules/services/web-apps/plausible.nix
@@ -296,6 +296,6 @@ in {
     ];
   };
 
-  meta.maintainers = with maintainers; [ ma27 ];
+  meta.maintainers = with maintainers; [ ];
   meta.doc = ./plausible.md;
 }
diff --git a/nixos/modules/services/web-apps/shiori.nix b/nixos/modules/services/web-apps/shiori.nix
index f0505e052e1c..71b5ad4d4c06 100644
--- a/nixos/modules/services/web-apps/shiori.nix
+++ b/nixos/modules/services/web-apps/shiori.nix
@@ -29,6 +29,13 @@ in {
         default = 8080;
         description = lib.mdDoc "The port of the Shiori web application";
       };
+
+      webRoot = mkOption {
+        type = types.str;
+        default = "/";
+        example = "/shiori";
+        description = lib.mdDoc "The root of the Shiori web application";
+      };
     };
   };
 
@@ -40,7 +47,7 @@ in {
       environment.SHIORI_DIR = "/var/lib/shiori";
 
       serviceConfig = {
-        ExecStart = "${package}/bin/shiori serve --address '${address}' --port '${toString port}'";
+        ExecStart = "${package}/bin/shiori serve --address '${address}' --port '${toString port}' --webroot '${webRoot}'";
 
         DynamicUser = true;
         StateDirectory = "shiori";
diff --git a/nixos/modules/services/web-apps/snipe-it.nix b/nixos/modules/services/web-apps/snipe-it.nix
index e861a4185194..4fbf2bad750b 100644
--- a/nixos/modules/services/web-apps/snipe-it.nix
+++ b/nixos/modules/services/web-apps/snipe-it.nix
@@ -18,19 +18,23 @@ let
   inherit (snipe-it.passthru) phpPackage;
 
   # shell script for local administration
-  artisan = pkgs.writeScriptBin "snipe-it" ''
+  artisan = (pkgs.writeScriptBin "snipe-it" ''
     #! ${pkgs.runtimeShell}
-    cd ${snipe-it}
+    cd "${snipe-it}/share/php/snipe-it"
     sudo=exec
     if [[ "$USER" != ${user} ]]; then
       sudo='exec /run/wrappers/bin/sudo -u ${user}'
     fi
     $sudo ${phpPackage}/bin/php artisan $*
-  '';
+  '').overrideAttrs (old: {
+    meta = old.meta // {
+      mainProgram = "snipe-it";
+    };
+  });
 in {
   options.services.snipe-it = {
 
-    enable = mkEnableOption (lib.mdDoc "A free open source IT asset/license management system");
+    enable = mkEnableOption (lib.mdDoc "snipe-it, a free open source IT asset/license management system");
 
     user = mkOption {
       default = "snipeit";
@@ -357,7 +361,7 @@ in {
     services.nginx = {
       enable = mkDefault true;
       virtualHosts."${cfg.hostName}" = mkMerge [ cfg.nginx {
-        root = mkForce "${snipe-it}/public";
+        root = mkForce "${snipe-it}/share/php/snipe-it/public";
         extraConfig = optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;";
         locations = {
           "/" = {
@@ -394,7 +398,7 @@ in {
         RuntimeDirectory = "snipe-it/cache";
         RuntimeDirectoryMode = "0700";
       };
-      path = [ pkgs.replace-secret ];
+      path = [ pkgs.replace-secret artisan ];
       script =
         let
           isSecret  = v: isAttrs v && v ? _secret && (isString v._secret || builtins.isPath v._secret);
@@ -451,7 +455,7 @@ in {
           rm "${cfg.dataDir}"/bootstrap/cache/*.php || true
 
           # migrate db
-          ${phpPackage}/bin/php artisan migrate --force
+          ${lib.getExe artisan} migrate --force
 
           # A placeholder file for invalid barcodes
           invalid_barcode_location="${cfg.dataDir}/public/uploads/barcodes/invalid_barcode.gif"
diff --git a/nixos/modules/services/web-apps/writefreely.nix b/nixos/modules/services/web-apps/writefreely.nix
index a7671aa717f4..f92afa9276e3 100644
--- a/nixos/modules/services/web-apps/writefreely.nix
+++ b/nixos/modules/services/web-apps/writefreely.nix
@@ -120,7 +120,7 @@ let
     withConfigFile ''
       query () {
         local result=$(${sqlite}/bin/sqlite3 \
-          '${cfg.stateDir}/${settings.database.filename}'
+          '${cfg.stateDir}/${settings.database.filename}' \
           "$1" \
         )
 
diff --git a/nixos/modules/services/web-apps/zitadel.nix b/nixos/modules/services/web-apps/zitadel.nix
index f225d138cc43..99b0a0bc56f6 100644
--- a/nixos/modules/services/web-apps/zitadel.nix
+++ b/nixos/modules/services/web-apps/zitadel.nix
@@ -9,7 +9,7 @@ in
   options.services.zitadel =
     let inherit (lib) mkEnableOption mkOption mkPackageOption types;
     in {
-      enable = mkEnableOption "ZITADEL, a user and identity access management platform.";
+      enable = mkEnableOption "ZITADEL, a user and identity access management platform";
 
       package = mkPackageOption pkgs "ZITADEL" { default = [ "zitadel" ]; };