Move all flakes into modules/ and sep't, create deploy script for a1d2

Change-Id: Ie4d50fb8f16da193195beb139922a366b72b0b0a
diff --git a/modules/common/boot.nix b/modules/common/boot.nix
new file mode 100644
index 0000000..26ca4c2
--- /dev/null
+++ b/modules/common/boot.nix
@@ -0,0 +1 @@
+{ boot.loader.systemd-boot.enable = true; }
diff --git a/modules/common/cache.nix b/modules/common/cache.nix
new file mode 100644
index 0000000..89bbce6
--- /dev/null
+++ b/modules/common/cache.nix
@@ -0,0 +1,7 @@
+{
+  nix.settings = {
+    substituters = [ "https://cache.garnix.io" ];
+    trusted-public-keys =
+      [ "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" ];
+  };
+}
diff --git a/modules/common/clamav.nix b/modules/common/clamav.nix
new file mode 100644
index 0000000..d3573e5
--- /dev/null
+++ b/modules/common/clamav.nix
@@ -0,0 +1,7 @@
+{ pkgs, ... }: {
+  services.clamav = {
+    updater.enable = true;
+    daemon.enable = true;
+  };
+  environment.systemPackages = [ pkgs.clamav ];
+}
diff --git a/modules/common/cloudflare-ddns.nix b/modules/common/cloudflare-ddns.nix
new file mode 100644
index 0000000..a1ebb61
--- /dev/null
+++ b/modules/common/cloudflare-ddns.nix
@@ -0,0 +1,18 @@
+{ config, ... }: {
+  services.cloudflare-dyndns = {
+    enable = true;
+    proxied = false;
+    ipv4 = true;
+    ipv6 = false;
+    domains = [ "d1.a1.crawling.us" ];
+    apiTokenFile = config.sops.secrets.cloudflare_ddns__api_token.path;
+  };
+
+  sops.secrets.cloudflare_ddns__api_token = {
+    mode = "0600";
+    owner = config.users.users.root.name;
+    group = config.users.users.root.group;
+    sopsFile = ../../secrets/cloudflare-ddns.env.bin;
+    format = "binary";
+  };
+}
diff --git a/modules/common/dmarc.nix b/modules/common/dmarc.nix
new file mode 100644
index 0000000..3266214
--- /dev/null
+++ b/modules/common/dmarc.nix
@@ -0,0 +1,49 @@
+{ config, lib, pkgs, ... }: {
+  users.users.parsedmarc = {
+    isSystemUser = true;
+    createHome = true;
+    home = "/services/parsedmarc";
+    group = config.users.groups.clicks.name;
+    shell = pkgs.bashInteractive;
+  };
+  sops.secrets = lib.pipe [ "imap_password" "maxmind_license_key" ] [
+    (map (name: {
+      inherit name;
+      value = {
+        mode = "0400";
+        owner = config.users.users.parsedmarc.name;
+        group = config.users.users.parsedmarc.group;
+        sopsFile = ../../secrets/dmarc.json;
+        format = "json";
+      };
+    }))
+    builtins.listToAttrs
+  ];
+
+  services.parsedmarc = {
+    enable = true;
+    settings.imap = {
+      host = "mail.clicks.codes";
+      user = "dmarc@clicks.codes";
+      password = { _secret = config.sops.secrets.imap_password.path; };
+    };
+    settings.mailbox = {
+      watch = true;
+      delete = false;
+    };
+  };
+  services.geoipupdate.settings = {
+    AccountID = 863877;
+    LicenseKey = { _secret = config.sops.secrets.maxmind_license_key.path; };
+  };
+  systemd.services.geoipupdate-create-db-dir.script = lib.mkForce ''
+    set -o errexit -o pipefail -o nounset -o errtrace
+    shopt -s inherit_errexit
+
+    mkdir -p ${config.services.geoipupdate.settings.DatabaseDirectory}
+    chmod 0750 ${config.services.geoipupdate.settings.DatabaseDirectory}
+
+    chgrp clicks ${config.services.geoipupdate.settings.DatabaseDirectory}
+    # The license agreement does not allow us to let non-clicks users access the database
+  '';
+}
diff --git a/modules/common/dnsmasq.nix b/modules/common/dnsmasq.nix
new file mode 100644
index 0000000..3366cfb
--- /dev/null
+++ b/modules/common/dnsmasq.nix
@@ -0,0 +1,25 @@
+{ config, ... }: {
+  services = {
+    nscd.enableNsncd = true;
+    dnsmasq = {
+      enable = true;
+      settings = {
+        server = [ "1.1.1.1" "1.0.0.1" ];
+        local = "/local/";
+        domain = "local";
+        expand-hosts = true;
+      };
+    };
+    avahi = {
+      enable = true;
+      nssmdns = true;
+      ipv4 = true;
+      ipv6 = true;
+      publish = {
+        enable = true;
+        addresses = true;
+        workstation = true;
+      };
+    };
+  };
+}
diff --git a/modules/common/doas.nix b/modules/common/doas.nix
new file mode 100644
index 0000000..6a725bf
--- /dev/null
+++ b/modules/common/doas.nix
@@ -0,0 +1,12 @@
+{ config, ... }: {
+  security = {
+    doas = {
+      enable = true;
+      wheelNeedsPassword = false;
+    };
+    sudo.enable = false;
+  };
+
+  environment.shellAliases.sudo =
+    "${config.security.wrapperDir}/${config.security.wrappers.doas.program}";
+}
diff --git a/modules/common/docker.nix b/modules/common/docker.nix
new file mode 100644
index 0000000..a1b4e8c
--- /dev/null
+++ b/modules/common/docker.nix
@@ -0,0 +1,6 @@
+{ pkgs, ... }: {
+  environment.systemPackages = [ pkgs.docker-compose ];
+  virtualisation.docker.enable = true;
+  users.users.mailu.extraGroups = [ "docker" ];
+  users.users.kavita.extraGroups = [ "docker" ];
+}
diff --git a/modules/common/drivePaths.nix b/modules/common/drivePaths.nix
new file mode 100644
index 0000000..2f99b4d
--- /dev/null
+++ b/modules/common/drivePaths.nix
@@ -0,0 +1,9 @@
+{ drive_paths, lib, ... }:
+if drive_paths == null
+then {}
+else {
+  fileSystems = lib.mapAttrs' (name: value: {
+    name = value.path;
+    value.device = "/dev/disk/by-uuid/${value.uuid}";
+  }) drive_paths;
+}
diff --git a/modules/common/ecryptfs.nix b/modules/common/ecryptfs.nix
new file mode 100644
index 0000000..dd85ca9
--- /dev/null
+++ b/modules/common/ecryptfs.nix
@@ -0,0 +1,16 @@
+{ pkgs, ... }: {
+  environment.systemPackages = with pkgs;
+    let
+      unlock-database-script = writeScriptBin "unlock-database-encryption" ''
+        if [ $UID -ne 0 ]; then
+          echo "unlock-database-encryption must be run as root"
+          exit 1
+        fi
+        ECRYPTFS_SIG=$(( stty -echo; printf "Passphrase: " 1>&2; read PASSWORD; stty echo; echo $PASSWORD; ) | ecryptfs-insert-wrapped-passphrase-into-keyring ~/.ecryptfs/wrapped-passphrase - | sed -nr 's/.*\[(.*)\].*/\1/p')
+
+        keyctl link @u @s
+
+        mount -i -t ecryptfs /var/db/.mongodb-encrypted/ /var/db/mongodb -o ecryptfs_sig=$ECRYPTFS_SIG,ecryptfs_fnek_sig=$ECRYPTFS_SIG,ecryptfs_cipher=aes,ecryptfs_key_bytes=32,ecryptfs_unlink_sigs
+      '';
+    in [ ecryptfs keyutils unlock-database-script ];
+}
diff --git a/modules/common/fail2ban.nix b/modules/common/fail2ban.nix
new file mode 100644
index 0000000..5368094
--- /dev/null
+++ b/modules/common/fail2ban.nix
@@ -0,0 +1,74 @@
+{ config, ... }: {
+  services.fail2ban = {
+    enable = true;
+    jails = {
+      mailu-auth-fail = ''
+        enabled = true
+        backend = systemd
+        filter = mailu-auth-fail
+        bantime = 604800
+        findtime = 600
+        maxretry = 5
+      '';
+      mailu-auth-limit = ''
+        enabled = true
+        backend = systemd
+        filter = mailu-auth-limit
+        bantime = 604800
+        findtime = 900
+        maxretry = 15
+      '';
+      samba = ''
+        filter=samba-filter
+        enabled=true
+        logpath=/var/log/messages
+        maxretry=1
+        findtime=600
+        bantime=2592000
+      '';
+    };
+    banaction-allports = "iptables-allports";
+    banaction = config.services.fail2ban.banaction-allports;
+    bantime = "24h";
+    bantime-increment = {
+      enable = true;
+      rndtime = "1h";
+      overalljails = true;
+      factor = "24";
+    };
+  };
+  environment.etc = {
+    "fail2ban/filter.d/mailu-auth-fail.conf".text = ''
+      [Definition]
+      failregex = ^\s?\S+ mailu\-front\[\d+\]: \S+ \S+ \[info\] \d+#\d+: \*\d+ client login failed: \"AUTH not supported\" while in http auth state, client: <HOST>, server:
+      ignoreregex =
+      journalmatch = CONTAINER_TAG=mailu-front
+    '';
+
+    "fail2ban/filter.d/mailu-auth-limit.conf".text = ''
+      [Definition]
+      failregex = : Authentication attempt from <HOST> has been rate-limited\.$
+      ignoreregex =
+      journalmatch = CONTAINER_TAG=mailu-admin
+    '';
+
+    "fail2ban/filter.d/samba-filter.conf".text = ''
+      [Definition]
+      # Honeypot file regex. The files in the honeypot folder MUST match this regex
+      __honeypot_files_re=(-sync-decrypted\.)
+
+      # Known ransomware extensions regex
+      __known_ransom_extensions_re=(\.k$|\.encoderpass$|\.key$|\.ecc$|\.ezz$|\.exx$|\.zzz$|\.xyz$|\.aaa$|\.abc$|\.ccc$|\.vvv$|\.xxx$|\.ttt$|\.micro$|\.encrypted$|\.locked$|\.crypto$|_crypt$|\.crinf$|\.r5a$|\.xrtn$|\.XTBL$|\.crypt$|\.R16M01D05$|\.pzdc$|\.good$|\.LOL\!$|\.OMG\!$|\.RDM$|\.RRK$|\.encryptedRSA$|\.crjoker$|\.EnCiPhErEd$|\.LeChiffre$|\.keybtc@inbox_com$|\.0x0$|\.bleep$|\.1999$|\.vault$|\.HA3$|\.toxcrypt$|\.magic$|\.SUPERCRYPT$|\.CTBL$|\.CTB2$|\.locky$|\.wnry$|\.wcry$|\.wncry$|\.wncryt$|\.uiwix$)
+      # Known ransomware files regex
+      __known_ransom_files_re=(HELPDECRYPT\.TXT$|HELP_YOUR_FILES\.TXT$|HELP_TO_DECRYPT_YOUR_FILES\.txt$|RECOVERY_KEY\.txt$|HELP_RESTORE_FILES\.txt$|HELP_RECOVER_FILES\.txt$|HELP_TO_SAVE_FILES\.txt$|DecryptAllFiles\.txt$|DECRYPT_INSTRUCTIONS\.TXT$|INSTRUCCIONES_DESCIFRADO\.TXT$|How_To_Recover_Files\.txt$|YOUR_FILES\.HTML$|YOUR_FILES\.url$|Help_Decrypt\.txt$|DECRYPT_INSTRUCTION\.TXT$|HOW_TO_DECRYPT_FILES\.TXT$|ReadDecryptFilesHere\.txt$|Coin\.Locker\.txt$|_secret_code\.txt$|About_Files\.txt$|Read\.txt$|ReadMe\.txt$|DECRYPT_ReadMe\.TXT$|DecryptAllFiles\.txt$|FILESAREGONE\.TXT$|IAMREADYTOPAY\.TXT$|HELLOTHERE\.TXT$|READTHISNOW\!\!\!\.TXT$|SECRETIDHERE\.KEY$|IHAVEYOURSECRET\.KEY$|SECRET\.KEY$|HELPDECYPRT_YOUR_FILES\.HTML$|help_decrypt_your_files\.html$|HELP_TO_SAVE_FILES\.txt$|RECOVERY_FILES\.txt$|RECOVERY_FILE\.TXT$|RECOVERY_FILE.*\.txt$|HowtoRESTORE_FILES\.txt$|HowtoRestore_FILES\.txt$|howto_recover_file\.txt$|restorefiles\.txt$|howrecover\+.*\.txt$|_how_recover\.txt$|recoveryfile.*\.txt$|recoverfile.*\.txt$|recoveryfile.*\.txt$|Howto_Restore_FILES\.TXT$|help_recover_instructions\+.*\.txt$|_Locky_recover_instructions\.txt$)
+
+      # Match on known ransomware regex or generic honeypot
+      failregex = smbd.*:\ IP=<HOST>\ .*%(__honeypot_files_re)s
+            smbd.*:\ IP=<HOST>\ .*%(__known_ransom_extensions_re)s
+            smbd.*:\ IP=<HOST>\ .*%(__known_ransom_files_re)s
+
+      # Filter generously provided by https://github.com/CanaryTek/ransomware-samba-tools
+      # Provided under GPL3
+    '';
+  };
+}
diff --git a/modules/common/gerrit.nix b/modules/common/gerrit.nix
new file mode 100644
index 0000000..d7a780e
--- /dev/null
+++ b/modules/common/gerrit.nix
@@ -0,0 +1,167 @@
+{ pkgs, config, lib, base, system, ... }:
+let cfg = config.services.gerrit;
+in lib.recursiveUpdate {
+  users.users.gerrit = {
+    isSystemUser = true;
+    createHome = true;
+    home = "/var/lib/gerrit";
+    group = config.users.groups.gerrit.name;
+    shell = pkgs.bashInteractive;
+  };
+  users.groups.gerrit = { };
+
+  systemd.services.gerrit.serviceConfig.User = "gerrit";
+  systemd.services.gerrit.serviceConfig.Group = "gerrit";
+  systemd.services.gerrit.serviceConfig.DynamicUser = lib.mkForce false;
+
+  services.gerrit = {
+    enable = true;
+
+    settings = {
+      accounts = {
+        visibility = "SAME_GROUP";
+        defaultDisplayName = "USERNAME";
+      };
+      addReviewer = {
+        maxWithoutConfirmation = 3;
+        maxAllowed = 10;
+      };
+      auth = {
+        type = "OAUTH";
+        registerEmailPrivateKey = "!!gerrit_email_private_key!!";
+        userNameCaseInsensitive = true;
+        gitBasicAuthPolicy = "HTTP";
+      };
+      plugin."gerrit-oauth-provider-keycloak-oauth" = {
+        root-url = "https://login.clicks.codes";
+        realm = "clicks";
+        client-id = "git";
+        client-secret = "!!gerrit_oauth_client_secret!!";
+        use-preferred-username = true;
+      };
+      change = {
+        topicLimit = 0;
+        mergeabilityComputationBehavior = "API_REF_UPDATED_AND_CHANGE_REINDEX";
+        sendNewPatchsetEmails = false;
+        showAssigneeInChangesTable = true;
+        submitWholeTopic = true;
+        diff3ConflictView = true;
+      };
+      changeCleanup = {
+        abandonAfter = "3 weeks";
+        abandonMessage =
+          "This change was abandoned due to 3 weeks of inactivity. If you still want it, please restore it";
+        startTime = "00:00";
+        interval = "1 day";
+      };
+      attentionSet = {
+        readdAfter = "1 week";
+        readdMessage =
+          "I've given the owner a *ping* as nothing has happened for a week. If in two weeks time the change is still inactive, I'll abandon it for you. If you still want it, please do something before then";
+        startTime = "00:00";
+        interval = "1 day";
+      };
+      commentlink.gerrit = {
+        match = "(I[0-9a-f]{8,40})";
+        link = "/q/$1";
+      };
+      gc = {
+        aggressive = true;
+        startTime = "Sun 00:00";
+        interval = "1 week";
+      };
+      gerrit = {
+        basePath = "/var/lib/gerrit/repos";
+        defaultBranch = "refs/heads/main";
+        canonicalWebUrl = "https://git.clicks.codes/";
+        canonicalGitUrl = "ssh://ssh.clicks.codes/";
+        gitHttpUrl = "https://git.clicks.codes/";
+        reportBugUrl =
+          "https://discord.gg/bPaNnxe"; # TODO: kinda obnoxious, better to setup openproject
+        enablePeerIPInReflogRecord = true;
+        instanceId = "a1d1";
+        instanceName = "a1d1.clicks";
+      };
+      mimetype = lib.pipe [ "image/*" "video/*" "application/pdf" ] [
+        (map (name: {
+          inherit name;
+          value.safe = true;
+        }))
+        builtins.listToAttrs
+      ];
+      receive.enableSignedPush = true;
+      sendemail.enable = false; # TODO: add credentials to git@clicks.codes
+      sshd.advertisedAddress = "ssh.clicks.codes:29418";
+      user = {
+        name = "Clicks Gerrit";
+        email = "git@clicks.codes";
+        anonymousCoward = "Anonymous";
+      };
+      httpd.listenUrl = "proxy-https://${cfg.listenAddress}";
+    };
+
+    plugins = [
+      (pkgs.fetchurl {
+        url = "https://gerrit-ci.gerritforge.com/job/plugin-oauth-bazel-master-master/55/artifact/bazel-bin/plugins/oauth/oauth.jar";
+        hash = "sha256-Qil1CIh/+XC15rKfW0iYR9u370eF2TXnCNSmQfr+7/8=";
+      })
+    ];
+    builtinPlugins = [
+      "codemirror-editor"
+      "commit-message-length-validator"
+      "delete-project"
+      "download-commands"
+      "gitiles"
+      "hooks"
+      "reviewnotes"
+      "singleusergroup"
+      "webhooks"
+    ];
+    serverId = "45f277d0-fce7-43b7-9eb3-2e3234e0110f";
+
+    listenAddress = "127.0.0.255:1000";
+  };
+
+  sops.secrets = {
+    gerrit_email_private_key = {
+      mode = "0400";
+      owner = config.users.users.root.name;
+      group = config.users.users.root.group;
+      sopsFile = ../../secrets/gerrit.json;
+      format = "json";
+    };
+    gerrit_oauth_client_secret = {
+      mode = "0400";
+      owner = config.users.users.root.name;
+      group = config.users.users.root.group;
+      sopsFile = ../../secrets/gerrit.json;
+      format = "json";
+    };
+  };
+} (let isDerived = base != null;
+in if isDerived then
+  let
+    gerrit_cfgfile =
+      pkgs.writeText "gerrit.conf" (lib.generators.toGitINI cfg.settings);
+  in {
+    scalpel.trafos."gerrit.conf" = {
+      source = toString gerrit_cfgfile;
+      matchers."gerrit_email_private_key".secret =
+        config.sops.secrets.gerrit_email_private_key.path;
+      matchers."gerrit_oauth_client_secret".secret =
+        config.sops.secrets.gerrit_oauth_client_secret.path;
+      owner = config.users.users.root.name;
+      group = "gerrit";
+      mode = "0040";
+    };
+
+    systemd.services.gerrit.preStart =
+      base.config.systemd.services.gerrit.preStart + ''
+        rm etc/gerrit.config
+        ln -sfv ${
+          config.scalpel.trafos."gerrit.conf".destination
+        } etc/gerrit.config
+      '';
+  }
+else
+  { })
diff --git a/modules/common/git.nix b/modules/common/git.nix
new file mode 100644
index 0000000..d086cfd
--- /dev/null
+++ b/modules/common/git.nix
@@ -0,0 +1,3 @@
+{ config, pkgs, ... }: {
+  environment.systemPackages = with pkgs; [ gh git git-review ];
+}
diff --git a/modules/common/grafana.nix b/modules/common/grafana.nix
new file mode 100644
index 0000000..f6ca62a
--- /dev/null
+++ b/modules/common/grafana.nix
@@ -0,0 +1,95 @@
+{ lib, config, base, pkgs, helpers, ... }:
+lib.recursiveUpdate {
+  services.grafana = {
+    enable = true;
+
+    settings = {
+      server = rec {
+        domain = "logs.clicks.codes";
+        root_url = "https://${domain}";
+        http_port = 9052;
+        enable_gzip = true;
+      };
+      analytics.reporting_enabled = false;
+      "auth.generic_oauth" = {
+        enabled = true;
+        name = "Clicks OAuth";
+        allow_sign_up = true;
+        client_id = "grafana";
+        client_secret = "!!client_secret!!";
+        scopes = "openid email profile offline_access roles";
+        email_attribute_path = "email";
+        login_attribute_path = "login";
+        name_attribute_path = "name";
+        auth_url =
+          "https://login.clicks.codes/realms/clicks/protocol/openid-connect/auth";
+        token_url =
+          "https://login.clicks.codes/realms/clicks/protocol/openid-connect/token";
+        api_url =
+          "https://login.clicks.codes/realms/clicks/protocol/openid-connect/userinfo";
+        role_attribute_path =
+          "contains(resource_access.grafana.roles[*], 'server_admin') && 'GrafanaAdmin' || contains(resource_access.grafana.roles[*], 'admin') && 'Admin' || contains(resource_access.grafana.roles[*], 'editor') && 'Editor' || 'Viewer'";
+        allow_assign_grafana_admin = true;
+        auto_login = true;
+      };
+      "auth.basic".enabled = false;
+      auth.disable_login_form = true;
+    };
+
+    provision.datasources.settings.datasources = [{
+      name = "clicks-postgresql";
+      type = "postgres";
+      access = "proxy";
+
+      url = "postgres://localhost:${toString config.services.postgresql.port}";
+      user = "clicks_grafana";
+      password =
+        "$__file{${config.sops.secrets.clicks_grafana_db_password.path}}";
+      # defined in postgres.nix
+    }];
+  };
+
+  sops.secrets.clicks_grafana_client_secret = {
+    mode = "0600";
+    owner = config.users.users.root.name;
+    group = config.users.users.root.group;
+    sopsFile = ../../secrets/grafana.json;
+    format = "json";
+  };
+} (let isDerived = base != null;
+in if isDerived then
+  let
+    generators = lib.generators;
+    cfg = config.services.grafana;
+    settingsFormatIni = pkgs.formats.ini {
+      listToValue =
+        lib.concatMapStringsSep " " (generators.mkValueStringDefault { });
+      mkKeyValue = generators.mkKeyValueDefault {
+        mkValueString = v:
+          if v == null then "" else generators.mkValueStringDefault { } v;
+      } "=";
+    };
+    grafana_cfgfile = settingsFormatIni.generate "config.ini" cfg.settings;
+  in {
+    scalpel.trafos."grafana.ini" = {
+      source = toString grafana_cfgfile;
+      matchers."client_secret".secret =
+        config.sops.secrets.clicks_grafana_client_secret.path;
+      owner = config.users.users.grafana.name;
+      group = config.users.users.root.name;
+      mode = "0400";
+    };
+
+    systemd.services.grafana.serviceConfig.ExecStart = lib.mkForce
+      (pkgs.writeShellScript "grafana-start" ''
+        set -o errexit -o pipefail -o nounset -o errtrace
+        shopt -s inherit_errexit
+
+        exec ${cfg.package}/bin/grafana-server -homepath ${cfg.dataDir} -config ${
+          config.scalpel.trafos."grafana.ini".destination
+        }
+      '');
+    systemd.services.grafana.restartTriggers = [ grafana_cfgfile ];
+  }
+else
+  { })
diff --git a/modules/common/home-manager-users.nix b/modules/common/home-manager-users.nix
new file mode 100644
index 0000000..fe1c15d
--- /dev/null
+++ b/modules/common/home-manager-users.nix
@@ -0,0 +1,39 @@
+# Home manager is used separately from this deploy, but we still need to create
+# user accounts in the system config
+{ base, pkgs, lib, config, ... }:
+let
+  mkUser = username:
+    {
+      isSystemUser = true;
+      linger = true;
+      createHome = true;
+      home = "/services/${username}";
+      group = "clicks";
+      shell = pkgs.bashInteractive;
+    } // (if builtins.pathExists "${../../services}/${username}/system.nix" then
+      import "${../../services}/${username}/system.nix"
+    else
+      { });
+in {
+  users.users = lib.pipe ../../services [
+    builtins.readDir
+    (lib.filterAttrs (_name: value: value == "directory"))
+    builtins.attrNames
+    (map (name: {
+      inherit name;
+      value = mkUser name;
+    }))
+    builtins.listToAttrs
+  ];
+} // (if (base != null) then
+  {
+    /* users.groups = lib.mapAttrs'
+       (_: user: {
+         name = user.group;
+         value = { };
+       })
+       base.config.users.users;
+    */
+  }
+else
+  { })
diff --git a/modules/common/keycloak.nix b/modules/common/keycloak.nix
new file mode 100644
index 0000000..30e078b
--- /dev/null
+++ b/modules/common/keycloak.nix
@@ -0,0 +1,26 @@
+{ config, ... }: {
+  services.keycloak = {
+    enable = true;
+    settings = {
+      http-host = "127.0.0.1";
+      http-port = 9083;
+      https-port = 9084;
+      http-enabled = true;
+
+      proxy = "edge";
+
+      # https-port = 9084;
+      hostname = "login.clicks.codes";
+      hostname-strict = false;
+
+      https-certificate-file = "/var/keycloak/login.clicks.codes.rsa.cert.pem";
+      https-certificate-key-file =
+        "/var/keycloak/login.clicks.codes.rsa.private.pem";
+    };
+    database = {
+      createLocally = false;
+      port = config.services.postgresql.port;
+      passwordFile = config.sops.secrets.clicks_keycloak_db_password.path;
+    };
+  };
+}
diff --git a/modules/common/kitty.nix b/modules/common/kitty.nix
new file mode 100644
index 0000000..b85b35a
--- /dev/null
+++ b/modules/common/kitty.nix
@@ -0,0 +1 @@
+{ pkgs, ... }: { environment.systemPackages = [ pkgs.kitty ]; }
diff --git a/modules/common/loginctl-linger.nix b/modules/common/loginctl-linger.nix
new file mode 100644
index 0000000..adc5c84
--- /dev/null
+++ b/modules/common/loginctl-linger.nix
@@ -0,0 +1,49 @@
+{ config, lib, pkgs, ... }:
+
+# A temporary hack to `loginctl enable-linger $somebody` (for
+# multiplexer sessions to last), until this one is unresolved:
+# https://github.com/NixOS/nixpkgs/issues/3702
+#
+# Usage: `users.extraUsers.somebody.linger = true` or slt.
+# Originally from
+# https://gist.githubusercontent.com/graham33/fdbdcc18317a621d9dd54beb36be6683/raw/776ed252749313470f1c9a286a0419ba9746d133/loginctl-linger.nix,
+# modified by Minion3665
+
+with lib;
+
+let
+
+  dataDir = "/var/lib/systemd/linger";
+
+  lingeringUsers = map (u: u.name)
+    (attrValues (flip filterAttrs config.users.users (n: u: u.linger)));
+
+  lingeringUsersFile = builtins.toFile "lingering-users" (concatStrings (map
+    (s: ''
+      ${s}
+    '') (sort (a: b: a < b)
+      lingeringUsers))); # this sorting is important for `comm` to work correctly
+
+  updateLingering = ''
+    if [ -e ${dataDir} ] ; then
+      ${pkgs.gawk}/bin/awk -F':' '{ print $1}' /etc/passwd | sort > /tmp/users-that-actually-exist
+      ls ${dataDir} | sort | comm -3 -1 ${lingeringUsersFile} - | comm -3 -2 /tmp/users-that-actually-exist - | xargs -r ${pkgs.systemd}/bin/loginctl disable-linger
+      ls ${dataDir} | sort | comm -3 -2 ${lingeringUsersFile} - | xargs -r ${pkgs.systemd}/bin/loginctl  enable-linger
+      ls ${dataDir} | sort | comm -3 -1 /tmp/users-that-actually-exist - | ${pkgs.gawk}/bin/awk '{print "${dataDir}/"$1}' | xargs -r rm
+      rm -f /tmp/users-that-actually-exist
+    fi
+  '';
+
+  userOptions = { options.linger = mkEnableOption "Lingering for the user"; };
+
+in {
+  options = {
+    users.users =
+      mkOption { type = with types; attrsOf (submodule userOptions); };
+  };
+
+  config = {
+    system.activationScripts.update-lingering =
+      stringAfter [ "users" ] updateLingering;
+  };
+}
diff --git a/modules/common/matrix.nix b/modules/common/matrix.nix
new file mode 100644
index 0000000..c71ba37
--- /dev/null
+++ b/modules/common/matrix.nix
@@ -0,0 +1,208 @@
+{ base, config, lib, pkgs, ... }:
+lib.recursiveUpdate {
+  services.matrix-synapse = {
+    enable = true;
+    withJemalloc = true;
+
+    plugins = with config.services.matrix-synapse.package.plugins;
+      [ matrix-synapse-mjolnir-antispam ];
+
+    settings = rec {
+      server_name = "coded.codes";
+      auto_join_rooms = [ "#general:${server_name}" ];
+      enable_registration = true;
+      registration_requires_token = true;
+      allow_public_rooms_over_federation = true;
+      allow_device_name_lookup_over_federation = true;
+      registration_shared_secret = "!!registration_shared_secret!!";
+      public_baseurl = "https://matrix-backend.coded.codes/";
+      max_upload_size = "100M";
+      listeners = [{
+        x_forwarded = true;
+        tls = false;
+        resources = [{
+          names = [ "client" "federation" ];
+          compress = true;
+        }];
+        port = 4527;
+      }];
+      enable_metrics = true;
+      database.args.database = "synapse";
+      turn_uris = [
+
+        /* "turn:turn.coded.codes:3478?transport=udp"
+           "turn:turn.coded.codes:3478?transport=tcp"
+           "turns:turn.coded.codes:5349?transport=udp"
+           "turns:turn.coded.codes:5349?transport=tcp"
+        */
+      ]; # Please use matrix.org turn
+      # turn_shared_secret = "!!turn_shared_secret!!";
+
+      log_config = lib.pipe {
+        version = 1;
+        formatters = {
+          precise = {
+            format =
+              "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s";
+          };
+        };
+        handlers = {
+          console = {
+            class = "logging.StreamHandler";
+            formatter = "precise";
+          };
+        };
+        loggers = { "synapse.storage.SQL" = { level = "WARNING"; }; };
+        root = {
+          level = "ERROR";
+          handlers = [ "console" ];
+        };
+        "disable_existing_loggers" = false;
+      } [ builtins.toJSON (builtins.toFile "logcfg.yaml") ];
+    };
+  };
+
+  networking.firewall.allowedTCPPorts = [ 3478 5349 ];
+  networking.firewall.allowedUDPPorts = [ 3478 5349 ];
+
+  services.mjolnir = {
+    enable = true;
+
+    settings = {
+      autojoinOnlyIfManager = true;
+      automaticallyRedactForReasons =
+        [ "nsfw" "gore" "spam" "harassment" "hate" ];
+      recordIgnoredInvites = true;
+      admin.enableMakeRoomAdminCommand = true;
+      allowNoPrefix = true;
+      protections.wordlist.words = [ ];
+      protectedRooms = [ "https://matrix.to/#/#global:coded.codes" ];
+    };
+
+    pantalaimon = {
+      enable = true;
+      username = "system";
+      passwordFile = config.sops.secrets.mjolnir_password.path;
+      options = {
+        ssl = false;
+        listenAddress = "127.0.0.1";
+      };
+    };
+
+    homeserverUrl = "http://localhost:4527";
+
+    managementRoom = "#moderation-commands:coded.codes";
+  };
+
+  services.coturn = {
+    enable = false;
+
+    use-auth-secret = true;
+    # static-auth-secret-file = config.sops.secrets.turn_shared_secret.path;
+
+    realm = "turn.coded.codes";
+
+    no-tcp-relay = true;
+
+    no-cli = true;
+
+    extraConfig = ''
+      external-ip=turn.coded.codes
+    '';
+  };
+
+  sops.secrets = {
+    #turn_shared_secret = {
+    #  mode = "0440";
+    #  owner = "turnserver";
+    #  group = "matrix-synapse";
+    #  sopsFile = ../../secrets/matrix.json;
+    #  format = "json";
+    #};
+    registration_shared_secret = {
+      mode = "0400";
+      owner = config.users.users.root.name;
+      group = config.users.users.root.group;
+      sopsFile = ../../secrets/matrix.json;
+      format = "json";
+    };
+    matrix_private_key = {
+      mode = "0600";
+      owner = config.users.users.matrix-synapse.name;
+      group = config.users.users.matrix-synapse.group;
+      sopsFile = ../../secrets/matrix_private_key.pem;
+      format = "binary";
+      path = config.services.matrix-synapse.settings.signing_key_path;
+    };
+    mjolnir_password = {
+      mode = "0600";
+      owner = config.users.users.mjolnir.name;
+      group = config.users.users.mjolnir.group;
+      sopsFile = ../../secrets/matrix.json;
+      format = "json";
+    };
+  };
+} (let isDerived = base != null;
+in if isDerived
+# We cannot use mkIf as both sides are evaluated no matter the condition value
+# Given we use base as an attrset, mkIf will error if base is null in here
+then
+  let synapse_cfgfile = config.services.matrix-synapse.configFile;
+  in {
+    scalpel.trafos."synapse.yaml" = {
+      source = toString synapse_cfgfile;
+      matchers."registration_shared_secret".secret =
+        config.sops.secrets.registration_shared_secret.path;
+      # matchers."turn_shared_secret".secret =
+      #   config.sops.secrets.turn_shared_secret.path;
+      owner = config.users.users.matrix-synapse.name;
+      group = config.users.users.matrix-synapse.group;
+      mode = "0400";
+    };
+
+    systemd.services.matrix-synapse.serviceConfig.ExecStart = lib.mkForce
+      (builtins.replaceStrings [ "${synapse_cfgfile}" ]
+        [ "${config.scalpel.trafos."synapse.yaml".destination}" ]
+        "${base.config.systemd.services.matrix-synapse.serviceConfig.ExecStart}");
+
+    systemd.services.matrix-synapse.preStart = lib.mkForce
+      (builtins.replaceStrings [ "${synapse_cfgfile}" ]
+        [ "${config.scalpel.trafos."synapse.yaml".destination}" ]
+        "${base.config.systemd.services.matrix-synapse.preStart}");
+
+    systemd.services.matrix-synapse.restartTriggers = [ synapse_cfgfile ];
+
+    environment.systemPackages = with lib;
+      let
+        cfg = config.services.matrix-synapse;
+        registerNewMatrixUser = let
+          isIpv6 = x: lib.length (lib.splitString ":" x) > 1;
+          listener = lib.findFirst (listener:
+            lib.any (resource: lib.any (name: name == "client") resource.names)
+            listener.resources) (lib.last cfg.settings.listeners)
+            cfg.settings.listeners;
+          # FIXME: Handle cases with missing client listener properly,
+          # don't rely on lib.last, this will not work.
+
+          # add a tail, so that without any bind_addresses we still have a useable address
+          bindAddress = head (listener.bind_addresses ++ [ "127.0.0.1" ]);
+          listenerProtocol = if listener.tls then "https" else "http";
+        in pkgs.writeShellScriptBin "matrix-synapse-register_new_matrix_user" ''
+          exec ${cfg.package}/bin/register_new_matrix_user \
+            $@ \
+            ${
+              lib.concatMapStringsSep " " (x: "-c ${x}")
+              ([ config.scalpel.trafos."synapse.yaml".destination ]
+                ++ cfg.extraConfigFiles)
+            } \
+            "${listenerProtocol}://${
+              if (isIpv6 bindAddress) then
+                "[${bindAddress}]"
+              else
+                "${bindAddress}"
+            }:${builtins.toString listener.port}/"
+        '';
+      in [ (lib.meta.hiPrio registerNewMatrixUser) ];
+  }
+else
+  { })
diff --git a/modules/common/mongodb.nix b/modules/common/mongodb.nix
new file mode 100644
index 0000000..0c4ebc5
--- /dev/null
+++ b/modules/common/mongodb.nix
@@ -0,0 +1,16 @@
+{ config, nixpkgs-mongodb, system, ... }: let
+  pkgs = import nixpkgs-mongodb {
+    config.allowUnfree = true;
+    inherit system;
+  };
+in {
+  environment.systemPackages = [ pkgs.mongosh pkgs.mongodb-tools ];
+  services.mongodb.enable = true;
+  services.mongodb.enableAuth = true;
+  services.mongodb.bind_ip = "0.0.0.0";
+  services.mongodb.initialRootPassword = "changeme";
+  services.mongodb.package = pkgs.mongodb-6_0;
+
+  networking.firewall.extraCommands =
+    "iptables -A INPUT -s 192.168.0.4 -p tcp --dport 27017 -j ACCEPT";
+}
diff --git a/modules/common/networking.nix b/modules/common/networking.nix
new file mode 100644
index 0000000..e546db9
--- /dev/null
+++ b/modules/common/networking.nix
@@ -0,0 +1,14 @@
+{
+  networking.firewall.allowedTCPPorts =
+    [ 80 443 25 465 587 110 995 143 993 29418 ];
+
+  networking.hosts = {
+    "127.0.0.1" = [ "standard" ];
+    "127.0.0.2" = [ "clicks" ];
+    "127.0.0.3" = [ "caramels" ];
+    "127.0.0.255" = [ "generic" ];
+
+    "192.168.0.4" = [ "CodedPi" ];
+    "192.168.0.5" = [ "SamuelDesktop" ];
+  };
+}
diff --git a/modules/common/nextcloud.nix b/modules/common/nextcloud.nix
new file mode 100644
index 0000000..93e2828
--- /dev/null
+++ b/modules/common/nextcloud.nix
@@ -0,0 +1,107 @@
+{ config, pkgs, lib, ... }: {
+  sops.secrets.clicks_nextcloud_db_password = {
+    mode = lib.mkForce "0440";
+    group = lib.mkForce "nextcloud";
+  };
+
+  users.users.nextcloud = {
+    isSystemUser = true;
+    createHome = true;
+    home = "/var/lib/nextcloud";
+    group = config.users.groups.nextcloud.name;
+    shell = pkgs.bashInteractive;
+  };
+  users.groups.nextcloud = { };
+
+  services.nextcloud.enable = true;
+  services.nextcloud.https = true;
+  services.nextcloud.config.adminpassFile =
+    config.sops.secrets.nextcloud_admin_password.path;
+  services.nextcloud.hostName = "nextcloud.clicks.codes";
+  services.nginx.virtualHosts.${config.services.nextcloud.hostName} = {
+    enableACME = true;
+    forceSSL = true;
+  };
+  services.nextcloud.package = pkgs.nextcloud27;
+  services.nextcloud.poolSettings = {
+    pm = "dynamic";
+    "pm.max_children" = "32";
+    "pm.max_requests" = "500";
+    "pm.max_spare_servers" = "4";
+    "pm.min_spare_servers" = "2";
+    "pm.start_servers" = "2";
+    "listen.owner" = config.users.users.nextcloud.name;
+    "listen.group" = config.users.users.nextcloud.group;
+  };
+
+  services.nextcloud.config = {
+    dbtype = "pgsql";
+    dbport = config.services.postgresql.port;
+    dbpassFile = config.sops.secrets.clicks_nextcloud_db_password.path;
+    dbname = "nextcloud";
+    dbhost = "localhost";
+    extraTrustedDomains = [ "cloud.clicks.codes" "docs.clicks.codes" ];
+  };
+
+  services.nextcloud.extraOptions = { social_login_auto_redirect = true; };
+
+  services.nextcloud.extraApps = {
+    sociallogin = pkgs.fetchNextcloudApp {
+      url =
+        "https://github.com/zorn-v/nextcloud-social-login/releases/download/v5.5.3/release.tar.gz";
+      sha256 = "sha256-96/wtK7t23fXVRcntDONjgb5bYtZuaNZzbvQCa5Gsj4=";
+    };
+    richdocumentscode = pkgs.fetchNextcloudApp {
+      url = "https://github.com/CollaboraOnline/richdocumentscode/releases/download/23.5.503/richdocumentscode.tar.gz";
+      sha256 = "sha256-5BEN2YXRsMy+zyBBO0KLRMCkTOGv1RdPp1xcDFRNr2I=";
+    };
+    richdocuments = pkgs.fetchNextcloudApp {
+      url =
+        "https://github.com/nextcloud-releases/richdocuments/releases/download/v8.2.0/richdocuments-v8.2.0.tar.gz";
+      sha256 = "sha256-PKw7FXSWvden2+6XjnUDOvbTF71slgeTF/ktS/l2+Dk=";
+    };
+    calendar = pkgs.fetchNextcloudApp {
+      url =
+        "https://github.com/nextcloud-releases/calendar/releases/download/v4.5.2/calendar-v4.5.2.tar.gz";
+      sha256 = "sha256-n7GjgAyw2SLoZTEfakmI3IllWUk6o1MF89Zt3WGhR6A=";
+    };
+    contacts = pkgs.fetchNextcloudApp {
+      url =
+        "https://github.com/nextcloud-releases/contacts/releases/download/v5.4.2/contacts-v5.4.2.tar.gz";
+      sha256 = "sha256-IkKHJ3MY/UPZqa4H86WGOEOypffMIHyJ9WvMqkq/4t8=";
+    };
+    tasks = pkgs.fetchNextcloudApp {
+      url =
+        "https://github.com/nextcloud/tasks/releases/download/v0.15.0/tasks.tar.gz";
+      sha256 = "sha256-zMMqtEWiXmhB1C2IeWk8hgP7eacaXLkT7Tgi4NK6PCg=";
+    };
+    appointments = pkgs.fetchNextcloudApp {
+      url =
+        "https://github.com/SergeyMosin/Appointments/raw/v1.15.4/build/artifacts/appstore/appointments.tar.gz";
+      sha256 = "sha256-2Oo7MJBPiBUBf4kti4or5nX+QiXT1Tkw3KowUGCj67E=";
+    };
+    mail = pkgs.fetchNextcloudApp {
+      url =
+        "https://github.com/nextcloud-releases/mail/releases/download/v3.4.4/mail-v3.4.4.tar.gz";
+      sha256 = "sha256-2+EUVjeFW0mrnR23aU5UHZtGjqpDE11qHXu6PWhUTCs=";
+    };
+    spreed = pkgs.fetchNextcloudApp {  # nextcloud talk
+      url =
+        "https://github.com/nextcloud-releases/spreed/releases/download/v17.1.2/spreed-v17.1.2.tar.gz";
+      sha256 = "sha256-OvZD/k1t4MAJ/BXbHzli6+V/bsgzE6iZQGrC9cG3b8E=";
+    };
+    notes = pkgs.fetchNextcloudApp {
+      url =
+        "https://github.com/nextcloud-releases/notes/releases/download/v4.8.1/notes.tar.gz";
+      sha256 = "sha256-7GkTGyGTvtDbZsq/zOdbBE7xh6DZO183W6I5XX1ekbw=";
+    };
+  };
+
+  sops.secrets.nextcloud_admin_password = {
+    mode = "0600";
+    owner = config.users.users.nextcloud.name;
+    group = config.users.users.nextcloud.group;
+    sopsFile = ../../secrets/nextcloud.json;
+    format = "json";
+  };
+}
diff --git a/modules/common/nginx-routes.nix b/modules/common/nginx-routes.nix
new file mode 100644
index 0000000..9a8534e
--- /dev/null
+++ b/modules/common/nginx-routes.nix
@@ -0,0 +1,107 @@
+{ pkgs, helpers, config, lib, ... }: {
+  clicks.nginx.services = with helpers.nginx; [
+    (Host "signup.hopescaramels.com" (ReverseProxy "caramels:1024"))
+    (Host "freeflowtaekwondo.com" (ReverseProxy "generic:1026"))
+    (Host "homebridge.coded.codes" (ReverseProxy "CodedPi:8581"))
+    (Host "codedpc.coded.codes" (ReverseProxy "SamuelDesktop:3389"))
+    (Host "testing.coded.codes" (ReverseProxy "SamuelDesktop:3000"))
+    (Hosts [ "kavita.coded.codes" "reading.coded.codes" ]
+      (ReverseProxy "127.0.0.1:5000"))
+    (Host "clicks.codes" (ReverseProxy "127.0.0.1:3000"))
+    (Host "passwords.clicks.codes" (ReverseProxy "127.0.0.1:8452"))
+    (Host "login.clicks.codes" (ReverseProxy "127.0.0.1:9083"))
+    (Hosts [
+      "syncthing.clicks.codes"
+      "syncthing.coded.codes"
+      "syncthing.thecoded.prof"
+      "syncthing.hopescaramels.com"
+    ] (ReverseProxy "127.0.0.1:8384"))
+    (Hosts [ "gerrit.clicks.codes" "git.clicks.codes" ]
+      (ReverseProxy "127.0.0.255:1000"))
+    (Hosts [ "grafana.clicks.codes" "logs.clicks.codes" ]
+      (ReverseProxy "127.0.0.1:9052"))
+    (InsecureHosts [
+      "mail.clicks.codes"
+      "mail.coded.codes"
+      "mail.hopescaramels.com"
+      "autoconfig.coded.codes"
+      "autoconfig.clicks.codes"
+      "autoconfig.hopescaramels.com"
+      "imap.coded.codes"
+      "imap.clicks.codes"
+      "imap.hopescaramels.com"
+      "pop.coded.codes"
+      "pop.clicks.codes"
+      "pop.hopescaramels.com"
+      "smtp.coded.codes"
+      "smtp.clicks.codes"
+      "smtp.hopescaramels.com"
+    ] (ReverseProxy "127.0.0.1:1080"))
+    (Host "matrix.coded.codes" (Directory "${builtins.toString
+      (pkgs.schildichat-web.override {
+        conf = {
+          default_server_config =
+            lib.pipe ./nginx/coded.codes/.well-known/matrix [
+              builtins.readFile
+              builtins.fromJSON
+            ];
+          features = {
+            feature_report_to_moderators = true;
+            feature_latex_maths = true;
+            feature_pinning = true;
+            feature_mjolnir = true;
+            feature_presence_in_room_list = true;
+            feature_custom_themes = true;
+            feature_dehydration = true;
+          };
+          setting_defaults = { "fallbackICEServerAllowed" = true; };
+          default_theme = "dark";
+          permalink_prefix = "https://matrix.coded.codes";
+          disable_guests = true;
+          disable_3pid_login = true;
+        };
+      })}"))
+    (Host "api.clicks.codes"
+      (Path "/nucleus/" (ReverseProxy "127.0.0.1:10000")))
+    (Host "api.coded.codes"
+      (Path "/nucleus/" (ReverseProxy "SamuelDesktop:10000")))
+    (Host "coded.codes" (Compose [
+      (Path "/.well-known/matrix/"
+        (File ./nginx/coded.codes/.well-known/matrix))
+      (Redirect "https://clicks.codes$request_uri")
+    ]))
+    (Host "matrix-backend.coded.codes" (Compose [
+      (Path "/_synapse/admin/" (Status 403))
+      (ReverseProxy "127.0.0.1:4527")
+    ]))
+  ];
+  clicks.nginx.serviceAliases = with helpers.nginx; [
+    (Aliases "nextcloud.clicks.codes" [
+      "cloud.clicks.codes"
+      "docs.clicks.codes"
+      "www.cloud.clicks.codes"
+      "www.docs.clicks.codes"
+      "www.nextcloud.clicks.codes"
+    ])
+    (Aliases "privatebin" [
+      "paste.clicks.codes"
+      "paste.coded.codes"
+      "paste.thecoded.prof"
+      "paste.hopescaramels.com"
+      "www.paste.clicks.codes"
+      "www.paste.coded.codes"
+      "www.paste.thecoded.prof"
+      "www.paste.hopescaramels.com"
+      "www.privatebin.clicks.codes"
+    ])
+  ];
+  clicks.nginx.streams = with helpers.nginx; [
+    (ProxyStream 143 "127.0.0.1:1143" "tcp") # imap
+    (ProxyStream 993 "127.0.0.1:1993" "tcp") # imap
+    (ProxyStream 110 "127.0.0.1:1110" "tcp") # pop3
+    (ProxyStream 995 "127.0.0.1:1995" "tcp") # pop3
+    (ProxyStream 25 "127.0.0.1:1025" "tcp") # smtp
+    (ProxyStream 465 "127.0.0.1:1465" "tcp") # smtp
+    (ProxyStream 587 "127.0.0.1:1587" "tcp") # smtp
+  ];
+}
diff --git a/modules/common/nginx.nix b/modules/common/nginx.nix
new file mode 100644
index 0000000..6bd95e3
--- /dev/null
+++ b/modules/common/nginx.nix
@@ -0,0 +1,199 @@
+{ config, lib, pkgs, helpers, base, ... }:
+lib.recursiveUpdate {
+  options.clicks = {
+    nginx = {
+      services = lib.mkOption {
+        type = with lib.types;
+          listOf (submodule {
+            options = {
+              host = lib.mkOption { type = str; };
+              extraHosts = lib.mkOption { type = listOf str; };
+              secure = lib.mkOption { type = bool; };
+              service = lib.mkOption {
+                type = let
+                  validServiceTypes = {
+                    "redirect" = {
+                      to = [ "string" str ];
+                      permanent = [ "bool" bool ];
+                    };
+                    "reverseproxy" = { to = [ "string" str ]; };
+                    "php" = {
+                      root = [ "string" str ];
+                      socket = [ "string" str ];
+                    };
+                    "directory" = {
+                      private = [ "bool" bool ];
+                      root = [ "string" str ];
+                    };
+                    "file" = { path = [ "string" str ]; };
+                    "path" = {
+                      path = [ "string" str ];
+                      service = [ "set" serviceType ];
+                    };
+                    "compose" = { services = [ "list" (listOf serviceType) ]; };
+                    "status" = { statusCode = [ "int" int ]; };
+                  };
+
+                  serviceType = mkOptionType {
+                    name = "Service";
+
+                    description = "clicks Nginx service";
+                    descriptionClass = "noun";
+
+                    check = (x:
+                      if (builtins.typeOf x) != "set" then
+                        lib.warn
+                        "clicks nginx services must be sets but ${x} is not a set"
+                        false
+                      else if !(builtins.hasAttr "type" x) then
+                        lib.warn
+                        "clicks nginx services must have a type attribute but ${x} does not"
+                        false
+                      else if !(builtins.hasAttr x.type validServiceTypes) then
+                        lib.warn
+                        "clicks nginx services must have a valid type, but ${x.type} is not one"
+                        false
+                      else
+                        (let
+                          optionTypes =
+                            (builtins.mapAttrs (n: o: builtins.elemAt o 0)
+                              validServiceTypes.${x.type}) // {
+                                type = "string";
+                              };
+                        in (lib.pipe x [
+                          (builtins.mapAttrs (n: o:
+                            (builtins.hasAttr n optionTypes) && optionTypes.${n}
+                            == (builtins.typeOf o)))
+                          lib.attrValues
+                          (builtins.all (x: x))
+                        ]) && (lib.pipe optionTypes [
+                          (builtins.mapAttrs (n: _: builtins.hasAttr n x))
+                          lib.attrValues
+                          (builtins.all (x: x))
+                        ])));
+                  };
+                in serviceType;
+              };
+              type = lib.mkOption { type = strMatching "hosts"; };
+            };
+          });
+        example = lib.literalExpression ''
+          with helpers.nginx; [
+            (Host "example.clicks.codes" (ReverseProxy "generic:1001"))
+          ]'';
+        description = lib.mdDoc ''
+          Connects hostnames to services for your nginx server. We recommend using the Clicks helper to generate these
+        '';
+        default = [ ];
+      };
+      serviceAliases = lib.mkOption {
+        type = with lib.types;
+          listOf (submodule {
+            options = {
+              host = lib.mkOption {
+                type = str;
+                example = "example.clicks.codes";
+                description = ''
+                  The ServerName of the server. If you override this in the nginx server block, you still need to put in the name of the attribute
+                '';
+              };
+              aliases = lib.mkOption {
+                type = listOf str;
+                example = [ "example2.clicks.codes" "example.coded.codes" ];
+                description = ''
+                  A list of servers to add as aliases
+                '';
+              };
+              type = lib.mkOption { type = strMatching "aliases"; };
+            };
+          });
+        example = lib.literalExpression ''
+          with helpers.nginx; [
+            (Host "example.clicks.codes" (ReverseProxy "generic:1001"))
+          ]'';
+        description = lib.mdDoc ''
+          Adds additional host names to your nginx server. If you're using `clicks.nginx.services`
+          you should generally use a Hosts block instead
+        '';
+        default = [ ];
+      };
+      streams = lib.mkOption {
+        type = with lib.types;
+          listOf (submodule {
+            options = {
+              internal = lib.mkOption { type = str; };
+              external = lib.mkOption { type = port; };
+              protocol = lib.mkOption { type = strMatching "^(tcp|udp)$"; };
+              haproxy = lib.mkOption { type = bool; };
+            };
+          });
+        example = lib.literalExpression ''
+          with helpers.nginx; [
+            (Stream 1001 "generic:1002" "tcp")
+          ]'';
+        description = lib.mdDoc ''
+          A list of servers to be placed in the nginx streams block. We recommend using the Clicks helper to generate these
+        '';
+        default = [ ];
+      };
+    };
+  };
+  config = {
+    services.nginx = {
+      enable = true;
+      enableReload = true;
+
+      virtualHosts = lib.recursiveUpdate (helpers.nginx.Merge
+        config.clicks.nginx.services) # clicks.nginx.services
+        (lib.pipe config.clicks.nginx.serviceAliases [
+          (map (alias: {
+            name = alias.host;
+            value.serverAliases = alias.aliases;
+          }))
+          builtins.listToAttrs
+        ]); # clicks.nginx.serviceAliases
+
+      streamConfig = builtins.concatStringsSep "\n" (map (stream: ''
+        server {
+            listen ${builtins.toString stream.external}${
+              lib.optionalString (stream.protocol == "udp") " udp"
+            };
+            proxy_pass ${stream.internal};
+            ${if stream.haproxy then "proxy_protocol on;" else ""}
+        }
+      '') config.clicks.nginx.streams);
+    };
+
+    networking.firewall.allowedTCPPorts = lib.pipe config.clicks.nginx.streams [
+      (builtins.filter (stream: stream.protocol == "tcp"))
+      (map (stream: stream.external))
+    ];
+    networking.firewall.allowedUDPPorts = lib.pipe config.clicks.nginx.streams [
+      (builtins.filter (stream: stream.protocol == "udp"))
+      (map (stream: stream.external))
+    ];
+
+    security.acme.defaults = {
+      email = "admin@clicks.codes";
+      credentialsFile = config.sops.secrets.cloudflare_cert__api_token.path;
+    };
+    security.acme.acceptTerms = true;
+
+    sops.secrets.cloudflare_cert__api_token = {
+      mode = "0660";
+      owner = config.users.users.nginx.name;
+      group = config.users.users.acme.group;
+      sopsFile = ../../secrets/cloudflare-cert.env.bin;
+      format = "binary";
+    };
+
+    users.users.nginx.extraGroups = [ config.users.users.acme.group ];
+  };
+} (if base != null then {
+  config.security.acme.certs = lib.mkForce (builtins.mapAttrs (_: v:
+    (lib.filterAttrs (n: _: n != "directory") v) // {
+      webroot = null;
+      dnsProvider = "cloudflare";
+    }) base.config.security.acme.certs);
+} else
+  { })
diff --git a/modules/common/nginx/coded.codes/.well-known/matrix b/modules/common/nginx/coded.codes/.well-known/matrix
new file mode 100644
index 0000000..9c679da
--- /dev/null
+++ b/modules/common/nginx/coded.codes/.well-known/matrix
@@ -0,0 +1,7 @@
+{
+  "m.server": "matrix-backend.coded.codes:443",
+  "m.homeserver": {
+    "base_url": "https://matrix-backend.coded.codes:443",
+    "server_name": "coded.codes"
+  }
+}
diff --git a/modules/common/node.nix b/modules/common/node.nix
new file mode 100644
index 0000000..85cb87e
--- /dev/null
+++ b/modules/common/node.nix
@@ -0,0 +1,9 @@
+{ config, pkgs, ... }: {
+  environment.systemPackages = with pkgs; [
+    nodejs_20
+    nodePackages.typescript
+    nodePackages.pnpm
+    yarn
+    nodePackages.pm2
+  ];
+}
diff --git a/modules/common/postgres.nix b/modules/common/postgres.nix
new file mode 100644
index 0000000..397a377
--- /dev/null
+++ b/modules/common/postgres.nix
@@ -0,0 +1,115 @@
+{ lib, config, pkgs, ... }: {
+  services.postgresql = {
+    enable = true;
+
+    package = pkgs.postgresql;
+    settings = {
+      log_connections = true;
+      logging_collector = true;
+      log_disconnections = true;
+      log_destination = lib.mkForce "syslog";
+    };
+
+    ensureDatabases =
+      [ "vaultwarden" "gerrit" "privatebin" "keycloak" "nextcloud" ];
+
+    ensureUsers = [
+      {
+        name = "clicks_grafana";
+        ensurePermissions = {
+          "ALL TABLES IN SCHEMA public" = "SELECT";
+          "SCHEMA public" = "USAGE";
+        };
+      }
+      {
+        name = "synapse";
+        ensurePermissions = { "DATABASE synapse" = "ALL PRIVILEGES"; };
+      }
+      {
+        name = "keycloak";
+        ensurePermissions = { "DATABASE keycloak" = "ALL PRIVILEGES"; };
+      }
+      {
+        name = "vaultwarden";
+        ensurePermissions = { "DATABASE vaultwarden" = "ALL PRIVILEGES"; };
+      }
+      {
+        name = "privatebin";
+        ensurePermissions = { "DATABASE privatebin" = "ALL PRIVILEGES"; };
+      }
+      {
+        name = "nextcloud";
+        ensurePermissions = { "DATABASE nextcloud" = "ALL PRIVILEGES"; };
+      }
+    ] ++ (map (name: ({
+      inherit name;
+      ensurePermissions = { "ALL TABLES IN SCHEMA public" = "ALL PRIVILEGES"; };
+    })) [ "minion" "coded" "pineafan" ]);
+
+  };
+
+  systemd.services.postgresql.postStart = lib.mkMerge [
+    (let
+      database = "synapse";
+      cfg = config.services.postgresql;
+    in lib.mkBefore (''
+      PSQL="psql --port=${toString cfg.port}"
+
+      while ! $PSQL -d postgres -c "" 2> /dev/null; do
+          if ! kill -0 "$MAINPID"; then exit 1; fi
+          sleep 0.1
+      done
+
+      $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${database}'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "${database}" WITH LC_CTYPE="C" LC_COLLATE="C" TEMPLATE="template0"'
+    '') # synapse needs C collation, so we can't use ensureDatabases for it
+    )
+    (lib.mkAfter (lib.pipe [
+      {
+        user = "clicks_grafana";
+        passwordFile = config.sops.secrets.clicks_grafana_db_password.path;
+      }
+      {
+        user = "keycloak";
+        passwordFile = config.sops.secrets.clicks_keycloak_db_password.path;
+      }
+      {
+        user = "vaultwarden";
+        passwordFile = config.sops.secrets.clicks_bitwarden_db_password.path;
+      }
+      {
+        user = "privatebin";
+        passwordFile = config.sops.secrets.clicks_privatebin_db_password.path;
+      }
+      {
+        user = "nextcloud";
+        passwordFile = config.sops.secrets.clicks_nextcloud_db_password.path;
+      }
+    ] [
+      (map (userData: ''
+        $PSQL -tAc "ALTER USER ${userData.user} PASSWORD '$(cat ${userData.passwordFile})';"
+      ''))
+      (lib.concatStringsSep "\n")
+    ]))
+  ];
+
+  sops.secrets = lib.pipe [
+    "clicks_grafana_db_password"
+    "clicks_keycloak_db_password"
+    "clicks_bitwarden_db_password"
+    "clicks_privatebin_db_password"
+    "clicks_nextcloud_db_password"
+  ] [
+    (map (name: {
+      inherit name;
+      value = {
+        mode = "0400";
+        owner = config.services.postgresql.superUser;
+        group =
+          config.users.users.${config.services.postgresql.superUser}.group;
+        sopsFile = ../../secrets/postgres.json;
+        format = "json";
+      };
+    }))
+    builtins.listToAttrs
+  ];
+}
diff --git a/modules/common/privatebin.nix b/modules/common/privatebin.nix
new file mode 100644
index 0000000..eece255
--- /dev/null
+++ b/modules/common/privatebin.nix
@@ -0,0 +1,77 @@
+{ config, lib, base, ... }:
+lib.recursiveUpdate {
+  services.nginx.virtualHosts.privatebin = {
+    serverName = lib.mkForce "privatebin.clicks.codes";
+    enableACME = lib.mkForce true;
+    forceSSL = lib.mkForce true;
+  };
+  services.privatebin = {
+    enable = true;
+    settings = {
+      main = {
+        name = "Clicks Minute Paste";
+        basepath = "https://privatebin.clicks.codes/";
+        opendiscussion = true;
+        fileupload = true;
+
+        defaultformatter = "syntaxhighlighting";
+        syntaxhighlightingtheme = "sons-of-obsidian";
+        template = "bootstrap-dark";
+
+        info = ''
+          Powered by <a href="https://privatebin.info/">PrivateBin</a>. Provided as a service free-of-charge by Clicks. Come chat with us <a href="https://matrix.to/#/#global:coded.codes"> on Matrix</a>'';
+        notice =
+          "This service has no guarantee of uptime, and pastes are not backed up. If you need somewhere to host the last words of your wise old grandfather for time immemorial this is not the place.";
+
+        langaugeselection = true;
+      };
+
+      expire.default = "1month";
+
+      nginx.forceSSL = lib.mkForce true;
+
+      expire_options = {
+        "5min" = 300;
+        "10min" = 600;
+        "1hour" = 3600;
+        "1day" = 86400;
+        "1week" = 604800;
+        "1month" = 2592000;
+      };
+
+      formatter_options = {
+        syntaxhighlighting = "Source Code";
+        markdown = "Markdown";
+        plaintext = "Plain Text";
+      };
+
+      traffic = {
+        exempted =
+          "10.0.0.0/8,127.0.0.0/8,169.254.0.0/16,172.16.0.0/12,192.168.0.0/16";
+      };
+
+      model.class = "Database";
+      model_options = {
+        dsn = "pgsql:host=localhost;dbname=privatebin";
+        tbl = "privatebin";
+        usr = "privatebin";
+        pwd._env = "PRIVATEBIN_DB_PASSWORD";
+      };
+    };
+  };
+} (if base != null then {
+  services.privatebin.environmentFiles =
+    [ config.scalpel.trafos."privatebin.env".destination ];
+
+  scalpel.trafos."privatebin.env" = {
+    source = builtins.toFile "privatebin.env" ''
+      PRIVATEBIN_DB_PASSWORD=!!privatebin_db_password!!
+    '';
+    matchers."privatebin_db_password".secret =
+      config.sops.secrets.clicks_privatebin_db_password.path;
+    owner = config.users.users.privatebin.name;
+    group = config.users.users.privatebin.group;
+    mode = "0400";
+  };
+} else
+  { })
diff --git a/modules/common/scalpel.nix b/modules/common/scalpel.nix
new file mode 100644
index 0000000..49ae103
--- /dev/null
+++ b/modules/common/scalpel.nix
@@ -0,0 +1,12 @@
+{ lib, config, ... }:
+let cfg = config.scalpel;
+in {
+  system.activationScripts.scalpelCreateStore.text = lib.mkForce ''
+    echo "[scalpel] Ensuring existance of ${cfg.secretsDir}"
+    mkdir -p ${cfg.secretsDir}
+    grep -q "${cfg.secretsDir} ramfs" /proc/mounts || mount -t ramfs none "${cfg.secretsDir}" -o nodev,nosuid,mode=0751
+
+    echo "[scalpel] Clearing old secrets from ${cfg.secretsDir}"
+    find '${cfg.secretsDir}' -wholename '${cfg.secretsDir}' -o -prune -exec rm -rf -- {} +
+  '';
+}
diff --git a/modules/common/shell.nix b/modules/common/shell.nix
new file mode 100644
index 0000000..18889bd
--- /dev/null
+++ b/modules/common/shell.nix
@@ -0,0 +1,4 @@
+{ pkgs, ... }: {
+  programs.zsh.enable = true;
+  environment.systemPackages = with pkgs; [ vim wget ];
+}
diff --git a/modules/common/ssh.nix b/modules/common/ssh.nix
new file mode 100644
index 0000000..70e1ebb
--- /dev/null
+++ b/modules/common/ssh.nix
@@ -0,0 +1,7 @@
+{
+  services.openssh = {
+    enable = true;
+    settings.PasswordAuthentication = false;
+  };
+  networking.firewall.allowedTCPPorts = [ 22 ];
+}
diff --git a/modules/common/static-ip.nix b/modules/common/static-ip.nix
new file mode 100644
index 0000000..d577c96
--- /dev/null
+++ b/modules/common/static-ip.nix
@@ -0,0 +1,9 @@
+{
+  networking.useDHCP = true;
+  networking.dhcpcd.extraConfig = ''
+    interface enp5s0
+    static ip_address=192.168.185.178/16
+    static routers=192.168.0.1
+    static domain_name_servers=127.0.0.1
+  '';
+}
diff --git a/modules/common/syncthing.nix b/modules/common/syncthing.nix
new file mode 100644
index 0000000..cea1012
--- /dev/null
+++ b/modules/common/syncthing.nix
@@ -0,0 +1,6 @@
+{ pkgs, ... }: {
+  services.syncthing.enable = true;
+  services.syncthing.openDefaultPorts = true;
+
+  services.syncthing.guiAddress = "0.0.0.0:8384";
+}
diff --git a/modules/common/tesseract.nix b/modules/common/tesseract.nix
new file mode 100644
index 0000000..d598323
--- /dev/null
+++ b/modules/common/tesseract.nix
@@ -0,0 +1 @@
+{ pkgs, ... }: { environment.systemPackages = [ pkgs.tesseract5 ]; }
diff --git a/modules/common/users.nix b/modules/common/users.nix
new file mode 100644
index 0000000..58dc7d3
--- /dev/null
+++ b/modules/common/users.nix
@@ -0,0 +1,48 @@
+{ pkgs, ... }: {
+  users.users.minion = {
+    isNormalUser = true;
+    extraGroups = [ "wheel" ];
+
+    openssh.authorizedKeys.keys = [
+      "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIIteIdlZv52nUDxW2SUsoJ2NZi/w9j1NZwuHanQ/o/DuAAAAHnNzaDpjb2xsYWJvcmFfeXViaWtleV9yZXNpZGVudA== collabora_yubikey_resident"
+      "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIJRzQbQjXFpHKtt8lpNKmoNx57+EJ/z3wnKOn3/LjM6cAAAAFXNzaDppeXViaWtleV9yZXNpZGVudA== iyubikey_resident"
+      "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIOhzJ0p9bFRSURUjV05rrt5jCbxPXke7juNbEC9ZJXS/AAAAGXNzaDp0aW55X3l1YmlrZXlfcmVzaWRlbnQ= tiny_yubikey_resident"
+    ];
+  };
+  users.users.coded = {
+    isNormalUser = true;
+    extraGroups = [ "wheel" ];
+    shell = pkgs.zsh;
+
+    openssh.authorizedKeys.keys = [
+      "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCZGErwcw5YUlJS9tAfIYOSqkiuDRZZRTJjMlrDaAiNwTjqUML/Lrcau/1KA6a0+sXCM8DhQ1e0qhh2Qxmh/kxZWO6XMVK2EB7ELPNojqFI16T8Bbhq2t7yVAqbPUhXLQ4xKGvWMCPWOCo/dY72P9yu7kkMV0kTW3nq25+8nvqIvvuQOlOUx1uyR7qEfO706O86wjVTIuwfZKyzMDIC909vyg0xS+SfFlD7MkBuGzevQnOAV3U6tyafg6XW4PaJuDLyGXwpKz6asY08F7gRL/7/GhlMB09nfFfT4sZggmqPdGAtxwsFuwHPjNSlktHz5nlHtpS0LjefR9mWiGIhw5Hw1z33lxP0rfmiEU9J7kFcXv9B8QAWFwWfNEZfeqB7h7DJruo+QRStGeDz4SwRG3GR+DB4iNJLt7n0ALkVFJpOpskeo8TV4+Fwok+hYs2GsvdEmh9Cj7dC/9CyRhJeam9iLIi/iVGZhXEE3tIiqEktZPjiK7JwQyr97zhGJ7Rj4oE= samue@SamuelDesktop"
+      "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIH+TJRuMpDPgh6Wp2h+E+O/WoyEAVyWo6SN8oxm2JZNVAAAABHNzaDo= samue@SamuelDesktop"
+      "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAILrwKN4dJQ0BiLmjsA/66QHhu06+JyokWtHkLcjhWU79AAAABHNzaDo= coded-sk-resident-1"
+    ];
+  };
+  users.users.pineafan = {
+    isNormalUser = true;
+    extraGroups = [ "wheel" ];
+    shell = pkgs.zsh;
+
+    openssh.authorizedKeys.keys = [
+      "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIFXa8ow7H8XpTrwYI+oSgLFfb6YNZanwv/QCKvEKiERSAAAABHNzaDo= pineapplefan@Pineapplefan"
+      "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAJNFMUYiEepGrIAbUM+Hlw/OuGWc8CNQsYlJ7519RVmeu+/vqEQbhchySTelibD19YqsZ7ICfYxAeQzOqHdXfs="
+    ];
+  };
+  users.users.nucleus = {
+    isSystemUser = true;
+    createHome = true;
+    home = "/services/nucleus";
+    group = "clicks";
+    shell = pkgs.bashInteractive;
+  };
+  users.users.websites = {
+    isSystemUser = true;
+    createHome = true;
+    home = "/services/websites";
+    group = "clicks";
+    shell = pkgs.bashInteractive;
+  };
+  users.groups.clicks = { };
+}
diff --git a/modules/common/vaultwarden.nix b/modules/common/vaultwarden.nix
new file mode 100644
index 0000000..40047dd
--- /dev/null
+++ b/modules/common/vaultwarden.nix
@@ -0,0 +1,161 @@
+{ base, pkgs, drive_paths, lib, config, ... }:
+if drive_paths == null
+then {}
+else lib.recursiveUpdate {
+  environment.systemPackages = with pkgs; [ vaultwarden ];
+
+  services.vaultwarden.enable = true;
+  services.vaultwarden.dbBackend = "postgresql";
+
+  sops.secrets = lib.pipe [
+    "ADMIN_TOKEN"
+    "SMTP_PASSWORD"
+    "YUBICO_SECRET_KEY"
+    "HIBP_API_KEY"
+  ] [
+    (map (name: {
+      inherit name;
+      value = {
+        mode = "0400";
+        owner = config.users.users.root.name;
+        group = config.users.users.root.group;
+        sopsFile = ../../secrets/vaultwarden.json;
+        format = "json";
+      };
+    }))
+    builtins.listToAttrs
+  ];
+} (let isDerived = base != null;
+in if isDerived
+# We cannot use mkIf as both sides are evaluated no matter the condition value
+# Given we use base as an attrset, mkIf will error if base is null in here
+then
+  with lib;
+  let
+    cfg = config.services.vaultwarden;
+
+    vaultwarden_config = {
+      # Server Settings
+      DOMAIN = "https://passwords.clicks.codes";
+      ROCKET_ADDRESS = "127.0.0.1";
+      ROCKET_PORT = 8452;
+
+      # General Settings
+      SIGNUPS_ALLOWED = false;
+      INVITATIONS_ALLOWED = true;
+      SIGNUPS_DOMAINS_WHITELIST =
+        "clicks.codes,coded.codes,thecoded.prof,starrysky.fyi,hopescaramels.com,pinea.dev,trans.gg";
+      SIGNUPS_VERIFY = true;
+
+      RSA_KEY_FILENAME =
+        "${drive_paths.External1000SSD.path}/bitwarden/rsa_key";
+      ICON_CACHE_FOLDER =
+        "${drive_paths.External1000SSD.path}/bitwarden/icon_cache";
+      ATTACHMENTS_FOLDER =
+        "${drive_paths.External4000HDD.path}/bitwarden/attachments";
+      SENDS_FOLDER = "${drive_paths.External4000HDD.path}/bitwarden/sends";
+      TMP_FOLDER = "${drive_paths.External4000HDD.path}/bitwarden/tmp";
+
+      DISABLE_2FA_REMEMBER = true;
+
+      # Admin Account
+      ADMIN_TOKEN = "!!ADMIN_TOKEN!!";
+
+      # Database Settings
+      DATABASE_URL =
+        "postgresql://vaultwarden:!!clicks_bitwarden_db_secret!!@127.0.0.1:${
+          toString config.services.postgresql.port
+        }/vaultwarden";
+
+      # Mail Settings
+      SMTP_HOST = "mail.clicks.codes";
+      SMTP_FROM = "bitwarden@clicks.codes";
+      SMTP_FROM_NAME = "Clicks Bitwarden";
+      SMTP_SECURITY = "starttls";
+      SMTP_PORT = 587;
+
+      SMTP_USERNAME = "bitwarden@clicks.codes";
+      SMTP_PASSWORD = "!!SMTP_PASSWORD!!";
+
+      REQUIRE_DEVICE_EMAIL = true;
+
+      IP_HEADER = "X-Forwarded-For";
+
+      # YubiKey Settings
+      YUBICO_CLIENT_ID = "89788";
+      YUBICO_SECRET_KEY = "!!YUBICO_SECRET_KEY!!";
+
+      # TODO: Buy a license
+      # HIBP Settings
+      # HIBP_API_KEY="!!HIBP_API_KEY!!";
+
+      ORG_ENABLE_GROUPS = true;
+      # I have looked at the risks. They seem relatively small in comparison to the utility
+      # (stuff like sync issues if you don't refresh your page)
+      # Also a general lack of real-world testing. Which, honestly, doesn't
+      # seem too bad. Please contact me *immediately* upon noticing issues
+      # as I want to make sure that as little as possible is lost if we need
+      # to restore from backups (although I doubt it'll come to that)
+    };
+
+    nameToEnvVar = name:
+      let
+        parts = builtins.split "([A-Z0-9]+)" name;
+        partsToEnvVar = parts:
+          foldl' (key: x:
+            let last = stringLength key - 1;
+            in if isList x then
+              key
+              + optionalString (key != "" && substring last 1 key != "_") "_"
+              + head x
+            else if key != "" && elem (substring 0 1 x)
+            lowerChars then # to handle e.g. [ "disable" [ "2FAR" ] "emember" ]
+              substring 0 last key
+              + optionalString (substring (last - 1) 1 key != "_") "_"
+              + substring last 1 key + toUpper x
+            else
+              key + toUpper x) "" parts;
+      in if builtins.match "[A-Z0-9_]+" name != null then
+        name
+      else
+        partsToEnvVar parts;
+
+    # Due to the different naming schemes allowed for config keys,
+    # we can only check for values consistently after converting them to their corresponding environment variable name.
+    configEnv = let
+      configEnv = concatMapAttrs (name: value:
+        optionalAttrs (value != null) {
+          ${nameToEnvVar name} =
+            if isBool value then boolToString value else toString value;
+        }) vaultwarden_config;
+    in {
+      DATA_FOLDER = "/var/lib/bitwarden_rs";
+    } // optionalAttrs (!(configEnv ? WEB_VAULT_ENABLED)
+      || configEnv.WEB_VAULT_ENABLED == "true") {
+        WEB_VAULT_FOLDER = "${cfg.webVaultPackage}/share/vaultwarden/vault";
+      } // configEnv;
+
+    configFile = pkgs.writeText "vaultwarden.env" (concatStrings (mapAttrsToList
+      (name: value: ''
+        ${name}=${value}
+      '') configEnv));
+  in {
+    scalpel.trafos."vaultwarden.env" = {
+      source = toString configFile;
+      matchers."ADMIN_TOKEN".secret = config.sops.secrets.ADMIN_TOKEN.path;
+      matchers."SMTP_PASSWORD".secret = config.sops.secrets.SMTP_PASSWORD.path;
+      matchers."YUBICO_SECRET_KEY".secret =
+        config.sops.secrets.YUBICO_SECRET_KEY.path;
+      matchers."HIBP_API_KEY".secret = config.sops.secrets.HIBP_API_KEY.path;
+      matchers."clicks_bitwarden_db_secret".secret =
+        config.sops.secrets.clicks_bitwarden_db_password.path;
+      owner = config.users.users.vaultwarden.name;
+      group = config.users.groups.vaultwarden.name;
+      mode = "0400";
+    };
+
+    services.vaultwarden.environmentFile =
+      config.scalpel.trafos."vaultwarden.env".destination;
+  }
+else
+  { })