about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore6
-rw-r--r--README.md5
-rw-r--r--configuration.nix158
-rw-r--r--distributed-test.nix2
-rw-r--r--docker.nix43
-rw-r--r--flake.nix279
-rwxr-xr-xfonts/update.sh8
-rw-r--r--kittybox-rs/Cargo.lock (renamed from Cargo.lock)0
-rw-r--r--kittybox-rs/Cargo.toml (renamed from Cargo.toml)0
-rwxr-xr-xkittybox-rs/dev.sh (renamed from dev.sh)0
-rw-r--r--kittybox-rs/src/bin/kittybox_bulk_import.rs (renamed from src/bin/kittybox_bulk_import.rs)0
-rw-r--r--kittybox-rs/src/bin/kittybox_database_converter.rs (renamed from src/bin/kittybox_database_converter.rs)0
-rw-r--r--kittybox-rs/src/bin/pyindieblog_to_kittybox.rs (renamed from src/bin/pyindieblog_to_kittybox.rs)0
-rw-r--r--kittybox-rs/src/database/file/mod.rs (renamed from src/database/file/mod.rs)0
-rw-r--r--kittybox-rs/src/database/memory.rs (renamed from src/database/memory.rs)0
-rw-r--r--kittybox-rs/src/database/mod.rs (renamed from src/database/mod.rs)0
-rw-r--r--kittybox-rs/src/database/redis/edit_post.lua (renamed from src/database/redis/edit_post.lua)0
-rw-r--r--kittybox-rs/src/database/redis/mod.rs (renamed from src/database/redis/mod.rs)0
-rw-r--r--kittybox-rs/src/frontend/login.rs (renamed from src/frontend/login.rs)0
-rw-r--r--kittybox-rs/src/frontend/mod.rs (renamed from src/frontend/mod.rs)0
-rw-r--r--kittybox-rs/src/frontend/onboarding.css (renamed from src/frontend/onboarding.css)0
-rw-r--r--kittybox-rs/src/frontend/onboarding.js (renamed from src/frontend/onboarding.js)0
-rw-r--r--kittybox-rs/src/frontend/style.css (renamed from src/frontend/style.css)0
-rw-r--r--kittybox-rs/src/index.html (renamed from src/index.html)0
-rw-r--r--kittybox-rs/src/indieauth.rs (renamed from src/indieauth.rs)0
-rw-r--r--kittybox-rs/src/lib.rs (renamed from src/lib.rs)0
-rw-r--r--kittybox-rs/src/main.rs (renamed from src/main.rs)0
-rw-r--r--kittybox-rs/src/media/mod.rs (renamed from src/media/mod.rs)0
-rw-r--r--kittybox-rs/src/metrics.rs (renamed from src/metrics.rs)0
-rw-r--r--kittybox-rs/src/micropub/get.rs (renamed from src/micropub/get.rs)0
-rw-r--r--kittybox-rs/src/micropub/mod.rs (renamed from src/micropub/mod.rs)0
-rw-r--r--kittybox-rs/src/micropub/post.rs (renamed from src/micropub/post.rs)0
-rw-r--r--kittybox-rs/templates/Cargo.toml (renamed from templates/Cargo.toml)0
-rw-r--r--kittybox-rs/templates/src/lib.rs (renamed from templates/src/lib.rs)0
-rw-r--r--kittybox-rs/templates/src/login.rs (renamed from templates/src/login.rs)0
-rw-r--r--kittybox-rs/templates/src/onboarding.rs (renamed from templates/src/onboarding.rs)0
-rw-r--r--kittybox-rs/templates/src/templates.rs (renamed from templates/src/templates.rs)0
-rw-r--r--kittybox-rs/util/Cargo.toml (renamed from util/Cargo.toml)0
-rw-r--r--kittybox-rs/util/src/lib.rs (renamed from util/src/lib.rs)0
-rw-r--r--kittybox.nix19
-rw-r--r--smoke-test.nix36
41 files changed, 275 insertions, 281 deletions
diff --git a/.gitignore b/.gitignore
index 8b721e8..71a754a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-/target
+/kittybox-rs/target
 .direnv
 /result-*
 /result
@@ -8,6 +8,6 @@ dump.rdb
 .\#*
 .*~
 *~
