summary refs log tree commit diff
path: root/nixos/modules/services/web-apps/hedgedoc.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services/web-apps/hedgedoc.nix')
-rw-r--r--nixos/modules/services/web-apps/hedgedoc.nix1038
1 files changed, 1038 insertions, 0 deletions
diff --git a/nixos/modules/services/web-apps/hedgedoc.nix b/nixos/modules/services/web-apps/hedgedoc.nix
new file mode 100644
index 00000000000..9eeabb9d566
--- /dev/null
+++ b/nixos/modules/services/web-apps/hedgedoc.nix
@@ -0,0 +1,1038 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.hedgedoc;
+
+  # 21.03 will not be an official release - it was instead 21.05.  This
+  # 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";
+
+  prettyJSON = conf:
+    pkgs.runCommandLocal "hedgedoc-config.json" {
+      nativeBuildInputs = [ pkgs.jq ];
+    } ''
+      echo '${builtins.toJSON conf}' | jq \
+        '{production:del(.[]|nulls)|del(.[][]?|nulls)}' > $out
+    '';
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "codimd" ] [ "services" "hedgedoc" ])
+  ];
+
+  options.services.hedgedoc = {
+    enable = mkEnableOption "the HedgeDoc Markdown Editor";
+
+    groups = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        Groups to which the service user should be added.
+      '';
+    };
+
+    workDir = mkOption {
+      type = types.path;
+      default = "/var/lib/${name}";
+      description = ''
+        Working directory for the HedgeDoc service.
+      '';
+    };
+
+    configuration = {
+      debug = mkEnableOption "debug mode";
+      domain = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "hedgedoc.org";
+        description = ''
+          Domain name for the HedgeDoc instance.
+        '';
+      };
+      urlPath = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/url/path/to/hedgedoc";
+        description = ''
+          Path under which HedgeDoc is accessible.
+        '';
+      };
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = ''
+          Address to listen on.
+        '';
+      };
+      port = mkOption {
+        type = types.int;
+        default = 3000;
+        example = 80;
+        description = ''
+          Port to listen on.
+        '';
+      };
+      path = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/run/hedgedoc.sock";
+        description = ''
+          Specify where a UNIX domain socket should be placed.
+        '';
+      };
+      allowOrigin = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "localhost" "hedgedoc.org" ];
+        description = ''
+          List of domains to whitelist.
+        '';
+      };
+      useSSL = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable to use SSL server. This will also enable
+          <option>protocolUseSSL</option>.
+        '';
+      };
+      hsts = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Whether to enable HSTS if HTTPS is also enabled.
+          '';
+        };
+        maxAgeSeconds = mkOption {
+          type = types.int;
+          default = 31536000;
+          description = ''
+            Max duration for clients to keep the HSTS status.
+          '';
+        };
+        includeSubdomains = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Whether to include subdomains in HSTS.
+          '';
+        };
+        preload = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            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 = ''
+          Specify the Content Security Policy which is passed to Helmet.
+          For configuration details see <link xlink:href="https://helmetjs.github.io/docs/csp/"
+          >https://helmetjs.github.io/docs/csp/</link>.
+        '';
+      };
+      protocolUseSSL = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable to use TLS for resource paths.
+          This only applies when <option>domain</option> is set.
+        '';
+      };
+      urlAddPort = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable to add the port to callback URLs.
+          This only applies when <option>domain</option> is set
+          and only for ports other than 80 and 443.
+        '';
+      };
+      useCDN = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to use CDN resources or not.
+        '';
+      };
+      allowAnonymous = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to allow anonymous usage.
+        '';
+      };
+      allowAnonymousEdits = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to allow guests to edit existing notes with the `freely' permission,
+          when <option>allowAnonymous</option> is enabled.
+        '';
+      };
+      allowFreeURL = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to allow note creation by accessing a nonexistent note URL.
+        '';
+      };
+      defaultPermission = mkOption {
+        type = types.enum [ "freely" "editable" "limited" "locked" "private" ];
+        default = "editable";
+        description = ''
+          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 = ''
+          Specify which database to use.
+          HedgeDoc supports mysql, postgres, sqlite and mssql.
+          See <link xlink:href="https://sequelize.readthedocs.io/en/v3/">
+          https://sequelize.readthedocs.io/en/v3/</link> for more information.
+          Note: This option overrides <option>db</option>.
+        '';
+      };
+      db = mkOption {
+        type = types.attrs;
+        default = {};
+        example = literalExpression ''
+          {
+            dialect = "sqlite";
+            storage = "/var/lib/${name}/db.${name}.sqlite";
+          }
+        '';
+        description = ''
+          Specify the configuration for sequelize.
+          HedgeDoc supports mysql, postgres, sqlite and mssql.
+          See <link xlink:href="https://sequelize.readthedocs.io/en/v3/">
+          https://sequelize.readthedocs.io/en/v3/</link> for more information.
+          Note: This option overrides <option>db</option>.
+        '';
+      };
+      sslKeyPath= mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/var/lib/hedgedoc/hedgedoc.key";
+        description = ''
+          Path to the SSL key. Needed when <option>useSSL</option> is enabled.
+        '';
+      };
+      sslCertPath = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/var/lib/hedgedoc/hedgedoc.crt";
+        description = ''
+          Path to the SSL cert. Needed when <option>useSSL</option> is enabled.
+        '';
+      };
+      sslCAPath = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "/var/lib/hedgedoc/ca.crt" ];
+        description = ''
+          SSL ca chain. Needed when <option>useSSL</option> is enabled.
+        '';
+      };
+      dhParamPath = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/var/lib/hedgedoc/dhparam.pem";
+        description = ''
+          Path to the SSL dh params. Needed when <option>useSSL</option> is enabled.
+        '';
+      };
+      tmpPath = mkOption {
+        type = types.str;
+        default = "/tmp";
+        description = ''
+          Path to the temp directory HedgeDoc should use.
+          Note that <option>serviceConfig.PrivateTmp</option> 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 = "./public/default.md";
+        description = ''
+          Path to the default Note file.
+          (Non-canonical paths are relative to HedgeDoc's base directory)
+        '';
+      };
+      docsPath = mkOption {
+        type = types.nullOr types.str;
+        default = "./public/docs";
+        description = ''
+          Path to the docs directory.
+          (Non-canonical paths are relative to HedgeDoc's base directory)
+        '';
+      };
+      indexPath = mkOption {
+        type = types.nullOr types.str;
+        default = "./public/views/index.ejs";
+        description = ''
+          Path to the index template file.
+          (Non-canonical paths are relative to HedgeDoc's base directory)
+        '';
+      };
+      hackmdPath = mkOption {
+        type = types.nullOr types.str;
+        default = "./public/views/hackmd.ejs";
+        description = ''
+          Path to the hackmd template file.
+          (Non-canonical paths are relative to HedgeDoc's base directory)
+        '';
+      };
+      errorPath = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        defaultText = literalExpression "./public/views/error.ejs";
+        description = ''
+          Path to the error template file.
+          (Non-canonical paths are relative to HedgeDoc's base directory)
+        '';
+      };
+      prettyPath = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        defaultText = literalExpression "./public/views/pretty.ejs";
+        description = ''
+          Path to the pretty template file.
+          (Non-canonical paths are relative to HedgeDoc's base directory)
+        '';
+      };
+      slidePath = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        defaultText = literalExpression "./public/views/slide.hbs";
+        description = ''
+          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 "/var/lib/${name}/uploads";
+        description = ''
+          Path under which uploaded files are saved.
+        '';
+      };
+      sessionName = mkOption {
+        type = types.str;
+        default = "connect.sid";
+        description = ''
+          Specify the name of the session cookie.
+        '';
+      };
+      sessionSecret = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          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 = ''
+          Session life time in milliseconds.
+        '';
+      };
+      heartbeatInterval = mkOption {
+        type = types.int;
+        default = 5000;
+        description = ''
+          Specify the socket.io heartbeat interval.
+        '';
+      };
+      heartbeatTimeout = mkOption {
+        type = types.int;
+        default = 10000;
+        description = ''
+          Specify the socket.io heartbeat timeout.
+        '';
+      };
+      documentMaxLength = mkOption {
+        type = types.int;
+        default = 100000;
+        description = ''
+          Specify the maximum document length.
+        '';
+      };
+      email = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable email sign-in.
+        '';
+      };
+      allowEmailRegister = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable email registration.
+        '';
+      };
+      allowGravatar = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to use gravatar as profile picture source.
+        '';
+      };
+      imageUploadType = mkOption {
+        type = types.enum [ "imgur" "s3" "minio" "filesystem" ];
+        default = "filesystem";
+        description = ''
+          Specify where to upload images.
+        '';
+      };
+      minio = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            accessKey = mkOption {
+              type = types.str;
+              description = ''
+                Minio access key.
+              '';
+            };
+            secretKey = mkOption {
+              type = types.str;
+              description = ''
+                Minio secret key.
+              '';
+            };
+            endpoint = mkOption {
+              type = types.str;
+              description = ''
+                Minio endpoint.
+              '';
+            };
+            port = mkOption {
+              type = types.int;
+              default = 9000;
+              description = ''
+                Minio listen port.
+              '';
+            };
+            secure = mkOption {
+              type = types.bool;
+              default = true;
+              description = ''
+                Whether to use HTTPS for Minio.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the minio third-party integration.";
+      };
+      s3 = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            accessKeyId = mkOption {
+              type = types.str;
+              description = ''
+                AWS access key id.
+              '';
+            };
+            secretAccessKey = mkOption {
+              type = types.str;
+              description = ''
+                AWS access key.
+              '';
+            };
+            region = mkOption {
+              type = types.str;
+              description = ''
+                AWS S3 region.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the s3 third-party integration.";
+      };
+      s3bucket = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Specify the bucket name for upload types <literal>s3</literal> and <literal>minio</literal>.
+        '';
+      };
+      allowPDFExport = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable PDF exports.
+        '';
+      };
+      imgur.clientId = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Imgur API client ID.
+        '';
+      };
+      azure = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            connectionString = mkOption {
+              type = types.str;
+              description = ''
+                Azure Blob Storage connection string.
+              '';
+            };
+            container = mkOption {
+              type = types.str;
+              description = ''
+                Azure Blob Storage container name.
+                It will be created if non-existent.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the azure third-party integration.";
+      };
+      oauth2 = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            authorizationURL = mkOption {
+              type = types.str;
+              description = ''
+                Specify the OAuth authorization URL.
+              '';
+            };
+            tokenURL = mkOption {
+              type = types.str;
+              description = ''
+                Specify the OAuth token URL.
+              '';
+            };
+            baseURL = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Specify the OAuth base URL.
+              '';
+            };
+            userProfileURL = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Specify the OAuth userprofile URL.
+              '';
+            };
+            userProfileUsernameAttr = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Specify the name of the attribute for the username from the claim.
+              '';
+            };
+            userProfileDisplayNameAttr = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Specify the name of the attribute for the display name from the claim.
+              '';
+            };
+            userProfileEmailAttr = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Specify the name of the attribute for the email from the claim.
+              '';
+            };
+            scope = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Specify the OAuth scope.
+              '';
+            };
+            providerName = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Specify the name to be displayed for this strategy.
+              '';
+            };
+            rolesClaim = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Specify the role claim name.
+              '';
+            };
+            accessRole = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Specify role which should be included in the ID token roles claim to grant access
+              '';
+            };
+            clientID = mkOption {
+              type = types.str;
+              description = ''
+                Specify the OAuth client ID.
+              '';
+            };
+            clientSecret = mkOption {
+              type = types.str;
+              description = ''
+                Specify the OAuth client secret.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the OAuth integration.";
+      };
+      facebook = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            clientID = mkOption {
+              type = types.str;
+              description = ''
+                Facebook API client ID.
+              '';
+            };
+            clientSecret = mkOption {
+              type = types.str;
+              description = ''
+                Facebook API client secret.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the facebook third-party integration";
+      };
+      twitter = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            consumerKey = mkOption {
+              type = types.str;
+              description = ''
+                Twitter API consumer key.
+              '';
+            };
+            consumerSecret = mkOption {
+              type = types.str;
+              description = ''
+                Twitter API consumer secret.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the Twitter third-party integration.";
+      };
+      github = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            clientID = mkOption {
+              type = types.str;
+              description = ''
+                GitHub API client ID.
+              '';
+            };
+            clientSecret = mkOption {
+              type = types.str;
+              description = ''
+                Github API client secret.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the GitHub third-party integration.";
+      };
+      gitlab = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            baseURL = mkOption {
+              type = types.str;
+              default = "";
+              description = ''
+                GitLab API authentication endpoint.
+                Only needed for other endpoints than gitlab.com.
+              '';
+            };
+            clientID = mkOption {
+              type = types.str;
+              description = ''
+                GitLab API client ID.
+              '';
+            };
+            clientSecret = mkOption {
+              type = types.str;
+              description = ''
+                GitLab API client secret.
+              '';
+            };
+            scope = mkOption {
+              type = types.enum [ "api" "read_user" ];
+              default = "api";
+              description = ''
+                GitLab API requested scope.
+                GitLab snippet import/export requires api scope.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the GitLab third-party integration.";
+      };
+      mattermost = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            baseURL = mkOption {
+              type = types.str;
+              description = ''
+                Mattermost authentication endpoint.
+              '';
+            };
+            clientID = mkOption {
+              type = types.str;
+              description = ''
+                Mattermost API client ID.
+              '';
+            };
+            clientSecret = mkOption {
+              type = types.str;
+              description = ''
+                Mattermost API client secret.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the Mattermost third-party integration.";
+      };
+      dropbox = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            clientID = mkOption {
+              type = types.str;
+              description = ''
+                Dropbox API client ID.
+              '';
+            };
+            clientSecret = mkOption {
+              type = types.str;
+              description = ''
+                Dropbox API client secret.
+              '';
+            };
+            appKey = mkOption {
+              type = types.str;
+              description = ''
+                Dropbox app key.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the Dropbox third-party integration.";
+      };
+      google = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            clientID = mkOption {
+              type = types.str;
+              description = ''
+                Google API client ID.
+              '';
+            };
+            clientSecret = mkOption {
+              type = types.str;
+              description = ''
+                Google API client secret.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the Google third-party integration.";
+      };
+      ldap = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            providerName = mkOption {
+              type = types.str;
+              default = "";
+              description = ''
+                Optional name to be displayed at login form, indicating the LDAP provider.
+              '';
+            };
+            url = mkOption {
+              type = types.str;
+              example = "ldap://localhost";
+              description = ''
+                URL of LDAP server.
+              '';
+            };
+            bindDn = mkOption {
+              type = types.str;
+              description = ''
+                Bind DN for LDAP access.
+              '';
+            };
+            bindCredentials = mkOption {
+              type = types.str;
+              description = ''
+                Bind credentials for LDAP access.
+              '';
+            };
+            searchBase = mkOption {
+              type = types.str;
+              example = "o=users,dc=example,dc=com";
+              description = ''
+                LDAP directory to begin search from.
+              '';
+            };
+            searchFilter = mkOption {
+              type = types.str;
+              example = "(uid={{username}})";
+              description = ''
+                LDAP filter to search with.
+              '';
+            };
+            searchAttributes = mkOption {
+              type = types.listOf types.str;
+              example = [ "displayName" "mail" ];
+              description = ''
+                LDAP attributes to search with.
+              '';
+            };
+            userNameField = mkOption {
+              type = types.str;
+              default = "";
+              description = ''
+                LDAP field which is used as the username on HedgeDoc.
+                By default <option>useridField</option> is used.
+              '';
+            };
+            useridField = mkOption {
+              type = types.str;
+              example = "uid";
+              description = ''
+                LDAP field which is a unique identifier for users on HedgeDoc.
+              '';
+            };
+            tlsca = mkOption {
+              type = types.str;
+              example = "server-cert.pem,root.pem";
+              description = ''
+                Root CA for LDAP TLS in PEM format.
+              '';
+            };
+          };
+        });
+        default = null;
+        description = "Configure the LDAP integration.";
+      };
+      saml = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            idpSsoUrl = mkOption {
+              type = types.str;
+              example = "https://idp.example.com/sso";
+              description = ''
+                IdP authentication endpoint.
+              '';
+            };
+            idpCert = mkOption {
+              type = types.path;
+              example = "/path/to/cert.pem";
+              description = ''
+                Path to IdP certificate file in PEM format.
+              '';
+            };
+            issuer = mkOption {
+              type = types.str;
+              default = "";
+              description = ''
+                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 = ''
+                Optional name identifier format.
+              '';
+            };
+            groupAttribute = mkOption {
+              type = types.str;
+              default = "";
+              example = "memberOf";
+              description = ''
+                Optional attribute name for group list.
+              '';
+            };
+            externalGroups = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              example = [ "Temporary-staff" "External-users" ];
+              description = ''
+                Excluded group names.
+              '';
+            };
+            requiredGroups = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              example = [ "Hedgedoc-Users" ];
+              description = ''
+                Required group names.
+              '';
+            };
+            attribute = {
+              id = mkOption {
+                type = types.str;
+                default = "";
+                description = ''
+                  Attribute map for `id'.
+                  Defaults to `NameID' of SAML response.
+                '';
+              };
+              username = mkOption {
+                type = types.str;
+                default = "";
+                description = ''
+                  Attribute map for `username'.
+                  Defaults to `NameID' of SAML response.
+                '';
+              };
+              email = mkOption {
+                type = types.str;
+                default = "";
+                description = ''
+                  Attribute map for `email'.
+                  Defaults to `NameID' of SAML response if
+                  <option>identifierFormat</option> has
+                  the default value.
+                '';
+              };
+            };
+          };
+        });
+        default = null;
+        description = "Configure the SAML integration.";
+      };
+    };
+
+    environmentFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      example = "/var/lib/hedgedoc/hedgedoc.env";
+      description = ''
+        Environment file as defined in <citerefentry>
+        <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+        </citerefentry>.
+
+        Secrets may be passed to the service without adding them to the world-readable
+        Nix store, by specifying placeholder variables as the option value in Nix and
+        setting these variables accordingly in the environment file.
+
+        <programlisting>
+          # snippet of HedgeDoc-related config
+          services.hedgedoc.configuration.dbURL = "postgres://hedgedoc:\''${DB_PASSWORD}@db-host:5432/hedgedocdb";
+          services.hedgedoc.configuration.minio.secretKey = "$MINIO_SECRET_KEY";
+        </programlisting>
+
+        <programlisting>
+          # content of the environment file
+          DB_PASSWORD=verysecretdbpassword
+          MINIO_SECRET_KEY=verysecretminiokey
+        </programlisting>
+
+        Note that this file needs to be available on the host on which
+        <literal>HedgeDoc</literal> is running.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.hedgedoc;
+      defaultText = literalExpression "pkgs.hedgedoc";
+      description = ''
+        Package that provides HedgeDoc.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.configuration.db == {} -> (
+          cfg.configuration.dbURL != "" && cfg.configuration.dbURL != null
+        );
+        message = "Database configuration for HedgeDoc missing."; }
+    ];
+    users.groups.${name} = {};
+    users.users.${name} = {
+      description = "HedgeDoc service user";
+      group = name;
+      extraGroups = cfg.groups;
+      home = cfg.workDir;
+      createHome = true;
+      isSystemUser = true;
+    };
+
+    systemd.services.hedgedoc = {
+      description = "HedgeDoc Service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ];
+      preStart = ''
+        ${pkgs.envsubst}/bin/envsubst \
+          -o ${cfg.workDir}/config.json \
+          -i ${prettyJSON cfg.configuration}
+      '';
+      serviceConfig = {
+        WorkingDirectory = cfg.workDir;
+        ExecStart = "${cfg.package}/bin/hedgedoc";
+        EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
+        Environment = [
+          "CMD_CONFIG_FILE=${cfg.workDir}/config.json"
+          "NODE_ENV=production"
+        ];
+        Restart = "always";
+        User = name;
+        PrivateTmp = true;
+      };
+    };
+  };
+}