Replace dendrite with synapse
diff --git a/modules/matrix.nix b/modules/matrix.nix
index f669943..967e9d9 100644
--- a/modules/matrix.nix
+++ b/modules/matrix.nix
@@ -1,71 +1,48 @@
 { base, config, lib, pkgs, ... }:
-let
-  postgresUrlFor = service:
-    "postgres://dendrite:!!dendrite_db_password!!@localhost:${toString config.services.postgresql.port}/dendrite_${service}?sslmode=disable";
-in
 {
-  services.dendrite = {
+  services.matrix-synapse = {
     enable = true;
-    httpPort = 4527;
-    settings = {
-      global = {
-        server_name = "coded.codes";
-        private_key = config.sops.secrets.matrix_private_key.path;
-      };
-      user_api = {
-        account_database.connection_string = postgresUrlFor "account_database";
-        device_database.connection_string = postgresUrlFor "device_database";
-      };
-      sync_api = {
-        search.enable = true;
-        database.connection_string = postgresUrlFor "sync_api";
-      };
-      room_server.database.connection_string = postgresUrlFor "room_server";
-      mscs.database.connection_string = postgresUrlFor "mscs";
-      media_api.database.connection_string = postgresUrlFor "media_api";
-      key_server.database.connection_string = postgresUrlFor "key_server";
-      federation_api.database.connection_string = postgresUrlFor "federation_api";
-      app_service_api.database.connection_string = postgresUrlFor "app_service_api";
+    withJemalloc = true;
 
-      client_api.registration_shared_secret = "!!registration_shared_secret!!";
+    settings = {
+      server_name = "coded.codes";
+      enable_registration = true;
+      registration_requires_token = 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";
     };
   };
 
-  users.users.dendrite = {
-    isSystemUser = true;
-    createHome = true;
-    home = config.systemd.services.dendrite.serviceConfig.WorkingDirectory;
-    group = "clicks";
-    shell = pkgs.bashInteractive;
-  };
-
-  systemd.services.dendrite.serviceConfig = {
-    DynamicUser = lib.mkForce false;
-    User = lib.mkForce config.users.users.dendrite.name;
-    Group = lib.mkForce config.users.users.dendrite.group;
-  };
-
-  sops.secrets = (lib.pipe [
-    "registration_shared_secret"
-  ] [
-    (map (name: {
-      inherit name;
-      value = {
-        mode = "0400";
-        owner = config.users.users.root.name;
-        group = config.users.users.nobody.group;
-        sopsFile = ../secrets/matrix.json;
-        format = "json";
-      };
-    }))
-    builtins.listToAttrs
-  ]) // {
-    matrix_private_key = {
+  sops.secrets = {
+    registration_shared_secret = {
       mode = "0400";
-      owner = config.users.users.dendrite.name;
-      group = config.users.users.dendrite.group;
+      owner = config.users.users.root.name;
+      group = config.users.users.nobody.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;
     };
   };
 } // (
@@ -77,27 +54,77 @@
   # Given we use base as an attrset, mkIf will error if base is null in here
   then
     let
-      ExecStartPre = "${base.config.systemd.services.dendrite.serviceConfig.ExecStartPre}";
-      dendrite_cfgfile = builtins.head (builtins.match ".*-i ([^[:space:]]+).*" "${ExecStartPre}");
+      synapse_cfgfile = config.services.matrix-synapse.configFile;
     in
     {
-      scalpel.trafos."dendrite.yaml" = {
-        source = dendrite_cfgfile;
-        matchers."dendrite_db_password".secret =
-          config.sops.secrets.dendrite_db_password.path; # Defined in postgres.nix
+      scalpel.trafos."synapse.yaml" = {
+        source = toString synapse_cfgfile;
         matchers."registration_shared_secret".secret =
           config.sops.secrets.registration_shared_secret.path;
-        owner = config.users.users.dendrite.name;
-        group = config.users.users.dendrite.group;
+        owner = config.users.users.matrix-synapse.name;
+        group = config.users.users.matrix-synapse.group;
         mode = "0400";
       };
 
-      systemd.services.dendrite.serviceConfig.ExecStartPre = lib.mkForce (
+      systemd.services.matrix-synapse.serviceConfig.ExecStart = lib.mkForce (
         builtins.replaceStrings
-          [ "${dendrite_cfgfile}" ]
-          [ "${config.scalpel.trafos."dendrite.yaml".destination}" ]
-          "${ExecStartPre}"
+          [ "${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}"
+      );
+
+      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/postgres.nix b/modules/postgres.nix
index 63e7697..742e3d4 100644
--- a/modules/postgres.nix
+++ b/modules/postgres.nix
@@ -5,7 +5,6 @@
     package = pkgs.postgresql;
     settings = {
       log_connections = true;
-      log_statement = "all";
       logging_collector = true;
       log_disconnections = true;
       log_destination = lib.mkForce "syslog";
@@ -20,17 +19,9 @@
         };
       }
       {
-        name = "dendrite";
+        name = "synapse";
         ensurePermissions = {
-          "DATABASE dendrite_account_database" = "ALL PRIVILEGES";
-          "DATABASE dendrite_device_database" = "ALL PRIVILEGES";
-          "DATABASE dendrite_sync_api" = "ALL PRIVILEGES";
-          "DATABASE dendrite_room_server" = "ALL PRIVILEGES";
-          "DATABASE dendrite_mscs" = "ALL PRIVILEGES";
-          "DATABASE dendrite_media_api" = "ALL PRIVILEGES";
-          "DATABASE dendrite_key_server" = "ALL PRIVILEGES";
-          "DATABASE dendrite_federation_api" = "ALL PRIVILEGES";
-          "DATABASE dendrite_app_service_api" = "ALL PRIVILEGES";
+          "DATABASE synapse" = "ALL PRIVILEGES";
         };
       }
     ] ++ (map
@@ -41,33 +32,39 @@
         }
       )) [ "minion" "coded" "pinea" ]);
 
-    ensureDatabases = [
-      "dendrite_account_database"
-      "dendrite_device_database"
-      "dendrite_sync_api"
-      "dendrite_sync_api"
-      "dendrite_room_server"
-      "dendrite_mscs"
-      "dendrite_media_api"
-      "dendrite_key_server"
-      "dendrite_federation_api"
-      "dendrite_app_service_api"
-    ];
   };
 
-  systemd.services.postgresql.postStart = lib.mkAfter (lib.pipe [
-    { user = "clicks_grafana"; passwordFile = config.sops.secrets.clicks_grafana_db_password.path; }
-    { user = "dendrite"; passwordFile = config.sops.secrets.dendrite_db_password.path; }
-  ] [
-    (map (userData: ''
-      $PSQL -tAc "ALTER USER ${userData.user} PASSWORD '$(cat ${userData.passwordFile})';"
-    ''))
-    (lib.concatStringsSep "\n")
-  ]);
+  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; }
+    ] [
+      (map (userData: ''
+        $PSQL -tAc "ALTER USER ${userData.user} PASSWORD '$(cat ${userData.passwordFile})';"
+      ''))
+      (lib.concatStringsSep "\n")
+    ]))
+  ];
 
   sops.secrets = lib.pipe [
     "clicks_grafana_db_password"
-    "dendrite_db_password"
   ] [
     (map (name: {
       inherit name;
diff --git a/secrets/matrix_private_key.pem b/secrets/matrix_private_key.pem
index c28a422..fa67e12 100644
--- a/secrets/matrix_private_key.pem
+++ b/secrets/matrix_private_key.pem
@@ -1,5 +1,5 @@
 {
-	"data": "ENC[AES256_GCM,data:PKyWohFo987nrxaUxAEkVqyoqK5KDvNE9sJ/Mfqnh/VGQkETpiH+1RUmo9bJ/yghk9oEAzoKGig5kty9DJn6ge1y2/aV2HuwWhJtNGrPQmNle2KArR8R7zGLTIW8BONlmzz8CiXyzEJA+VxlacVYIEwTpdkhmCIq86ZOR5XG8mpI2LR8onZf1u0=,iv:xRGXx8jjK4IbrDRX5US5h+4rBEHhQshBXS+ag9mdfw8=,tag:MsUY0EB8k2dq4xDTRmeUEg==,type:str]",
+	"data": "ENC[AES256_GCM,data:HwjpdFoIk97E6knRuZ/0UItIRGoSm9e5ETIq6sGtiX+TdvNrjOgDVDe9M2G2xYWJzkL16B6J5ibToCU=,iv:uP2e3TramDRDg/MBV7mw4jrDE9tSO+Rm2RrzfNnigCg=,tag:cnAAATSKBV9eLJG0hyWCMw==,type:str]",
 	"sops": {
 		"kms": null,
 		"gcp_kms": null,
@@ -15,8 +15,8 @@
 				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxZm1ycXhVeEFCenFlcUQy\nWGEzN0cxYkd1alVsbkcxN25jK0paQjl4azBNClp0OVhXV1RjNlVOVDJObE1iYWd2\nSzVIUVRKUlc5ZEI0ZnVuWklhVG1zNDQKLS0tIDBpZFJaZzhxMzhad2tlWDFQNUJI\nVjdIblVqS1FhRTVHUkVoQXF5MGNwaDQK18s1piDOpkneG/Z+8CIW4X2c33QciDSD\nSgO3BIxY1KhGsRN/5LL7622WIeRjtMikysEcUYsG34XbEbUp7E7ibw==\n-----END AGE ENCRYPTED FILE-----\n"
 			}
 		],
-		"lastmodified": "2023-05-19T12:16:14Z",
-		"mac": "ENC[AES256_GCM,data:WeeZ1Yoj9aKzmdlvNBn3BPCD07GYhwXCs09PPgh0QtTtydmy4hYZRXQWnnwmA6nwSh1un5CZ9ZoCMXp9TPdJh6o8C5hqNV7sj+763nMFIycFu/u4dDtnQyawdhibHuqFM6yH0W0ZQ2r+6RdL9RpTPZqBkFuNVrKSh4jiVas7TgU=,iv:jOSlWvT9+AdO0xIGhJlR3yohjZs/eybIczn4i7Z39k8=,tag:JhheDVX1K61O+v5y3x4jXg==,type:str]",
+		"lastmodified": "2023-05-20T17:59:44Z",
+		"mac": "ENC[AES256_GCM,data:dtPjUdoiNArcOOPPd3H/JcrTQZZ5bVYezVRw+r89rgOMpiumYhKtzzanBAO1dxXKbEEjxevLEQ01scBq1X3tfwbp1Ghfe30TFZLivdB0zMI8mH50vYrXw0ZT8NYvNs82gXkxYy2NvDFtF+w/Jaut9EDNPRKzOLUqDGv/uFvOHX0=,iv:S7k7AiH5PrUAuIUR6Z/De0lX8mhrYfaJ+X98nJHw7nI=,tag:Rju73ZeWubCAmtje7ir99Q==,type:str]",
 		"pgp": null,
 		"unencrypted_suffix": "_unencrypted",
 		"version": "3.7.3"
diff --git a/secrets/postgres.json b/secrets/postgres.json
index 5755938..b8e71af 100644
--- a/secrets/postgres.json
+++ b/secrets/postgres.json
@@ -1,6 +1,5 @@
 {
-	"clicks_grafana_db_password": "ENC[AES256_GCM,data:0iiAk7Stag5pRLJgRac6XF57CWz4VIW4KPW6Q9iZxgA6FaQmhK0c9cLMpfsNpBn0LNqjxT/rjBveB0tKLMeIaTlMnGAOstMHiPY4q7/bnCEu8QXY3lDmz881Jq3JSuQrbZaiEWVS35jHsz+Gbb4q3m+TKGR4KWCkngF4mhbrMZESYixe2GgAnPbX/R+zs+HiQR/Z0x8PfjXiV56Kcqh0uQoVVEyusni64YtxHfA3bOkLQ+Prieq6yTLhdxKpsv3dtpKbj9GXD/p73KaZlWKr5TIDT+7NYuoK77OsSAGnOFjE4jd38fzk4El1e7qpc6Gk3w2wr72o5syVIQxOYBDGng==,iv:HYWWOPUgM3JCSvLFeRxXT55KjsFDN5kMSZXuOl2mO/g=,tag:wZmsdpmXUfEbqlePwko9hw==,type:str]",
-	"dendrite_db_password": "ENC[AES256_GCM,data:cCDapkXgw+tq37NrzbemcR68clsHmOuDdncpCVydtK3Wc4sjw4+1V/TYDvFsBIP+0YJPvIdNGh7QtLpl1HJD1HMHQM1wIOu0Bp4pjYA+OU7XiBXxgzrZcrQ2WyjVTa+Oh/8k16C7ZhyidAvQJF76mZjCEmoX9/yxNNQQ8vcdSYC6Z8vRNY/X4fGKAnXnNp4DsjEOqs8/WvvLAfgqCH7EcNc/ZWM8AjDR7fm+zFh7hGkSetthSOzjT7BmD61yYKYxGuYuKaBi2QFkSRlepAhAJ3GjS4xL03znZ8Mt1p7CoyARkSGoQPqrM3WKHxfpXUZOJSxFTmYOd9h9vcv+aJtXfA==,iv:t5UULLnERyMhZ6Kp+Hrb3138nMJyOBLOjQ8QqkrTHjw=,tag:AQQwn51asl39SK0SWMmb3g==,type:str]",
+	"clicks_grafana_db_password": "ENC[AES256_GCM,data:tFByC3OyhRLkDlfjwq3Kmc7PnTHWmkXpXuqOGb2AzA9dkAijPggPhgvCrbkY8/oL8QwQDaI24+XV3U/8A2UwLbzu0L5oaWV/E4EJbyvi8UKp8Wg8Au25E0nD5tJZm7QQ3FVERgoUefcB8AEPJ4Z8Rgx1PuoBeun9toT1GkJtmuYNNHpOcFrbmaI/Qf1MP+yFZLYjvB1jz07V04RGTv4jow61lWFknS2aPJyat43Ogp64lIkfjen7zCvj3CWghfJx87uxeXsnFHMrRwfONozUdw19Bq1uLUJ7xvPqDtr/1WKi1xvBe5ez7/PkPslNJlIToIlL89xN/lOm2iQR2BNeXg==,iv:ruC4PzKpWYsz2qe0KImUo0YhRt2cisYx306yfPtzi6c=,tag:U8vg7w1zyqXAWH3WzNAHFA==,type:str]",
 	"sops": {
 		"kms": null,
 		"gcp_kms": null,
@@ -16,8 +15,8 @@
 				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwNVB5L3hPcUFuaU1qNzdX\nL3psaUdpcHp1WkxmYS9Sdi9IY1BLbEd0aHdRCkp1TWlEWHY1RERMNWltK3N4a3d3\nYzhENENNYnVlaDdBcXBYYnQwSWRkTGMKLS0tIDlvWWlwNkNleVB5Y0RpZGJhRUR4\ndlVzY2plQ2loU2YwZXpIRnh2UFVFZTgKrgf/fmoatZbtnUSn1zVs0UJdcyipCqn7\nFh+K3dbT25JYD2U4glE3xW6D9TMTUBNGguHE3MQPJaK74FfGp7L/Bw==\n-----END AGE ENCRYPTED FILE-----\n"
 			}
 		],
-		"lastmodified": "2023-05-19T12:28:32Z",
-		"mac": "ENC[AES256_GCM,data:piOQ+0xxTxNvrpEXMxvPoglE9/uN4xseGfuavvluGl+koSi/Orde/cl6Ck9p8jl+bxc5Vs44qQcxIHAtjnXw1PrnD900yTELtZygtZkCdyBYBugUaP8sF/FJoorIBcPOhx0ld0Z+UdX+IdiPsyjZ9STvn2zBmnnSLOVW/qAp2Qs=,iv:snLwST4vK96HEL+//+k3ffYvRFMTtKPIlNHpnmesNIY=,tag:5g8Ja0BpG+xkABnOspC71A==,type:str]",
+		"lastmodified": "2023-05-20T19:42:49Z",
+		"mac": "ENC[AES256_GCM,data:UgiFqVr7UMXZUsbYtx7Q09Z9BwqXtAfqvaXybzsvT1XKFRaXnWT/XwjihU7EPjsDO9KzG3lXKln9DMzshAFJw3hjpN4D0LOmFySyduZl2qt5O/yKL7rrxhGgs57hITYB53DQvbkJ87W2sRAdpJlKWB1rrJ5J6RCqoxG/A3HjDAY=,iv:earJ7dyBUR2BGUVG7OhxgWMsPJl+JRB5xbKCe64AA7Q=,tag:UcuGo+aSfMkCHWtg8IDOSA==,type:str]",
 		"pgp": null,
 		"unencrypted_suffix": "_unencrypted",
 		"version": "3.7.3"