-/test-dir
-fonts/*
+/kittybox-rs/test-dir
+/kittybox-rs/fonts/*
 /token.txt
diff --git a/README.md b/README.md
index be18318..b756805 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,7 @@ any C libraries for building, preferring to use their Rust equivalents
 for memory safety and ease of building.
 
 ```console
+$ cd ./kittybox-rs/
 $ cargo build
 ```
 
@@ -66,10 +67,10 @@ $ cargo check
 { config, pkgs, lib, ...}: let 
   # Included as an example. You should probably use `flake.nix` instead.
   # You will get version pinning and you will probably be happier.
-  kittybox-flake = (builtins.getFlake "git+https://git.sr.ht/~vikanezrimaya/kittybox?ref=main");
+  kittybox = (builtins.getFlake "git+https://git.sr.ht/~vikanezrimaya/kittybox?ref=main");
 in {
   imports = [
-    kittybox-flake.nixosModule
+    kittybox.nixosModules.default
   ];
   
   services.kittybox = {
diff --git a/configuration.nix b/configuration.nix
new file mode 100644
index 0000000..411b7b2
--- /dev/null
+++ b/configuration.nix
@@ -0,0 +1,158 @@
+kittybox:
+{ config, pkgs, lib, ... }:
+with lib;
+let
+  cfg = config.services.kittybox;
+in {
+  options = {
+    services.kittybox = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Kittybox, the IndieWeb blogging solution.
+        '';
+      };
+      package = mkOption {
+        type = types.package;
+        default = kittybox.packages.${config.nixpkgs.localSystem.system}.kittybox;
+        defaultText = "<kittybox package from the upstream flake>";
+        description = "Which Kittybox derivation to use.";
+      };
+
+      bind = mkOption {
+        type = types.nullOr types.str;
+        default = "127.0.0.1";
+        description = "The host for Kittybox to bind to.";
+        example = "192.168.1.100";
+      };
+      port = mkOption {
+        type = types.int;
+        default = 8080;
+        description = "The port for Kittybox to listen at.";
+        example = 16420;
+      };
+      logLevel = mkOption {
+        type = types.str;
+        default = "warn";
+        example = "info";
+        description = "Specify the server verbosity level. Uses RUST_LOG environment variable internally.";
+      };
+      backendUri = mkOption {
+        type = types.str;
+        default = "file:///var/lib/kittybox/data";
+        example = "redis://192.168.1.200:6379";
+        description = ''
+          Set the backend used for storing data. Available backends are:
+           - file:// - static folder backend (recommended)
+           - redis:// - Redis backend
+
+          Make sure that if you are using the file backend, the state
+          directory is accessible by Kittybox. By default, the unit config
+          uses DynamicUser=true, which prevents the unit from accessing
+          data outside of its directory. It is recommended to use a
+          bind-mount to /var/lib/private/kittybox if you require the state
+          directory to reside elsewhere.
+        '';
+      };
+      tokenEndpoint = mkOption {
+        type = types.str;
+        example = "https://tokens.indieauth.com/token";
+        description = "Token endpoint to use for authenticating Micropub requests. Use the example if you are unsure.";
+      };
+      authorizationEndpoint = mkOption {
+        type = types.str;
+        example = "https://indieauth.com/auth";
+        description = "Authorization endpoint to use to authenticate the user. You can use the default if you are unsure.";
+      };
+      mediaEndpoint = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "The URL of a media endpoint to announce when asked by a Micropub client. Strongly recommended if you plan to upload images.";
+      };
+      microsubServer = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "https://aperture.p3k.io/microsub/69420";
+        description = ''
+          The URL of your Microsub server, which saves feeds for you
+          and allows you to browse Web content from one place. Try
+          https://aperture.p3k.io/ if you don't have one yet!
+        '';
+      };
+      webmentionEndpoint = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "https://webmention.io/example.com/webmention";
+        description = ''
+          The URL of your webmention endpoint, which allows you to
+          receive notifications about your site's content being featured
+          or interacted with elsewhere on the IndieWeb.
+
+          By default Kittybox expects the Webmention endpoint to post
+          updates using an internal token. kittybox-webmention is an
+          endpoint capable of that.
+        '';
+      };
+      internalTokenFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/run/secrets/kittybox-shared-secret";
+        description = "A shared secret that will, when passed, allow unlimited editing access to database. Keep it safe.";
+      };
+      cookieSecretFile = mkOption {
+        type = types.str;
+        default = "/var/lib/kittybox/cookie_secret_key";
+        example = "/run/secrets/kittybox-cookie-secret";
+        description = "A secret file to encrypt cookies with the contents of. Should be at least 32 bytes in length. A random persistent file will be generated if this variable is left untouched.";
+      };
+    };
+  };
+  config = lib.mkIf cfg.enable {
+    systemd.services.kittybox = {
+      description = "An IndieWeb-enabled blog engine";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      restartTriggers = [
+        cfg.package
+        cfg.backendUri cfg.tokenEndpoint
+        cfg.authorizationEndpoint
+        cfg.internalTokenFile
+        cfg.bind cfg.port
+        cfg.cookieSecretFile
+      ];
+
+      environment = {
+        SERVE_AT = "${cfg.bind}:${builtins.toString cfg.port}";
+        AUTHORIZATION_ENDPOINT = cfg.authorizationEndpoint;
+        TOKEN_ENDPOINT = cfg.tokenEndpoint;
+        MEDIA_ENDPOINT = cfg.mediaEndpoint;
+        MICROSUB_ENDPOINT = cfg.microsubServer;
+        WEBMENTION_ENDPOINT = cfg.webmentionEndpoint;
+        #REDIS_URI = if (cfg.redisUri == null) then "redis://127.0.0.1:6379/" else cfg.redisUri;
+        BACKEND_URI = cfg.backendUri;
+        RUST_LOG = "${cfg.logLevel}";
+        COOKIE_SECRET_FILE = "${cfg.cookieSecretFile}";
+      };
+
+      script = ''
+        ${lib.optionalString (cfg.internalTokenFile != null) ''
+          if [[ -f ${cfg.internalTokenFile} ]]; then
+            export KITTYBOX_INTERNAL_TOKEN=$(${pkgs.coreutils}/bin/cat ${cfg.internalTokenFile})
+          fi
+        ''}
+        if [[ ${cfg.cookieSecretFile} == /var/lib/kittybox/cookie_secret_key && ! -f /var/lib/kittybox/cookie_secret_key ]]; then
+            cat /dev/urandom | tr -Cd '[:alnum:]' | head -c 128 > /var/lib/kittybox/cookie_secret_key
+        fi
+        exec ${cfg.package}/bin/kittybox
+      '';
+
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "kittybox";
+      };
+    };
+  };
+}
diff --git a/distributed-test.nix b/distributed-test.nix
index 9b62542..539bd0c 100644
--- a/distributed-test.nix
+++ b/distributed-test.nix
@@ -3,7 +3,7 @@
 kittybox:
 { lib, system, ... }: let
   kittyboxModule = { config, pkgs, lib, ... }: {
-    imports = [ kittybox.nixosModule commonModule ];
+    imports = [ kittybox.nixosModules.default commonModule ];
 
     services.kittybox = {
       enable = true;
diff --git a/docker.nix b/docker.nix
new file mode 100644
index 0000000..2ac09b6
--- /dev/null
+++ b/docker.nix
@@ -0,0 +1,43 @@
+{ kittybox, dockerTools, runtimeShell, rev ? "development", lastModifiedDate }:
+dockerTools.buildImage {
+  name = "kittybox";
+  tag = rev;
+  created = let
+    date = lastModifiedDate;
+  in builtins.concatStringsSep "" [
+    (builtins.substring 0 4 date)
+    "-"
+    (builtins.substring 4 2 date)
+    "-"
+    (builtins.substring 6 2 date)
+    "T"
+    (builtins.substring 8 2 date)
+    ":"
+    (builtins.substring 10 2 date)
+    ":"
+    (builtins.substring 12 2 date)
+    "Z"
+  ];
+
+  runAsRoot = ''
+    #!${runtimeShell}
+    ${dockerTools.shadowSetup}
+    groupadd -r kittybox
+    useradd -r -g kittybox kittybox
+    mkdir -p /data
+    chown kittybox:kittybox /data
+  '';
+
+  config = {
+    Cmd = [ "${kittybox}/bin/kittybox" ];
+    Env = [
+      "SERVE_AT=0.0.0.0:8080"
+      "BACKEND_URI=file:///data"
+      "RUST_LOG=info"
+    ];
+    WorkingDir = "/data";
+    Volumes = { "/data" = {}; };
+    User = "kittybox";
+    ExposedPorts = { "8080" = {}; };
+  };
+}
diff --git a/flake.nix b/flake.nix
index 5eae2da..d437310 100644
--- a/flake.nix
+++ b/flake.nix
@@ -18,285 +18,30 @@
     supportedSystems = ["aarch64-linux" "x86_64-linux"];
     forAllSystems = f: flake-utils.lib.eachSystem supportedSystems f;
   in {
-    nixosModule = { config, pkgs, lib, ... }: with lib; let
-      cfg = config.services.kittybox;
-    in {
-      options = {
-        services.kittybox = {
-          enable = mkOption {
-            type = types.bool;
-            default = false;
-            description = ''
-              Whether to enable Kittybox, the IndieWeb blogging solution.
-            '';
-          };
-          package = mkOption {
-            type = types.package;
-            default = self.packages.${config.nixpkgs.localSystem.system}.kittybox;
-            defaultText = "<kittybox package from the upstream flake>";
-            description = "Which Kittybox derivation to use.";
-          };
-
-          bind = mkOption {
-            type = types.nullOr types.str;
-            default = "127.0.0.1";
-            description = "The host for Kittybox to bind to.";
-            example = "192.168.1.100";
-          };
-          port = mkOption {
-            type = types.int;
-            default = 8080;
-            description = "The port for Kittybox to listen at.";
-            example = 16420;
-          };
-          logLevel = mkOption {
-            type = types.str;
-            default = "warn";
-            example = "info";
-            description = "Specify the server verbosity level. Uses RUST_LOG environment variable internally.";
-          };
-          backendUri = mkOption {
-            type = types.str;
-            default = "file:///var/lib/kittybox/data";
-            example = "redis://192.168.1.200:6379";
-            description = ''
-              Set the backend used for storing data. Available backends are:
-               - file:// - static folder backend (recommended)
-               - redis:// - Redis backend
-
-              Make sure that if you are using the file backend, the state
-              directory is accessible by Kittybox. By default, the unit config
-              uses DynamicUser=true, which prevents the unit from accessing
-              data outside of its directory. It is recommended to use a
-              bind-mount to /var/lib/private/kittybox if you require the state
-              directory to reside elsewhere.
-            '';
-          };
-          tokenEndpoint = mkOption {
-            type = types.str;
-            example = "https://tokens.indieauth.com/token";
-            description = "Token endpoint to use for authenticating Micropub requests. Use the example if you are unsure.";
-          };
-          authorizationEndpoint = mkOption {
-            type = types.str;
-            example = "https://indieauth.com/auth";
-            description = "Authorization endpoint to use to authenticate the user. You can use the default if you are unsure.";
-          };
-          mediaEndpoint = mkOption {
-            type = types.nullOr types.str;
-            default = null;
-            description = "The URL of a media endpoint to announce when asked by a Micropub client. Strongly recommended if you plan to upload images.";
-          };
-          microsubServer = mkOption {
-            type = types.nullOr types.str;
-            default = null;
-            example = "https://aperture.p3k.io/microsub/69420";
-            description = ''
-              The URL of your Microsub server, which saves feeds for you
-              and allows you to browse Web content from one place. Try
-              https://aperture.p3k.io/ if you don't have one yet!
-            '';
-          };
-          webmentionEndpoint = mkOption {
-            type = types.nullOr types.str;
-            default = null;
-            example = "https://webmention.io/example.com/webmention";
-            description = ''
-              The URL of your webmention endpoint, which allows you to
-              receive notifications about your site's content being featured
-              or interacted with elsewhere on the IndieWeb.
-
-              By default Kittybox expects the Webmention endpoint to post
-              updates using an internal token. kittybox-webmention is an
-              endpoint capable of that.
-            '';
-          };
-          internalTokenFile = mkOption {
-            type = types.nullOr types.str;
-            default = null;
-            example = "/run/secrets/kittybox-shared-secret";
-            description = "A shared secret that will, when passed, allow unlimited editing access to database. Keep it safe.";
-          };
-          cookieSecretFile = mkOption {
-            type = types.str;
-            default = "/var/lib/kittybox/cookie_secret_key";
-            example = "/run/secrets/kittybox-cookie-secret";
-            description = "A secret file to encrypt cookies with the contents of. Should be at least 32 bytes in length. A random persistent file will be generated if this variable is left untouched.";
-          };
-        };
-      };
-      config = lib.mkIf cfg.enable {
-        systemd.services.kittybox = {
-          description = "An IndieWeb-enabled blog engine";
-
-          wantedBy = [ "multi-user.target" ];
-          after = [ "network.target" ];
-
-          restartTriggers = [
-            cfg.package
-            cfg.backendUri cfg.tokenEndpoint
-            cfg.authorizationEndpoint
-            cfg.internalTokenFile
-            cfg.bind cfg.port
-            cfg.cookieSecretFile
-          ];
-
-          environment = {
-            SERVE_AT = "${cfg.bind}:${builtins.toString cfg.port}";
-            AUTHORIZATION_ENDPOINT = cfg.authorizationEndpoint;
-            TOKEN_ENDPOINT = cfg.tokenEndpoint;
-            MEDIA_ENDPOINT = cfg.mediaEndpoint;
-            MICROSUB_ENDPOINT = cfg.microsubServer;
-            WEBMENTION_ENDPOINT = cfg.webmentionEndpoint;
-            #REDIS_URI = if (cfg.redisUri == null) then "redis://127.0.0.1:6379/" else cfg.redisUri;
-            BACKEND_URI = cfg.backendUri;
-            RUST_LOG = "${cfg.logLevel}";
-            COOKIE_SECRET_FILE = "${cfg.cookieSecretFile}";
-          };
-
-          script = ''
-            ${lib.optionalString (cfg.internalTokenFile != null) ''
-              if [[ -f ${cfg.internalTokenFile} ]]; then
-                export KITTYBOX_INTERNAL_TOKEN=$(${pkgs.coreutils}/bin/cat ${cfg.internalTokenFile})
-              fi
-            ''}
-            if [[ ${cfg.cookieSecretFile} == /var/lib/kittybox/cookie_secret_key && ! -f /var/lib/kittybox/cookie_secret_key ]]; then
-                cat /dev/urandom | tr -Cd '[:alnum:]' | head -c 128 > /var/lib/kittybox/cookie_secret_key
-            fi
-            exec ${cfg.package}/bin/kittybox
-          '';
-
-          serviceConfig = {
-            DynamicUser = true;
-            StateDirectory = "kittybox";
-          };
-        };
-      };
-    };
+    nixosModules.default = import ./configuration.nix self;
   } // forAllSystems (system: let
     pkgs = nixpkgs.legacyPackages.${system};
-    packages = {
-      kittybox = { stdenv, lib, runCommandNoCC, openssl, zlib, pkg-config, protobuf, naersk-lib, lld, mold }:
-      naersk-lib.buildPackage {
-        pname = "kittybox";
-        version = "0.1.0";
-
-        src = runCommandNoCC "kittybox-src" {} ''
-          mkdir -p $out
-          cp -r ${./Cargo.toml} $out/Cargo.toml
-          cp -r ${./Cargo.lock} $out/Cargo.lock
-          cp -r ${./src} $out/src
-          cp -r ${./templates} $out/templates
-          cp -r ${./util} $out/util
-          cp -r ${./fonts} $out/fonts
-        '';
-
-        checkInputs = [ openssl.dev zlib ];
-        nativeBuildInputs = [ pkg-config protobuf ];
-        nativeCheckInputs = [ pkg-config ];
-
-        doCheck = stdenv.hostPlatform == stdenv.targetPlatform;
-
-        meta = with lib.meta; {
-          maintainers = with lib.maintainers; [ vikanezrimaya ];
-          platforms = supportedSystems;
-          mainProgram = "kittybox";
-        };
-      };
+    naersk-lib = naersk.lib.${system}.override {
+      inherit (pkgs) rustc cargo;
     };
   in {
-    packages = let
-      naersk-lib = naersk.lib.${system}.override {
-        inherit (pkgs) rustc cargo;
-      };
-    in {
-      kittybox = pkgs.callPackage packages.kittybox { inherit naersk-lib; };
+    packages = {
+      kittybox = pkgs.callPackage ./kittybox.nix { inherit naersk-lib; };
+      default = self.packages.${system}.kittybox;
     };
-    defaultPackage = self.packages.${system}.kittybox;
 
     checks = {
       kittybox = self.packages.${system}.kittybox;
       distributed-test = pkgs.nixosTest (import ./distributed-test.nix self);
-      nixos-test = (pkgs.nixosTest ({ lib }: {
-        name = "nixos-kittybox";
-
-        nodes = {
-          kittybox = { config, pkgs, lib, ... }: {
-            imports = [ self.nixosModule ];
-
-            services.kittybox = {
-              enable = true;
-              # It never actually contacts those endpoints anyway unless we use Micropub so it's fine!
-              # TODO: Once we have self-hosted software for those endpoints,
-              #       make an e2e test for common workflows (e.g. making a post)
-              tokenEndpoint = "https://example.com";
-              authorizationEndpoint = "https://example.com";
-              logLevel = "info,kittybox=debug,retainer::cache=warn,h2=warn,rustls=warn";
-            };
-
-            environment.systemPackages = with pkgs; [
-              curl
-            ];
-          };
-        };
-
-        testScript = ''
-          with subtest("Verify that Kittybox started correctly..."):
-              kittybox.wait_for_open_port(8080)
-              kittybox.succeed("curl --silent http://localhost:8080/micropub")
-
-          with subtest("Onboarding should correctly work..."):
-              kittybox.copy_from_host("${./onboarding.json}", "/root/onboarding.json")
-              kittybox.succeed("curl -vvv http://localhost:8080/onboarding -d@/root/onboarding.json -H 'Content-Type: application/json'")
-              # Testing for a known string is the easiest way to determine that the onboarding worked
-              kittybox.succeed("curl --silent http://localhost:8080/ | grep 'vestige of the past long gone'")
-        '';
-      }));
-      dockerContainer = pkgs.dockerTools.buildImage {
-        name = "kittybox";
-        tag = self.rev or "development";
-        created = let
-          date = self.lastModifiedDate;
-        in builtins.concatStringsSep "" [
-          (builtins.substring 0 4 date)
-          "-"
-          (builtins.substring 4 2 date)
-          "-"
-          (builtins.substring 6 2 date)
-          "T"
-          (builtins.substring 8 2 date)
-          ":"
-          (builtins.substring 10 2 date)
-          ":"
-          (builtins.substring 12 2 date)
-          "Z"
-        ];
-
-        runAsRoot = ''
-          #!${pkgs.runtimeShell}
-          ${pkgs.dockerTools.shadowSetup}
-          groupadd -r kittybox
-          useradd -r -g kittybox kittybox
-          mkdir -p /data
-          chown kittybox:kittybox /data
-        '';
-
-        config = {
-          Cmd = [ "${self.packages.${system}.kittybox}/bin/kittybox" ];
-          Env = [
-            "SERVE_AT=0.0.0.0:8080"
-            "BACKEND_URI=file:///data"
-            "RUST_LOG=info"
-          ];
-          WorkingDir = "/data";
-          Volumes = { "/data" = {}; };
-          User = "kittybox";
-          ExposedPorts = { "8080" = {}; };
-        };
+      nixos-test = pkgs.nixosTest (import ./smoke-test.nix self);
+      dockerContainer = pkgs.callPackage ./docker.nix {
+        inherit (self.packages.${system}) kittybox;
+        rev = self.rev or "development";
+        inherit (self) lastModifiedDate;
       };
     };
 
-    devShell = pkgs.mkShell {
+    devShells.default = pkgs.mkShell {
       name = "rust-dev-shell";
 
       nativeBuildInputs = with pkgs; [
diff --git a/fonts/update.sh b/fonts/update.sh
deleted file mode 100755
index 02dcbee..0000000
--- a/fonts/update.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env bash
-set -e
-cd fonts
-url='https://fonts.google.com/download?family=Lato|Caveat|Noto%20Color%20Emoji|Noto%20Colr%20Emoji%20Glyf'
-tmp=$(mktemp)
-curl "$url" > "$tmp"
-unzip -uo "$tmp"
-rm "$tmp"
diff --git a/Cargo.lock b/kittybox-rs/Cargo.lock
index 7b2bbef..7b2bbef 100644
--- a/Cargo.lock
+++ b/kittybox-rs/Cargo.lock
diff --git a/Cargo.toml b/kittybox-rs/Cargo.toml
index d43f0f4..d43f0f4 100644
--- a/Cargo.toml
+++ b/kittybox-rs/Cargo.toml
diff --git a/dev.sh b/kittybox-rs/dev.sh
index fa39dcc..fa39dcc 100755
--- a/dev.sh
+++ b/kittybox-rs/dev.sh
diff --git a/src/bin/kittybox_bulk_import.rs b/kittybox-rs/src/bin/kittybox_bulk_import.rs
index 7e1f6af..7e1f6af 100644
--- a/src/bin/kittybox_bulk_import.rs
+++ b/kittybox-rs/src/bin/kittybox_bulk_import.rs
diff --git a/src/bin/kittybox_database_converter.rs b/kittybox-rs/src/bin/kittybox_database_converter.rs
index bc355c9..bc355c9 100644
--- a/src/bin/kittybox_database_converter.rs
+++ b/kittybox-rs/src/bin/kittybox_database_converter.rs
diff --git a/src/bin/pyindieblog_to_kittybox.rs b/kittybox-rs/src/bin/pyindieblog_to_kittybox.rs
index 38590c3..38590c3 100644
--- a/src/bin/pyindieblog_to_kittybox.rs
+++ b/kittybox-rs/src/bin/pyindieblog_to_kittybox.rs
diff --git a/src/database/file/mod.rs b/kittybox-rs/src/database/file/mod.rs
index 1e7aa96..1e7aa96 100644
--- a/src/database/file/mod.rs
+++ b/kittybox-rs/src/database/file/mod.rs
diff --git a/src/database/memory.rs b/kittybox-rs/src/database/memory.rs
index 786466c..786466c 100644
--- a/src/database/memory.rs
+++ b/kittybox-rs/src/database/memory.rs
diff --git a/src/database/mod.rs b/kittybox-rs/src/database/mod.rs
index 6bf5409..6bf5409 100644
--- a/src/database/mod.rs
+++ b/kittybox-rs/src/database/mod.rs
diff --git a/src/database/redis/edit_post.lua b/kittybox-rs/src/database/redis/edit_post.lua
index a398f8d..a398f8d 100644
--- a/src/database/redis/edit_post.lua
+++ b/kittybox-rs/src/database/redis/edit_post.lua
diff --git a/src/database/redis/mod.rs b/kittybox-rs/src/database/redis/mod.rs
index eeaa3f2..eeaa3f2 100644
--- a/src/database/redis/mod.rs
+++ b/kittybox-rs/src/database/redis/mod.rs
diff --git a/src/frontend/login.rs b/kittybox-rs/src/frontend/login.rs
index 9665ce7..9665ce7 100644
--- a/src/frontend/login.rs
+++ b/kittybox-rs/src/frontend/login.rs
diff --git a/src/frontend/mod.rs b/kittybox-rs/src/frontend/mod.rs
index b87f9c6..b87f9c6 100644
--- a/src/frontend/mod.rs
+++ b/kittybox-rs/src/frontend/mod.rs
diff --git a/src/frontend/onboarding.css b/kittybox-rs/src/frontend/onboarding.css
index 6f191b9..6f191b9 100644
--- a/src/frontend/onboarding.css
+++ b/kittybox-rs/src/frontend/onboarding.css
diff --git a/src/frontend/onboarding.js b/kittybox-rs/src/frontend/onboarding.js
index 7f9aa32..7f9aa32 100644
--- a/src/frontend/onboarding.js
+++ b/kittybox-rs/src/frontend/onboarding.js
diff --git a/src/frontend/style.css b/kittybox-rs/src/frontend/style.css
index 109bba0..109bba0 100644
--- a/src/frontend/style.css
+++ b/kittybox-rs/src/frontend/style.css
diff --git a/src/index.html b/kittybox-rs/src/index.html
index 1fc2a96..1fc2a96 100644
--- a/src/index.html
+++ b/kittybox-rs/src/index.html
diff --git a/src/indieauth.rs b/kittybox-rs/src/indieauth.rs
index 57c0301..57c0301 100644
--- a/src/indieauth.rs
+++ b/kittybox-rs/src/indieauth.rs
diff --git a/src/lib.rs b/kittybox-rs/src/lib.rs
index 1800b5b..1800b5b 100644
--- a/src/lib.rs
+++ b/kittybox-rs/src/lib.rs
diff --git a/src/main.rs b/kittybox-rs/src/main.rs
index eb70885..eb70885 100644
--- a/src/main.rs
+++ b/kittybox-rs/src/main.rs
diff --git a/src/media/mod.rs b/kittybox-rs/src/media/mod.rs
index 0d46e0c..0d46e0c 100644
--- a/src/media/mod.rs
+++ b/kittybox-rs/src/media/mod.rs
diff --git a/src/metrics.rs b/kittybox-rs/src/metrics.rs
index 48f5d9b..48f5d9b 100644
--- a/src/metrics.rs
+++ b/kittybox-rs/src/metrics.rs
diff --git a/src/micropub/get.rs b/kittybox-rs/src/micropub/get.rs
index 718714a..718714a 100644
--- a/src/micropub/get.rs
+++ b/kittybox-rs/src/micropub/get.rs
diff --git a/src/micropub/mod.rs b/kittybox-rs/src/micropub/mod.rs
index f426c77..f426c77 100644
--- a/src/micropub/mod.rs
+++ b/kittybox-rs/src/micropub/mod.rs
diff --git a/src/micropub/post.rs b/kittybox-rs/src/micropub/post.rs
index cf9f3d9..cf9f3d9 100644
--- a/src/micropub/post.rs
+++ b/kittybox-rs/src/micropub/post.rs
diff --git a/templates/Cargo.toml b/kittybox-rs/templates/Cargo.toml
index fe8ac19..fe8ac19 100644
--- a/templates/Cargo.toml
+++ b/kittybox-rs/templates/Cargo.toml
diff --git a/templates/src/lib.rs b/kittybox-rs/templates/src/lib.rs
index 39f1075..39f1075 100644
--- a/templates/src/lib.rs
+++ b/kittybox-rs/templates/src/lib.rs
diff --git a/templates/src/login.rs b/kittybox-rs/templates/src/login.rs
index 042c308..042c308 100644
--- a/templates/src/login.rs
+++ b/kittybox-rs/templates/src/login.rs
diff --git a/templates/src/onboarding.rs b/kittybox-rs/templates/src/onboarding.rs
index 9d0f2e1..9d0f2e1 100644
--- a/templates/src/onboarding.rs
+++ b/kittybox-rs/templates/src/onboarding.rs
diff --git a/templates/src/templates.rs b/kittybox-rs/templates/src/templates.rs
index 0054c91..0054c91 100644
--- a/templates/src/templates.rs
+++ b/kittybox-rs/templates/src/templates.rs
diff --git a/util/Cargo.toml b/kittybox-rs/util/Cargo.toml
index 31c6bca..31c6bca 100644
--- a/util/Cargo.toml
+++ b/kittybox-rs/util/Cargo.toml
diff --git a/util/src/lib.rs b/kittybox-rs/util/src/lib.rs
index bc41689..bc41689 100644
--- a/util/src/lib.rs
+++ b/kittybox-rs/util/src/lib.rs
diff --git a/kittybox.nix b/kittybox.nix
new file mode 100644
index 0000000..f42884a
--- /dev/null
+++ b/kittybox.nix
@@ -0,0 +1,19 @@
+{ stdenv, lib, openssl, zlib, pkg-config, protobuf, naersk-lib, lld, mold }:
+naersk-lib.buildPackage {
+  pname = "kittybox";
+  version = "0.1.0";
+
+  src = ./kittybox-rs;
+
+  checkInputs = [ openssl.dev zlib ];
+  nativeBuildInputs = [ pkg-config protobuf ];
+  nativeCheckInputs = [ pkg-config ];
+
+  doCheck = stdenv.hostPlatform == stdenv.targetPlatform;
+
+  meta = with lib.meta; {
+    maintainers = with lib.maintainers; [ vikanezrimaya ];
+    platforms = ["aarch64-linux" "x86_64-linux"];
+    mainProgram = "kittybox";
+  };
+}
diff --git a/smoke-test.nix b/smoke-test.nix
new file mode 100644
index 0000000..fbbc8a7
--- /dev/null
+++ b/smoke-test.nix
@@ -0,0 +1,36 @@
+kittybox:
+{ lib, ... }: {
+  name = "nixos-kittybox";
+
+  nodes = {
+    kittybox = { config, pkgs, lib, ... }: {
+      imports = [ kittybox.nixosModules.default ];
+
+      services.kittybox = {
+        enable = true;
+        # It never actually contacts those endpoints anyway unless we use Micropub so it's fine!
+        # TODO: Once we have self-hosted software for those endpoints,
+        #       make an e2e test for common workflows (e.g. making a post)
+        tokenEndpoint = "https://example.com";
+        authorizationEndpoint = "https://example.com";
+        logLevel = "info,kittybox=debug,retainer::cache=warn,h2=warn,rustls=warn";
+      };
+
+      environment.systemPackages = with pkgs; [
+        curl
+      ];
+    };
+  };
+
+  testScript = ''
+    with subtest("Verify that Kittybox started correctly..."):
+        kittybox.wait_for_open_port(8080)
+        kittybox.succeed("curl --silent http://localhost:8080/micropub")
+
+    with subtest("Onboarding should correctly work..."):
+        kittybox.copy_from_host("${./onboarding.json}", "/root/onboarding.json")
+        kittybox.succeed("curl -vvv http://localhost:8080/onboarding -d@/root/onboarding.json -H 'Content-Type: application/json'")
+        # Testing for a known string is the easiest way to determine that the onboarding worked
+        kittybox.succeed("curl --silent http://localhost:8080/ | grep 'vestige of the past long gone'")
+'';
+}