diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md
index 7afc14347f5c..763cb1df3202 100644
--- a/nixos/doc/manual/release-notes/rl-2405.section.md
+++ b/nixos/doc/manual/release-notes/rl-2405.section.md
@@ -138,6 +138,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
- [Scrutiny](https://github.com/AnalogJ/scrutiny), a S.M.A.R.T monitoring tool for hard disks with a web frontend.
+- [davis](https://github.com/tchapi/davis), a simple CardDav and CalDav server inspired by Baïkal. Available as [services.davis]($opt-services-davis.enable).
+
- [systemd-lock-handler](https://git.sr.ht/~whynothugo/systemd-lock-handler/), a bridge between logind D-Bus events and systemd targets. Available as [services.systemd-lock-handler.enable](#opt-services.systemd-lock-handler.enable).
- [wastebin](https://github.com/matze/wastebin), a pastebin server written in rust. Available as [services.wastebin](#opt-services.wastebin.enable).
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 9cbc421239ba..9a1bfe94a405 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1309,6 +1309,7 @@
./services/web-apps/cloudlog.nix
./services/web-apps/code-server.nix
./services/web-apps/convos.nix
+ ./services/web-apps/davis.nix
./services/web-apps/dex.nix
./services/web-apps/discourse.nix
./services/web-apps/documize.nix
diff --git a/nixos/modules/services/web-apps/davis.md b/nixos/modules/services/web-apps/davis.md
new file mode 100644
index 000000000000..9775d8221b5b
--- /dev/null
+++ b/nixos/modules/services/web-apps/davis.md
@@ -0,0 +1,32 @@
+# Davis {#module-services-davis}
+
+[Davis](https://github.com/tchapi/davis/) is a caldav and carrddav server. It
+has a simple, fully translatable admin interface for sabre/dav based on Symfony
+5 and Bootstrap 5, initially inspired by Baïkal.
+
+## Basic Usage {#module-services-davis-basic-usage}
+
+At first, an application secret is needed, this can be generated with:
+```ShellSession
+$ cat /dev/urandom | tr -dc a-zA-Z0-9 | fold -w 48 | head -n 1
+```
+
+After that, `davis` can be deployed like this:
+```
+{
+ services.davis = {
+ enable = true;
+ hostname = "davis.example.com";
+ mail = {
+ dsn = "smtp://username@example.com:25";
+ inviteFromAddress = "davis@example.com";
+ };
+ adminLogin = "admin";
+ adminPasswordFile = "/run/secrets/davis-admin-password";
+ appSecretFile = "/run/secrets/davis-app-secret";
+ nginx = {};
+ };
+}
+```
+
+This deploys Davis using a sqlite database running out of `/var/lib/davis`.
diff --git a/nixos/modules/services/web-apps/davis.nix b/nixos/modules/services/web-apps/davis.nix
new file mode 100644
index 000000000000..325ede38d2a1
--- /dev/null
+++ b/nixos/modules/services/web-apps/davis.nix
@@ -0,0 +1,554 @@
+{
+ config,
+ lib,
+ pkgs,
+ ...
+}:
+
+let
+ cfg = config.services.davis;
+ db = cfg.database;
+ mail = cfg.mail;
+
+ mysqlLocal = db.createLocally && db.driver == "mysql";
+ pgsqlLocal = db.createLocally && db.driver == "postgresql";
+
+ user = cfg.user;
+ group = cfg.group;
+
+ isSecret = v: lib.isAttrs v && v ? _secret && (lib.isString v._secret || builtins.isPath v._secret);
+ davisEnvVars = lib.generators.toKeyValue {
+ mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
+ mkValueString =
+ v:
+ if builtins.isInt v then
+ toString v
+ else if lib.isString v then
+ "\"${v}\""
+ else if true == v then
+ "true"
+ else if false == v then
+ "false"
+ else if null == v then
+ ""
+ else if isSecret v then
+ if (lib.isString v._secret) then
+ builtins.hashString "sha256" v._secret
+ else
+ builtins.hashString "sha256" (builtins.readFile v._secret)
+ else
+ throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty { }) v}";
+ };
+ };
+ secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config);
+ mkSecretReplacement = file: ''
+ replace-secret ${
+ lib.escapeShellArgs [
+ (
+ if (lib.isString file) then
+ builtins.hashString "sha256" file
+ else
+ builtins.hashString "sha256" (builtins.readFile file)
+ )
+ file
+ "${cfg.dataDir}/.env.local"
+ ]
+ }
+ '';
+ secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
+ filteredConfig = lib.converge (lib.filterAttrsRecursive (
+ _: v:
+ !lib.elem v [
+ { }
+ null
+ ]
+ )) cfg.config;
+ davisEnv = pkgs.writeText "davis.env" (davisEnvVars filteredConfig);
+in
+{
+ options.services.davis = {
+ enable = lib.mkEnableOption (lib.mdDoc "Davis is a caldav and carddav server");
+
+ user = lib.mkOption {
+ default = "davis";
+ description = lib.mdDoc "User davis runs as.";
+ type = lib.types.str;
+ };
+
+ group = lib.mkOption {
+ default = "davis";
+ description = lib.mdDoc "Group davis runs as.";
+ type = lib.types.str;
+ };
+
+ package = lib.mkPackageOption pkgs "davis" { };
+
+ dataDir = lib.mkOption {
+ type = lib.types.path;
+ default = "/var/lib/davis";
+ description = lib.mdDoc ''
+ Davis data directory.
+ '';
+ };
+
+ hostname = lib.mkOption {
+ type = lib.types.str;
+ example = "davis.yourdomain.org";
+ description = lib.mdDoc ''
+ Domain of the host to serve davis under. You may want to change it if you
+ run Davis on a different URL than davis.yourdomain.
+ '';
+ };
+
+ config = lib.mkOption {
+ type = lib.types.attrsOf (
+ lib.types.nullOr (
+ lib.types.either
+ (lib.types.oneOf [
+ lib.types.bool
+ lib.types.int
+ lib.types.port
+ lib.types.path
+ lib.types.str
+ ])
+ (
+ lib.types.submodule {
+ options = {
+ _secret = lib.mkOption {
+ type = lib.types.nullOr (
+ lib.types.oneOf [
+ lib.types.str
+ lib.types.path
+ ]
+ );
+ description = lib.mdDoc ''
+ The path to a file containing the value the
+ option should be set to in the final
+ configuration file.
+ '';
+ };
+ };
+ }
+ )
+ )
+ );
+ default = { };
+
+ example = '''';
+ description = lib.mdDoc '''';
+ };
+
+ adminLogin = lib.mkOption {
+ type = lib.types.str;
+ default = "root";
+ description = lib.mdDoc ''
+ Username for the admin account.
+ '';
+ };
+ adminPasswordFile = lib.mkOption {
+ type = lib.types.path;
+ description = lib.mdDoc ''
+ The full path to a file that contains the admin's password. Must be
+ readable by the user.
+ '';
+ example = "/run/secrets/davis-admin-pass";
+ };
+
+ appSecretFile = lib.mkOption {
+ type = lib.types.path;
+ description = lib.mdDoc ''
+ A file containing the Symfony APP_SECRET - Its value should be a series
+ of characters, numbers and symbols chosen randomly and the recommended
+ length is around 32 characters. Can be generated with cat
+ /dev/urandom | tr -dc a-zA-Z0-9 | fold -w 48 | head -n 1
.
+ '';
+ example = "/run/secrets/davis-appsecret";
+ };
+
+ database = {
+ driver = lib.mkOption {
+ type = lib.types.enum [
+ "sqlite"
+ "postgresql"
+ "mysql"
+ ];
+ default = "sqlite";
+ description = lib.mdDoc "Database type, required in all circumstances.";
+ };
+ urlFile = lib.mkOption {
+ type = lib.types.nullOr lib.types.path;
+ default = null;
+ example = "/run/secrets/davis-db-url";
+ description = lib.mdDoc ''
+ A file containing the database connection url. If set then it
+ overrides all other database settings (except driver). This is
+ mandatory if you want to use an external database, that is when
+ `services.davis.database.createLocally` is `false`.
+ '';
+ };
+ name = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = "davis";
+ description = lib.mdDoc "Database name, only used when the databse is created locally.";
+ };
+ createLocally = lib.mkOption {
+ type = lib.types.bool;
+ default = true;
+ description = lib.mdDoc "Create the database and database user locally.";
+ };
+ };
+
+ mail = {
+ dsn = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = lib.mdDoc "Mail DSN for sending emails. Mutually exclusive with `services.davis.mail.dsnFile`.";
+ example = "smtp://username:password@example.com:25";
+ };
+ dsnFile = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ example = "/run/secrets/davis-mail-dsn";
+ description = lib.mdDoc "A file containing the mail DSN for sending emails. Mutually exclusive with `servies.davis.mail.dsn`.";
+ };
+ inviteFromAddress = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = lib.mdDoc "Email address to send invitations from.";
+ example = "no-reply@dav.example.com";
+ };
+ };
+
+ nginx = lib.mkOption {
+ type = lib.types.submodule (
+ lib.recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) { }
+ );
+ default = null;
+ example = ''
+ {
+ serverAliases = [
+ "dav.''${config.networking.domain}"
+ ];
+ # To enable encryption and let let's encrypt take care of certificate
+ forceSSL = true;
+ enableACME = true;
+ }
+ '';
+ description = lib.mdDoc ''
+ With this option, you can customize the nginx virtualHost settings.
+ '';
+ };
+
+ poolConfig = lib.mkOption {
+ type = lib.types.attrsOf (
+ lib.types.oneOf [
+ lib.types.str
+ lib.types.int
+ lib.types.bool
+ ]
+ );
+ default = {
+ "pm" = "dynamic";
+ "pm.max_children" = 32;
+ "pm.start_servers" = 2;
+ "pm.min_spare_servers" = 2;
+ "pm.max_spare_servers" = 4;
+ "pm.max_requests" = 500;
+ };
+ description = lib.mdDoc ''
+ Options for the davis PHP pool. See the documentation on