Install privatebin, a pastebin alternative
diff --git a/flake.lock b/flake.lock
index 2bdf002..a7fb059 100644
--- a/flake.lock
+++ b/flake.lock
@@ -91,6 +91,22 @@
         "type": "github"
       }
     },
+    "nixpkgs-privatebin": {
+      "locked": {
+        "lastModified": 1691328775,
+        "narHash": "sha256-nz7Myc/3sW7/tN1QDYKrmHnH5f5eGdbcFt1FRDstavk=",
+        "owner": "e1mo",
+        "repo": "nixpkgs",
+        "rev": "e1b0550bc7498d61ba95bcd089d62e256ef1677a",
+        "type": "github"
+      },
+      "original": {
+        "owner": "e1mo",
+        "ref": "privatebin",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
     "nixpkgs-stable": {
       "locked": {
         "lastModified": 1685758009,
@@ -129,6 +145,7 @@
         "flake-utils": "flake-utils",
         "home-manager": "home-manager",
         "nixpkgs": "nixpkgs_2",
+        "nixpkgs-privatebin": "nixpkgs-privatebin",
         "scalpel": "scalpel",
         "sops-nix": "sops-nix"
       }
diff --git a/flake.nix b/flake.nix
index 8575fca..caa0571 100644
--- a/flake.nix
+++ b/flake.nix
@@ -15,12 +15,26 @@
   inputs.scalpel.inputs.nixpkgs.follows = "nixpkgs";
   inputs.scalpel.inputs.sops-nix.follows = "sops-nix";
 
-  outputs = { self, nixpkgs, deploy-rs, home-manager, sops-nix, scalpel, ... }@inputs:
+  inputs.nixpkgs-privatebin.url = "github:e1mo/nixpkgs/privatebin";
+
+  outputs =
+    { self
+    , nixpkgs
+    , deploy-rs
+    , home-manager
+    , sops-nix
+    , scalpel
+    , nixpkgs-privatebin
+    , ...
+    }@inputs:
     let
       system = "x86_64-linux";
       pkgs = import nixpkgs {
         inherit system;
         config.allowUnfree = true;
+        overlays = [
+          (final: prev: { inherit (nixpkgs-privatebin.legacyPackages.${system}) privatebin pbcli; })
+        ];
       };
     in
     rec {
@@ -51,6 +65,7 @@
               ./modules/mongodb.nix
               ./modules/node.nix
               ./modules/postgres.nix
+              ./modules/privatebin.nix
               ./modules/samba.nix
               ./modules/scalpel.nix
               ./modules/ssh.nix
@@ -59,6 +74,7 @@
               ./modules/tesseract.nix
               ./modules/vaultwarden.nix
               sops-nix.nixosModules.sops
+              "${nixpkgs-privatebin}/nixos/modules/services/web-apps/privatebin.nix"
               {
                 users.mutableUsers = false;
               }
diff --git a/modules/caddy/caddyfile.nix b/modules/caddy/caddyfile.nix
index f3c8b20..69fbe2b 100644
--- a/modules/caddy/caddyfile.nix
+++ b/modules/caddy/caddyfile.nix
@@ -18,6 +18,80 @@
     match = [{ host = hosts; }];
     terminal = true;
   };
+  PHPRoute = hosts: root: socket: {
+    handle = [
+      {
+        handler = "subroute";
+        routes = [
+          {
+            handle = [
+              {
+                handler = "vars";
+                inherit root;
+              }
+            ];
+          }
+          {
+            handle = [
+              {
+                handler = "static_response";
+                headers.Location = [ "{http.request.orig_uri.path}/" ];
+                status_code = 307;
+              }
+            ];
+            match = [
+              {
+                file.try_files = [ "{http.request.uri.path}/index.php" ];
+                not = [ { path = ["*/"]; } ];
+              }
+            ];
+          }
+          {
+            handle = [
+              {
+                handler = "rewrite";
+                uri = "{http.matchers.file.relative}";
+              }
+            ];
+            match = [
+              {
+                file = {
+                  split_path = [ ".php" ];
+                  try_files = [
+                    "{http.request.uri.path}"
+                    "{http.request.uri.path}/index.php"
+                    "index.php"
+                  ];
+                };
+              }
+            ];
+          }
+          {
+            handle = [
+              {
+                handler = "reverse_proxy";
+                transport = {
+                  protocol = "fastcgi";
+                  split_path = [".php"];
+                };
+                upstreams = [{ dial = socket; }];
+              }
+            ];
+            match = [{ path = ["*.php"]; }];
+          }
+          {
+            handle = [
+              {
+                handler = "file_server";
+              }
+            ];
+          }
+        ];
+      }
+    ];
+    match = [{ host = hosts; }];
+    terminal = true;
+  };
   HTTPRedirectRoute = hosts: goto: {
     handle = [
       {
@@ -286,6 +360,11 @@
             "syncthing.thecoded.prof"
             "syncthing.hopescaramels.com"
           ] [ "localhost:8384" ])
+          (PHPRoute
+            [ "paste.clicks.codes" "paste.coded.codes" ]
+            "${pkgs.privatebin}/share/privatebin"
+            "unix/${config.services.phpfpm.pools.privatebin.socket}"
+          )
         ];
       };
       srv1 = {
diff --git a/modules/postgres.nix b/modules/postgres.nix
index 8f6c5f1..7a5074a 100644
--- a/modules/postgres.nix
+++ b/modules/postgres.nix
@@ -12,6 +12,7 @@
 
     ensureDatabases = [
       "vaultwarden"
+      "privatebin"
     ];
 
     ensureUsers = [
@@ -34,6 +35,12 @@
           "DATABASE vaultwarden" = "ALL PRIVILEGES";
         };
       }
+      {
+        name = "privatebin";
+        ensurePermissions = {
+          "DATABASE privatebin" = "ALL PRIVILEGES";
+        };
+      }
     ] ++ (map
       (name: (
         {
@@ -66,6 +73,7 @@
     (lib.mkAfter (lib.pipe [
       { user = "clicks_grafana"; passwordFile = config.sops.secrets.clicks_grafana_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; }
     ] [
       (map (userData: ''
         $PSQL -tAc "ALTER USER ${userData.user} PASSWORD '$(cat ${userData.passwordFile})';"
@@ -77,6 +85,7 @@
   sops.secrets = lib.pipe [
     "clicks_grafana_db_password"
     "clicks_bitwarden_db_password"
+    "clicks_privatebin_db_password"
   ] [
     (map (name: {
       inherit name;
diff --git a/modules/privatebin.nix b/modules/privatebin.nix
new file mode 100644
index 0000000..5dd6a28
--- /dev/null
+++ b/modules/privatebin.nix
@@ -0,0 +1,73 @@
+{ config, lib, base, ... }:
+lib.recursiveUpdate
+{
+  services.nginx.enable = false; # PrivateBin attempts to enable nginx but we already use caddy
+  services.privatebin = {
+    enable = true;
+    settings = {
+      main = {
+        name = "Clicks Minute Paste";
+        basepath = "https://paste.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";
+
+      expire_options = {
+        "5min" = 300; # looks bonkers, but I'm trying to keep the list ordered while also keeping the privatebin label formatter happy
+        "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/secrets/postgres.json b/secrets/postgres.json
index ae3bcfc..5bb202b 100644
--- a/secrets/postgres.json
+++ b/secrets/postgres.json
@@ -1,6 +1,7 @@
 {
 	"clicks_grafana_db_password": "ENC[AES256_GCM,data:tFByC3OyhRLkDlfjwq3Kmc7PnTHWmkXpXuqOGb2AzA9dkAijPggPhgvCrbkY8/oL8QwQDaI24+XV3U/8A2UwLbzu0L5oaWV/E4EJbyvi8UKp8Wg8Au25E0nD5tJZm7QQ3FVERgoUefcB8AEPJ4Z8Rgx1PuoBeun9toT1GkJtmuYNNHpOcFrbmaI/Qf1MP+yFZLYjvB1jz07V04RGTv4jow61lWFknS2aPJyat43Ogp64lIkfjen7zCvj3CWghfJx87uxeXsnFHMrRwfONozUdw19Bq1uLUJ7xvPqDtr/1WKi1xvBe5ez7/PkPslNJlIToIlL89xN/lOm2iQR2BNeXg==,iv:ruC4PzKpWYsz2qe0KImUo0YhRt2cisYx306yfPtzi6c=,tag:U8vg7w1zyqXAWH3WzNAHFA==,type:str]",
 	"clicks_bitwarden_db_password": "ENC[AES256_GCM,data:Xr9h/QVazxn1tzSNJgPH9uk6RE2bk0fNyOVNxSlflQ14wnna0xA9Uw4CcN9DbPyvBCvYsKrxTJOAzLOn/K4rc/G1C7dPKpGADiTH5O+1wX+PifrDHtQyuqvaarwO3QA20ZW48A==,iv:yKYDF3X9fNO+rbnguo5DAiXfCkjH06VkpGBjB9NGv/s=,tag:W8fAVyJGh6jQxwHMN+RAPg==,type:str]",
+	"clicks_privatebin_db_password": "ENC[AES256_GCM,data:FqTQzRxzv1Jelr7uPgQO0g9n653ilAriacvfj0ZQ7mnD8PEpE5SsZ4z8pOZJxCyIHFGE1n1iFudTJ3RXdSdsQ3Rxw7/eoz7IlblnQFQQKdTbpApNAkKyCKr5tUhUPqjTFixxGwNO8yxNUCTtdrBBbVZoxyAwl8lWXhI78fiFGA8=,iv:lqCfX0P+xHtIJfRW1DUjfwNlmvRCkg+y+qogsETL0RM=,tag:H855oMdkflQP6Drlefv6dA==,type:str]",
 	"sops": {
 		"kms": null,
 		"gcp_kms": null,
@@ -20,8 +21,8 @@
 				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByN0hMaXF2S2xiTTNqbm9h\nSGNieGNjNThJTk0wZXNQejBXaW9MOEp2akNFCm8rWVc3WG9pVndERFZzUnFOZnVG\naFBxTENlQ0x0ZjdqUHF6SFBPKzQ4bUEKLS0tIGVJTmdveTZrNTRiSTl0cHRQeWhl\nY2RmUTVQVTNoMFhLdkc3WFZEcHAycnMKqr42TSx7Pqcu62XgX4gj/iq2tbkZjFxg\nOcWBsLzqOsu/r0w5cK2Ple6JFGIJwmT2SqVqZh1pPbPwYHHXHbEphg==\n-----END AGE ENCRYPTED FILE-----\n"
 			}
 		],
-		"lastmodified": "2023-06-13T21:01:47Z",
-		"mac": "ENC[AES256_GCM,data:vm4yvl63zP9abxXuCtjjRh9LOVq63Q3nc8o8p2wj+v0pd1ltVZAMKmi/c6f4q+s12M0EYNQbqspLpG4OYva6WinAIyVJIvjvBJGKseH+lB2GrO+3jwlanOiHRl2ueAhihfGgj3dgqGiLUKoT1jFewZMoAY+PiGhp+Bup/Ziky9g=,iv:4YGy2zExsKUGQ9WAir0MFH9mFirbCPkjS523w1wpys4=,tag:nF8pO9I57AGAydFupYYU0w==,type:str]",
+		"lastmodified": "2023-07-30T14:59:54Z",
+		"mac": "ENC[AES256_GCM,data:Miimq3cBEfHhtF+zUWzYUs0pGfEEvA0ZXJzgJabH19FfIMSR6dXpdN807i7Of1AGYNYlGpxS3aPbDrpB5wsKkDFwzTCIZDsaKeq0G/YVzNVxA7BVdw/I3048f/VqrGSenwPhbpElADrGsL+waXGbQ/GA5F4387yihi6ZenxeQIs=,iv:Tx4bRqLVaaNACumwumZrCPcfTg4i19TSpwd1Ifu1PmE=,tag:nFg7zNFiR9vah4bgId0jjA==,type:str]",
 		"pgp": null,
 		"unencrypted_suffix": "_unencrypted",
 		"version": "3.7.3"