From: self <self@awful.systems>
Date: Mon, 3 Jul 2023 00:43:59 +0000 (-0700)
Subject: configure lemmy staging and dev instances
X-Git-Url: http://these/git/%7B%60/feeds/inbox/%22%7B%7D/static/gitweb.js?a=commitdiff_plain;h=ade4923a9d4d9b8597481b7fc4a2af2506e27189;p=awful.systems.git

configure lemmy staging and dev instances
---

diff --git a/hosts/these/configuration.nix b/hosts/these/configuration.nix
index 86e11e0..bf527a2 100644
--- a/hosts/these/configuration.nix
+++ b/hosts/these/configuration.nix
@@ -5,15 +5,36 @@
     ../../hardware/hetzner-cloud/cx21.nix
     ../../secrets
     ../../pass
-    #../../lemmy
+    ../../lemmy/staging
+    ../../lemmy/dev
     ../../maint-mode
     ../../git
   ];
 
   networking.hostName = "these";
 
-  awful.systems.maint-mode = {
-    enable = true;
-    virtualHost = "awful.systems";
+  services.nginx = {
+    recommendedProxySettings = true;
+    recommendedTlsSettings = true;
+
+    virtualHosts = {
+      "breaking.awful.systems" = {
+        addSSL = true;
+        enableACME = true;
+      };
+
+      "making.awful.systems" = {
+        addSSL = true;
+        enableACME = true;
+      };
+
+    };
   };
+
+  security.acme = {
+    acceptTerms = true;
+    defaults.email = "self@awful.systems";
+  };
+
+  networking.firewall.allowedTCPPorts = [ 443 ];
 }
diff --git a/lemmy/dev/default.nix b/lemmy/dev/default.nix
new file mode 100644
index 0000000..7bf0d31
--- /dev/null
+++ b/lemmy/dev/default.nix
@@ -0,0 +1,65 @@
+{ config, lib, pkgs, pkgs-unstable, ... }:
+
+{
+  imports = [ ./module.nix ];
+
+  services.lemmy-dev = {
+    enable = true;
+    server.package = pkgs.callPackage ../server.nix { Security = null; };
+    ui.package = pkgs.callPackage ../ui.nix { };
+    nginx.enable = true;
+    database.createLocally = true;
+
+    settings = {
+      hostname = "making.awful.systems";
+      setup = {
+        admin_username = "self";
+        admin_email = "self@awful.systems";
+        site_name = "a.s dev";
+      };
+      database = {
+        user = "lemmy_dev";
+        database = "lemmy_dev";
+      };
+    };
+  };
+
+  sops.secrets."lemmy-dev/initial_admin_password" = { };
+  sops.secrets."lemmy-dev/smtp_server" = { };
+  sops.secrets."lemmy-dev/smtp_login" = { };
+  sops.secrets."lemmy-dev/smtp_password" = { };
+  sops.secrets."lemmy-dev/smtp_from_address" = { };
+
+  sops.templates.lemmy-dev.content = builtins.toJSON
+    (config.services.lemmy-dev.settings // {
+      setup = config.services.lemmy-dev.settings.setup // {
+        admin_password = config.sops.placeholder."lemmy-dev/initial_admin_password";
+      };
+
+      # email = {
+      #   smtp_server = config.sops.placeholder."lemmy-dev/smtp_server";
+      #   smtp_login = config.sops.placeholder."lemmy-dev/smtp_login";
+      #   smtp_password = config.sops.placeholder."lemmy-dev/smtp_password";
+      #   smtp_from_address = config.sops.placeholder."lemmy-dev/smtp_from_address";
+      #   tls_type = "tls";
+      # };
+    });
+
+  systemd.services.lemmy-dev = {
+    serviceConfig = {
+      User = "lemmy_dev";
+      Group = "lemmy_dev";
+      LoadCredential =
+        "lemmy-dev:${config.sops.templates.lemmy-dev.path}";
+    };
+
+    environment = {
+      LEMMY_CONFIG_LOCATION = lib.mkForce "%d/lemmy-dev";
+      RUST_BACKTRACE = "full";
+      LEMMY_DATABASE_URL = pkgs.lib.mkForce
+        "postgres:///lemmy_dev?host=/run/postgresql&user=lemmy_dev";
+    };
+  };
+
+  networking.firewall.allowedTCPPorts = [ 80 ];
+}
diff --git a/lemmy/dev/module.nix b/lemmy/dev/module.nix
new file mode 100644
index 0000000..8ae458d
--- /dev/null
+++ b/lemmy/dev/module.nix
@@ -0,0 +1,266 @@
+{ lib, pkgs, config, ... }:
+with lib;
+let
+  cfg = config.services.lemmy-dev;
+  settingsFormat = pkgs.formats.json { };
+in {
+  imports = [
+    (mkRemovedOptionModule [ "services" "lemmy-dev" "jwtSecretPath" ]
+      "As of v0.13.0, Lemmy auto-generates the JWT secret.")
+  ];
+
+  options.services.lemmy-dev = {
+
+    enable = mkEnableOption
+      (lib.mdDoc "lemmy a federated alternative to reddit in rust");
+
+    server = { package = mkPackageOptionMD pkgs "lemmy-server" { }; };
+
+    ui = {
+      package = mkPackageOptionMD pkgs "lemmy-ui" { };
+
+      port = mkOption {
+        type = types.port;
+        default = 1236;
+        description =
+          lib.mdDoc "Port where lemmy-ui should listen for incoming requests.";
+      };
+    };
+
+    caddy.enable =
+      mkEnableOption (lib.mdDoc "exposing lemmy with the caddy reverse proxy");
+    nginx.enable =
+      mkEnableOption (lib.mdDoc "exposing lemmy with the nginx reverse proxy");
+
+    database = {
+      createLocally =
+        mkEnableOption (lib.mdDoc "creation of database on the instance");
+
+      uri = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = lib.mdDoc
+          "The connection URI to use. Takes priority over the configuration file if set.";
+      };
+    };
+
+    settings = mkOption {
+      default = { };
+      description = lib.mdDoc "Lemmy configuration";
+
+      type = types.submodule {
+        freeformType = settingsFormat.type;
+
+        options.hostname = mkOption {
+          type = types.str;
+          default = null;
+          description =
+            lib.mdDoc "The domain name of your instance (eg 'lemmy.ml').";
+        };
+
+        options.port = mkOption {
+          type = types.port;
+          default = 8538;
+          description =
+            lib.mdDoc "Port where lemmy should listen for incoming requests.";
+        };
+
+        options.captcha = {
+          enabled = mkOption {
+            type = types.bool;
+            default = true;
+            description = lib.mdDoc "Enable Captcha.";
+          };
+          difficulty = mkOption {
+            type = types.enum [ "easy" "medium" "hard" ];
+            default = "medium";
+            description = lib.mdDoc "The difficultly of the captcha to solve.";
+          };
+        };
+      };
+    };
+
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.lemmy-dev.settings = (mapAttrs (name: mkDefault) {
+      bind = "127.0.0.1";
+      tls_enabled = true;
+      pictrs_url = with config.services.pict-rs;
+        "http://${address}:${toString port}";
+      actor_name_max_length = 20;
+
+      rate_limit.message = 180;
+      rate_limit.message_per_second = 60;
+      rate_limit.post = 6;
+      rate_limit.post_per_second = 600;
+      rate_limit.register = 3;
+      rate_limit.register_per_second = 3600;
+      rate_limit.image = 6;
+      rate_limit.image_per_second = 3600;
+    } // {
+      database = mapAttrs (name: mkDefault) {
+        user = "lemmy";
+        host = "/run/postgresql";
+        port = 5432;
+        database = "lemmy";
+        pool_size = 5;
+      };
+    });
+
+    services.postgresql = mkIf cfg.database.createLocally {
+      enable = true;
+      ensureDatabases = [ cfg.settings.database.database ];
+      ensureUsers = [{
+        name = cfg.settings.database.user;
+        ensurePermissions."DATABASE ${cfg.settings.database.database}" =
+          "ALL PRIVILEGES";
+      }];
+    };
+
+    services.pict-rs.enable = true;
+
+    services.caddy = mkIf cfg.caddy.enable {
+      enable = mkDefault true;
+      virtualHosts."${cfg.settings.hostname}" = {
+        extraConfig = ''
+          handle_path /static/* {
+            root * ${cfg.ui.package}/dist
+            file_server
+          }
+          @for_backend {
+            path /api/* /pictrs/* /feeds/* /nodeinfo/*
+          }
+          handle @for_backend {
+            reverse_proxy 127.0.0.1:${toString cfg.settings.port}
+          }
+          @post {
+            method POST
+          }
+          handle @post {
+            reverse_proxy 127.0.0.1:${toString cfg.settings.port}
+          }
+          @jsonld {
+            header Accept "application/activity+json"
+            header Accept "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
+          }
+          handle @jsonld {
+            reverse_proxy 127.0.0.1:${toString cfg.settings.port}
+          }
+          handle {
+            reverse_proxy 127.0.0.1:${toString cfg.ui.port}
+          }
+        '';
+      };
+    };
+
+    services.nginx = mkIf cfg.nginx.enable {
+      enable = mkDefault true;
+      virtualHosts."${cfg.settings.hostname}".locations = let
+        ui = "http://127.0.0.1:${toString cfg.ui.port}";
+        backend = "http://127.0.0.1:${toString cfg.settings.port}";
+      in {
+        "~ ^/(api|pictrs|feeds|nodeinfo|.well-known)" = {
+          # backend requests
+          proxyPass = backend;
+          proxyWebsockets = true;
+          recommendedProxySettings = true;
+        };
+        "/" = {
+          # mixed frontend and backend requests, based on the request headers
+          proxyPass = "$proxpass";
+          recommendedProxySettings = true;
+          extraConfig = ''
+            set $proxpass "${ui}";
+            if ($http_accept = "application/activity+json") {
+              set $proxpass "${backend}";
+            }
+            if ($http_accept = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") {
+              set $proxpass "${backend}";
+            }
+            if ($request_method = POST) {
+              set $proxpass "${backend}";
+            }
+
+            # Cuts off the trailing slash on URLs to make them valid
+            rewrite ^(.+)/+$ $1 permanent;
+          '';
+        };
+      };
+    };
+
+    assertions = [
+      {
+        assertion = cfg.database.createLocally -> cfg.settings.database.host
+          == "localhost" || cfg.settings.database.host == "/run/postgresql";
+        message =
+          "if you want to create the database locally, you need to use a local database";
+      }
+      {
+        assertion = (!(hasAttrByPath [ "federation" ] cfg.settings))
+          && (!(hasAttrByPath [ "federation" "enabled" ] cfg.settings));
+        message =
+          "`services.lemmy.settings.federation` was removed in 0.17.0 and no longer has any effect";
+      }
+    ];
+
+    systemd.services.lemmy-dev = {
+      description = "Lemmy server (dev)";
+
+      environment = {
+        LEMMY_CONFIG_LOCATION =
+          "${settingsFormat.generate "config.hjson" cfg.settings}";
+        LEMMY_DATABASE_URL = mkIf (cfg.database.uri != null) cfg.database.uri;
+      };
+
+      documentation = [
+        "https://join-lemmy.org/docs/en/admins/from_scratch.html"
+        "https://join-lemmy.org/docs/en/"
+      ];
+
+      wantedBy = [ "multi-user.target" ];
+
+      after = [ "pict-rs.service" ]
+        ++ lib.optionals cfg.database.createLocally [ "postgresql.service" ];
+
+      requires =
+        lib.optionals cfg.database.createLocally [ "postgresql.service" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        RuntimeDirectory = "lemmy";
+        ExecStart = "${cfg.server.package}/bin/lemmy_server";
+      };
+    };
+
+    systemd.services.lemmy-ui-dev = {
+      description = "Lemmy UI (dev)";
+
+      environment = {
+        LEMMY_UI_HOST = "127.0.0.1:${toString cfg.ui.port}";
+        LEMMY_UI_LEMMY_INTERNAL_HOST = "127.0.0.1:${toString cfg.settings.port}";
+        LEMMY_UI_LEMMY_EXTERNAL_HOST = cfg.settings.hostname;
+        LEMMY_UI_HTTPS = "false";
+      };
+
+      documentation = [
+        "https://join-lemmy.org/docs/en/admins/from_scratch.html"
+        "https://join-lemmy.org/docs/en/"
+      ];
+
+      wantedBy = [ "multi-user.target" ];
+
+      after = [ "lemmy-dev.service" ];
+
+      requires = [ "lemmy-dev.service" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        WorkingDirectory = "${cfg.ui.package}";
+        ExecStart =
+          "${pkgs.nodejs}/bin/node ${cfg.ui.package}/dist/js/server.js";
+      };
+    };
+  };
+
+}
diff --git a/lemmy/default.nix b/lemmy/prod/default.nix
similarity index 91%
rename from lemmy/default.nix
rename to lemmy/prod/default.nix
index 85ae4ca..c615c02 100644
--- a/lemmy/default.nix
+++ b/lemmy/prod/default.nix
@@ -1,12 +1,12 @@
 { config, lib, pkgs, pkgs-unstable, ... }:
 
 {
-  imports = [ ./prod.nix ];
+  imports = [ ./module.nix ];
 
   services.lemmy-prod = {
     enable = true;
-    server.package = pkgs.callPackage ./server.nix { Security = null; };
-    ui.package = pkgs.callPackage ./ui.nix { };
+    server.package = pkgs.callPackage ../server.nix { Security = null; };
+    ui.package = pkgs.callPackage ../ui.nix { };
     nginx.enable = true;
     database.createLocally = true;
 
diff --git a/lemmy/prod.nix b/lemmy/prod/module.nix
similarity index 100%
rename from lemmy/prod.nix
rename to lemmy/prod/module.nix
diff --git a/lemmy/staging/default.nix b/lemmy/staging/default.nix
new file mode 100644
index 0000000..58f3a82
--- /dev/null
+++ b/lemmy/staging/default.nix
@@ -0,0 +1,65 @@
+{ config, lib, pkgs, pkgs-unstable, ... }:
+
+{
+  imports = [ ./module.nix ];
+
+  services.lemmy-staging = {
+    enable = true;
+    server.package = pkgs.callPackage ../server.nix { Security = null; };
+    ui.package = pkgs.callPackage ../ui.nix { };
+    nginx.enable = true;
+    database.createLocally = true;
+
+    settings = {
+      hostname = "breaking.awful.systems";
+      setup = {
+        admin_username = "self";
+        admin_email = "self@awful.systems";
+        site_name = "a.s staging";
+      };
+      database = {
+        user = "lemmy_staging";
+        database = "lemmy_staging";
+      };
+    };
+  };
+
+  sops.secrets."lemmy-staging/initial_admin_password" = { };
+  sops.secrets."lemmy-staging/smtp_server" = { };
+  sops.secrets."lemmy-staging/smtp_login" = { };
+  sops.secrets."lemmy-staging/smtp_password" = { };
+  sops.secrets."lemmy-staging/smtp_from_address" = { };
+
+  sops.templates.lemmy-staging.content = builtins.toJSON
+    (config.services.lemmy-staging.settings // {
+      setup = config.services.lemmy-staging.settings.setup // {
+        admin_password = config.sops.placeholder."lemmy-staging/initial_admin_password";
+      };
+
+      # email = {
+      #   smtp_server = config.sops.placeholder."lemmy-staging/smtp_server";
+      #   smtp_login = config.sops.placeholder."lemmy-staging/smtp_login";
+      #   smtp_password = config.sops.placeholder."lemmy-staging/smtp_password";
+      #   smtp_from_address = config.sops.placeholder."lemmy-staging/smtp_from_address";
+      #   tls_type = "tls";
+      # };
+    });
+
+  systemd.services.lemmy-staging = {
+    serviceConfig = {
+      User = "lemmy_staging";
+      Group = "lemmy_staging";
+      LoadCredential =
+        "lemmy-staging:${config.sops.templates.lemmy-staging.path}";
+    };
+
+    environment = {
+      LEMMY_CONFIG_LOCATION = lib.mkForce "%d/lemmy-staging";
+      RUST_BACKTRACE = "full";
+      LEMMY_DATABASE_URL = pkgs.lib.mkForce
+        "postgres:///lemmy_staging?host=/run/postgresql&user=lemmy_staging";
+    };
+  };
+
+  networking.firewall.allowedTCPPorts = [ 80 ];
+}
diff --git a/lemmy/staging/module.nix b/lemmy/staging/module.nix
new file mode 100644
index 0000000..87a9063
--- /dev/null
+++ b/lemmy/staging/module.nix
@@ -0,0 +1,266 @@
+{ lib, pkgs, config, ... }:
+with lib;
+let
+  cfg = config.services.lemmy-staging;
+  settingsFormat = pkgs.formats.json { };
+in {
+  imports = [
+    (mkRemovedOptionModule [ "services" "lemmy-staging" "jwtSecretPath" ]
+      "As of v0.13.0, Lemmy auto-generates the JWT secret.")
+  ];
+
+  options.services.lemmy-staging = {
+
+    enable = mkEnableOption
+      (lib.mdDoc "lemmy a federated alternative to reddit in rust");
+
+    server = { package = mkPackageOptionMD pkgs "lemmy-server" { }; };
+
+    ui = {
+      package = mkPackageOptionMD pkgs "lemmy-ui" { };
+
+      port = mkOption {
+        type = types.port;
+        default = 1235;
+        description =
+          lib.mdDoc "Port where lemmy-ui should listen for incoming requests.";
+      };
+    };
+
+    caddy.enable =
+      mkEnableOption (lib.mdDoc "exposing lemmy with the caddy reverse proxy");
+    nginx.enable =
+      mkEnableOption (lib.mdDoc "exposing lemmy with the nginx reverse proxy");
+
+    database = {
+      createLocally =
+        mkEnableOption (lib.mdDoc "creation of database on the instance");
+
+      uri = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = lib.mdDoc
+          "The connection URI to use. Takes priority over the configuration file if set.";
+      };
+    };
+
+    settings = mkOption {
+      default = { };
+      description = lib.mdDoc "Lemmy configuration";
+
+      type = types.submodule {
+        freeformType = settingsFormat.type;
+
+        options.hostname = mkOption {
+          type = types.str;
+          default = null;
+          description =
+            lib.mdDoc "The domain name of your instance (eg 'lemmy.ml').";
+        };
+
+        options.port = mkOption {
+          type = types.port;
+          default = 8537;
+          description =
+            lib.mdDoc "Port where lemmy should listen for incoming requests.";
+        };
+
+        options.captcha = {
+          enabled = mkOption {
+            type = types.bool;
+            default = true;
+            description = lib.mdDoc "Enable Captcha.";
+          };
+          difficulty = mkOption {
+            type = types.enum [ "easy" "medium" "hard" ];
+            default = "medium";
+            description = lib.mdDoc "The difficultly of the captcha to solve.";
+          };
+        };
+      };
+    };
+
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.lemmy-staging.settings = (mapAttrs (name: mkDefault) {
+      bind = "127.0.0.1";
+      tls_enabled = true;
+      pictrs_url = with config.services.pict-rs;
+        "http://${address}:${toString port}";
+      actor_name_max_length = 20;
+
+      rate_limit.message = 180;
+      rate_limit.message_per_second = 60;
+      rate_limit.post = 6;
+      rate_limit.post_per_second = 600;
+      rate_limit.register = 3;
+      rate_limit.register_per_second = 3600;
+      rate_limit.image = 6;
+      rate_limit.image_per_second = 3600;
+    } // {
+      database = mapAttrs (name: mkDefault) {
+        user = "lemmy";
+        host = "/run/postgresql";
+        port = 5432;
+        database = "lemmy";
+        pool_size = 5;
+      };
+    });
+
+    services.postgresql = mkIf cfg.database.createLocally {
+      enable = true;
+      ensureDatabases = [ cfg.settings.database.database ];
+      ensureUsers = [{
+        name = cfg.settings.database.user;
+        ensurePermissions."DATABASE ${cfg.settings.database.database}" =
+          "ALL PRIVILEGES";
+      }];
+    };
+
+    services.pict-rs.enable = true;
+
+    services.caddy = mkIf cfg.caddy.enable {
+      enable = mkDefault true;
+      virtualHosts."${cfg.settings.hostname}" = {
+        extraConfig = ''
+          handle_path /static/* {
+            root * ${cfg.ui.package}/dist
+            file_server
+          }
+          @for_backend {
+            path /api/* /pictrs/* /feeds/* /nodeinfo/*
+          }
+          handle @for_backend {
+            reverse_proxy 127.0.0.1:${toString cfg.settings.port}
+          }
+          @post {
+            method POST
+          }
+          handle @post {
+            reverse_proxy 127.0.0.1:${toString cfg.settings.port}
+          }
+          @jsonld {
+            header Accept "application/activity+json"
+            header Accept "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
+          }
+          handle @jsonld {
+            reverse_proxy 127.0.0.1:${toString cfg.settings.port}
+          }
+          handle {
+            reverse_proxy 127.0.0.1:${toString cfg.ui.port}
+          }
+        '';
+      };
+    };
+
+    services.nginx = mkIf cfg.nginx.enable {
+      enable = mkDefault true;
+      virtualHosts."${cfg.settings.hostname}".locations = let
+        ui = "http://127.0.0.1:${toString cfg.ui.port}";
+        backend = "http://127.0.0.1:${toString cfg.settings.port}";
+      in {
+        "~ ^/(api|pictrs|feeds|nodeinfo|.well-known)" = {
+          # backend requests
+          proxyPass = backend;
+          proxyWebsockets = true;
+          recommendedProxySettings = true;
+        };
+        "/" = {
+          # mixed frontend and backend requests, based on the request headers
+          proxyPass = "$proxpass";
+          recommendedProxySettings = true;
+          extraConfig = ''
+            set $proxpass "${ui}";
+            if ($http_accept = "application/activity+json") {
+              set $proxpass "${backend}";
+            }
+            if ($http_accept = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") {
+              set $proxpass "${backend}";
+            }
+            if ($request_method = POST) {
+              set $proxpass "${backend}";
+            }
+
+            # Cuts off the trailing slash on URLs to make them valid
+            rewrite ^(.+)/+$ $1 permanent;
+          '';
+        };
+      };
+    };
+
+    assertions = [
+      {
+        assertion = cfg.database.createLocally -> cfg.settings.database.host
+          == "localhost" || cfg.settings.database.host == "/run/postgresql";
+        message =
+          "if you want to create the database locally, you need to use a local database";
+      }
+      {
+        assertion = (!(hasAttrByPath [ "federation" ] cfg.settings))
+          && (!(hasAttrByPath [ "federation" "enabled" ] cfg.settings));
+        message =
+          "`services.lemmy.settings.federation` was removed in 0.17.0 and no longer has any effect";
+      }
+    ];
+
+    systemd.services.lemmy-staging = {
+      description = "Lemmy server (staging)";
+
+      environment = {
+        LEMMY_CONFIG_LOCATION =
+          "${settingsFormat.generate "config.hjson" cfg.settings}";
+        LEMMY_DATABASE_URL = mkIf (cfg.database.uri != null) cfg.database.uri;
+      };
+
+      documentation = [
+        "https://join-lemmy.org/docs/en/admins/from_scratch.html"
+        "https://join-lemmy.org/docs/en/"
+      ];
+
+      wantedBy = [ "multi-user.target" ];
+
+      after = [ "pict-rs.service" ]
+        ++ lib.optionals cfg.database.createLocally [ "postgresql.service" ];
+
+      requires =
+        lib.optionals cfg.database.createLocally [ "postgresql.service" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        RuntimeDirectory = "lemmy";
+        ExecStart = "${cfg.server.package}/bin/lemmy_server";
+      };
+    };
+
+    systemd.services.lemmy-ui-staging = {
+      description = "Lemmy UI (staging)";
+
+      environment = {
+        LEMMY_UI_HOST = "127.0.0.1:${toString cfg.ui.port}";
+        LEMMY_UI_LEMMY_INTERNAL_HOST = "127.0.0.1:${toString cfg.settings.port}";
+        LEMMY_UI_LEMMY_EXTERNAL_HOST = cfg.settings.hostname;
+        LEMMY_UI_HTTPS = "false";
+      };
+
+      documentation = [
+        "https://join-lemmy.org/docs/en/admins/from_scratch.html"
+        "https://join-lemmy.org/docs/en/"
+      ];
+
+      wantedBy = [ "multi-user.target" ];
+
+      after = [ "lemmy-staging.service" ];
+
+      requires = [ "lemmy-staging.service" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        WorkingDirectory = "${cfg.ui.package}";
+        ExecStart =
+          "${pkgs.nodejs}/bin/node ${cfg.ui.package}/dist/js/server.js";
+      };
+    };
+  };
+
+}
diff --git a/secrets/secrets.yaml b/secrets/secrets.yaml
index 4e2432e..530e9e2 100644
--- a/secrets/secrets.yaml
+++ b/secrets/secrets.yaml
@@ -4,6 +4,18 @@ lemmy:
     smtp_login: ENC[AES256_GCM,data:ud57LkAv,iv:leOnFXK8Lm5W33UcrgAEMyb+baMcVMu687s7ih1Zp/g=,tag:7llUqPlMHEQuzsa+kNqaoA==,type:str]
     smtp_password: ENC[AES256_GCM,data:vrqLNdNO0v3gSGF9A/I8rJ7JhgRpQXHulnJlFj0OTNDBWt+Mi7g1r8n2XDvf8YFPh6N15i98QvzHZ0lWo3PCRoYqbOz9,iv:3+/S6OzsdsNivGNEpsm74rmq5pp4n1RcwC0dNuhALcM=,tag:Oao44DUS4I8TjeQuyKzDmw==,type:str]
     smtp_from_address: ENC[AES256_GCM,data:kpzULJxgqLbO5W6aDOao9OI=,iv:uCKQHZKFmStYmY4M4h1PnBwT1lHXiZeXXEYHPrbxEl8=,tag:ioMYOYuUYfxvX/K21KlzLw==,type:str]
+lemmy-staging:
+    initial_admin_password: ENC[AES256_GCM,data:lAI52w5iEj+200bM+WNLdw==,iv:t4vMoyAzrUt5KnL5uT3loEz5zNcx+608A8/09DAFgTk=,tag:ZSLMdPRTUeX4xnYpwXpGvQ==,type:str]
+    smtp_server: ENC[AES256_GCM,data:CJZz4rxPgnwRyXVXY+aBP/c16Hag,iv:2D05+ZmLlbrhxaCFAZSwkR2itZOpPzfaPdvUJeQClZg=,tag:WHfZIf+1B8b15mtlTxgXgg==,type:str]
+    smtp_login: ENC[AES256_GCM,data:c05hsXpk,iv:S6m/BmUedMZvExTFdvkK41MhKcdOpXIFFsy8JRzDBpA=,tag:ISzUbU9V7iKC8rRrQjaqkQ==,type:str]
+    smtp_password: ENC[AES256_GCM,data:7uLqIkz9Hs/qFl20Mu6nvRgYNfzwN1fVf5jrgKCNVc2tCkek0LoMuFMoqSDohPRaLd459PNIoA+VrEwoVr9t23zpFJfe,iv:+OKiouo6YSsSRhN5GNrRhkJ48pw2fcHxUVhCtXj+vBw=,tag:sWiLDDfNfEOVNmMlRyeAvQ==,type:str]
+    smtp_from_address: ENC[AES256_GCM,data:DczTYEKW5PMhax/lp7Zmj6o=,iv:prcUFdyCst7DnCNqbM9cE+g3pap0KEHdyMrS/2CsxE0=,tag:X2e7IjkXbiQUxUEw/Cty8w==,type:str]
+lemmy-dev:
+    initial_admin_password: ENC[AES256_GCM,data:7cQbzsfOn9l/6RI/2o6sWA==,iv:hdOWBMmt7WxD9vIUZ/V9PbEkGXCmj0NDVBwWIqjTmd4=,tag:iAgEJQO+mXSHN6MB8iwc0Q==,type:str]
+    smtp_server: ENC[AES256_GCM,data:Ldk72toeDAZGkyZhyr2PlNg3HM/e,iv:e+mCtSWi0qI5XGaI1RABQWeJMGmUucWqHDZdsK42O90=,tag:6ChHbPw2bNehtlXSmEnMkA==,type:str]
+    smtp_login: ENC[AES256_GCM,data:qwwuoUn2,iv:i+UQJZ30e7tLsV7755TYlAeGmB453t3F4rL4FgJaoio=,tag:54ZO7qMpbjuNT45HF7vaIQ==,type:str]
+    smtp_password: ENC[AES256_GCM,data:gx9P8gv3RuH7RkdapuqZ8z+y4nkwKOXJ9dYRRn+TEh/jQ2Zl3yWWJ1HKxrNJlzdidPtTEph7Z7YrQn5PzlNEfdBsMsXy,iv:4Aaa/2/X6NWPkX9UvN5FhyynbF4ADWG3+FTHXJCRlkY=,tag:RbVXS2lQNIiIseE7TTcmtg==,type:str]
+    smtp_from_address: ENC[AES256_GCM,data:55Ljo/B0JJBOBuUvFoFXino=,iv:WXQz60z/nROYtrYX8qXKK8o1Q/LISb2Ewj9ac8+Btzw=,tag:KZBPNJVtI8SRX84aQDeWgg==,type:str]
 sops:
     kms: []
     gcp_kms: []
@@ -37,8 +49,8 @@ sops:
             RW13UEl1NEtza2NnTXY4YnFOY1d1QmsKxs+hTpa+s1jaG8T1tPo7FUtkEQA0WZpj
             qjgrYGhFpg6dicovfkY6Ksyx4WXgw52GTMQZjyEo6FJObUvSF6TmGg==
             -----END AGE ENCRYPTED FILE-----
-    lastmodified: "2023-06-29T10:57:55Z"
-    mac: ENC[AES256_GCM,data:cV3/ptlgCPM0G62bfxVJCW5xgx0rBsiaClifdFhPdqLbaJ2MpMCbujgw8RbX7RSKpq7tNMIrPaCvAmp5RQETd08FWnQbMjaKy2dDoQefYFspaDrv0atXU5ObXM37EEc2NMUgg/7U/JJPoeqUIBAOTyPA/Uf77HrY02LTxpW2Pwk=,iv:2C3RpLOo1ghkpygw9bWWX3JuSMJy2YHJZbLYJ1yLrmw=,tag:ZoLdrFEmM/ZFXLH1lV9vJA==,type:str]
+    lastmodified: "2023-07-02T22:27:37Z"
+    mac: ENC[AES256_GCM,data:QwRu83wzt8N0cIrxeA/7mtAbIDSXWwTKD6j794hT6yCkaWmTFDhX3S1+TAIQzJXVQTvJRlPZ1sW8XD/lQy3wTgW/nKVFtNovFCABGAKWen/o8j5gjLvxKMLNjSTm93JdhvLO+vDaJSszs/50O8z3pCIYZxxFlXhz2CMtK0DRPJ4=,iv:nq3hika6ACbl+QI96i7Z5G47iWs5laPUCLztga5aPbA=,tag:YNUDgHcXV4yaEJ2VqxvVPg==,type:str]
     pgp: []
     unencrypted_suffix: _unencrypted
     version: 3.7.3