about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Makefile19
-rw-r--r--README.md10
-rw-r--r--flake.lock83
-rw-r--r--flake.nix127
-rw-r--r--nix/hosts/caladan/darwin-configuration.nix9
-rw-r--r--nix/hosts/caladan/emacs_config.el203
-rw-r--r--nix/hosts/caladan/home_emile.nix316
-rw-r--r--nix/hosts/caladan/nvim_plugins.nix21
-rw-r--r--nix/hosts/caladan/overlay.nix5
-rw-r--r--nix/hosts/chusuk/configuration.nix4
-rw-r--r--nix/hosts/corrino/configuration.nix68
-rw-r--r--nix/hosts/corrino/ports.nix1
-rw-r--r--nix/hosts/corrino/secrets/garage_admin_metrics_secret.age7
-rw-r--r--nix/hosts/corrino/secrets/garage_admin_token_secret.age7
-rw-r--r--nix/hosts/corrino/secrets/garage_env.agebin0 -> 602 bytes
-rw-r--r--nix/hosts/corrino/secrets/garage_rpc_secret.agebin387 -> 387 bytes
-rw-r--r--nix/hosts/corrino/secrets/goapp_oidc_secret.age7
-rw-r--r--nix/hosts/corrino/secrets/goapp_oidc_session_key.age8
-rw-r--r--nix/hosts/corrino/secrets/miniflux_admin_file.age7
-rw-r--r--nix/hosts/corrino/secrets/miniflux_oidc_secret.agebin0 -> 395 bytes
-rw-r--r--nix/hosts/corrino/secrets/tailscale-corrino-cert.agebin0 -> 3247 bytes
-rw-r--r--nix/hosts/corrino/secrets/tailscale-corrino-key.age8
-rw-r--r--nix/hosts/corrino/vm.nix55
-rw-r--r--nix/hosts/corrino/www/cs.emile.space.nix23
-rw-r--r--nix/hosts/corrino/www/goapp.emile.space.nix89
-rw-r--r--nix/hosts/corrino/www/grafana.emile.space.nix19
-rw-r--r--nix/hosts/corrino/www/hydra.emile.space.nix2
-rw-r--r--nix/hosts/corrino/www/mc.emile.space.nix150
-rw-r--r--nix/hosts/corrino/www/md.emile.space.nix22
-rw-r--r--nix/hosts/corrino/www/miniflux.emile.space.nix73
-rw-r--r--nix/hosts/corrino/www/sb.emile.space.nix114
-rw-r--r--nix/hosts/corrino/www/sso.emile.space.nix35
-rw-r--r--nix/hosts/hacknix/burpsuitepro/default.nix4
-rw-r--r--nix/hosts/hacknix/configuration.nix15
-rw-r--r--nix/hosts/lampadas/configuration.nix63
-rw-r--r--nix/hosts/lernaeus/configuration.nix14
-rw-r--r--nix/hosts/mail/mail.nix31
-rw-r--r--nix/lib/flake-helper.nix15
-rw-r--r--nix/modules/goapp-frontend/default.nix134
-rw-r--r--nix/modules/libvirtnix/config.nix59
-rw-r--r--nix/modules/libvirtnix/default.nix109
-rw-r--r--nix/modules/libvirtnix/domain.nix483
-rw-r--r--nix/modules/libvirtnix/test.nix10
-rw-r--r--nix/modules/libvirtnix/xml.nix45
-rw-r--r--nix/modules/vm/default.nix86
-rw-r--r--nix/modules/x86_64-linux.nix2
-rw-r--r--nix/pkgs/aarch64-darwin.nix1
-rw-r--r--nix/pkgs/glibc-all-in-one/default.nix25
-rw-r--r--nix/pkgs/libc-database/default.nix34
-rw-r--r--nix/pkgs/overlay.nix9
-rw-r--r--nix/pkgs/x86_64-linux.nix1
-rw-r--r--nix/templates/ctf/flake.lock141
-rw-r--r--nix/templates/ctf/flake.nix62
-rw-r--r--nix/templates/ctf/solve.py15
-rw-r--r--nix/templates/goapp/README.md39
-rw-r--r--nix/templates/goapp/flake.lock62
-rw-r--r--nix/templates/goapp/flake.nix63
-rw-r--r--nix/templates/goapp/frontend/default.nix23
-rw-r--r--nix/templates/goapp/frontend/go.mod32
-rw-r--r--nix/templates/goapp/frontend/go.sum84
-rw-r--r--nix/templates/goapp/frontend/main.dbbin0 -> 8192 bytes
-rwxr-xr-xnix/templates/goapp/frontend/run.sh11
-rw-r--r--nix/templates/goapp/frontend/server.log181
-rw-r--r--nix/templates/goapp/frontend/sessions.dbbin0 -> 24576 bytes
-rw-r--r--nix/templates/goapp/frontend/src/db.go37
-rw-r--r--nix/templates/goapp/frontend/src/handlers.go252
-rw-r--r--nix/templates/goapp/frontend/src/init.go95
-rw-r--r--nix/templates/goapp/frontend/src/log.go34
-rw-r--r--nix/templates/goapp/frontend/src/main.go99
-rw-r--r--nix/templates/goapp/frontend/src/sqlitestore.go285
-rw-r--r--nix/templates/goapp/frontend/src/templates.go42
-rw-r--r--nix/templates/goapp/frontend/src/types.go68
-rw-r--r--nix/templates/goapp/frontend/src/util.go58
-rw-r--r--nix/templates/goapp/frontend/templates/footer.html8
-rw-r--r--nix/templates/goapp/frontend/templates/head.html153
-rw-r--r--nix/templates/goapp/frontend/templates/index.html83
-rw-r--r--nix/templates/goapp/frontend/templates/login.html41
-rw-r--r--nix/templates/goapp/frontend/templates/nav.html41
-rwxr-xr-xsecret_create.sh2
80 files changed, 4053 insertions, 591 deletions
diff --git a/.gitignore b/.gitignore
index e43b0f9..d5fec98 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 .DS_Store
+result
diff --git a/Makefile b/Makefile
index a9d041e..6708a52 100644
--- a/Makefile
+++ b/Makefile
@@ -8,30 +8,31 @@ help:
 	@echo "dry-activate - build and show different"
 
 corrino:
-	deploy .#corrino --skip-checks -- --show-trace -L
+	time deploy .#corrino --skip-checks -- --show-trace -L
 
 build:
-	nix run nixpkgs#nix-output-monitor build ".#nixosConfigurations.${HOSTNAME}.config.system.build.toplevel"
+	time nix run nixpkgs#nix-output-monitor build ".#nixosConfigurations.${HOSTNAME}.config.system.build.toplevel"
 
 update:
-	nix flake update --commit-lock-file
+	time nix flake update --commit-lock-file
 
 switch-caladan:
-	nix run https://github.com/LnL7/nix-darwin/archive/master.tar.gz -- switch --flake .#caladan
+	time nix run https://github.com/LnL7/nix-darwin/archive/master.tar.gz -- switch --flake .#caladan
 
 build-corrino:
-	nix run nixpkgs#nix-output-monitor build .#nixosConfigurations.${HOSTNAME}.config.system.build.toplevel
+	time nix run nixpkgs#nix-output-monitor build .#nixosConfigurations.${HOSTNAME}.config.system.build.toplevel
 
-deploy: build
-	nix run -- nixpkgs#nixos-rebuild switch \
+deploy: # build
+	time nix run -- nixpkgs#nixos-rebuild switch \
+		-vvv \
 		--fast \
 		--build-host root@${BUILDHOST} \
 		--target-host root@${HOSTNAME} \
 		--flake ".#${HOSTNAME}"
 
 check:
-	nix flake check
+	time nix flake check
 
 dry-activate:
-	nix run -- nixpkgs#nixos-rebuild dry-activate --target-host root@${HOSTNAME} --flake ".#corrino" 
+	time nix run -- nixpkgs#nixos-rebuild dry-activate --target-host root@${HOSTNAME} --flake ".#corrino" 
 
diff --git a/README.md b/README.md
index ab237de..a925db3 100644
--- a/README.md
+++ b/README.md
@@ -24,12 +24,10 @@ Print the generated secrets file as follows:
 
 ## Deploy
 
-Using [deploy-rs](https://github.com/serokell/deploy-rs) to deploy
-
 ```bash
-; deploy .#corrino
-; deploy .#caladan
-; deploy .#lampadas
-; deploy .#lernaeus
+; BUILDHOST=corrino HOSTNAME=corrino make deploy
+; BUILDHOST=corrino HOSTNAME=lampadas make deploy
+; BUILDHOST=corrino HOSTNAME=lankiveil make deploy
+; make switch-caladan
 ```
 
diff --git a/flake.lock b/flake.lock
index db1bfbd..851a6d8 100644
--- a/flake.lock
+++ b/flake.lock
@@ -10,11 +10,11 @@
         "systems": "systems"
       },
       "locked": {
-        "lastModified": 1723293904,
-        "narHash": "sha256-b+uqzj+Wa6xgMS9aNbX4I+sXeb5biPDi39VgvSFqFvU=",
+        "lastModified": 1736955230,
+        "narHash": "sha256-uenf8fv2eG5bKM8C/UvFaiJMZ4IpUFaQxk9OH5t/1gA=",
         "ref": "refs/heads/main",
-        "rev": "f6291c5935fdc4e0bef208cfc0dcab7e3f7a1c41",
-        "revCount": 314,
+        "rev": "e600439ec4c273cf11e06fe4d9d906fb98fa097c",
+        "revCount": 320,
         "type": "git",
         "url": "https://github.com/ryantm/agenix"
       },
@@ -52,15 +52,16 @@
         ]
       },
       "locked": {
-        "lastModified": 1732603785,
-        "narHash": "sha256-AEjWTJwOmSnVYsSJCojKgoguGfFfwel6z/6ud6UFMU8=",
-        "ref": "refs/heads/master",
-        "rev": "6ab87b7c84d4ee873e937108c4ff80c015a40c7a",
-        "revCount": 1930,
+        "lastModified": 1739553546,
+        "narHash": "sha256-L4ou3xfOr17EAe836djRoQ7auVkYOREMtiQa82wVGqU=",
+        "ref": "nix-darwin-24.11",
+        "rev": "353846417f985e74fdc060555f17939e4472ea2c",
+        "revCount": 2010,
         "type": "git",
         "url": "https://github.com/lnl7/nix-darwin"
       },
       "original": {
+        "ref": "nix-darwin-24.11",
         "type": "git",
         "url": "https://github.com/lnl7/nix-darwin"
       }
@@ -104,19 +105,22 @@
         "type": "github"
       }
     },
-    "hefe-internal": {
+    "flake-utils": {
+      "inputs": {
+        "systems": "systems_3"
+      },
       "locked": {
-        "lastModified": 1733861329,
-        "narHash": "sha256-nof2TIYOlKKc7EPb/N/LeLY3c+ZzGGZskCpr/nE9btA=",
+        "lastModified": 1731533236,
+        "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
         "ref": "refs/heads/main",
-        "rev": "bd91c9a6d25df1f00cb77ce66ee5f1738835cc05",
-        "revCount": 129,
+        "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+        "revCount": 102,
         "type": "git",
-        "url": "file:///Users/emile/hefe-internal"
+        "url": "https://github.com/numtide/flake-utils"
       },
       "original": {
         "type": "git",
-        "url": "file:///Users/emile/hefe-internal"
+        "url": "https://github.com/numtide/flake-utils"
       }
     },
     "home-manager": {
@@ -147,11 +151,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1732466619,
-        "narHash": "sha256-T1e5oceypZu3Q8vzICjv1X/sGs9XfJRMW5OuXHgpB3c=",
+        "lastModified": 1739757849,
+        "narHash": "sha256-Gs076ot1YuAAsYVcyidLKUMIc4ooOaRGO0PqTY7sBzA=",
         "ref": "release-24.11",
-        "rev": "f3111f62a23451114433888902a55cf0692b408d",
-        "revCount": 3863,
+        "rev": "9d3d080aec2a35e05a15cedd281c2384767c2cfe",
+        "revCount": 3881,
         "type": "git",
         "url": "https://github.com/nix-community/home-manager"
       },
@@ -168,11 +172,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1721727458,
-        "narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=",
+        "lastModified": 1739824009,
+        "narHash": "sha256-fcNrCMUWVLMG3gKC5M9CBqVOAnJtyRvGPxptQFl5mVg=",
         "ref": "refs/heads/master",
-        "rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11",
-        "revCount": 345,
+        "rev": "e5130d37369bfa600144c2424270c96f0ef0e11d",
+        "revCount": 353,
         "type": "git",
         "url": "https://github.com/nix-community/naersk"
       },
@@ -183,10 +187,10 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1735669367,
-        "narHash": "sha256-tfYRbFhMOnYaM4ippqqid3BaLOXoFNdImrfBfCp4zn0=",
+        "lastModified": 1740603184,
+        "narHash": "sha256-t+VaahjQAWyA+Ctn2idyo1yxRIYpaDxMgHkgCNiMJa4=",
         "ref": "nixos-24.11",
-        "rev": "edf04b75c13c2ac0e54df5ec5c543e300f76f1c9",
+        "rev": "f44bd8ca21e026135061a0a57dcf3d0775b67a49",
         "shallow": true,
         "type": "git",
         "url": "ssh://git@github.com/nixos/nixpkgs.git"
@@ -200,11 +204,11 @@
     },
     "nixpkgs-unstable": {
       "locked": {
-        "lastModified": 1732238832,
-        "narHash": "sha256-sQxuJm8rHY20xq6Ah+GwIUkF95tWjGRd1X8xF+Pkk38=",
+        "lastModified": 1740547748,
+        "narHash": "sha256-Ly2fBL1LscV+KyCqPRufUBuiw+zmWrlJzpWOWbahplg=",
         "ref": "nixpkgs-unstable",
-        "rev": "8edf06bea5bcbee082df1b7369ff973b91618b8d",
-        "revCount": 711046,
+        "rev": "3a05eebede89661660945da1f151959900903b6a",
+        "revCount": 759303,
         "type": "git",
         "url": "https://github.com/nixos/nixpkgs"
       },
@@ -219,7 +223,7 @@
         "agenix": "agenix",
         "darwin": "darwin_2",
         "deploy-rs": "deploy-rs",
-        "hefe-internal": "hefe-internal",
+        "flake-utils": "flake-utils",
         "home-manager": "home-manager_2",
         "naersk": "naersk",
         "nixpkgs": "nixpkgs",
@@ -256,6 +260,21 @@
         "type": "github"
       }
     },
+    "systems_3": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    },
     "utils": {
       "inputs": {
         "systems": "systems_2"
diff --git a/flake.nix b/flake.nix
index 5d05f73..cad6ed4 100644
--- a/flake.nix
+++ b/flake.nix
@@ -3,7 +3,11 @@
     nixpkgs.url = "git+ssh://git@github.com/nixos/nixpkgs.git?shallow=1&ref=nixos-24.11";
     nixpkgs-unstable.url = "git+https://github.com/nixos/nixpkgs?ref=nixpkgs-unstable";
 
-    darwin.url = "git+https://github.com/lnl7/nix-darwin";
+    # nix darwin version must match nixpkgs version:
+    #   nixpkgs:    nixos-xx.yy
+    #   nix-darwin: nix-darwin-xx.yy
+    # with xx.yy being the same
+    darwin.url = "git+https://github.com/lnl7/nix-darwin?ref=nix-darwin-24.11";
     darwin.inputs.nixpkgs.follows = "nixpkgs";
 
     deploy-rs.url = "git+https://github.com/serokell/deploy-rs?ref=master";
@@ -18,7 +22,9 @@
     naersk.url = "git+https://github.com/nix-community/naersk";
     naersk.inputs.nixpkgs.follows = "nixpkgs";
 
-    hefe-internal.url = "git+file:///Users/emile/hefe-internal";
+    flake-utils.url = "git+https://github.com/numtide/flake-utils";
+
+    # hefe-internal.url = "git+file:///Users/emile/hefe-internal";
     # hefe-internal.url = "git+ssh://git@git.emile.space/hefe-internal";
 
     # nix registry add flake:mylocalrepo git+file:///path/to/local/repo
@@ -29,19 +35,50 @@
   outputs =
     {
       self,
-      nixpkgs,          # packages
+      nixpkgs, # packages
       nixpkgs-unstable, # unstable branch
-      darwin,           # darwin related stuff
-      deploy-rs,        # deploy the hosts
-      agenix,           # store secrets crypted using age
-      home-manager,     # manage my home envs
-      naersk,           # build rust stuff
-      hefe-internal,    # internal tooling
+      darwin, # darwin related stuff
+      deploy-rs, # deploy the hosts
+      agenix, # store secrets crypted using age
+      home-manager, # manage my home envs
+      naersk, # build rust stuff
+      flake-utils, # common flake utils
+      # hefe-internal,    # internal tooling
       ...
     }@inputs:
     let
       lib = import ./nix/lib inputs;
       helper = lib.flake-helper;
+
+      # TODO(emile): move all these functions into the helper, keeping the flake.nix clean
+
+      # A function taking an attribute set of flake templates, importing their
+      # flake.nix/output/packages (if there are any) and returning an attribute set
+      # of their packages (if the template has one or more)
+      template-packages =
+        builtins.mapAttrs (name: value:
+          (((import ./nix/templates/${name}/flake.nix).outputs)
+            { inherit nixpkgs flake-utils; })
+            .packages or { }
+        );
+
+      # apply the above function to the templates
+      templates = template-packages self.templates;
+
+      # Merge template packages into root packages with template prefix
+      merged-template-packages =
+        system:
+        let
+          lib = nixpkgs.lib;
+        in
+        lib.foldl (
+          acc: tplName:
+          let
+            tplPkgs = templates.${tplName}.${system} or { };
+            prefixed = lib.mapAttrs' (pkgName: pkg: lib.nameValuePair "${tplName}-${pkgName}" pkg) tplPkgs;
+          in
+          acc // prefixed
+        ) { } (builtins.attrNames templates);
     in
     {
       hosts = {
@@ -56,15 +93,15 @@
         };
 
         # main vm host
-        # 
+        #
         # in case of broken config, reboot into recovery, then:
-        # 
+        #
         # cryptsetup luksOpen /dev/md1 luks0
         # mount /dev/disk/by-label/root /mnt
         # mkdir /mnt/boot
         # mount /dev/disk/by-label/root /mnt
         # grub-reboot --boot-directory /mnt/boot "Ni" <- press tab and choose wisely
-        # 
+        #
         # also see
         # //nix/hosts/corrino/hetzner-dedicated-wipe-and-install-nixos-luks-raid-lvm.sh
         corrino = {
@@ -72,7 +109,19 @@
           ip = "corrino";
           description = "Hetzner AX41 dual 512GB NVME";
           modules = [
-            hefe-internal.nixosModules.corrino
+            # hefe-internal.nixosModules.corrino
+            (
+              { self, ... }:
+              {
+                nixpkgs.overlays = [
+                  (final: prev: {
+                    inherit (self.packages.x86_64-linux)
+                      goapp-frontend
+                      ;
+                  })
+                ];
+              }
+            )
           ];
         };
         chusuk = {
@@ -113,7 +162,7 @@
 
         # lankiveil = {
         #   system = "x86_64-linux"; # ???, ???, RTX A2000
-        #   description = "";
+        #   description = "Router";
         # };
         # poritrin = {
         #   description = "lankiveil bmc";
@@ -148,19 +197,27 @@
       };
 
       overlays = {
-        emile = import ./nix/pkgs/overlay.nix;
+        default = self.overlays.x86_64-linux;
 
         x86_64-linux = import ./nix/pkgs/x86_64-linux.nix;
         aarch64-darwin = import ./nix/pkgs/aarch64-darwin.nix;
 
-        default = self.overlays.x86_64-linux;
-
         unstable = final: prev: {
           unstable = import nixpkgs-unstable {
             system = "x86_64-linux";
             config.allowUnfree = true;
           };
         };
+
+        # no clue why, but when rebuilding corrino and this not being commented,
+        # something in the hardware.bluetooth module breaks
+        #
+        # unstable-darwin = final: prev: {
+        #   unstable-darwin = import nixpkgs-unstable {
+        #     system = "aarch64-darwin";
+        #     config.allowUnfree = true;
+        #   };
+        # };
       };
 
       deploy.nodes = helper.mapToDeployRsConfiguration self.hosts;
@@ -178,7 +235,6 @@
               pkgs = import nixpkgs {
                 inherit system;
                 overlays = [
-
                   (
                     if system == "x86_64-linux" then
                       self.overlays.x86_64-linux
@@ -187,14 +243,15 @@
                     else
                       null
                   )
-                  # self.overlays.emile
-
                   # some arguments for packages
                   (_: _: { inherit naersk; })
                 ];
               };
             in
-            {
+            # take all the packages exposed from templates and add them to
+            # the packages exposed by this flake
+            merged-template-packages system
+            // {
               inherit (pkgs) vokobe r2wars-web remarvin;
             }
           );
@@ -202,6 +259,7 @@
       hydraJobs = {
         inherit (self) packages;
         nixosConfigurations = helper.buildHosts self.nixosConfigurations;
+        templates = template-packages self.templates;
       };
 
       templates = {
@@ -211,18 +269,25 @@
           description = "A basic ctf env with pwn, rev, ... tools";
           path = ./nix/templates/ctf;
           welcomeText = ''
-            # A basic CTF env
-            ## Intended usage
-            The intended usage of this flake is...
-
-            ## More info
-            - [Rust language](https://www.rust-lang.org/)
-            - [Rust on the NixOS Wiki](https://nixos.wiki/wiki/Rust)
-            - ...
+            # CTF flake template
+
+            Run `nix develop` to get a shell with pwntools, pwndbg, pycryptodome, ...
+
+            Add packages in the flake as you like.
+          '';
+        };
+        goapp = {
+          description = "A basic golang service";
+          path = ./nix/templates/goapp;
+          welcomeText = ''
+            # A basic golang service
+             
+            - using gorilla/mux
           '';
         };
-      };
 
-      # checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib;
+        # checks = builtins.mapAttrs (system: deployLib:
+        #     deployLib.deployChecks self.deploy) deploy-rs.lib;
+      };
     };
 }
diff --git a/nix/hosts/caladan/darwin-configuration.nix b/nix/hosts/caladan/darwin-configuration.nix
index ef18642..6fdbdaa 100644
--- a/nix/hosts/caladan/darwin-configuration.nix
+++ b/nix/hosts/caladan/darwin-configuration.nix
@@ -85,10 +85,10 @@
       {
         hostName = "corrino.emile.space";
         system = "x86_64-linux";
-        maxJobs = 16;
+        maxJobs = 10;
         speedFactor = 2;
 
-        # Feature	      | Derivations requiring it
+        # Feature	        | Derivations requiring it
         # ----------------|-----------------------------------------------------
         # kvm	            | Everything which builds inside a vm, like NixOS tests
         # nixos-test	    | Machine can run NixOS tests
@@ -110,8 +110,9 @@
     ];
   };
 
-  nixpkgs = {
-    config.allowUnfree = true;
+  nixpkgs.config = {
+    allowUnfree = true;
+    allowUnsupportedSystem = true;
   };
 
   services.nix-daemon.enable = true;
diff --git a/nix/hosts/caladan/emacs_config.el b/nix/hosts/caladan/emacs_config.el
new file mode 100644
index 0000000..01cf5bd
--- /dev/null
+++ b/nix/hosts/caladan/emacs_config.el
@@ -0,0 +1,203 @@
+;;; emacs-config --- My emacs config
+
+;;; Commentary:
+;;; This is my (currently often changing) Emacs config
+
+;;; Code:
+
+(require 'package)
+(package-initialize)
+(add-to-list 'package-archives
+             '("melpa" . "https://melpa.org/packages/") t)
+
+(unless package-archive-contents
+  (package-refresh-contents))
+
+(dolist (package '(use-package sly corfu org))
+  (unless (package-installed-p package)
+    (package-install package)))
+
+(scroll-bar-mode -1)
+(load-theme 'leuven) ;; light theme
+(setq pixel-scroll-precision-mode 1)
+(xterm-mouse-mode 1)
+
+(setq standard-indent 2)
+(setq create-lockfiles nil)
+(setq delete-old-versions -1)
+(setq make-backup-files nil) ; stop creating ~ files
+(setq version-control t)
+(setq coding-system-for-write 'utf-8)
+(setq-default indent-tabs-mode nil) ;; use spaces, not tabs
+(setq show-paren-delay 0)
+(show-paren-mode)
+
+(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
+
+(add-to-list 'display-buffer-alist
+             '("\\`\\*\\(Warnings\\|Compile-Log\\)\\*\\'"
+               (display-buffer-no-window)
+               (allow-no-window . t)))
+
+;; general purpose emacs settings
+(use-package emacs
+  :init
+
+  ;; do not allow cursor in the minibuffer prompt
+  (setq minibuffer-prompt-properties
+        '(read-only t cursor-intangible t face minibuffer-prompt))
+  (add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)
+
+  ;; support opening new minibuffers from inside existing minibuffers
+  (setq enable-recursive-minibuffers t)
+
+  ;; Emacs 28 and newer: Hide commands in M-x which do not work in the current
+  ;; mode. Vertico commands are hidden in normal buffers. This setting is
+  ;; useful beyond Vertico.
+  (setq read-extended-command-predicate #'command-completion-default-include-p))
+
+;; org-mode
+(use-package org)
+(define-key global-map "\C-cl" 'org-store-link)
+(define-key global-map "\C-ca" 'org-agenda)
+(setq org-log-done t)
+
+;; =============== plugins ==================
+
+;; Corfu - COmpletion in Region FUnction
+;; https://github.com/minad/corfu
+(declare-function global-corfu-mode "proced")
+(declare-function corfu-history-mode "proced")
+(declare-function corfu-mode "proced")
+(defvar corfu-map)
+(use-package corfu
+  :custom
+  (corfu-cycle-tab t)
+  (corfu-auto t)
+  (corfu-auto-prefix 2)
+  (corfu-auto-delay 0.0)
+  (corfu-quit-at-boundary 'separator)
+  (corfu-echo-documentation 0.5)
+  (corfu-preview-current 'insert)
+  (corfu-preselect 'prompt)
+  :bind (:map corfu-map
+              ("M-SPC" . corfu-insert-separator)
+              ("RET" . nil)
+              ("TAB" . corfu-next)
+              ([tab] . corfu-next)
+              ("S-TAB" . corfu-previous)
+              ([backtab] . corfu-previous)
+              ("S-<return>" . corfu-insert))
+  :init
+  (global-corfu-mode)
+  (corfu-history-mode)
+
+  :config
+  (add-hook 'eshell-mode-hook
+            (lambda ()
+              (setq-local corfu-quit-at-boundary t
+                          corfu-quit-no-match t
+                          corfu-auto nil)
+              (corfu-mode))))
+
+;; Marginalia - Marginalia in the minibuffer
+;; https://github.com/minad/marginalia
+(declare-function marginalia-mode "proced")
+(use-package marginalia
+  :custom
+  (marginalia-max-relative-age 0)
+  (marginalia-align 'right)
+  :init
+  (marginalia-mode))
+
+;; == Fancy icons ==
+;; all-the-icons
+;; https://github.com/domtronn/all-the-icons.el
+(use-package all-the-icons
+  :if (display-graphic-p))
+
+;; ... also in completions
+(declare-function all-the-icons-completion-mode "proced")
+(use-package all-the-icons-completion
+  :after (marginalia all-the-icons)
+  :hook (marginalia . all-the-icons-completion-marginalia-setup)
+  :init (all-the-icons-completion-mode))
+
+;; vectico.el - VERTical Interactive COmpletion
+;; https://github.com/minad/vertico
+(declare-function vertico-mode "proced")
+(use-package vertico
+  :init (vertico-mode)
+  :custom (vertico-count 13)
+  (vertico-resize t)
+  (vertico-cycle nil)
+  :config (vertico-mode))
+
+;; orderless - completion
+;; This allows searching for space separated terms in any order
+;; https://github.com/oantolin/orderless
+(use-package orderless
+  :init (setq completion-styles '(orderless basic)
+              completion-category-defaults nil
+              completion-category-overrides '((file (styles partial-completion)))))
+              
+
+;; markdown mode
+;; https://jblevins.org/projects/markdown-mode/
+(defvar markdown-command)
+(use-package markdown-mode
+  :ensure t
+  :mode ("README\\.md\\'" . gfm-mode)
+  :init (setq markdown-command "multimarkdown"))
+
+;; Minibuffer with help when waiting too long
+;; In emacs per default with Emacs v30
+(declare-function which-key-mode "proced")
+(defvar which-key-idle-delay)
+(defvar which-key-idle-secondary-delay)
+(use-package which-key
+  :ensure t
+  :config
+  (setq which-key-idle-delay 0.1)
+  (setq which-key-idle-secondary-delay 0.1)
+  (which-key-mode))
+
+;; flycheck - Syntax checking for GNU Emacs¶
+;; https://www.flycheck.org/en/latest/
+(declare-function global-flycheck-mode "proced")
+(use-package flycheck
+  :ensure t
+  :init (global-flycheck-mode))
+
+;; allow the deletion of selected text (don't know why this isn't implemented by default)
+(use-package delsel
+  :ensure nil ; no need to install it as it is built-in, but needs to be activated
+  :hook (after-init . delete-selection-mode))
+
+;; Configure the Lisp program for SLY
+(add-to-list 'exec-path "/Users/emile/.nix-profile/bin")
+(defvar inferior-lisp-program "sbcl")
+
+;; configure parinfer to be enabled as a mode when the major lisp mode is enabled
+(add-to-list 'load-path "/Users/emile/parinfer-rust")
+(add-hook 'emacs-lisp-mode 'parinfer-rust-mode)
+(declare-function lispy-mode "proced")
+(add-hook 'emacs-lisp-mods (lambda () (lispy-mode 1)))
+
+;; pixel perfect ultra precise low latency scrolling
+(declare-function ultra-scroll-mode "proced")
+(use-package ultra-scroll
+     ; if you git clone'd instead of package-vc-install
+     ;:load-path "~/code/emacs/ultra-scroll"
+
+     :init
+     (setq scroll-conservatively 101 ; important!
+         scroll-margin 0)
+     :config
+     (ultra-scroll-mode 1))
+
+(use-package breadcrumb
+     :ensure t)
+
+(provide '.emacs)
+;;; emacs_config.el ends here
diff --git a/nix/hosts/caladan/home_emile.nix b/nix/hosts/caladan/home_emile.nix
index 2a52db9..545c4d5 100644
--- a/nix/hosts/caladan/home_emile.nix
+++ b/nix/hosts/caladan/home_emile.nix
@@ -1,4 +1,4 @@
-{ pkgs, ... }:
+{ lib, pkgs, ... }:
 
 {
   home = {
@@ -60,6 +60,18 @@
       '';
     };
 
+    neovim = let
+      custom_plugins = pkgs.callPackage ./nvim_plugins.nix { };
+    in {
+      enable = true;
+      plugins = with pkgs.vimPlugins // custom_plugins; [
+        neovim-ayu
+        lisp.vlime
+      ];
+      extraConfig = ''
+      '';
+    };
+
     emacs = {
       enable = true;
       package = pkgs.emacs;
@@ -69,299 +81,8 @@
           magit # Git
           parinfer-rust-mode # Lisp Parens
           tuareg # OCaml
-          howm # Notes
         ];
-      extraConfig = ''
-        (require 'package)
-        (package-initialize)
-        (add-to-list 'package-archives
-          '("melpa" . "https://melpa.org/packages/") t)
-        (unless package-archive-contents
-          (package-refresh-contents))
-
-        (dolist (package '(use-package sly corfu org))
-          (unless (package-installed-p package)
-            (package-install package)))
-
-        (when (display-graphic-p)
-          (tool-bar-mode 0)
-          (scroll-bar-mode 'left))
-
-        (load-theme 'leuven) ;; light theme
-        (setq pixel-scroll-precision-mode 1)
-
-        (setq standard-indent 2)
-        (setq create-lockfiles nil)
-        (setq delete-old-versions -1)
-        (setq make-backup-files nil) ; stop creating ~ files
-        (setq version-control t)
-        (setq coding-system-for-write 'utf-8)
-        (setq-default indent-tabs-mode nil) ;; use spaces, not tabs
-        (setq show-paren-delay 0)
-        (show-paren-mode)
-
-        (setq custom-file (expand-file-name "custom.el" user-emacs-directory))
-
-        (add-to-list 'display-buffer-alist
-                     '("\\`\\*\\(Warnings\\|Compile-Log\\)\\*\\'"
-                       (display-buffer-no-window)
-                       (allow-no-window . t)))
-
-        ;; general purpose emacs settings
-        (use-package emacs
-          :init
-
-          ;; do not allow cursor in the minibuffer prompt
-          (setq minibuffer-prompt-properties
-                '(read-only t cursor-intangible t face minibuffer-prompt))
-          (add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)
-
-          ;; support opening new minibuffers from inside existing minibuffers
-          (setq enable-recursive-minibuffers t)
-
-          ;; Emacs 28 and newer: Hide commands in M-x which do not work in the current
-          ;; mode. Vertico commands are hidden in normal buffers. This setting is
-          ;; useful beyond Vertico.
-          (setq read-extended-command-predicate #'command-completion-default-include-p))
-
-        ;; Add "lisp" to the list of languages babel is allowed to eval
-        ;(setq-default org-babel-lisp-eval-fn #'sly-eval)
-        (org-babel-do-load-languages
-         'org-babel-load-languages
-         '((lisp . t)))
-
-        ;; org-mode
-        (use-package org)
-        (define-key global-map "\C-cl" 'org-store-link)
-        (define-key global-map "\C-ca" 'org-agenda)
-        (setq org-log-done t)
-
-        ;; =============== plugins ==================
-
-        ;; Corfu - COmpletion in Region FUnction
-        ;; https://github.com/minad/corfu
-        (use-package corfu
-          :custom
-          (corfu-cycle-tab t)
-          (corfu-auto t)
-          (corfu-auto-prefix 2)
-          (corfu-auto-delay 0.0)
-          (corfu-quit-at-boundary 'separator)
-          (corfu-echo-documentation 0.5)
-          (corfu-preview-current 'insert)
-          (corfu-preselect 'prompt)
-          :bind (:map corfu-map
-                      ("M-SPC" . corfu-insert-separator)
-                      ("RET" . nil)
-                      ("TAB" . corfu-next)
-                      ([tab] . corfu-next)
-                      ("S-TAB" . corfu-previous)
-                      ([backtab] . corfu-previous)
-                      ("S-<return>" . corfu-insert))
-          :init
-          (global-corfu-mode)
-          (corfu-history-mode)
-
-          :config
-          (add-hook 'eshell-mode-hook
-                    (lambda ()
-                      (setq-local corfu-quit-at-boundary t
-                                  corfu-quit-no-match t
-                                  corfu-auto nil)
-                      (corfu-mode))))
-
-        ;; Marginalia - Marginalia in the minibuffer
-        ;; https://github.com/minad/marginalia
-        (use-package marginalia
-          :custom
-          (marginalia-max-relative-age 0)
-          (marginalia-align 'right)
-          :init
-          (marginalia-mode))
-
-        ;; == Fancy icons ==
-        ;; all-the-icons
-        ;; https://github.com/domtronn/all-the-icons.el
-        (use-package all-the-icons
-          :if (display-graphic-p))
-
-        ;; ... also in completions 
-        (use-package all-the-icons-completion
-          :after (marginalia all-the-icons)
-          :hook (marginalia . all-the-icons-completion-marginalia-setup)
-          :init (all-the-icons-completion-mode))
-
-        ;; vectico.el - VERTical Interactive COmpletion
-        ;; https://github.com/minad/vertico
-        (use-package vertico
-          :init (vertico-mode)
-          :custom (vertico-count 13)
-          (vertico-resize t)
-          (vertico-cycle nil)
-          :config (vertico-mode))
-
-        ;; orderless - completion
-        ;; This allows searching for space separated terms in any order
-        ;; https://github.com/oantolin/orderless
-        (use-package orderless
-          :init (setq completion-styles '(orderless basic)
-                      completion-category-defaults nil
-                      completion-category-overrides '((file (styles partial-completion)))
-                      ))
-
-
-        ;; markdown mode
-        ;; https://jblevins.org/projects/markdown-mode/
-        (use-package markdown-mode
-          :ensure t
-          :mode ("README\\.md\\'" . gfm-mode)
-          :init (setq markdown-command "multimarkdown"))
-
-        ;; Minibuffer with help when waiting too long
-        ;; In emacs per default with Emacs v30
-        (use-package which-key
-          :ensure t
-          :config
-          (setq which-key-idle-delay 0.1)
-          (setq which-key-idle-secondary-delay 0.1)
-          (which-key-mode))
-
-        ;; imenu-list - Display imenu (symbols) in a separate buffer
-        ;; https://github.com/bmag/imenu-list
-        (use-package imenu-list :ensure t
-          :init
-          (setq imenu-list-auto-resize t)
-          (setq imenu-list-focus-after-activation t))
-
-        ;; flycheck - Syntax checking for GNU Emacs¶
-        ;; https://www.flycheck.org/en/latest/
-        (use-package flycheck
-          :ensure t
-          :init (global-flycheck-mode))
-
-        ;; allow the deletion of selected text (don't know why this isn't implemented by default)
-        (use-package delsel
-          :ensure nil ; no need to install it as it is built-in, but needs to be activated
-          :hook (after-init . delete-selection-mode))
-
-        ; howm mode
-        ; (require 'howm)
-        (use-package howm
-          :ensure t
-          :init
-          ;; Where to store the files?
-          (setq howm-file-name-format "%Y/%m/%Y-%m-%d-%H%M%S.md")
-          (setq howm-view-title-header "#") ; markdown mode!
-          (setq howm-directory "~/Notes")
-          (setq howm-home-directory howm-directory)
-          (setq howm-keyword-file (expand-file-name ".howm-keys" howm-home-directory))
-          (setq howm-history-file (expand-file-name ".howm-history" howm-home-directory))
-
-          ;; Use ripgrep as grep
-          (setq howm-view-use-grep t)
-          (setq howm-view-grep-command "rg")
-          (setq howm-view-grep-option "-nH --no-heading --color never")
-          (setq howm-view-grep-extended-option nil)
-          (setq howm-view-grep-fixed-option "-F")
-          (setq howm-view-grep-expr-option nil)
-          (setq howm-view-grep-file-stdin-option nil))
-
-
-        ;; Rename buffers to their title
-        (add-hook 'howm-mode-hook 'howm-mode-set-buffer-name)
-        (add-hook 'after-save-hook 'howm-mode-set-buffer-name)
-
-        ; OCaml mode
-        (use-package tuareg)
-        (setq tuareg-indent-align-with-first-arg t)
-
-        (defun insert-date ()
-          "Insert today's date at point"
-          (interactive "*")
-          (insert (format-time-string "%F")))
-        (global-set-key (kbd "C-c C-.") #'insert-date)
-
-
-        ;; Configure the Lisp program for SLIME
-        (add-to-list 'exec-path "/Users/emile/.nix-profile/bin")
-        (defvar inferior-lisp-program "sbcl")
-
-        ;; configure parinfer to be enabled as a mode when the major lisp mode is enabled
-        (add-to-list 'load-path "/Users/emile/parinfer-rust")
-        (add-hook 'emacs-lisp-mode 'parinfer-rust-mode)
-        (add-hook 'emacs-lisp-mods (lambda () (lispy-mode 1)))
-
-        ;; erc (emacs irc) settings
-        (use-package erc
-            :config
-            (setopt erc-modules
-                (seq-union '(sals nicks bufbar nickbar scrolltobottom)
-                           etc-modules))
-            (setopt erc-sasl-mechanism 'external)
-
-
-            :custom
-            (erc-prompt-for-nickserv-password nil)
-            (erc-inhibit-multiline-input t)
-            (erc-send-whitespace-lines t)
-            (erc-ask-about-multiline-input t)
-            (erc-server-reconnect-timeout 30)
-            (erc-interactive-display 'buffer)
-
-            (erc-autojoin-timing 'ident)
-            (erc-autojoin-channels-alist '((Libera.Chat "#r2wars")))
-
-            :bind
-            ;; Insert \n when hitting <RET> and send messages using C-c C-c
-            (:map erc-mode-map
-                  ("RET" . nil)
-                  ("C-c C-c" . #'erc-send-current-line)))
-
-        (use-package ultra-scroll
-             ; if you git clone'd instead of package-vc-install
-             ;:load-path "~/code/emacs/ultra-scroll"
-
-             :init
-             (setq scroll-conservatively 101 ; important!
-                 scroll-margin 0) 
-             :config
-                 (ultra-scroll-mode 1))
-        
-        (global-set-key (kbd "C-c e l") (lambda ()
-            (interactive)
-            (if (get-buffer "irc.libera.chat")
-                (erc-track-switch-buffer 1)
-                (when (y-or-n-p "Start ERC? ")
-                  (erc-tls :server "irc.libera.chat"
-                           :port 6697
-                           :nick "hanemile"
-                           :client-certificate
-                             '(,(expand-file-name "~/libera.key")
-                               ,(expand-file-name "~/libera.crt")))))))
-
-        (use-package org-roam
-          :ensure t
-          :custom
-          (org-roam-directory (file-truename "/Users/emile/notes"))
-          :bind (("C-c n l" . org-roam-buffer-toggle)
-                 ("C-c n f" . org-roam-node-find)
-                 ("C-c n g" . org-roam-graph)
-                 ("C-c n i" . org-roam-node-insert)
-                 ("C-c n c" . org-roam-capture)
-                 ;; Dailies
-                 ("C-c n j" . org-roam-dailies-capture-today))
-          :config
-
-          ;; If you're using a vertical completion framework, you might want a more informative completion interface
-          (setq org-roam-node-display-template
-            (concat "${title:*} " (propertize "${tags:10}" 'face 'org-tag)))
-          (org-roam-db-autosync-mode)
-
-          ;; If using org-roam-protocol
-          (require 'org-roam-protocol))
-
-        (provide '.emacs)                       ; makes flycheck happy
-      '';
+      extraConfig = builtins.readFile ./emacs_config.el;
     };
 
     kitty = {
@@ -465,7 +186,7 @@
     nixos-rebuild
 
     # editor
-    unstable.helix
+    unstable-darwin.helix
 
     ## formatter
     nixfmt-rfc-style # official formatter for nix code
@@ -491,6 +212,7 @@
     # go foo
     go
     delve
+    gotools
 
     # c foo
     cmake
@@ -540,6 +262,12 @@
 
     drawio
 
+    # cargo rustup
+    cargo
+
+    # custom
+    libc-database
+
     # blender
 
     # rustdesk
diff --git a/nix/hosts/caladan/nvim_plugins.nix b/nix/hosts/caladan/nvim_plugins.nix
new file mode 100644
index 0000000..7f07816
--- /dev/null
+++ b/nix/hosts/caladan/nvim_plugins.nix
@@ -0,0 +1,21 @@
+{ vimUtils, fetchgit, ... }:
+
+let
+  build = ({name, owner, rev, sha256}: vimUtils.buildVimPlugin {
+    inherit name;
+    src = fetchgit {
+      inherit rev sha256;
+      url = "https://github.com/${owner}/${name}";
+    };
+    dependencies = [];
+  });
+in {
+  lisp = {
+    vlime = build {
+      name = "vlime";
+      owner = "l04m33";
+      rev = "065b95f3ac7a455314c2bdefeb2b792f290034df";
+      sha256 = "1bmmskdwvbl6lvbnjp9lls86rz0vzmk73y644bjb9ix9ygmjbia4";
+    };
+  };
+}
diff --git a/nix/hosts/caladan/overlay.nix b/nix/hosts/caladan/overlay.nix
index c9bdd79..8f3b810 100644
--- a/nix/hosts/caladan/overlay.nix
+++ b/nix/hosts/caladan/overlay.nix
@@ -33,11 +33,6 @@
             rm tools/utils/passwd_test.go
           '';
         });
-
-        # helix-2303 = self.callPackage ../../pkgs/helix-2303 { };
-        # r2 = self.callPackage ../../pkgs/radare2-5.8.4 { };
-        # ansel = self.callPackage ../../pkgs/ansel { };
-        # typst = self.callPackage ../pkgs/radare2-5.8.4 { };
       })
     ];
     config = {
diff --git a/nix/hosts/chusuk/configuration.nix b/nix/hosts/chusuk/configuration.nix
index 5ec116f..364a1c6 100644
--- a/nix/hosts/chusuk/configuration.nix
+++ b/nix/hosts/chusuk/configuration.nix
@@ -116,15 +116,13 @@
   services = {
     openssh.enable = true;
     tailscale.enable = true;
+    displayManager.defaultSession = "none+i3";
     xserver = {
       enable = true;
       desktopManager = {
         xterm.enable = false;
       };
 
-      displayManager = {
-        defaultSession = "none+i3";
-      };
 
       windowManager.i3 = {
         enable = true;
diff --git a/nix/hosts/corrino/configuration.nix b/nix/hosts/corrino/configuration.nix
index eed5076..2f8954b 100644
--- a/nix/hosts/corrino/configuration.nix
+++ b/nix/hosts/corrino/configuration.nix
@@ -22,16 +22,19 @@ in
     ./hardware-configuration.nix
 
     ./ports.nix
+    # ./vm.nix
 
     ./www/git
     ./www/nix-cache
 
+    ./www/goapp.emile.space.nix
+
     # screego
 
     # web
     ./www/emile.space.nix
     ./www/tmp.emile.space.nix
-    # ./www/hydra.emile.space.nix
+    ./www/hydra.emile.space.nix
     ./www/netbox.emile.space.nix
     ./www/stats.emile.space.nix
     # ./www/grafana.emile.space.nix
@@ -41,14 +44,15 @@ in
 
     ./www/photo
 
-    # ./www/tickets.emile.space.nix
-    ./www/talks.emile.space.nix
+    ./www/tickets.emile.space.nix
+    # ./www/talks.emile.space.nix
+    ./www/miniflux.emile.space.nix
     # ./www/stream.emile.space.nix
     ./www/md.emile.space.nix
     ./www/social.emile.space.nix
     ./www/sso.emile.space.nix
     ./www/s3.emile.space.nix
-    ./www/cs.emile.space.nix
+    # ./www/cs.emile.space.nix
     ./www/irc.emile.space.nix
     # ./www/db.emile.space.nix
 
@@ -479,20 +483,22 @@ in
           "kvm"
         ];
       }
-      # {
-      #   hostName = "caladan.pinto-pike.ts.net";
-      #   system = "aarch64-darwin";
-      #   protocol = "ssh-ng";
-      #   maxJobs = 1;
-      #   speedFactor = 2;
-      #   supportedFeatures = [
-      #     "nixos-test"
-      #     "benchmark"
-      #     "big-parallel"
-      #     "kvm"
-      #   ];
-      #   mandatoryFeatures = [ ];
-      # }
+      {
+        hostName = "caladan.pinto-pike.ts.net";
+        sshUser = "hydra";
+        sshKey = "/var/lib/hydra/.ssh/id_ed25519";
+        system = "aarch64-darwin";
+        protocol = "ssh-ng";
+        maxJobs = 1;
+        speedFactor = 2;
+        supportedFeatures = [
+          "nixos-test"
+          "benchmark"
+          "big-parallel"
+          "kvm"
+        ];
+        mandatoryFeatures = [ ];
+      }
     ];
 
     distributedBuilds = true;
@@ -551,13 +557,25 @@ in
     };
 
     "/mnt/storagebox-bx11" = {
-      device = "//u331921.your-storagebox.de/backup";
-      fsType = "cifs";
-      options =
-        let
-          automount_opts = "_netdev,x-systemd.automount,noauto,x-systemd.idle-timeout=60,x-systemd.device-timeout=5s,x-systemd.mount-timeout=5s";
-        in
-        [ "${automount_opts},credentials=${config.age.secrets.storage_box_bx11_password.path}" ];
+      device = "u331921@u331921.your-storagebox.de:/home/backup";
+      fsType = "sshfs";
+      options = [ # Filesystem options
+        "allow_other"          # for non-root access
+        "_netdev"              # this is a network fs
+
+        # We don't mount on demand, as that will cause services like navidrome to fail
+        # as the share doesn't yet exist.
+        #"x-systemd.automount" # mount on demand, rather than boot
+
+        #"debug"               # print debug logging
+                               # warning: this causes the one-shot service to never exit
+
+        # SSH options
+        "StrictHostKeyChecking=no"  # prevent the connection from failing if the host's key hasn't been trusted yet
+        "ServerAliveInterval=15" # keep connections alive
+        "Port=23"
+        "IdentityFile=/root/.ssh/id_ed25519"
+      ];
     };
   };
 
diff --git a/nix/hosts/corrino/ports.nix b/nix/hosts/corrino/ports.nix
index 10ae593..bb64934 100644
--- a/nix/hosts/corrino/ports.nix
+++ b/nix/hosts/corrino/ports.nix
@@ -24,6 +24,7 @@
     restic = 8002;
     nocodb = 8003;
     goatcounter = 8004;
+    goapp = 8005;
     r2wars-web = 8089;
     ctf = 8338;
     magic-hash = 8339;
diff --git a/nix/hosts/corrino/secrets/garage_admin_metrics_secret.age b/nix/hosts/corrino/secrets/garage_admin_metrics_secret.age
new file mode 100644
index 0000000..e1af7da
--- /dev/null
+++ b/nix/hosts/corrino/secrets/garage_admin_metrics_secret.age
@@ -0,0 +1,7 @@
+age-encryption.org/v1
+-> ssh-ed25519 gvwQ2Q 7QkcpYGeeMsbW0GcXzNGPTc0jUf4ydpMiTO6ZxEIKGY
+OOxq2hMORsmUzBuoqOIPNJeLqJB0seve9PhorS6PKNs
+-> ssh-ed25519 m8VklA pF7mWG6tviFC6qD88dxoQRnXGfR0AuanVyY+bh8XgV0
+mrk4HgEs3i8y5P+BSGM1psweXpY/xO+8vK/DsXyhyiY
+--- zqEl/ZN/3jEgMZ/IbPbyTHGZJDDENLOnoQezaACeoSs
+íóõÃ÷ÔþlÉé…”,±ÿ‚W`£…\6éyh†Ž.
ÏéÞoVÐ(€?äEmå¶,Ü;Á(@¬0dVƒ£A=4©v¡É
\ No newline at end of file
diff --git a/nix/hosts/corrino/secrets/garage_admin_token_secret.age b/nix/hosts/corrino/secrets/garage_admin_token_secret.age
new file mode 100644
index 0000000..2a18a6b
--- /dev/null
+++ b/nix/hosts/corrino/secrets/garage_admin_token_secret.age
@@ -0,0 +1,7 @@
+age-encryption.org/v1
+-> ssh-ed25519 gvwQ2Q hcMMVkZSsObrOFjetml2z4eH+EfnuSsna+GaXEeMUA4
+y6lFBj49cMhOGuJBpILHsykpBMpKDHZpFXR4E4zZEbg
+-> ssh-ed25519 m8VklA Z6zLilTWlGWG17Q6jBx13m3KYs3gE93TPLq0CidHeTA
+eqMN5mDMasi/Nw2y5Kgwy2COna+3zbbFTTUrD/O26ls
+--- QdVyqrTLmEcGSB37Ft3Ur0Ry9Jk9DyHFI6fo88tnsgI
+X`w•YšÙ,ç<ˆçùŸA›Á$XeêDGe´	;±àµ†¿Âô>ü1n¥s€ª’‰±äÁNr_Y\µ`¬Á„)ü„F#{çãß´<Œ
\ No newline at end of file
diff --git a/nix/hosts/corrino/secrets/garage_env.age b/nix/hosts/corrino/secrets/garage_env.age
new file mode 100644
index 0000000..becb511
--- /dev/null
+++ b/nix/hosts/corrino/secrets/garage_env.age
Binary files differdiff --git a/nix/hosts/corrino/secrets/garage_rpc_secret.age b/nix/hosts/corrino/secrets/garage_rpc_secret.age
index e228d0d..ce8a65a 100644
--- a/nix/hosts/corrino/secrets/garage_rpc_secret.age
+++ b/nix/hosts/corrino/secrets/garage_rpc_secret.age
Binary files differdiff --git a/nix/hosts/corrino/secrets/goapp_oidc_secret.age b/nix/hosts/corrino/secrets/goapp_oidc_secret.age
new file mode 100644
index 0000000..ca96981
--- /dev/null
+++ b/nix/hosts/corrino/secrets/goapp_oidc_secret.age
@@ -0,0 +1,7 @@
+age-encryption.org/v1
+-> ssh-ed25519 gvwQ2Q Efi2feeWex0ApMR1lvO6osGzW58wiT24zLxcvWlThyc
+/UnBx5j3slzIBoNhbdp9ccdk/p8rFdr/i602sO3abi4
+-> ssh-ed25519 m8VklA o9Y9PUYV9RTcQIu2PLouB9336WdJVhtrEy61UTF62WU
+NQ+VueAUIqP4LGazGz/cOKtDmao4LKRWLj7fT2/0g5c
+--- jjCarc6U6Mxp3VtnS7mElpuJhKjc4fjUTd6dicekLgg
+AÔUÇí.Á#zÌ¥‚‡8wwW®ÑS¢ï¥£Š1)Žû•‰™’R?'}l¨¶
n/TCš‹'>&<b­ï_E¼Æ”ÚÅKÀ¶BBylDý,@ÝÑ(îÆ<¹ý4¤~ÌŒrÀëÇÕ£³
\ No newline at end of file
diff --git a/nix/hosts/corrino/secrets/goapp_oidc_session_key.age b/nix/hosts/corrino/secrets/goapp_oidc_session_key.age
new file mode 100644
index 0000000..938f97f
--- /dev/null
+++ b/nix/hosts/corrino/secrets/goapp_oidc_session_key.age
@@ -0,0 +1,8 @@
+age-encryption.org/v1
+-> ssh-ed25519 gvwQ2Q BAvDdIEUEgmo963+9Wd5VLJgrp3MBISvWR3+27bfJW8
+NOEj2ObYe/cM2CaqzmbgexSEUNZIEo1ZmvlamJaSOjo
+-> ssh-ed25519 m8VklA d/hwd3rGkPD3GDdlOP2XUsi687VH+tfKrAsKnImk+kI
+gFAyyMZT5DK7da7YXOf/5gUd4Bi9cEe3ddMKUMuctMU
+--- 9tVE+AAvptrlMZe5+UGJGzH9usnxa+ZICbikcRT0PYI
+g!X+/šØ0P	;™*{ª«V p!¶êÙ4£sÌöãa	ÎI+À/\Iœ‚Ô–,Äéo½p‡Î웘DP7"ŠíØÀ¦ä;p·„PÀpÍ
+%ÈcÒUÚœ8
\ No newline at end of file
diff --git a/nix/hosts/corrino/secrets/miniflux_admin_file.age b/nix/hosts/corrino/secrets/miniflux_admin_file.age
new file mode 100644
index 0000000..3e00b9b
--- /dev/null
+++ b/nix/hosts/corrino/secrets/miniflux_admin_file.age
@@ -0,0 +1,7 @@
+age-encryption.org/v1
+-> ssh-ed25519 gvwQ2Q OGds4NLmRiMmVjPTORP3jLe3iEkqrDyTqW4V7ceFfRk
+FFdZcsT9ZruNhpY5cb674qpQpK0qzHNwRPCfHvYaKcE
+-> ssh-ed25519 m8VklA 84XSPja8dzJEUVR5olwNONVzNn5QrsX+R+WeBHqxXDo
+5CVpnTDcO0EG3NsHdFsSABWNBIe3Xe16me13JIOlfos
+--- rlIUU/0gYwxIXmpRI5/3mmZXJ+JrG/tE/3IBtpo4uT4
+²Ý-9J¬N™ÌVŸëªG.l‚.eÈ?Á©&¾§rõ;V˜#®ahSJWjh‰ôèv˜Ñ³t;3è¸É‚ϫk Ž(^QU	‹ö*ºcz·ßF°bwPZ¼uqãšÄ‰Û
\ No newline at end of file
diff --git a/nix/hosts/corrino/secrets/miniflux_oidc_secret.age b/nix/hosts/corrino/secrets/miniflux_oidc_secret.age
new file mode 100644
index 0000000..c16754e
--- /dev/null
+++ b/nix/hosts/corrino/secrets/miniflux_oidc_secret.age
Binary files differdiff --git a/nix/hosts/corrino/secrets/tailscale-corrino-cert.age b/nix/hosts/corrino/secrets/tailscale-corrino-cert.age
new file mode 100644
index 0000000..07252cc
--- /dev/null
+++ b/nix/hosts/corrino/secrets/tailscale-corrino-cert.age
Binary files differdiff --git a/nix/hosts/corrino/secrets/tailscale-corrino-key.age b/nix/hosts/corrino/secrets/tailscale-corrino-key.age
new file mode 100644
index 0000000..36c132e
--- /dev/null
+++ b/nix/hosts/corrino/secrets/tailscale-corrino-key.age
@@ -0,0 +1,8 @@
+age-encryption.org/v1
+-> ssh-ed25519 gvwQ2Q P6b4m51AxFbXT3OOkgMe/BPZi3240e/Gii3weyMtPxI
+fRVIno8tPqh4F6e6TOj6YiW2uL9T3uqkro6EZ1mPXOc
+-> ssh-ed25519 m8VklA lRMfdLzmoVybkJJvTlY1lZgkMt1R0wyjA/NFcdRFKDM
+CLyqRXYetMUbsGhL8NRQ333WIy/TnJwhWX8UpxyLmbw
+--- zTjX+CIXtDurBc+TaT7zQ0xn/5Xx3mIrKkAviqMgn4c
+:$Š+DÆ\[Q¬.pS›<H¼QWMîTLŸT¹GxZVßܹ°ßî'‚÷^p®N­DÐG¦”Fêì­*ÃÚ¢Ô:6`ˉ`÷aŸMdžæbN\È£×	’ÅóClHö’7å>i£œ_!´ñ
%nÒóeUVI²4 sA6æú_”kPýäù«¬ÏÑ4-¿¦=æ.dxq…tç+Ú)–4`+üEÿ¥î2ðŽè‘ÇÈùRŒ&¾0__$V}²€¹HoâjܾŽø}¤aB|//Ÿâ¾‹Ð@L#öIK3DtýÿÆ
+µC?õ4Ÿ»÷?u}¼7!û
\ No newline at end of file
diff --git a/nix/hosts/corrino/vm.nix b/nix/hosts/corrino/vm.nix
new file mode 100644
index 0000000..78d818f
--- /dev/null
+++ b/nix/hosts/corrino/vm.nix
@@ -0,0 +1,55 @@
+{  pkgs, ... }:
+
+{
+  services.emile.libvirtnix = {
+    enable = true;
+    instances = {
+
+      vm1 = {
+        domain = {
+          name = "VM1";
+          title = "vm one";
+          description = "The first VM";
+          id = 1;
+
+          uuid = "E34DE478-1402-45BB-B3FD-FC960549258E";
+          genid = "CA1E2462-1E9D-404C-8DDB-19EEF9D9651B";
+
+          os = {
+            nvram = {
+              type = "network";
+              source = {
+                mirror = {
+                  abi = "copy";
+                  ready = "yes";
+                  type = "network";
+                };
+              };
+            };
+          };
+
+          packages = {
+            libvirt = pkgs.libvirt;
+            qemu = pkgs.qemu;
+          };
+          memory = 1024;
+        };
+      };
+
+      vm2 = {
+        domain = {
+          name = "VM2";
+          title = "vm one";
+          description = "The second VM";
+          id = 2;
+
+          uuid = "E34DE478-1402-45BB-B3FD-FC960549258E";
+          genid = "002D0D8F-B21A-4001-92BF-2313707EED9D";
+
+          memory = 2048;
+        };
+      };
+
+    };
+  };
+}
diff --git a/nix/hosts/corrino/www/cs.emile.space.nix b/nix/hosts/corrino/www/cs.emile.space.nix
index d07d9b2..e182f9c 100644
--- a/nix/hosts/corrino/www/cs.emile.space.nix
+++ b/nix/hosts/corrino/www/cs.emile.space.nix
@@ -44,18 +44,17 @@ in
   services.hound = {
     enable = true;
 
-    config = ''
-      			{
-      			  "dbpath": "/var/lib/hound/data",
-      			  "max-concurrent-indexers" : 6,
-      		    "vcs-config" : {
-      	        "git" : {
-                  "detect-ref" : true
-      	        }
-      		    },
-      			  "repos" : ${repos}
-      			}
-      		'';
+    settings = {
+              title = "cs.emile.space";
+      			  dbpath = "/var/lib/hound/data";
+      			  max-concurrent-indexers = 6;
+      		    vcs-config = {
+      	        git = {
+                  detect-ref = true;
+      	        };
+      		    };
+      			  repos = repos;
+      			};
 
     listen = "127.0.0.1:${toString config.emile.ports.hound}";
   };
diff --git a/nix/hosts/corrino/www/goapp.emile.space.nix b/nix/hosts/corrino/www/goapp.emile.space.nix
new file mode 100644
index 0000000..361e95a
--- /dev/null
+++ b/nix/hosts/corrino/www/goapp.emile.space.nix
@@ -0,0 +1,89 @@
+{ config, pkgs, ... }:
+
+{
+  services.nginx.virtualHosts."goapp.emile.space" = {
+    forceSSL = true;
+    enableACME = true;
+
+    locations = {
+      "/" = {
+        proxyPass = "http://${config.services.emile.goapp-frontend.host}:${toString config.services.emile.goapp-frontend.port}";
+      };
+    };
+  };
+
+  services.authelia.instances.main.settings.identity_providers.oidc.clients = [
+    {
+      id = "goapp";
+
+      # ; nix run nixpkgs#authelia -- crypto hash generate pbkdf2 --variant sha512 --random --random.length 72 --random.charset rfc3986
+      secret = "$pbkdf2-sha512$310000$LPXJRoGR9RyTcaT6cADljg$FK8RV5CnKj5ano4fXmRzzvXcX/00F7k/G6nd67t.8iewpwyq8FntV4JgYZSV8AynYMxz1qnL4j3BzITLCM0KgQ";
+      public = false;
+      authorization_policy = "two_factor";
+      redirect_uris = [
+        "https://goapp.emile.space/oauth2/callback"
+      ];
+      scopes = [
+        "openid"
+        "email"
+        "profile"
+        "groups"
+      ];
+      grant_types = [
+        "refresh_token"
+        "authorization_code"
+      ];
+      response_types = [ "code" ];
+      response_modes = [
+        "form_post"
+        "query"
+        "fragment"
+      ];
+      token_endpoint_auth_method = "client_secret_post";
+    }
+  ];
+
+  environment.systemPackages = with pkgs; [ goapp-frontend ];
+
+  # deploy:
+  # - push code
+  # - build in order to get the new hash (nix build .#goapp-frontend-pkg)
+  # - update hash in the package (//nix/templates/goapp/frontent/default.nix)
+  # - deploy
+  #
+  # https://goapp.emile.space/oauth2/callback?code=authelia_ac_iZKCXtRMnj2yjUAmiSkg_LBWjiME2-ghE6KMkxdb6Zw.nDLgCVpu9ctH1llEKUml5rr8szd3bkZYaGa_MAOtNLI&iss=https%3A%2F%2Fsso.emile.space&scope=openid+profile+email+groups&state=random-string-here
+  #
+  # Unable to exchange authorization code for tokens
+  #
+  # unable to exchange authorization code for tokens: oauth2: "invalid_client" "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method)."
+
+  services.emile.goapp-frontend = {
+    enable = true;
+    package = pkgs.goapp-frontend;
+
+    host = "127.0.0.1";
+    port = config.emile.ports.goapp;
+    public-url = "https://goapp.emile.space/";
+
+    oidc = {
+      id = "goapp";
+      issuer = "https://sso.emile.space";
+      cookie-name = "oidc-client";
+      scopes = [
+        "openid"
+        "profile"
+        "email"
+        "groups"
+      ];
+      # secret-path = "/run/goapp-frontend_oidc_secret";
+      secret-path = config.age.secrets.goapp_oidc_secret.path;
+    };
+
+    # TODO(emile): change these when going live
+    session-key-path = config.age.secrets.goapp_oidc_secret.path;
+
+    logfile-path = "/var/log/goapp-frontend.log";
+    database-path = "/var/lib/goapp-frontend/main.db";
+    sessiondb-path = "/var/lib/goapp-frontend/session.db";
+  };
+}
diff --git a/nix/hosts/corrino/www/grafana.emile.space.nix b/nix/hosts/corrino/www/grafana.emile.space.nix
index 22b444f..f8674a2 100644
--- a/nix/hosts/corrino/www/grafana.emile.space.nix
+++ b/nix/hosts/corrino/www/grafana.emile.space.nix
@@ -91,19 +91,30 @@
       provision = {
         dashboards.settings = { };
         datasources.settings = {
+          deleteDatasources = [
+            { name = "Prometheus"; orgId = 1; }  
+            { name = "Lampadas"; orgId = 1; }  
+          ];
           datasources = [
             {
               url = "http://localhost:${toString config.services.prometheus.port}";
               type = "prometheus";
-              name = "Prometheus";
+              name = "Prometheus Corrino";
               editable = false;
               access = "proxy"; # server = "proxy", browser = "direct"
             }
             {
-              name = "loki";
-              url = "http://${config.services.loki.configuration.common.instance_addr}:${toString config.services.loki.configuration.server.http_listen_port}";
-              type = "loki";
+              url = "http://lampadas:9009";
+              type = "prometheus";
+              name = "Prometheus Lampadas";
+              editable = false;
+              access = "proxy"; # server = "proxy", browser = "direct"
             }
+            # {
+            #   name = "loki";
+            #   url = "http://${config.services.loki.configuration.common.instance_addr}:${toString config.services.loki.configuration.server.http_listen_port}";
+            #   type = "loki";
+            # }
           ];
         };
 
diff --git a/nix/hosts/corrino/www/hydra.emile.space.nix b/nix/hosts/corrino/www/hydra.emile.space.nix
index 941ad5b..a5cdb53 100644
--- a/nix/hosts/corrino/www/hydra.emile.space.nix
+++ b/nix/hosts/corrino/www/hydra.emile.space.nix
@@ -32,8 +32,6 @@
     # Directory that holds Hydra garbage collector roots.
     gcRootsDir = "/nix/var/nix/gcroots/hydra";
 
-    # a standalone hydra will require you to unset the buildMachinesFiles list to avoid using a nonexistant /etc/nix/hosts
-    buildMachinesFiles = [ ];
     # you will probably also want, otherwise *everything* will be built from scratch
     useSubstitutes = true;
 
diff --git a/nix/hosts/corrino/www/mc.emile.space.nix b/nix/hosts/corrino/www/mc.emile.space.nix
new file mode 100644
index 0000000..8250a1d
--- /dev/null
+++ b/nix/hosts/corrino/www/mc.emile.space.nix
@@ -0,0 +1,150 @@
+{ config, pkgs, ... }:
+
+{
+  services.minecraft-server = {
+    package = pkgs.minecraft-server;
+    serverProperties = {
+      server-port = 43000;
+
+      # 0 peaceful
+      # 1 easy
+      # 2 normal
+      # 3 hard
+      difficulty = 1;
+
+      # 0 survival
+      # 1 creative
+      # 2 adventure
+      # 5 default
+      # "spectator" spectator
+      # gamemode = "survival";
+      gamemode = 0;
+
+      max-players = 10;
+      motd = "Neurodivergenter Hexenzirkel";
+      enable-rcon = true;
+      "rcon.password" = "hunter2";
+      enable-command-block = false;
+      enable-query = false;
+      spawn-protection = 0;
+
+      white-list = true;
+    };
+    openFirewall = true;
+
+    whitelist = {
+      "emileemail" = "a7614a53-b8b8-47b7-91cf-860e7c7f325f";
+      "dodonator23" = "f93506b6-76e8-437d-927d-dceeb833a33f";
+      "ChaosAyumi" = "223040ec-ca30-4238-8b58-c81597c30426";
+      "xerunala" = "962e41c8-1da8-4592-9a2f-e36cdb20d5a6";
+      "rappet" = "588377a5-362f-4ea1-8195-9cf97dd7a884";
+    };
+
+    jvmOpts = "-Xms4092M -Xmx4092M";
+    eula = true;
+    enable = true;
+    declarative = true;
+    dataDir = "/var/lib/minecraft";
+  };
+
+  services.nginx.virtualHosts."mc.emile.space" = {
+    forceSSL = true;
+    enableACME = true;
+  };
+
+  services.bluemap = {
+    enable = true;
+
+    enableNginx = true;
+    host = "mc.emile.space";
+
+    webappSettings = {
+      enabled = true;
+      webroot = config.services.bluemap.webRoot;
+    };
+
+    # webserverSettings = {};
+    webserverSettings.enabled = false; # using nginx;
+    webRoot = "/var/lib/bluemap/web";
+
+    # coreSettings = {};
+    coreSettings.data = "/var/lib/bluemap";
+    coreSettings.metrics = false; # don't send data to the devs
+
+    storage = {
+      "file" = {
+        root = "${config.services.bluemap.webRoot}/maps";
+      };
+    };
+    # storage.<name>.storage-type
+
+    maps = let
+      worldpath = "/var/lib/minecraft/world";
+    in {
+      "overworld" = {
+        world = "${worldpath}";
+        ambient-light = 0.1;
+        cave-detection-ocean-floor = -5;
+        dimension = "minecraft:overworld";
+      };
+
+      "nether" = {
+        world = "${worldpath}/DIM-1";
+        sorting = 100;
+        sky-color = "#290000";
+        void-color = "#150000";
+        ambient-light = 0.6;
+        world-sky-light = 0;
+        remove-caves-below-y = -10000;
+        cave-detection-ocean-floor = -5;
+        cave-detection-uses-block-light = true;
+        max-y = 90;
+        dimension = "minecraft:the_nether";
+      };
+
+      "end" = {
+        world = "${worldpath}/DIM1";
+        sorting = 200;
+        sky-color = "#080010";
+        void-color = "#080010";
+        ambient-light = 0.6;
+        world-sky-light = 0;
+        remove-caves-below-y = -10000;
+        cave-detection-ocean-floor = -5;
+        dimension = "minecraft:the_end";
+      };
+    };
+
+    # A set of resourcepacks, datapacks, and mods to extract resources from, loaded in alphabetical order.
+    packs = {};
+
+    # How often to trigger rendering the map, in the format of a systemd timer onCalendar configuration. See systemd.timer(5).
+    #
+    # This one means "every three hours":
+    # *-*-* */3:00:00
+    onCalendar = "*-*-* *:00:00";
+
+    eula = true;
+
+    enableRender = true;
+
+    # The world used by the default map ruleset. If you configure your own maps you do not need to set this.
+    # defaultWorld = "${config.services.minecraft.dataDir}/world";
+     
+    addons = {};
+  };
+
+  services.restic.backups."minecraft" = {
+    repository = "/mnt/storagebox-bx11/minecraft";
+    paths = [ "/var/lib/minecraft" ];
+    timerConfig = null;
+    passwordFile = config.age.secrets.restic_password.path;
+    initialize = true;
+    pruneOpts = [
+      "--keep-daily 7"
+      "--keep-weekly 5"
+      "--keep-monthly 12"
+      "--keep-yearly 75"
+    ];
+  };
+}
diff --git a/nix/hosts/corrino/www/md.emile.space.nix b/nix/hosts/corrino/www/md.emile.space.nix
index 6088ea0..d94c06c 100644
--- a/nix/hosts/corrino/www/md.emile.space.nix
+++ b/nix/hosts/corrino/www/md.emile.space.nix
@@ -6,7 +6,7 @@
     enableACME = true;
     locations = {
       "/" = {
-        proxyPass = "http://127.0.0.1:${toString config.services.hedgedoc.settings.port}";
+        proxyPass = "http://[${config.services.hedgedoc.settings.host}]:${toString config.services.hedgedoc.settings.port}";
       };
     };
   };
@@ -14,10 +14,10 @@
   # auth via authelia
   services.authelia.instances.main.settings.identity_providers.oidc.clients = [
     {
-      id = "HedgeDoc";
+      client_id = "HedgeDoc";
 
       # ; nix run nixpkgs#authelia -- crypto hash generate pbkdf2 --variant sha512 --random --random.length 72 --random.charset rfc3986
-      secret = "$pbkdf2-sha512$310000$l4Kyec7Q9oY2GAhWA/xMig$P/MYFmulfgsDNyyiclUzd6le0oSiOvqCIvl4op5DkXtVTxLWlMA3ZwhJ6Z7u.OfIREuEM2htH6asxWPhBhkpNQ";
+      client_secret = "$pbkdf2-sha512$310000$l4Kyec7Q9oY2GAhWA/xMig$P/MYFmulfgsDNyyiclUzd6le0oSiOvqCIvl4op5DkXtVTxLWlMA3ZwhJ6Z7u.OfIREuEM2htH6asxWPhBhkpNQ";
       public = false;
       authorization_policy = "two_factor";
       redirect_uris = [ "https://md.emile.space/auth/oauth2/callback" ];
@@ -47,7 +47,7 @@
     environmentFile = config.age.secrets.hedgedoc_environment_variables.path;
 
     settings = {
-      host = "127.0.0.1";
+      host = "::1";
       port = config.emile.ports.md;
 
       domain = "md.emile.space";
@@ -85,6 +85,20 @@
     };
   };
 
+  services.restic.backups."hedgedoc" = {
+    repository = "/mnt/storagebox-bx11/hedgedoc";
+    paths = [ "/var/lib/hedgedoc" ];
+    timerConfig = null;
+    passwordFile = config.age.secrets.restic_password.path;
+    initialize = true;
+    pruneOpts = [
+      "--keep-daily 7"
+      "--keep-weekly 5"
+      "--keep-monthly 12"
+      "--keep-yearly 75"
+    ];
+  };
+
   # backups
   # services.restic.backups."hedgedoc" = {
   #   user = "u331921";
diff --git a/nix/hosts/corrino/www/miniflux.emile.space.nix b/nix/hosts/corrino/www/miniflux.emile.space.nix
new file mode 100644
index 0000000..f5b9817
--- /dev/null
+++ b/nix/hosts/corrino/www/miniflux.emile.space.nix
@@ -0,0 +1,73 @@
+{ config, pkgs, ... }:
+
+{
+	services.nginx.virtualHosts."miniflux.emile.space" = {
+		forceSSL = true;
+		enableACME = true;
+		locations = {
+			"/" = {
+				proxyPass = "http://${config.services.miniflux.config.LISTEN_ADDR}";
+			};
+		};
+	};
+
+  # auth via authelia
+  services.authelia.instances.main.settings.identity_providers.oidc.clients = [
+    {
+      id = "miniflux";
+
+      # ; nix run nixpkgs#authelia -- crypto hash generate pbkdf2 --variant sha512 --random --random.length 72 --random.charset rfc3986
+      secret = "$pbkdf2-sha512$310000$rlOuqUDGc/kl3bw7JgcSpg$4COyNudsu/7L8qhnxfcQld5Fy.ru/JUp7RCI7dCHZMtzxRnhckW8A7uz3Xeuc7.BjCIwc4GdWusPt6.TiH6Kpw";
+      public = false;
+      authorization_policy = "two_factor";
+      redirect_uris = [ "https://miniflux.emile.space/oauth2/oidc/callback" ];
+      scopes = [
+        "openid"
+        "email"
+        "profile"
+      ];
+      grant_types = [
+        "refresh_token"
+        "authorization_code"
+      ];
+      response_types = [ "code" ];
+      response_modes = [
+        "form_post"
+        "query"
+        "fragment"
+      ];
+      token_endpoint_auth_method = "client_secret_post";
+    }
+  ];
+
+	services.miniflux = {
+		enable = true;
+		package = pkgs.miniflux;
+		config = {
+			BASE_URL = "https://miniflux.emile.space";
+
+			# Cleanup job frequency to remove old sessions and archive entries.
+		  CLEANUP_FREQUENCY = 48;
+
+			# Set to 1 to enable maintenance mode. Maintenance mode disables the web ui and show a text message to the users.
+			# MAINTENANCE_MODE = 1;
+			# MAINTENANCE_MESSAGE = "updating foo";
+			
+			OAUTH2_CLIENT_ID = "miniflux";
+			OAUTH2_CLIENT_SECRET_FILE = config.age.secrets.miniflux_oidc_secret.path;
+			OAUTH2_OIDC_DISCOVERY_ENDPOINT = "sso.emile.space";
+			OAUTH2_OIDC_PROVIDER_NAME = "authelia";
+			OAUTH2_PROVIDER = "oidc";
+			OAUTH2_REDIRECT_URL = "https://miniflux.emile.space/oauth2/oidc/callback";
+			
+		  LISTEN_ADDR = "[::1]:${toString config.emile.ports.miniflux}";
+		};
+		createDatabaseLocally = true;
+
+		# File containing the ADMIN_USERNAME and ADMIN_PASSWORD (length >= 6) in the format of an EnvironmentFile=, as described by systemd.exec(5).
+		adminCredentialsFile = config.age.secrets.miniflux_admin_file.path;
+	};
+	
+
+
+}
diff --git a/nix/hosts/corrino/www/sb.emile.space.nix b/nix/hosts/corrino/www/sb.emile.space.nix
new file mode 100644
index 0000000..0522e25
--- /dev/null
+++ b/nix/hosts/corrino/www/sb.emile.space.nix
@@ -0,0 +1,114 @@
+{ config, pkgs, ... }:
+
+{
+  services.nginx.virtualHosts."sb.emile.space" = {
+    forceSSL = true;
+    enableACME = true;
+    locations = {
+      "/" = {
+        proxyPass = "http://${config.services.silverbullet.listenAddress}:${toString config.services.silverbullet.listenPort}";
+        extraConfig = ''
+          ## Send a subrequest to Authelia to verify if the user is authenticated and has permission to access the resource.
+          auth_request /internal/authelia/authz;
+
+          ## Save the upstream metadata response headers from Authelia to variables.
+          auth_request_set $user $upstream_http_remote_user;
+          auth_request_set $groups $upstream_http_remote_groups;
+          auth_request_set $name $upstream_http_remote_name;
+          auth_request_set $email $upstream_http_remote_email;
+
+          ## Inject the metadata response headers from the variables into the request made to the backend.
+          proxy_set_header Remote-User $user;
+          proxy_set_header Remote-Groups $groups;
+          proxy_set_header Remote-Email $email;
+          proxy_set_header Remote-Name $name;
+
+          ## Configure the redirection when the authz failure occurs. Lines starting with 'Modern Method' and 'Legacy Method'
+          ## should be commented / uncommented as pairs. The modern method uses the session cookies configuration's authelia_url
+          ## value to determine the redirection URL here. It's much simpler and compatible with the mutli-cookie domain easily.
+
+          ## Modern Method: Set the $redirection_url to the Location header of the response to the Authz endpoint.
+          auth_request_set $redirection_url $upstream_http_location;
+
+          ## Modern Method: When there is a 401 response code from the authz endpoint redirect to the $redirection_url.
+          error_page 401 =302 $redirection_url;
+
+          ## Legacy Method: Set $target_url to the original requested URL.
+          ## This requires http_set_misc module, replace 'set_escape_uri' with 'set' if you don't have this module.
+          # set $target_url $scheme://$http_host$request_uri;
+
+          ## Legacy Method: When there is a 401 response code from the authz endpoint redirect to the portal with the 'rd'
+          ## URL parameter set to $target_url. This requires users update 'auth.example.com/' with their external authelia URL.
+          # error_page 401 =302 https://sso.emile.space/?rd=$target_url;
+        '';
+      };
+      "/internal/authelia/authz" = {
+        extraConfig = ''
+          ## Essential Proxy Configuration
+          internal;
+          proxy_pass https://sso.emile.space/api/authz/auth-request;
+
+          ## Headers
+          ## The headers starting with X-* are required.
+          proxy_set_header X-Original-Method $request_method;
+          proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
+          proxy_set_header X-Forwarded-For $remote_addr;
+          proxy_set_header Content-Length "";
+          proxy_set_header Connection "";
+
+          ## Basic Proxy Configuration
+          proxy_pass_request_body off;
+          proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; # Timeout if the real server is dead
+          proxy_redirect http:// $scheme://;
+          proxy_http_version 1.1;
+          proxy_cache_bypass $cookie_session;
+          proxy_no_cache $cookie_session;
+          proxy_buffers 4 32k;
+          client_body_buffer_size 128k;
+
+          ## Advanced Proxy Configuration
+          send_timeout 5m;
+          proxy_read_timeout 240;
+          proxy_send_timeout 240;
+          proxy_connect_timeout 240;
+        '';
+      };
+    };
+  };
+
+  # auth via authelia
+  # services.authelia.instances.main.settings.identity_providers.oidc.clients = [
+  #   {
+  #     id = "silverbullet";
+
+  #     # ; nix run nixpkgs#authelia -- crypto hash generate pbkdf2 --variant sha512 --random --random.length 72 --random.charset rfc3986
+  #     secret = "$pbkdf2-sha512$310000$mxk7uITQOZNYEqeinigQnw$wsF2S6RPL2zVRg1X0bAuINh8Lu5PuA/2/FYJSy3i/Ig5vtCzaIFb0xYEcus4jkqTIgyp3aBxtgSzAKjQKC.QKg";
+  #     public = false;
+  #     authorization_policy = "two_factor";
+  #     redirect_uris = [ "https://md.emile.space/auth/oauth2/callback" ];
+  #     scopes = [
+  #       "openid"
+  #       "email"
+  #       "profile"
+  #     ];
+  #     grant_types = [
+  #       "refresh_token"
+  #       "authorization_code"
+  #     ];
+  #     response_types = [ "code" ];
+  #     response_modes = [
+  #       "form_post"
+  #       "query"
+  #       "fragment"
+  #     ];
+  #     token_endpoint_auth_method = "client_secret_post";
+  #   }
+  # ];
+
+  services.silverbullet = {
+    enable = true;
+    spaceDir = "/var/lib/silverbullet";
+    listenPort = 3000;
+    listenAddress = "[::1]";
+  };
+}
diff --git a/nix/hosts/corrino/www/sso.emile.space.nix b/nix/hosts/corrino/www/sso.emile.space.nix
index 2596bbe..6ffff80 100644
--- a/nix/hosts/corrino/www/sso.emile.space.nix
+++ b/nix/hosts/corrino/www/sso.emile.space.nix
@@ -116,9 +116,9 @@ in
         theme = "dark";
 
         server = {
-          # address = "127.0.0.1:${toString config.emile.ports.authelia}";
-          host = "127.0.0.1";
-          port = config.emile.ports.authelia;
+          address = "127.0.0.1:${toString config.emile.ports.authelia}";
+          # host = "127.0.0.1";
+          # port = config.emile.ports.authelia;
         };
 
         # we're using a file to store the user information
@@ -141,9 +141,22 @@ in
         storage.local.path = "/var/lib/authelia-main/db.sqlite";
 
         session = {
-          domain = "sso.emile.space";
-          expiration = 3600; # 1 hour
-          inactivity = 300; # 5 minutes
+          # domain = "sso.emile.space";
+          # expiration = 3600; # 1 hour
+          # inactivity = 300; # 5 minutes
+
+          cookies = [
+            {
+              domain = "emile.space";
+              authelia_url = "https://sso.emile.space";
+              # The period of time the user can be inactive for until the session is destroyed. Useful if you want long session timers but don’t want unused devices to be vulnerable.
+              inactivity = "1h";
+              # The period of time before the cookie expires and the session is destroyed. This is overridden by remember_me when the remember me box is checked.
+              expiration = "1d";
+              # The period of time before the cookie expires and the session is destroyed when the remember me box is checked. Setting this to -1 disables this feature entirely for this session cookie domain
+              remember_me = "3M";
+            }
+          ];
         };
 
         notifier = {
@@ -196,6 +209,16 @@ in
           default_policy = "deny";
           rules = [
             {
+              # silverbullet needs access to these without auth
+              domain = "sb.emile.space";
+              policy = "bypass";
+              resources = [
+                "/.client/manifest.json$"
+                "/.client/[a-zA-Z0-9_-]+.png$"
+                "/service_worker.js$"
+              ];
+            }
+            {
               domain = "*.emile.space";
               policy = "two_factor";
             }
diff --git a/nix/hosts/hacknix/burpsuitepro/default.nix b/nix/hosts/hacknix/burpsuitepro/default.nix
index 8c9605d..a2b76b7 100644
--- a/nix/hosts/hacknix/burpsuitepro/default.nix
+++ b/nix/hosts/hacknix/burpsuitepro/default.nix
@@ -19,7 +19,7 @@ pkgs.stdenvNoCC.mkDerivation rec {
     eval "$(${pkgs.unzip}/bin/unzip -p ${src} chromium.properties)"
     mkdir -p "$HOME/.BurpSuite/burpbrowser/$linux64"
     ln -sf "${pkgs.chromium}/bin/chromium" "$HOME/.BurpSuite/burpbrowser/$linux64/chrome"
-    exec ${pkgs.jdk19}/bin/java -jar ${src} "$@"' > $out/bin/burpsuitepro
+    exec ${pkgs.jdk23}/bin/java -jar ${src} "$@"' > $out/bin/burpsuitepro
     chmod +x $out/bin/${pname}
     runHook postInstall
   '';
@@ -38,7 +38,7 @@ pkgs.stdenvNoCC.mkDerivation rec {
     downloadPage = "https://portswigger.net/burp/freedownload";
     sourceProvenance = with sourceTypes; [ binaryBytecode ];
     #license = licenses.unfree;
-    platforms = pkgs.jdk19.meta.platforms;
+    platforms = pkgs.jdk23.meta.platforms;
     hydraPlatforms = [ ];
     maintainers = with maintainers; [ hanemile ];
   };
diff --git a/nix/hosts/hacknix/configuration.nix b/nix/hosts/hacknix/configuration.nix
index 388626c..85a93af 100644
--- a/nix/hosts/hacknix/configuration.nix
+++ b/nix/hosts/hacknix/configuration.nix
@@ -47,12 +47,15 @@ in
 
   services = {
     dbus.enable = true;
+
+    displayManager.defaultSession = "xfce+i3";
+
     xserver = {
       enable = true;
 
       # Keyboard settings
-      layout = "us";
-      xkbOptions = "caps:compose";
+      xkb.layout = "us";
+      xkb.options = "caps:compose";
 
       desktopManager = {
         xterm.enable = false;
@@ -68,7 +71,6 @@ in
 
       # default display manager when logging in
       displayManager = {
-        defaultSession = "xfce+i3";
         sessionCommands = '''';
       };
 
@@ -92,7 +94,7 @@ in
   # Enable sound.
   # sound.enable = true;
   # hardware.pulseaudio.enable = true;
-  hardware.opengl.enable = true;
+  hardware.graphics.enable = true;
 
   # Enable touchpad support (enabled default in most desktopManager).
   # services.xserver.libinput.enable = true;
@@ -252,7 +254,10 @@ in
   # started in user sessions.
   programs = {
 
-    vim.defaultEditor = true;
+    vim = {
+      enable = true;
+      defaultEditor = true;
+    };
 
     htop = {
       enable = true;
diff --git a/nix/hosts/lampadas/configuration.nix b/nix/hosts/lampadas/configuration.nix
index cc829d8..007f8a1 100644
--- a/nix/hosts/lampadas/configuration.nix
+++ b/nix/hosts/lampadas/configuration.nix
@@ -2,7 +2,7 @@
 # your system. Help is available in the configuration.nix(5) man page, on
 # https://search.nixos.org/options and in the NixOS manual (`nixos-help`).
 
-{ pkgs, lib, ... }:
+{ config, pkgs, lib, ... }:
 
 let
   emile_keys = [
@@ -17,12 +17,34 @@ in
     ./hardware-configuration.nix
   ];
 
+  hardware.fancontrol = {
+    enable = true;
+    config = ''
+      # Configuration file generated by pwmconfig, changes will be lost
+      INTERVAL=10
+      DEVPATH=hwmon0=devices/platform/coretemp.0 hwmon1=devices/platform/nct6775.672
+      DEVNAME=hwmon0=coretemp hwmon1=nct6798
+      FCTEMPS=hwmon1/pwm3=hwmon0/temp2_input hwmon1/pwm2=hwmon1/temp2_input
+      FCFANS=hwmon1/pwm3=hwmon1/fan3_input hwmon1/pwm2=hwmon1/fan2_input
+      MINTEMP=hwmon1/pwm3=35 hwmon1/pwm2=35
+      MAXTEMP=hwmon1/pwm3=75 hwmon1/pwm2=75
+      MINSTART=hwmon1/pwm3=255 hwmon1/pwm2=255
+      MINSTOP=hwmon1/pwm3=30 hwmon1/pwm2=30
+      MINPWM=hwmon1/pwm3=30 hwmon1/pwm2=30
+      MAXPWM=hwmon1/pwm3=255 hwmon1/pwm2=255
+    '';
+  };
+
   boot = {
     loader = {
       systemd-boot.enable = true;
       efi.canTouchEfiVariables = true;
     };
     kernelParams = [ "ip=dhcp" ];
+    kernelModules = [
+      # fan speed modules, detected using `sensors-detect`
+      "coretemp" "nct6775"
+    ];
     initrd = {
       availableKernelModules = [ "r8169" ];
       systemd.users.root.shell = "/bin/cryptsetup-askpass";
@@ -159,10 +181,41 @@ in
     };
 
     # metric exporters
-    prometheus.exporters = {
-      node.enable = true; # port 9100
-      systemd.enable = true; # port 9558
-      smartctl.enable = true; # port 9633
+    prometheus = {
+      enable = true;
+      port = 9090;
+      listenAddress = "100.87.209.97";
+      scrapeConfigs = [
+        {
+          job_name = "node";
+          static_configs = [{
+            targets = [
+              "localhost:${toString config.services.prometheus.exporters.node.port}"
+            ];
+          }];
+        }  
+        {
+          job_name = "systemd";
+          static_configs = [{
+            targets = [
+              "localhost:${toString config.services.prometheus.exporters.systemd.port}"
+            ];
+          }];
+        }  
+        {
+          job_name = "smartctl";
+          static_configs = [{
+            targets = [
+              "localhost:${toString config.services.prometheus.exporters.smartctl.port}"
+            ];
+          }];
+        }  
+      ];
+      exporters = {
+        node.enable = true; # port 9100
+        systemd.enable = true; # port 9558
+        smartctl.enable = true; # port 9633
+      };
     };
 
     # shares
diff --git a/nix/hosts/lernaeus/configuration.nix b/nix/hosts/lernaeus/configuration.nix
index 57cacb9..43297a2 100644
--- a/nix/hosts/lernaeus/configuration.nix
+++ b/nix/hosts/lernaeus/configuration.nix
@@ -20,13 +20,6 @@ in
     ./hardware-configuration.nix
   ];
 
-  ##################
-  # sound
-
-  sound.enable = true;
-
-  hardware.pulseaudio.enable = true;
-  hardware.pulseaudio.support32Bit = true;    ## If compatibility with 32-bit applications is desired.
 
   users.extraUsers.emile.extraGroups = [ "audio" ];
 
@@ -39,11 +32,8 @@ in
   };
 
   # Enable OpenGL
-  hardware.opengl = {
-    enable = true;
-    driSupport = true;
-    driSupport32Bit = true;
-  };
+  hardware.graphics.enable = true;
+  hardware.graphics.enable32Bit = true;
 
   # Load nvidia driver for Xorg and Wayland
   services.xserver.videoDrivers = [ "nvidia" ];
diff --git a/nix/hosts/mail/mail.nix b/nix/hosts/mail/mail.nix
index 9c15e66..2dbfc35 100644
--- a/nix/hosts/mail/mail.nix
+++ b/nix/hosts/mail/mail.nix
@@ -1,17 +1,27 @@
-{ ... }:
+{ config, pkgs, ... }:
 let
-  release = "nixos-23.05";
-in
-{
+  #release = "nixos-21.11";
+  # release = "nixos-23.05";
+  # release = "nixos-24.05";
+  release = "nixos-24.11";
+in {
   imports = [
     (builtins.fetchTarball {
       # Pick a commit from the branch you are interested in
       url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz";
       # And set its hash
-      sha256 = "1ngil2shzkf61qxiqw11awyl81cr7ks2kv3r3k243zz7v2xakm5c";
+      #sha256 = "1i56llz037x416bw698v8j6arvv622qc0vsycd20lx3yx8n77n44";
+      #sha256 = "1ngil2shzkf61qxiqw11awyl81cr7ks2kv3r3k243zz7v2xakm5c";
+      #sha256 = "0000000000000000000000000000000000000000000000000000";
+      sha256 = "05k4nj2cqz1c5zgqa0c6b8sp3807ps385qca74fgs6cdc415y3qw";
     })
   ];
 
+  # temporary fix for the issue linked below that showed up after updating to
+  # nixos-24.05 and the nixos-24.05 release
+  # https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues/275
+  services.dovecot2.sieve.extensions = [ "fileinto" ];
+
   mailserver = {
     enable = true;
     fqdn = "mail.emile.space";
@@ -20,10 +30,10 @@ in
     # A list of all login accounts. To create the password hashes, use
     # nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
     loginAccounts = {
-      "mail@emile.space" = {
-        hashedPasswordFile = "/etc/nixos/keys/mail";
-        aliases = [ "@emile.space" ];
-      };
+        "mail@emile.space" = {
+            hashedPasswordFile = "/etc/nixos/keys/mail";
+            aliases = ["@emile.space"];
+        };
     };
 
     localDnsResolver = false;
@@ -33,12 +43,12 @@ in
     #certificateScheme = 3;
     certificateScheme = "acme-nginx";
 
+
     # Enable IMAP and POP3
     enableImap = true;
     enablePop3 = true;
     enableSubmission = true;
 
-    # as well with ssl
     enableImapSsl = true;
     enablePop3Ssl = true;
     enableSubmissionSsl = true;
@@ -46,6 +56,5 @@ in
     enableManageSieve = true;
 
     virusScanning = false;
-
   };
 }
diff --git a/nix/lib/flake-helper.nix b/nix/lib/flake-helper.nix
index ddace97..f841fa5 100644
--- a/nix/lib/flake-helper.nix
+++ b/nix/lib/flake-helper.nix
@@ -83,8 +83,6 @@ rec {
             { ... }:
             {
               nixpkgs.overlays = [
-                self.overlays.emile
-
                 (
                   if system == "x86_64-linux" then
                     self.overlays.x86_64-linux
@@ -94,6 +92,11 @@ rec {
                     null
                 )
 
+                # no clue why, but when rebuilding corrino and this not being commented,
+                # something in the hardware.bluetooth module breaks
+                #
+                # (if system == "aarch64-darwin" then self.overlays.unstable-darwin else null)
+
                 (_: _: { inherit (agenix.packages."x86_64-linux") agenix; })
 
                 (_: _: {
@@ -197,8 +200,12 @@ rec {
         user = "root"; # user to install as
         sshUser = sshUser; # user to ssh to as
 
-        # make sure people can use sudo 
-        sshOpts = ["-A" "-t" "-S"];
+        # make sure people can use sudo
+        sshOpts = [
+          "-A"
+          "-t"
+          "-S"
+        ];
         # sshOpts = [ "-o"  "ProxyCommand=none" ];
 
         # make sure to add the nix foo on the darwin hosts to ~/.zshenv
diff --git a/nix/modules/goapp-frontend/default.nix b/nix/modules/goapp-frontend/default.nix
new file mode 100644
index 0000000..c5f62aa
--- /dev/null
+++ b/nix/modules/goapp-frontend/default.nix
@@ -0,0 +1,134 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+
+let
+  cfg = config.services.emile.goapp-frontend;
+in
+with lib;
+{
+  options.services.emile.goapp-frontend = {
+    enable = mkEnableOption "Enable goapp-frontend";
+    package = mkPackageOption pkgs "goapp-frontend" { };
+
+    # ip, port and external host to listen on
+    host = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      example = "0.0.0.0";
+      description = "The host the service listens on";
+    };
+    port = mkOption {
+      type = types.int;
+      default = 8080;
+      example = 8080;
+      description = "The port the service listens on";
+    };
+    public-url = mkOption {
+      type = types.str;
+      default = "http://localhost:8080/";
+      example = "https://goapp.emile.space/";
+      description = ''
+        The domain that the service can be reached from externally. This is used by oidc for redirects and thus should be set, as you'll probably be running this behind some kind of reverse proxy.
+      '';
+    };
+
+    # the oidc config
+    oidc = mkOption {
+      type = types.submodule {
+        options = {
+          id = mkOption {
+            type = types.str;
+            default = "";
+            example = "AiliavahweiweeG5";
+            description = "The oidc id";
+          };
+          issuer = mkOption {
+            type = types.str;
+            default = "";
+            example = "https://sso.emile.space";
+            description = "The oidc identity provider";
+          };
+          cookie-name = mkOption {
+            type = types.str;
+            default = "oidc-client";
+            example = "CookieMcCookieface";
+            description = "The oidc cookie name";
+          };
+          scopes = mkOption {
+            type = types.listOf types.str;
+            default = [ "openid" "profile" "email" "groups" ];
+            example = [ "openid" "profile" "email" ];
+            description = "The openid scopes to request";
+          };
+          secret-path = mkOption {
+            type = types.str;
+            default = "";
+            example = "/run/goapp_oidc_secret";
+            description = "The path to the oidc secret";
+          };
+        };
+      };
+    };
+     
+    # paths to files
+    session-key-path = mkOption {
+      type = types.str;
+      default = "";
+      example = "/run/sesionkey";
+      description = "The path to a file containing the sessionKey";
+    };
+    logfile-path = mkOption {
+      type = types.str;
+      default = "/var/log/goapp-frontend.log";
+      example = "/var/log/goapp-frontend.log";
+      description = "The path to where the logfile should be written";
+    };
+
+    database-path = mkOption {
+      type = types.str;
+      default = "/var/lib/goapp-frontend/main.db";
+      example = "/var/lib/goapp-frontend/main.db";
+      description = "The path to the main database";
+    };
+    sessiondb-path = mkOption {
+      type = types.str;
+      default = "/var/lib/goapp-frontend/sessions.db";
+      example = "/var/lib/goapp-frontend/sessions.db";
+      description = "The path to the sessions database";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.goapp-frontend = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        RestartSec = 5;
+        Restart = "on-failure";
+      };
+      environment = {
+        VERSION = pkgs.goapp-frontend.version;
+      };
+      path = [ pkgs.goapp-frontend ];
+      serviceConfig.ExecStart = ''
+        ${pkgs.goapp-frontend}/bin/frontend \
+          --host ${cfg.host} \
+          --port ${toString cfg.port} \
+          --public-url ${cfg.public-url} \
+          --id ${cfg.oidc.id} \
+          --issuer ${cfg.oidc.issuer} \
+          --cookie-name ${cfg.oidc.cookie-name} \
+          --scopes ${concatStringsSep "," cfg.oidc.scopes} \
+          --oidc-secret-path ${cfg.oidc.secret-path} \
+          --logfilepath ${cfg.logfile-path} \
+          --databasepath ${cfg.database-path} \
+          --sessiondbpath ${cfg.sessiondb-path} \
+          --sessionkeypath ${cfg.session-key-path} \
+          --templatespath ${pkgs.goapp-frontend}/templates
+      '';
+    };
+  };
+}
diff --git a/nix/modules/libvirtnix/config.nix b/nix/modules/libvirtnix/config.nix
new file mode 100644
index 0000000..e1a8d28
--- /dev/null
+++ b/nix/modules/libvirtnix/config.nix
@@ -0,0 +1,59 @@
+{
+  services.emile.libvirtnix = {
+    enable = true;
+    vm = {
+      "alan" = {
+        vm_type = "kvm";
+        id = "1337";
+        name = "blub";
+        uuid = "cafebabe-d474-452b-80f4-c951c39bcf74";
+
+        metadata.libosinfo = "https://libosinfo.org/xmlns/libvirt/domain/1.0";
+        metadata.libosinfo_os = "https://nixos.org/nixos/unknown";
+
+        memory.unit = "KiB";
+        memory.value = 2097152;
+
+        currentMemory.unit = "KiB";
+        currentMemory.value = 2097152;
+
+        vcpu.placement = "static";
+        vcpu.count = 3;
+
+        resource.partition = "/machine";
+
+        os = {
+          type = {
+            arch = "x86_64";
+            machine = "pc-q35-3.1";
+            value = "hvm";
+          };
+
+          loader = {
+            readonly = "yes";
+            type = "pflash";
+            value = "/usr/share/OVMF/OVMF_CODE.fd";
+          };
+
+          nvram.value = "/var/lib/libvirt/qemu/nvram/fileserver2_VARS.fd";
+
+          boot.dev = "hd";
+        };
+
+        features = {
+          acpi = true;
+          apic = true;
+          vmport = {
+            state = "off";
+          };
+        };
+
+        cpu = {
+          mode = "host-passthrough";
+          check = "none";
+          migratable = "on";
+        };
+      };
+    };
+  };
+}
diff --git a/nix/modules/libvirtnix/default.nix b/nix/modules/libvirtnix/default.nix
new file mode 100644
index 0000000..a998966
--- /dev/null
+++ b/nix/modules/libvirtnix/default.nix
@@ -0,0 +1,109 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+
+let
+  cfg = config.services.emile.libvirtnix;
+in
+{
+  options.services.emile.libvirtnix = with lib; {
+    enable = mkEnableOption "enable libvirtnix";
+    instances = mkOption {
+      type = types.attrsOf (
+        types.submodule (
+          { name, ... }:
+          {
+            options = {
+              enable = mkEnableOption "enable this instance";
+              domain = mkOption {
+                type = types.submodule (import ./domain.nix);
+              };
+            };
+          }
+        )
+      );
+      default = { };
+      description = ''
+        A full libvirt config, statically defined using nix.
+      '';
+      example = ''
+        {
+          domain = {
+            name = "vm";
+          };
+        }
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services = lib.mapAttrs' (
+      name: guest:
+      lib.nameValuePair "libvirtd-guest-${name}" {
+        after = [ "libvirtd.service" ];
+        requires = [ "libvirtd.service" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          Type = "oneshot";
+          RemainAfterExit = "yes";
+        };
+        script =
+          let
+            xml =
+              pkgs.writeText "libvirt-guest-${name}.xml" ''<domain ''
+              + (lib.optionalString (guest.domain.type != "") "type='${guest.domain.type}' ")
+              + (lib.optionalString (guest.domain.id != 0) "id='${guest.domain.type}'")
+              + ''>''
+              + (lib.optionalString (guest.domain.name != "") "<name>${guest.domain.name}</name>")
+              + (lib.optionalString (guest.domain.name != "") "<name>${guest.domain.name}</name>")
+              + (lib.optionalString (guest.domain.uuid != "") "<uuid>${guest.domain.uuid}</uuid>")
+              + (lib.optionalString (guest.domain.genid != "") "<genid>${guest.domain.genid}</genid>")
+              + (lib.optionalString (guest.domain.title != "") "<title>${guest.domain.title}</title>")
+              + (lib.optionalString (
+                guest.domain.description != ""
+              ) "<description>${guest.domain.description}</description>")
+              + (lib.optionalString (guest.domain.metadata != "") "${guest.domain.metadata}")
+              + ''
+                  <os>
+                    <type>hvm</type>
+                  </os>
+                  <memory unit="GiB">${toString guest.domain.memory}</memory>
+                  <devices>
+                    <disk type="volume">
+                      <source volume="guest-${name}"/>
+                      <target dev="vda" bus="virtio"/>
+                    </disk>
+                    <graphics type="spice" autoport="yes"/>
+                    <input type="keyboard" bus="usb"/>
+                  </devices>
+                  <features>
+                    <acpi/>
+                  </features>
+                </domain>
+              '';
+          in
+          ''
+            uuid="$(${pkgs.libvirt}/bin/virsh domuuid '${name}' || true)"
+            ${pkgs.libvirt}/bin/virsh define <(sed "s/UUID/$uuid/" '${xml}')
+            ${pkgs.libvirt}/bin/virsh start '${name}'
+          '';
+        preStop = ''
+          ${pkgs.libvirt}/bin/virsh shutdown '${name}'
+          let "timeout = $(date +%s) + 10"
+          while [ "$(${pkgs.libvirt}/bin/virsh list --name | grep --count '^${name}$')" -gt 0 ]; do
+            if [ "$(date +%s)" -ge "$timeout" ]; then
+              # Meh, we warned it...
+              ${pkgs.libvirt}/bin/virsh destroy '${name}'
+            else
+              # The machine is still running, let's give it some time to shut down
+              sleep 0.5
+            fi
+          done
+        '';
+      }
+    ) config.services.emile.libvirtnix.instances;
+  };
+}
diff --git a/nix/modules/libvirtnix/domain.nix b/nix/modules/libvirtnix/domain.nix
new file mode 100644
index 0000000..22cd891
--- /dev/null
+++ b/nix/modules/libvirtnix/domain.nix
@@ -0,0 +1,483 @@
+{ config, lib, ... }:
+
+let
+  mkTag = import ./xml.nix { inherit lib; };
+  pkgs = import <nixpkgs> { };
+
+  stringOption =
+    {
+      default ? "",
+    }:
+    lib.mkOption {
+      type = lib.types.str;
+      default = "${default}";
+    };
+in
+{
+  options = {
+    services.emile.libvirtnix =
+      let
+        cpu = lib.mkOption {
+          type = lib.types.submodule {
+            options = {
+              mode = lib.mkOption {
+                type = lib.types.str;
+                example = "host-passthrough";
+                default = "";
+              };
+              check = lib.mkOption {
+                type = lib.types.str;
+                example = "none";
+                default = "";
+              };
+              migratable = lib.mkOption {
+                type = lib.types.str;
+                example = "on";
+                default = "";
+              };
+            };
+          };
+        };
+
+        features = lib.mkOption {
+          type = lib.types.submodule {
+            options = {
+              acpi = lib.mkEnableOption "enable acpi";
+              apic = lib.mkEnableOption "enable apic";
+
+              vmport = lib.mkOption {
+                type = lib.types.submodule {
+                  options = {
+                    state = lib.mkOption {
+                      type = lib.types.str;
+                      example = "off";
+                      default = "";
+                    };
+                  };
+                };
+              };
+            };
+          };
+        };
+      
+        os_boot = lib.mkOption {
+          type = lib.types.submodule {
+            options = {
+              dev = lib.mkOption {
+                type = lib.types.str;
+                example = "hd";
+                default = "";
+              };
+            };
+          };
+        };
+      
+        os_nvram = lib.mkOption {
+          type = lib.types.submodule {
+            options = {
+              value = lib.mkOption {
+                type = lib.types.str;
+                example = "/var/lib/libvirt/qemu/nvram/fileserver2_VARS.fd";
+                default = "";
+              };
+            };
+          };
+        };
+      
+        os_loader = lib.mkOption {
+          type = lib.types.submodule {
+            options = {
+              readonly = lib.mkOption {
+                type = lib.types.str;
+                example = "yes";
+                default = "";
+              };
+              type = lib.mkOption {
+                type = lib.types.str;
+                example = "pflash";
+                default = "";
+              };
+              value = lib.mkOption {
+                type = lib.types.str;
+                example = "/usr/share/OVMF/OVMF_CODE.fd";
+                default = "";
+              };
+            };
+          };
+        };
+      
+        os_type = lib.mkOption {
+          type = lib.types.submodule {
+            options = {
+              arch = lib.mkOption {
+                type = lib.types.str;
+                example = "x86_64";
+                default = "";
+              };
+              machine = lib.mkOption {
+                type = lib.types.str;
+                example = "pc-q35-3.1";
+                default = "";
+              };
+              value = lib.mkOption {
+                type = lib.types.str;
+                example = "hvm";
+                default = "";
+              };
+            };
+          };
+        };
+
+        os = lib.mkOption {
+          description = "os";
+          type = lib.types.submodule {
+            options = {
+              type = os_type;
+              loader = os_loader;
+              nvram = os_nvram;
+              boot = os_boot;
+            };
+          };
+        };
+
+        resource = lib.mkOption {
+          description = "resource";
+          type = lib.types.submodule {
+            options = {
+              partition = lib.mkOption {
+                type = lib.types.str;
+                example = "/machine";
+                default = "";
+              };
+            };
+          };
+        };
+
+        vcpu = lib.mkOption {
+          description = "vcpu";
+          type = lib.types.submodule {
+            options = {
+              placement = lib.mkOption {
+                # TODO(emile): fill up
+                type = lib.types.enum [ "static" ];
+                example = "static";
+                default = "static";
+              };
+              count = lib.mkOption {
+                type = lib.types.int;
+                example = 4;
+                default = 1;
+              };
+            };
+          };
+        };
+
+        mem = lib.mkOption {
+          description = "memory";
+          type = lib.types.submodule {
+            options = {
+              unit = lib.mkOption {
+                # TODO(emile): fill up
+                type = lib.types.enum [
+                  "KiB"
+                  "MiB"
+                  "GiB"
+                  "TiB"
+                ];
+                example = "KiB";
+                default = "KiB";
+              };
+              value = lib.mkOption {
+                type = lib.types.int;
+                example = 2097152;
+                default = 1000;
+              };
+            };
+          };
+        };
+
+        memory = mem;
+        currentMemory = mem;
+
+        metadata = lib.mkOption {
+          description = "metadata submodule";
+          type = lib.types.submodule {
+            options = {
+              libosinfo = lib.mkOption {
+                type = lib.types.str;
+                example = "https://libosinfo.org/xmlns/libvirt/domain/1.0";
+                default = "";
+              };
+              libosinfo_os = lib.mkOption {
+                type = lib.types.str;
+                example = "https://nixos.org/nixos/unknown";
+                default = "";
+              };
+            };
+          };
+        };
+
+        vm = lib.types.submodule {
+          options = {
+            vm_type = stringOption { default = "kvm"; };
+            id = stringOption { };
+            name = stringOption { };
+            uuid = stringOption { };
+
+            inherit
+              metadata
+              memory
+              currentMemory
+              vcpu
+              resource
+              os
+              features
+              cpu
+              ;
+          };
+        };
+      in
+      {
+        enable = lib.mkEnableOption "Enable r2wars-web";
+      
+
+        # Temporary output we write the domain to, in order to inspect the correctness of
+        # the generated xml
+        output.domain = lib.mkOption { type = lib.types.path; };
+
+        # An attrset of VMs
+        vm = lib.mkOption {
+          description = "VMs to define";
+          type = lib.types.attrsOf vm;
+        };
+      };
+  };
+
+  config = lib.mkIf config.services.emile.libvirtnix.enable {
+    services.emile.libvirtnix =
+      let
+        vm = config.services.emile.libvirtnix.vm;
+
+        cpu =
+          vm_name:
+          mkTag {
+            name = "cpu";
+            args = let
+              mode = vm.${vm_name}.cpu.mode;
+              check = vm.${vm_name}.cpu.check;
+              migratable= vm.${vm_name}.cpu.migratable;
+            in [
+              (if (mode != null)
+               then {key = "mode"; val = mode;}
+               else "")
+              (if (check != null)
+               then {key = "check"; val = check;}
+               else "")
+              (if (migratable != null)
+               then {key = "migratable"; val = migratable;}
+               else "")
+            ];
+            closing = false;
+          };
+        
+        features =
+          vm_name:
+          mkTag {
+            name = "features";
+            children = let
+              acpi = vm_name: mkTag {
+                name = "acpi";
+                closing = false;
+              };
+              apic = vm_name: mkTag {
+                name = "apic";
+                closing = false;
+              };
+              vmport = vm_name: mkTag {
+                name = "vmport";
+                args = [
+                  { key = "state"; val = vm.${vm_name}.features.vmport.state; }
+                ];
+                closing = false;
+              };
+            in [
+              (if (vm.${vm_name}.features.acpi != false) then (acpi vm_name) else "")
+              (if (vm.${vm_name}.features.apic != false) then (apic vm_name) else "")
+              (if (vm.${vm_name}.features.vmport != null) then (vmport vm_name) else "")
+            ];
+          };
+
+        os =
+          vm_name:
+          mkTag {
+            name = "os";
+            children = let
+              os_type = vm_name: mkTag {
+                name = "type";
+                args = [
+                  { key = "arch"; val = vm.${vm_name}.os.type.arch; }
+                  { key = "machine"; val = vm.${vm_name}.os.type.machine; }
+                ];
+                value = vm.${vm_name}.os.type.value;
+              };
+              os_loader = vm_name: mkTag {
+                name = "loader";
+                args = [
+                  { key = "readonly"; val = vm.${vm_name}.os.loader.readonly; }
+                  { key = "pflash"; val = vm.${vm_name}.os.loader.type; }
+                ];
+                value = vm.${vm_name}.os.loader.value;
+              };
+              os_nvram = vm_name: mkTag {
+                name = "nvram";
+                value = vm.${vm_name}.os.nvram.value;
+              };
+              os_boot = vm_name: mkTag {
+                name = "type";
+                args = [
+                  { key = "dev"; val = vm.${vm_name}.os.boot.dev; }
+                ];
+                closing = false;
+              };
+            in [
+              (os_type vm_name)
+              (os_loader vm_name)
+              (os_nvram vm_name)
+              (os_boot vm_name)
+            ];
+          };
+
+        resource =
+          vm_name:
+          mkTag {
+            name = "resource";
+            children = [
+              (mkTag {
+                name = "partition";
+                value = "${toString vm.${vm_name}.resource.partition}";
+              })
+            ];
+          };
+
+        vcpu =
+          vm_name:
+          mkTag {
+            name = "vcpu";
+            args = [
+              {
+                key = "placement";
+                val = vm.${vm_name}.vcpu.placement;
+              }
+            ];
+            value = "${toString vm.${vm_name}.vcpu.count}";
+          };
+
+        currentMemory =
+          vm_name:
+          mkTag {
+            name = "currentMemory";
+            args = [
+              {
+                key = "unit";
+                val = vm.${vm_name}.currentMemory.unit;
+              }
+            ];
+            value = "${toString vm.${vm_name}.currentMemory.value}";
+          };
+        memory =
+          vm_name:
+          mkTag {
+            name = "memory";
+            args = [
+              {
+                key = "unit";
+                val = vm.${vm_name}.memory.unit;
+              }
+            ];
+            value = "${toString vm.${vm_name}.memory.value}";
+          };
+
+        libosinfo_os =
+          vm_name:
+          mkTag {
+            name = "libosinfo:os";
+            args = [
+              {
+                key = "id";
+                val = vm.${vm_name}.metadata.libosinfo_os;
+              }
+            ];
+            closing = false;
+          };
+
+        libosinfo_libosinfo =
+          vm_name:
+          mkTag {
+            name = "libosinfo:libosinfo";
+            args = [
+              {
+                key = "xmlns:libosinfo";
+                val = vm.${vm_name}.metadata.libosinfo;
+              }
+            ];
+            children = [
+              (libosinfo_os vm_name)
+            ];
+          };
+
+        metadata =
+          vm_name:
+          mkTag {
+            name = "metadata";
+            children = [ (libosinfo_libosinfo vm_name) ];
+          };
+
+        uuid =
+          vm_name:
+          mkTag {
+            name = "uuid";
+            value = vm.${vm_name}.uuid;
+          };
+
+        name =
+          vm_name:
+          mkTag {
+            name = "name";
+            value = vm.${vm_name}.name;
+          };
+
+        # the vm_name has to be passed in, as we want to accesses the value for the given vm when
+        # generating the config
+        domain =
+          vm_name:
+          mkTag {
+            name = "domain";
+            args = [
+              {
+                key = "type";
+                val = vm.${vm_name}.vm_type;
+              }
+              {
+                key = "id";
+                val = vm.${vm_name}.id;
+              }
+            ];
+            children = [
+              (name vm_name)
+              (uuid vm_name)
+              (metadata vm_name)
+              (memory vm_name)
+              (currentMemory vm_name)
+              (vcpu vm_name)
+              (resource vm_name)
+              (os vm_name)
+              (features vm_name)
+              (cpu vm_name)
+            ];
+          };
+      in
+      {
+        output.domain = pkgs.writeText "libvirt-domain-config.xml" (domain "alan");
+      };
+  };
+}
diff --git a/nix/modules/libvirtnix/test.nix b/nix/modules/libvirtnix/test.nix
new file mode 100644
index 0000000..8542b7e
--- /dev/null
+++ b/nix/modules/libvirtnix/test.nix
@@ -0,0 +1,10 @@
+let
+	pkgs = import <nixpkgs> {};
+in
+	pkgs.lib.evalModules {
+		modules = [
+			# ./secret.nix
+			./domain.nix
+			./config.nix
+		];
+	}
diff --git a/nix/modules/libvirtnix/xml.nix b/nix/modules/libvirtnix/xml.nix
new file mode 100644
index 0000000..134a878
--- /dev/null
+++ b/nix/modules/libvirtnix/xml.nix
@@ -0,0 +1,45 @@
+{ lib, ... }:
+
+# takes a few args and creats a valid xml tag pair out of it
+#
+# testTag = mkTag {
+#   name = "name";
+#   args = [
+#     {
+#       key = "arg1";
+#       val = "arg1val";
+#     }
+#     {
+#       key = "arg2";
+#       val = "arg2val";
+#     }
+#   ];
+#   value = "qwe";
+#   children = [
+#     (mkTag { name = "nested"; args = []; value = "qwe"; children = [];})
+#   ];
+# };
+#
+# <name arg1=arg1val arg2=arg2val>
+#   value
+#   {children}
+# </name>
+{
+  name, # name of the tag to be used, such as `secret`, `description`, ...
+  args ? [ ], # args, [ { key="a"; val="b"; } { key="c"; val="d"; } ]
+  value ? "", # the value to place in the middle
+  children ? [ ], # the child elements
+  closing ? true, # add a closing tag
+}:
+let
+  args_str =
+    " " + lib.strings.concatStrings (lib.strings.intersperse " " (map (x: "${x.key}='${x.val}'") args));
+  child_evaled = lib.strings.concatStrings children;
+
+  cond = condition: value: if condition then value else "";
+
+  closingTag = if closing == true then "</${name}>" else "";
+in
+"<${name}${
+  lib.optionalString (args != [ ]) args_str
+}${cond (closing == false) "/"}>${value}${child_evaled}${lib.optionalString (closing) closingTag}"
diff --git a/nix/modules/vm/default.nix b/nix/modules/vm/default.nix
deleted file mode 100644
index 9428c94..0000000
--- a/nix/modules/vm/default.nix
+++ /dev/null
@@ -1,86 +0,0 @@
-{
-  config,
-  lib,
-  pkgs,
-  ...
-}:
-
-let
-  cfg = config.services.emile.vm;
-in
-with lib;
-{
-  options.services.emile.vm = {
-    enable = mkEnableOption "Enable vm";
-
-    # ip and port to listen on
-    guest = mkOption {
-      type = types.str;
-      default = "vmnameone";
-      example = "vmnameone";
-      description = "The name of the vm";
-    };
-  };
-
-  config = mkIf cfg.enable {
-    systemd.services = lib.mapAttrs' (
-      name: guest:
-      lib.nameValuePair "libvirtd-guest-${name}" {
-        after = [ "libvirtd.service" ];
-        requires = [ "libvirtd.service" ];
-        wantedBy = [ "multi-user.target" ];
-        serviceConfig = {
-          Type = "oneshot";
-          RemainAfterExit = "yes";
-        };
-        script =
-          let
-            xml = pkgs.writeText "libvirt-guest-${name}.xml" ''
-              <domain type="kvm">
-                <name>${name}</name>
-                <uuid>UUID</uuid>
-                <os>
-                  <type>hvm</type>
-                </os>
-                <memory unit="GiB">${guest.memory}</memory>
-                <devices>
-                  <disk type="volume">
-                    <source volume="guest-${name}"/>
-                    <target dev="vda" bus="virtio"/>
-                  </disk>
-                  <graphics type="spice" autoport="yes"/>
-                  <input type="keyboard" bus="usb"/>
-                  <interface type="direct">
-                    <source dev="${hostNic}" mode="bridge"/>
-                    <mac address="${guest.mac}"/>
-                    <model type="virtio"/>
-                  </interface>
-                </devices>
-                <features>
-                  <acpi/>
-                </features>
-              </domain>
-            '';
-          in
-          ''
-            uuid="$(${pkgs.libvirt}/bin/virsh domuuid '${name}' || true)"
-            ${pkgs.libvirt}/bin/virsh define <(sed "s/UUID/$uuid/" '${xml}')
-            ${pkgs.libvirt}/bin/virsh start '${name}'
-          '';
-        preStop = ''
-          ${pkgs.libvirt}/bin/virsh shutdown '${name}'
-          let "timeout = $(date +%s) + 10"
-          while [ "$(${pkgs.libvirt}/bin/virsh list --name | grep --count '^${name}$')" -gt 0 ]; do
-            if [ "$(date +%s)" -ge "$timeout" ]; then
-              # Meh, we warned it...
-              ${pkgs.libvirt}/bin/virsh destroy '${name}'
-            else
-              # The machine is still running, let's give it some time to shut down
-              sleep 0.5
-            fi
-          done
-        '';
-      }
-    ) guests;
-  };
-}
diff --git a/nix/modules/x86_64-linux.nix b/nix/modules/x86_64-linux.nix
index b913c68..62945b3 100644
--- a/nix/modules/x86_64-linux.nix
+++ b/nix/modules/x86_64-linux.nix
@@ -4,5 +4,7 @@
     ./r2wars-web
     ./remarvin
     ./filebrowser
+    # ./libvirtnix
+    ./goapp-frontend
   ];
 }
diff --git a/nix/pkgs/aarch64-darwin.nix b/nix/pkgs/aarch64-darwin.nix
index 081eb59..f5e8b60 100644
--- a/nix/pkgs/aarch64-darwin.nix
+++ b/nix/pkgs/aarch64-darwin.nix
@@ -2,4 +2,5 @@ final: prev: {
   vokobe = final.callPackage ./vokobe { inherit (final) naersk; };
   r2wars-web = final.callPackage ./r2wars-web { };
   remarvin = final.callPackage ./remarvin { };
+  libc-database = final.callPackage ./libc-database {};
 }
diff --git a/nix/pkgs/glibc-all-in-one/default.nix b/nix/pkgs/glibc-all-in-one/default.nix
new file mode 100644
index 0000000..bbb0824
--- /dev/null
+++ b/nix/pkgs/glibc-all-in-one/default.nix
@@ -0,0 +1,25 @@
+{ pkgs ? import <nixpkgs> {}, lib, fetchFromGitHub }:
+
+pkgs.stdenv.mkDerivation rec {
+  name = "glibc-all-in-one";
+  version = "master";
+
+  src = fetchFromGitHub {
+    owner = "fr0ster";
+    repo = "glibc-all-in-one";
+    rev = version;
+    sha256 = "Zysjhr76TenMarnoKo+M8DrTNbsnaXSoFZO1puPVoxU=";
+  };
+
+  buildPhase = '''';
+
+  installPhase = ''
+  '';
+
+  meta = {
+    description = "";
+    homepage = "https://github.com/fr0ster/glibc-all-in-one";
+    license = lib.licenses.mit;
+    maintainers = with lib.maintainers; [ hanemile ];
+  };
+ }
diff --git a/nix/pkgs/libc-database/default.nix b/nix/pkgs/libc-database/default.nix
new file mode 100644
index 0000000..b13a167
--- /dev/null
+++ b/nix/pkgs/libc-database/default.nix
@@ -0,0 +1,34 @@
+{
+  pkgs ? import <nixpkgs> { },
+  lib,
+  fetchFromGitHub,
+  ...
+}:
+
+pkgs.stdenv.mkDerivation rec {
+  name = "libc-database-${version}";
+  version = "master";
+
+  src = fetchFromGitHub {
+    owner = "niklasb";
+    repo = "libc-database";
+    rev = version;
+    sha256 = "Zysjhr76TenMarnoKo+M8DrTNbsnaXSoFZO1puPVoxU=";
+  };
+
+  # not building, we just want to download the repo
+  buildPhase = '''';
+  
+  installPhase = ''
+		mkdir -p $out/bin
+		ls -l
+		cp add download dump find get identify $out/bin/
+  '';
+
+  meta = {
+    description = "Build a database of libc offsets to simplify exploitation";
+    homepage = "https://github.com/niklasb/libc-database";
+    licenses = lib.license.mit;
+    maintainers = with lib.maintainers; [ hanemile ];
+  };
+}
diff --git a/nix/pkgs/overlay.nix b/nix/pkgs/overlay.nix
deleted file mode 100644
index e213533..0000000
--- a/nix/pkgs/overlay.nix
+++ /dev/null
@@ -1,9 +0,0 @@
-final: prev: {
-  vokobe = final.callPackage ./vokobe { inherit (final) naersk; };
-  r2wars-web = final.callPackage ./r2wars-web { };
-  remarvin = final.callPackage ./remarvin { };
-
-  pretalx_old = prev.pretalx.overrideAttrs ( old: {
-    version = "2024.1.0";
-  });
-}
diff --git a/nix/pkgs/x86_64-linux.nix b/nix/pkgs/x86_64-linux.nix
index 081eb59..c186cc4 100644
--- a/nix/pkgs/x86_64-linux.nix
+++ b/nix/pkgs/x86_64-linux.nix
@@ -2,4 +2,5 @@ final: prev: {
   vokobe = final.callPackage ./vokobe { inherit (final) naersk; };
   r2wars-web = final.callPackage ./r2wars-web { };
   remarvin = final.callPackage ./remarvin { };
+  # libc-database = final.callPackage ./libc-database {};
 }
diff --git a/nix/templates/ctf/flake.lock b/nix/templates/ctf/flake.lock
new file mode 100644
index 0000000..b756d8d
--- /dev/null
+++ b/nix/templates/ctf/flake.lock
@@ -0,0 +1,141 @@
+{
+  "nodes": {
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1740603184,
+        "narHash": "sha256-t+VaahjQAWyA+Ctn2idyo1yxRIYpaDxMgHkgCNiMJa4=",
+        "ref": "nixos-24.11",
+        "rev": "f44bd8ca21e026135061a0a57dcf3d0775b67a49",
+        "shallow": true,
+        "type": "git",
+        "url": "ssh://git@github.com/nixos/nixpkgs.git"
+      },
+      "original": {
+        "ref": "nixos-24.11",
+        "shallow": true,
+        "type": "git",
+        "url": "ssh://git@github.com/nixos/nixpkgs.git"
+      }
+    },
+    "nixpkgs_2": {
+      "locked": {
+        "lastModified": 1736241350,
+        "narHash": "sha256-CHd7yhaDigUuJyDeX0SADbTM9FXfiWaeNyY34FL1wQU=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "8c9fd3e564728e90829ee7dbac6edc972971cd0f",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixpkgs-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "pwndbg": {
+      "inputs": {
+        "nixpkgs": "nixpkgs_2",
+        "pyproject-build-systems": "pyproject-build-systems",
+        "pyproject-nix": "pyproject-nix",
+        "uv2nix": "uv2nix"
+      },
+      "locked": {
+        "lastModified": 1740333626,
+        "narHash": "sha256-OcwULIZcWOC1FNGa0SNGtyMyfbwTsBj17LBPpGOZL78=",
+        "ref": "refs/heads/dev",
+        "rev": "ef090ebf5eb75713b1f97c3d9aa3d7be636b0c3a",
+        "revCount": 2284,
+        "type": "git",
+        "url": "ssh://git@github.com/pwndbg/pwndbg"
+      },
+      "original": {
+        "type": "git",
+        "url": "ssh://git@github.com/pwndbg/pwndbg"
+      }
+    },
+    "pyproject-build-systems": {
+      "inputs": {
+        "nixpkgs": [
+          "pwndbg",
+          "nixpkgs"
+        ],
+        "pyproject-nix": [
+          "pwndbg",
+          "pyproject-nix"
+        ],
+        "uv2nix": [
+          "pwndbg",
+          "uv2nix"
+        ]
+      },
+      "locked": {
+        "lastModified": 1737338290,
+        "narHash": "sha256-gnXlfFEHA+/jMH7R+7y3JxrI3WfOjgBhzzJNuFW70UU=",
+        "owner": "pyproject-nix",
+        "repo": "build-system-pkgs",
+        "rev": "e1487e5cefda0c7990bdd2e660bee20971680e45",
+        "type": "github"
+      },
+      "original": {
+        "owner": "pyproject-nix",
+        "repo": "build-system-pkgs",
+        "type": "github"
+      }
+    },
+    "pyproject-nix": {
+      "inputs": {
+        "nixpkgs": [
+          "pwndbg",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1738204167,
+        "narHash": "sha256-J5M2sj3x4ocM93shScT/3Z4XWHZhwwW1NyQK+C+8Mys=",
+        "owner": "pyproject-nix",
+        "repo": "pyproject.nix",
+        "rev": "0d9f4b90cee1b5c5d6c142ef22de1e246e003ccc",
+        "type": "github"
+      },
+      "original": {
+        "owner": "pyproject-nix",
+        "repo": "pyproject.nix",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "nixpkgs": "nixpkgs",
+        "pwndbg": "pwndbg"
+      }
+    },
+    "uv2nix": {
+      "inputs": {
+        "nixpkgs": [
+          "pwndbg",
+          "nixpkgs"
+        ],
+        "pyproject-nix": [
+          "pwndbg",
+          "pyproject-nix"
+        ]
+      },
+      "locked": {
+        "lastModified": 1738653454,
+        "narHash": "sha256-tAFX8mPZtZ+zVE/+bwPC3U+u5MxjpNP0gG24DG26jVs=",
+        "owner": "pyproject-nix",
+        "repo": "uv2nix",
+        "rev": "05b0c148bc53aebc6a906b6d0ac41dde5954cd47",
+        "type": "github"
+      },
+      "original": {
+        "owner": "pyproject-nix",
+        "repo": "uv2nix",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/nix/templates/ctf/flake.nix b/nix/templates/ctf/flake.nix
index 67f33ca..f185bb7 100644
--- a/nix/templates/ctf/flake.nix
+++ b/nix/templates/ctf/flake.nix
@@ -1,14 +1,20 @@
 {
-  description = "ctf";
+  description = ''
+    One Flake to rule them all^W^Wcommon CTF problems, namely broken infa.
+
+    Usage:
+    ; nix flake init -t git+https://github.com/hanemile/hefe\#ctf
+  '';
   nixConfig.bash-prompt = "\[ctf\]; ";
 
   inputs = {
-    nixpkgs.url = "github:NixOS/nixpkgs";
+    nixpkgs.url = "git+ssh://git@github.com/nixos/nixpkgs.git?shallow=1&ref=nixos-24.11";
+    pwndbg.url = "git+ssh://git@github.com/pwndbg/pwndbg";
   };
 
   # Flake outputs
   outputs =
-    { self, nixpkgs }:
+    { nixpkgs, pwndbg, ... }@inputs:
     let
       # Systems supported
       allSystems = [
@@ -21,30 +27,46 @@
       # Helper to provide system-specific attributes
       nameValuePair = name: value: { inherit name value; };
       genAttrs = names: f: builtins.listToAttrs (map (n: nameValuePair n (f n)) names);
-      forAllSystems = f: genAttrs allSystems (system: f { pkgs = import nixpkgs { inherit system; }; });
+      forAllSystems = f: genAttrs allSystems (system: f {
+        pkgs = import nixpkgs { inherit system; };
+        pwndbg = inputs.pwndbg.packages.${system}.default;
+      });
     in
     {
       # Development environment output
       devShells = forAllSystems (
-        { pkgs }:
+        { pkgs, pwndbg }:
         {
           default =
-            let
-              python = pkgs.python311; # Use Python 3.11
-            in
             pkgs.mkShell {
-              packages =
-                with pkgs;
-                [ qemu ]
-                ++ [
-                  # Python plus helper tools
-                  (python.withPackages (
-                    ps: with ps; [
-                      pwntools
-                      pycryptodome
-                    ]
-                  ))
-                ];
+              shellHook = ''
+                cat << EOF > solve.py
+                from pwn import *
+
+                context.gdbinit="${pwndbg}/share/pwndbg/gdbinit.py"
+
+                # exe = ELF("./a.out")
+
+                p = remote("138.199.213.51", 31335)
+                #p = gdb.debug(exe.path, gdbscript=''''
+                #                break main
+                #                c
+                #              '''')
+
+                p.sendlineafter(b"> ", b"asd")
+
+                p.interactive()
+                EOF
+              '';
+              packages = [
+                  pkgs.gcc
+                  pwndbg
+                  (pkgs.python311.withPackages ( ps: with ps; [
+                    pwntools
+                    pwndbg
+                    pycryptodome
+                  ]))
+              ];
             };
         }
       );
diff --git a/nix/templates/ctf/solve.py b/nix/templates/ctf/solve.py
new file mode 100644
index 0000000..acc4a75
--- /dev/null
+++ b/nix/templates/ctf/solve.py
@@ -0,0 +1,15 @@
+from pwn import *
+
+context.gdbinit="/nix/store/jhvjf5drzzqq54xghzz94h0a6wsn1fs1-pwndbg/share/pwndbg/gdbinit.py"
+
+# exe = ELF("./a.out")
+
+p = remote("138.199.213.51", 31335)
+#p = gdb.debug(exe.path, gdbscript='''
+#                break main
+#                c
+#              ''')
+
+p.sendlineafter(b"> ", b"asd")
+
+p.interactive()
diff --git a/nix/templates/goapp/README.md b/nix/templates/goapp/README.md
new file mode 100644
index 0000000..e819496
--- /dev/null
+++ b/nix/templates/goapp/README.md
@@ -0,0 +1,39 @@
+# goapp
+
+A template for a go app.
+
+```bash
+; nix flake show
+warning: Git tree '/Users/emile/hefe' is dirty
+git+file:///Users/emile/hefe?dir=nix/templates/goapp
+├───devShells
+│   ├───aarch64-darwin
+│   │   └───default: development environment 'nix-shell'
+│   ├───aarch64-linux
+│   │   └───default omitted (use '--all-systems' to show)
+│   ├───x86_64-darwin
+│   │   └───default omitted (use '--all-systems' to show)
+│   └───x86_64-linux
+│       └───default omitted (use '--all-systems' to show)
+└───packages
+    ├───aarch64-darwin
+    │   ├───backend: package 'backend-0.0.1'
+    │   ├───backend-docker: package 'docker-image-backend.tar.gz'
+    │   ├───frontend: package 'frontend-0.0.1'
+    │   └───frontend-docker: package 'docker-image-frontend.tar.gz'
+    ├───aarch64-linux
+    │   ├───backend omitted (use '--all-systems' to show)
+    │   ├───backend-docker omitted (use '--all-systems' to show)
+    │   ├───frontend omitted (use '--all-systems' to show)
+    │   └───frontend-docker omitted (use '--all-systems' to show)
+    ├───x86_64-darwin
+    │   ├───backend omitted (use '--all-systems' to show)
+    │   ├───backend-docker omitted (use '--all-systems' to show)
+    │   ├───frontend omitted (use '--all-systems' to show)
+    │   └───frontend-docker omitted (use '--all-systems' to show)
+    └───x86_64-linux
+        ├───backend omitted (use '--all-systems' to show)
+        ├───backend-docker omitted (use '--all-systems' to show)
+        ├───frontend omitted (use '--all-systems' to show)
+        └───frontend-docker omitted (use '--all-systems' to show)
+```
diff --git a/nix/templates/goapp/flake.lock b/nix/templates/goapp/flake.lock
new file mode 100644
index 0000000..bde8278
--- /dev/null
+++ b/nix/templates/goapp/flake.lock
@@ -0,0 +1,62 @@
+{
+  "nodes": {
+    "flake-utils": {
+      "inputs": {
+        "systems": "systems"
+      },
+      "locked": {
+        "lastModified": 1731533236,
+        "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+        "ref": "refs/heads/main",
+        "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+        "revCount": 102,
+        "type": "git",
+        "url": "https://github.com/numtide/flake-utils"
+      },
+      "original": {
+        "type": "git",
+        "url": "https://github.com/numtide/flake-utils"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1739380326,
+        "narHash": "sha256-rsuM666LNSFbJXyAAoy6IjZ1nAMUtxyNL4qU4ji2lQQ=",
+        "ref": "refs/heads/master",
+        "rev": "911b3572a0dd4978144f7b2ca54ce51e79013d83",
+        "revCount": 752360,
+        "shallow": false,
+        "type": "git",
+        "url": "https://github.com/nixos/nixpkgs.git"
+      },
+      "original": {
+        "shallow": false,
+        "type": "git",
+        "url": "https://github.com/nixos/nixpkgs.git"
+      }
+    },
+    "root": {
+      "inputs": {
+        "flake-utils": "flake-utils",
+        "nixpkgs": "nixpkgs"
+      }
+    },
+    "systems": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/nix/templates/goapp/flake.nix b/nix/templates/goapp/flake.nix
new file mode 100644
index 0000000..385f1bf
--- /dev/null
+++ b/nix/templates/goapp/flake.nix
@@ -0,0 +1,63 @@
+{
+  inputs.nixpkgs.url = "git+https://github.com/nixos/nixpkgs.git?shallow=1?ref=nixos-24.11";
+  inputs.flake-utils.url = "git+https://github.com/numtide/flake-utils";
+
+  outputs =
+    { nixpkgs, flake-utils, ... }:
+    flake-utils.lib.eachDefaultSystem (
+      system:
+      let
+        pkgs = import nixpkgs {
+          inherit system;
+          overlays = [ ];
+        };
+
+        # take a name and return an attrset with a corresponding package and docker container
+        package-and-docker = name: (let
+          # define the name for the package and docker container
+          pkgname = name;
+          dockername = name + "-docker";
+
+          # import the package itself
+          package = import ./${name} { inherit pkgs name; };
+
+          # define the container
+          container = pkgs.dockerTools.buildImage {
+            name = "${name}"; # TODO(emile): this could simply be `inherit name;` iinw
+            config.Cmd = [ "${package}/bin/${name}" ];
+          };
+        in {
+          # the raw package
+          ${pkgname} = package;
+
+          # the docker image
+          ${dockername} = container;
+        });
+      in
+      {
+        packages = { }
+                   // (package-and-docker "frontend");
+
+        devShells.default = pkgs.mkShell {
+          buildInputs = builtins.attrValues {
+            inherit (pkgs)
+              go
+              gopls
+              helix
+              ripgrep
+              fd
+              tokei
+              tree
+              eza
+              ;
+          };
+
+          shellHook = ''
+            alias ls=eza
+            echo "goapp shell"
+            export PS1='>; '
+          '';
+        };
+      }
+    );
+}
diff --git a/nix/templates/goapp/frontend/default.nix b/nix/templates/goapp/frontend/default.nix
new file mode 100644
index 0000000..7b5caa8
--- /dev/null
+++ b/nix/templates/goapp/frontend/default.nix
@@ -0,0 +1,23 @@
+{ pkgs, name, ... }:
+
+let
+  version = "0.0.1";
+in
+pkgs.buildGoModule {
+  pname = "${name}";
+  version = "${version}";
+
+  src = ./.;
+
+  # use the dependencies directly from the vendor/ folder
+  # vendorHash = null;
+   
+  vendorHash = "sha256-dXWwAP0XM24cAcDV87XHQX9dLg6TDQ7ZVfEFgW/Q+J4=";
+
+  doCheck = false;
+
+  postInstall = ''
+    cp -r templates $out
+    mv $out/bin/{src,${name}}
+  '';
+}
diff --git a/nix/templates/goapp/frontend/go.mod b/nix/templates/goapp/frontend/go.mod
new file mode 100644
index 0000000..a71f5a0
--- /dev/null
+++ b/nix/templates/goapp/frontend/go.mod
@@ -0,0 +1,32 @@
+module github.com/hanemile/goapp/frontend
+
+go 1.23.5
+
+require (
+	github.com/coreos/go-oidc/v3 v3.12.0
+	github.com/gorilla/handlers v1.5.2
+	github.com/gorilla/mux v1.8.1
+	github.com/gorilla/securecookie v1.1.2
+	github.com/gorilla/sessions v1.4.0
+	github.com/mattn/go-sqlite3 v1.14.24
+	github.com/spf13/cobra v1.9.1
+	golang.org/x/oauth2 v0.21.0
+	modernc.org/sqlite v1.34.5
+)
+
+require (
+	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/felixge/httpsnoop v1.0.4 // indirect
+	github.com/go-jose/go-jose/v4 v4.0.2 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/ncruces/go-strftime v0.1.9 // indirect
+	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+	github.com/spf13/pflag v1.0.6 // indirect
+	golang.org/x/crypto v0.33.0 // indirect
+	golang.org/x/sys v0.30.0 // indirect
+	modernc.org/libc v1.55.3 // indirect
+	modernc.org/mathutil v1.6.0 // indirect
+	modernc.org/memory v1.8.0 // indirect
+)
diff --git a/nix/templates/goapp/frontend/go.sum b/nix/templates/goapp/frontend/go.sum
new file mode 100644
index 0000000..15bbb6f
--- /dev/null
+++ b/nix/templates/goapp/frontend/go.sum
@@ -0,0 +1,84 @@
+github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
+github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
+github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
+github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
+github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
+github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
+github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
+github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
+github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
+golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
+golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
+golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
+golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
+golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
+modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
+modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
+modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
+modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
+modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
+modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
+modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
+modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
+modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
+modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
+modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
+modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
+modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
+modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
+modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
+modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
+modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
+modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/nix/templates/goapp/frontend/main.db b/nix/templates/goapp/frontend/main.db
new file mode 100644
index 0000000..da9d88e
--- /dev/null
+++ b/nix/templates/goapp/frontend/main.db
Binary files differdiff --git a/nix/templates/goapp/frontend/run.sh b/nix/templates/goapp/frontend/run.sh
new file mode 100755
index 0000000..b2624f3
--- /dev/null
+++ b/nix/templates/goapp/frontend/run.sh
@@ -0,0 +1,11 @@
+# these are tests creds that aren't valid, try it :D
+export CLIENT_ID=goapp
+export CLIENT_SECRET=KGFO5LQnUxu1Zs.35gOem3MaG8odthg1U0v0.kScVPS6TPTWVRnAdT_nj4PYYSfuU6jdzTM6
+export CLIENT_CALLBACK_URL=http://localhost:8080/oauth2/callback
+export VERSION=0.0.1
+export SESSION_KEY=aes1Itheich4aeQu9Ouz7ahcaiVoogh9
+go run ./... \
+  --id goapp \
+  --issuer "https://sso.emile.space" \
+  --secret "KGFO5LQnUxu1Zs.35gOem3MaG8odthg1U0v0.kScVPS6TPTWVRnAdT_nj4PYYSfuU6jdzTM6" \
+  $@
diff --git a/nix/templates/goapp/frontend/server.log b/nix/templates/goapp/frontend/server.log
new file mode 100644
index 0000000..6c4d6dc
--- /dev/null
+++ b/nix/templates/goapp/frontend/server.log
@@ -0,0 +1,181 @@
+::1 - - [16/Feb/2025:23:03:46 +0100] "GET / HTTP/1.1" 200 5445
+::1 - - [16/Feb/2025:23:04:16 +0100] "GET / HTTP/1.1" 200 5445
+::1 - - [16/Feb/2025:23:04:17 +0100] "GET / HTTP/1.1" 200 5445
+::1 - - [16/Feb/2025:23:04:58 +0100] "GET / HTTP/1.1" 200 5509
+::1 - - [16/Feb/2025:23:05:03 +0100] "GET / HTTP/1.1" 200 5509
+::1 - - [16/Feb/2025:23:05:23 +0100] "GET / HTTP/1.1" 200 5573
+::1 - - [16/Feb/2025:23:05:33 +0100] "GET / HTTP/1.1" 200 5573
+::1 - - [16/Feb/2025:23:06:07 +0100] "GET / HTTP/1.1" 200 5614
+::1 - - [16/Feb/2025:23:06:17 +0100] "GET / HTTP/1.1" 200 5630
+::1 - - [16/Feb/2025:23:06:37 +0100] "GET / HTTP/1.1" 200 5651
+::1 - - [16/Feb/2025:23:06:59 +0100] "GET / HTTP/1.1" 200 5652
+::1 - - [16/Feb/2025:23:07:28 +0100] "GET / HTTP/1.1" 200 5676
+::1 - - [16/Feb/2025:23:08:26 +0100] "GET / HTTP/1.1" 200 5662
+::1 - - [16/Feb/2025:23:08:30 +0100] "GET / HTTP/1.1" 200 5662
+::1 - - [16/Feb/2025:23:08:42 +0100] "GET / HTTP/1.1" 200 5662
+::1 - - [16/Feb/2025:23:08:43 +0100] "GET / HTTP/1.1" 200 5662
+::1 - - [16/Feb/2025:23:08:54 +0100] "GET / HTTP/1.1" 200 5677
+::1 - - [16/Feb/2025:23:09:25 +0100] "GET / HTTP/1.1" 200 5614
+::1 - - [16/Feb/2025:23:09:44 +0100] "GET / HTTP/1.1" 200 5639
+::1 - - [16/Feb/2025:23:10:13 +0100] "GET / HTTP/1.1" 200 5661
+::1 - - [16/Feb/2025:23:10:48 +0100] "GET / HTTP/1.1" 200 5672
+::1 - - [16/Feb/2025:23:10:49 +0100] "GET / HTTP/1.1" 200 5672
+::1 - - [16/Feb/2025:23:11:32 +0100] "GET / HTTP/1.1" 200 5648
+::1 - - [16/Feb/2025:23:11:32 +0100] "GET / HTTP/1.1" 200 5648
+::1 - - [16/Feb/2025:23:11:57 +0100] "GET / HTTP/1.1" 200 5655
+::1 - - [16/Feb/2025:23:12:11 +0100] "GET / HTTP/1.1" 200 5675
+::1 - - [16/Feb/2025:23:12:13 +0100] "GET / HTTP/1.1" 200 5675
+::1 - - [16/Feb/2025:23:12:46 +0100] "GET / HTTP/1.1" 200 5724
+::1 - - [16/Feb/2025:23:12:54 +0100] "GET / HTTP/1.1" 200 5722
+::1 - - [16/Feb/2025:23:13:03 +0100] "GET / HTTP/1.1" 200 5721
+::1 - - [16/Feb/2025:23:13:11 +0100] "GET / HTTP/1.1" 200 5721
+::1 - - [16/Feb/2025:23:13:42 +0100] "GET / HTTP/1.1" 200 5719
+::1 - - [17/Feb/2025:10:27:07 +0100] "GET / HTTP/1.1" 200 5719
+::1 - - [17/Feb/2025:10:27:09 +0100] "GET / HTTP/1.1" 200 5719
+::1 - - [17/Feb/2025:10:28:32 +0100] "GET / HTTP/1.1" 200 5854
+::1 - - [17/Feb/2025:10:28:40 +0100] "GET / HTTP/1.1" 200 5854
+::1 - - [17/Feb/2025:10:28:46 +0100] "GET / HTTP/1.1" 200 5854
+::1 - - [17/Feb/2025:10:28:52 +0100] "GET / HTTP/1.1" 200 5854
+::1 - - [17/Feb/2025:10:29:22 +0100] "GET / HTTP/1.1" 200 5858
+::1 - - [17/Feb/2025:10:29:54 +0100] "GET / HTTP/1.1" 200 5841
+::1 - - [17/Feb/2025:10:30:09 +0100] "GET / HTTP/1.1" 200 5802
+::1 - - [17/Feb/2025:10:30:10 +0100] "GET / HTTP/1.1" 200 5802
+::1 - - [17/Feb/2025:10:30:20 +0100] "GET / HTTP/1.1" 200 5866
+::1 - - [17/Feb/2025:10:31:14 +0100] "GET / HTTP/1.1" 200 5866
+::1 - - [17/Feb/2025:10:31:26 +0100] "GET / HTTP/1.1" 200 5866
+::1 - - [17/Feb/2025:10:31:31 +0100] "GET / HTTP/1.1" 200 5866
+::1 - - [17/Feb/2025:10:31:47 +0100] "GET / HTTP/1.1" 200 5786
+::1 - - [17/Feb/2025:10:34:25 +0100] "GET / HTTP/1.1" 200 5786
+::1 - - [17/Feb/2025:10:35:22 +0100] "GET / HTTP/1.1" 200 5786
+::1 - - [17/Feb/2025:10:36:10 +0100] "GET / HTTP/1.1" 200 5786
+::1 - - [17/Feb/2025:10:37:10 +0100] "POST /submit HTTP/1.1" 200 10
+::1 - - [17/Feb/2025:10:37:51 +0100] "POST /submit HTTP/1.1" 200 15
+::1 - - [17/Feb/2025:10:37:55 +0100] "GET / HTTP/1.1" 200 5786
+::1 - - [17/Feb/2025:10:37:59 +0100] "POST /submit HTTP/1.1" 200 15
+::1 - - [17/Feb/2025:10:38:26 +0100] "POST /submit HTTP/1.1" 200 35
+::1 - - [17/Feb/2025:10:38:46 +0100] "POST /submit HTTP/1.1" 200 21
+::1 - - [17/Feb/2025:10:40:03 +0100] "GET / HTTP/1.1" 500 33
+::1 - - [17/Feb/2025:10:40:28 +0100] "GET / HTTP/1.1" 500 33
+::1 - - [17/Feb/2025:10:41:20 +0100] "GET / HTTP/1.1" 200 5786
+::1 - - [17/Feb/2025:10:41:38 +0100] "GET / HTTP/1.1" 200 5806
+::1 - - [17/Feb/2025:10:41:44 +0100] "GET / HTTP/1.1" 200 5806
+::1 - - [17/Feb/2025:10:41:53 +0100] "GET / HTTP/1.1" 200 6331
+::1 - - [17/Feb/2025:10:42:24 +0100] "GET / HTTP/1.1" 200 6238
+::1 - - [17/Feb/2025:10:43:00 +0100] "GET / HTTP/1.1" 200 6268
+::1 - - [17/Feb/2025:10:43:10 +0100] "GET / HTTP/1.1" 200 6260
+::1 - - [17/Feb/2025:10:44:54 +0100] "GET / HTTP/1.1" 200 6260
+::1 - - [17/Feb/2025:10:46:05 +0100] "GET / HTTP/1.1" 200 6398
+::1 - - [17/Feb/2025:10:49:07 +0100] "GET / HTTP/1.1" 200 6398
+::1 - - [17/Feb/2025:10:51:15 +0100] "GET / HTTP/1.1" 200 6398
+::1 - - [17/Feb/2025:10:51:16 +0100] "GET / HTTP/1.1" 200 6398
+::1 - - [17/Feb/2025:10:51:26 +0100] "GET / HTTP/1.1" 200 6398
+::1 - - [17/Feb/2025:10:51:27 +0100] "GET / HTTP/1.1" 200 6398
+::1 - - [17/Feb/2025:10:51:45 +0100] "GET /login HTTP/1.1" 200 0
+::1 - - [17/Feb/2025:10:51:59 +0100] "GET /login HTTP/1.1" 200 0
+::1 - - [17/Feb/2025:10:52:56 +0100] "GET /login HTTP/1.1" 200 0
+::1 - - [17/Feb/2025:10:52:58 +0100] "GET /login HTTP/1.1" 200 0
+::1 - - [17/Feb/2025:10:52:59 +0100] "GET /login HTTP/1.1" 200 0
+::1 - - [17/Feb/2025:10:53:11 +0100] "GET /login HTTP/1.1" 200 0
+::1 - - [17/Feb/2025:10:53:13 +0100] "GET /login HTTP/1.1" 200 0
+::1 - - [17/Feb/2025:10:53:29 +0100] "GET /login HTTP/1.1" 200 0
+::1 - - [17/Feb/2025:10:53:30 +0100] "GET /login HTTP/1.1" 200 0
+::1 - - [17/Feb/2025:10:54:04 +0100] "GET /login HTTP/1.1" 200 6974
+::1 - - [17/Feb/2025:10:54:11 +0100] "GET /login HTTP/1.1" 200 6917
+::1 - - [17/Feb/2025:10:54:28 +0100] "GET /login HTTP/1.1" 200 6724
+::1 - - [17/Feb/2025:10:54:28 +0100] "GET /login HTTP/1.1" 200 6724
+::1 - - [17/Feb/2025:10:54:30 +0100] "GET /login HTTP/1.1" 200 6724
+::1 - - [17/Feb/2025:10:54:33 +0100] "GET /login HTTP/1.1" 200 6724
+::1 - - [17/Feb/2025:10:54:35 +0100] "GET /login HTTP/1.1" 200 6724
+::1 - - [17/Feb/2025:10:54:39 +0100] "GET /login HTTP/1.1" 200 6724
+::1 - - [17/Feb/2025:10:54:42 +0100] "GET / HTTP/1.1" 200 6398
+::1 - - [17/Feb/2025:10:54:55 +0100] "GET / HTTP/1.1" 200 6388
+::1 - - [17/Feb/2025:10:54:58 +0100] "POST /submit HTTP/1.1" 200 21
+::1 - - [17/Feb/2025:10:58:19 +0100] "GET / HTTP/1.1" 200 6388
+::1 - - [17/Feb/2025:10:58:42 +0100] "GET / HTTP/1.1" 200 6316
+::1 - - [17/Feb/2025:10:58:43 +0100] "GET / HTTP/1.1" 200 6316
+::1 - - [17/Feb/2025:10:58:43 +0100] "GET / HTTP/1.1" 200 6316
+::1 - - [17/Feb/2025:13:44:10 +0100] "GET / HTTP/1.1" 200 6316
+::1 - - [17/Feb/2025:13:44:11 +0100] "GET /login HTTP/1.1" 200 6724
+::1 - - [17/Feb/2025:13:44:23 +0100] "GET /login HTTP/1.1" 200 6724
+::1 - - [17/Feb/2025:14:37:43 +0100] "GET /login HTTP/1.1" 500 56
+::1 - - [17/Feb/2025:14:38:57 +0100] "GET / HTTP/1.1" 500 33
+::1 - - [17/Feb/2025:14:40:38 +0100] "GET / HTTP/1.1" 200 5683
+::1 - - [17/Feb/2025:14:43:24 +0100] "GET / HTTP/1.1" 200 5683
+::1 - - [17/Feb/2025:14:45:41 +0100] "GET / HTTP/1.1" 200 5683
+::1 - - [17/Feb/2025:14:46:07 +0100] "GET / HTTP/1.1" 200 5683
+::1 - - [17/Feb/2025:14:46:14 +0100] "GET / HTTP/1.1" 200 5894
+::1 - - [17/Feb/2025:14:46:23 +0100] "GET / HTTP/1.1" 200 5734
+::1 - - [17/Feb/2025:14:47:02 +0100] "GET / HTTP/1.1" 200 5835
+::1 - - [17/Feb/2025:14:47:14 +0100] "GET / HTTP/1.1" 200 5873
+::1 - - [17/Feb/2025:14:47:22 +0100] "GET / HTTP/1.1" 200 5899
+::1 - - [17/Feb/2025:14:48:10 +0100] "GET / HTTP/1.1" 200 5790
+::1 - - [17/Feb/2025:14:48:15 +0100] "GET / HTTP/1.1" 200 5899
+::1 - - [17/Feb/2025:14:48:47 +0100] "GET / HTTP/1.1" 200 5899
+::1 - - [17/Feb/2025:14:48:53 +0100] "GET / HTTP/1.1" 200 5790
+::1 - - [17/Feb/2025:14:49:07 +0100] "GET / HTTP/1.1" 200 5927
+::1 - - [17/Feb/2025:14:49:16 +0100] "GET / HTTP/1.1" 200 5790
+::1 - - [17/Feb/2025:14:49:24 +0100] "GET / HTTP/1.1" 200 5919
+::1 - - [17/Feb/2025:14:51:08 +0100] "GET / HTTP/1.1" 500 33
+::1 - - [17/Feb/2025:14:51:48 +0100] "GET / HTTP/1.1" 200 6285
+::1 - - [17/Feb/2025:14:52:24 +0100] "GET / HTTP/1.1" 200 6224
+::1 - - [17/Feb/2025:14:53:46 +0100] "GET / HTTP/1.1" 200 6242
+::1 - - [17/Feb/2025:14:54:34 +0100] "GET / HTTP/1.1" 200 6222
+::1 - - [17/Feb/2025:14:55:14 +0100] "GET / HTTP/1.1" 200 6284
+::1 - - [17/Feb/2025:14:55:18 +0100] "GET / HTTP/1.1" 200 6284
+::1 - - [17/Feb/2025:14:55:45 +0100] "GET / HTTP/1.1" 200 6360
+::1 - - [17/Feb/2025:14:57:06 +0100] "GET / HTTP/1.1" 500 33
+::1 - - [17/Feb/2025:14:57:43 +0100] "GET / HTTP/1.1" 500 33
+::1 - - [17/Feb/2025:14:58:27 +0100] "GET / HTTP/1.1" 500 33
+::1 - - [17/Feb/2025:15:01:47 +0100] "GET / HTTP/1.1" 500 33
+::1 - - [17/Feb/2025:15:02:03 +0100] "GET / HTTP/1.1" 500 33
+::1 - - [17/Feb/2025:15:03:49 +0100] "GET / HTTP/1.1" 200 6360
+::1 - - [17/Feb/2025:15:06:07 +0100] "GET / HTTP/1.1" 200 6412
+::1 - - [17/Feb/2025:15:07:19 +0100] "GET / HTTP/1.1" 200 6412
+::1 - - [17/Feb/2025:15:07:38 +0100] "GET / HTTP/1.1" 200 6412
+::1 - - [17/Feb/2025:15:07:46 +0100] "GET / HTTP/1.1" 200 6514
+::1 - - [17/Feb/2025:15:07:58 +0100] "GET / HTTP/1.1" 200 6491
+::1 - - [17/Feb/2025:15:08:04 +0100] "GET /login HTTP/1.1" 500 56
+::1 - - [17/Feb/2025:15:08:56 +0100] "GET /login HTTP/1.1" 302 87
+::1 - - [17/Feb/2025:15:08:56 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6491
+::1 - - [17/Feb/2025:15:08:58 +0100] "GET /login HTTP/1.1" 302 87
+::1 - - [17/Feb/2025:15:08:58 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6491
+::1 - - [17/Feb/2025:15:20:07 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6586
+::1 - - [17/Feb/2025:15:20:41 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6587
+::1 - - [17/Feb/2025:15:20:45 +0100] "GET /login HTTP/1.1" 302 87
+::1 - - [17/Feb/2025:15:20:45 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6587
+::1 - - [17/Feb/2025:15:20:53 +0100] "GET /login HTTP/1.1" 302 87
+::1 - - [17/Feb/2025:15:20:53 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6587
+::1 - - [17/Feb/2025:15:24:15 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6587
+::1 - - [17/Feb/2025:15:24:16 +0100] "GET /login HTTP/1.1" 302 87
+::1 - - [17/Feb/2025:15:24:16 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:18:57:18 +0100] "GET / HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:18:57:19 +0100] "GET /login HTTP/1.1" 302 87
+::1 - - [19/Feb/2025:18:57:19 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:18:57:44 +0100] "GET /login HTTP/1.1" 302 87
+::1 - - [19/Feb/2025:18:57:44 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:18:58:54 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:18:58:55 +0100] "GET /login HTTP/1.1" 302 87
+::1 - - [19/Feb/2025:18:58:55 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:19:06:37 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:10:24 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:10:36 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:14:14 +0100] "GET / HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:19:14:16 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:15:01 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:16:08 +0100] "GET / HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:19:16:10 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:18:51 +0100] "GET / HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:19:18:53 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:23:35 +0100] "GET / HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:19:23:37 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:25:20 +0100] "GET /oauth2/callback HTTP/1.1" 500 314
+::1 - - [19/Feb/2025:19:25:37 +0100] "GET /oauth2/callback?code=authelia_ac_lA-8rLxGY4flmo-_DerONxfFPIVk2vpMiaCYZh_6ke0.FBoivMumLtPFauH9sWNVRz51S0FqWjwlFtqKO5sEA88&iss=https%3A%2F%2Fsso.emile.space&scope=openid+profile+email+groups&state=random-string-here HTTP/1.1" 500 314
+::1 - - [19/Feb/2025:19:27:27 +0100] "GET / HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:19:27:28 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:43:04 +0100] "GET / HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:19:43:05 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:43:29 +0100] "GET /oauth2/callback?code=authelia_ac_8UdV__GJCN9gxJrYa629TC3FToyDDhsbacPbJzhvcJ4.uPw2-_N4jQr7xf7JNZ_IZBNHEq-eeOFoZup7Vwjx1Y0&iss=https%3A%2F%2Fsso.emile.space&scope=openid+profile+email+groups&state=random-string-here HTTP/1.1" 500 142
+::1 - - [19/Feb/2025:19:49:22 +0100] "GET / HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:19:49:23 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:56:09 +0100] "GET / HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:19:56:12 +0100] "GET /login HTTP/1.1" 302 242
diff --git a/nix/templates/goapp/frontend/sessions.db b/nix/templates/goapp/frontend/sessions.db
new file mode 100644
index 0000000..04d6727
--- /dev/null
+++ b/nix/templates/goapp/frontend/sessions.db
Binary files differdiff --git a/nix/templates/goapp/frontend/src/db.go b/nix/templates/goapp/frontend/src/db.go
new file mode 100644
index 0000000..fd3605a
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/db.go
@@ -0,0 +1,37 @@
+package main
+
+import (
+	"database/sql"
+	"log"
+
+	_ "github.com/mattn/go-sqlite3"
+)
+
+const create string = `
+CREATE TABLE IF NOT EXISTS users (
+	id INTEGER NOT NULL PRIMARY KEY,
+	created_at DATETIME NOT NULL,
+	name TEXT,
+	passwordHash TEXT
+);
+`
+
+type State struct {
+	db       *sql.DB      // the database storing the "business data"
+	sessions *SqliteStore // the database storing sessions
+}
+
+func NewState() (*State, error) {
+	db, err := sql.Open("sqlite3", databasePath)
+	if err != nil {
+		log.Println("Error opening the db: ", err)
+		return nil, err
+	}
+	if _, err := db.Exec(create); err != nil {
+		log.Println("Error creating the tables: ", err)
+		return nil, err
+	}
+	return &State{
+		db: db,
+	}, nil
+}
diff --git a/nix/templates/goapp/frontend/src/handlers.go b/nix/templates/goapp/frontend/src/handlers.go
new file mode 100644
index 0000000..2cbacde
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/handlers.go
@@ -0,0 +1,252 @@
+package main
+
+import (
+	"fmt"
+	"html/template"
+	"log"
+	"net/http"
+
+	"github.com/coreos/go-oidc/v3/oidc"
+	"github.com/gorilla/sessions"
+	"golang.org/x/oauth2"
+)
+
+func indexHandler(w http.ResponseWriter, r *http.Request) {
+	session, err := globalState.sessions.Get(r, options.CookieName)
+	if err != nil {
+		log.Println("error getting the session")
+	}
+
+	tpl := indexTplData{
+		Error: r.FormValue("error"),
+	}
+
+	tpl.Breadcrumbs = []Breadcrumb{
+		{
+			Link{"a", "b"},
+			[]Link{
+				{"c", "d"},
+				{"e", "f"},
+			},
+		},
+		{
+			Link{"g", "h"},
+			[]Link{
+				{"i", "j"},
+				{"k", "l"},
+			},
+		},
+	}
+
+	//  session.Values["id_token"] = claimsIDToken
+	//  session.Values["userinfo"] = claimsUserInfo
+	//  session.Values["logged"] = true
+	//
+	log.Println("logged", session.Values["logged"])
+	log.Println("id-token", session.Values["id_token"])
+	log.Println("userinfo", session.Values["userinfo"])
+
+	if logged, ok := session.Values["logged"].(bool); ok && logged {
+		tpl.LoggedIn = true
+		tpl.Claims.IDToken = session.Values["id_token"].(Claims)
+		tpl.Claims.UserInfo = session.Values["userinfo"].(Claims)
+
+		if len(options.GroupsFilter) >= 1 {
+			for _, group := range tpl.Claims.UserInfo.Groups {
+				if isStringInSlice(group, options.GroupsFilter) {
+					tpl.Groups = append(tpl.Groups, filterText(group, options.Filters))
+				}
+			}
+		} else {
+			tpl.Groups = filterSliceOfText(tpl.Claims.UserInfo.Groups, options.Filters)
+		}
+
+		tpl.Claims.IDToken.PreferredUsername = filterText(tpl.Claims.IDToken.PreferredUsername, options.Filters)
+		tpl.Claims.UserInfo.PreferredUsername = filterText(tpl.Claims.UserInfo.PreferredUsername, options.Filters)
+		tpl.Claims.IDToken.Audience = filterSliceOfText(tpl.Claims.IDToken.Audience, options.Filters)
+		tpl.Claims.UserInfo.Audience = filterSliceOfText(tpl.Claims.UserInfo.Audience, options.Filters)
+		tpl.Claims.IDToken.Issuer = filterText(tpl.Claims.IDToken.Issuer, options.Filters)
+		tpl.Claims.UserInfo.Issuer = filterText(tpl.Claims.UserInfo.Issuer, options.Filters)
+		tpl.Claims.IDToken.Email = filterText(tpl.Claims.IDToken.Email, options.Filters)
+		tpl.Claims.UserInfo.Email = filterText(tpl.Claims.UserInfo.Email, options.Filters)
+		tpl.Claims.IDToken.Name = filterText(tpl.Claims.IDToken.Name, options.Filters)
+		tpl.Claims.UserInfo.Name = filterText(tpl.Claims.UserInfo.Name, options.Filters)
+		tpl.RawToken = rawTokens[tpl.Claims.IDToken.JWTIdentifier]
+		tpl.AuthorizeCodeURL = acURLs[tpl.Claims.IDToken.JWTIdentifier].String()
+
+		tpl.NextLinks = []Link{{"Logout", "/logout"}}
+	} else {
+		tpl.NextLinks = []Link{{"Login", "/login"}}
+	}
+
+	w.Header().Add("Content-Type", "text/html")
+
+	// get the template
+	t, err := template.New("index").Funcs(templateFuncMap).ParseGlob(fmt.Sprintf("%s/*.html", options.TemplatesPath))
+	if err != nil {
+		log.Printf("Error reading the template Path: %s/*.html", options.TemplatesPath)
+		log.Println(err)
+		w.WriteHeader(http.StatusInternalServerError)
+		w.Write([]byte("500 - Error reading template file"))
+		return
+	}
+
+	// exec!
+	err = t.ExecuteTemplate(w, "index", tpl)
+	if err != nil {
+		log.Println(err)
+	}
+}
+
+func loginHandler(w http.ResponseWriter, r *http.Request) {
+	log.Println("[ ] Getting the global session from the session cookie:")
+	session, err := globalState.sessions.Get(r, options.CookieName)
+	if err != nil {
+		log.Println("[ ] Error getting the cookie")
+		writeErr(w, nil, "error getting cookie", http.StatusInternalServerError)
+		return
+	}
+
+	log.Println("[ ] Setting the redirect URL")
+	session.Values["redirect-url"] = "/"
+
+	log.Println("[ ] Saving the session")
+	if err = session.Save(r, w); err != nil {
+		writeErr(w, err, "error saving session", http.StatusInternalServerError)
+		return
+	}
+
+	log.Printf("[ ] Redirecting to %s", oauth2Config.AuthCodeURL("random-string"))
+	http.Redirect(w, r, oauth2Config.AuthCodeURL("random-string-here"), http.StatusFound)
+}
+
+func logoutHandler(w http.ResponseWriter, r *http.Request) {
+	session, err := globalState.sessions.Get(r, options.CookieName)
+	if err != nil {
+		writeErr(w, err, "error getting cookie", http.StatusInternalServerError)
+		return
+	}
+
+	// wet the session
+	session.Values = make(map[interface{}]interface{})
+
+	if err = session.Save(r, w); err != nil {
+		writeErr(w, err, "error saving session", http.StatusInternalServerError)
+		return
+	}
+
+	http.Redirect(w, r, "/", http.StatusFound)
+}
+
+func oauthCallbackHandler(w http.ResponseWriter, r *http.Request) {
+	log.Println("hit the oauth callback handler")
+	if r.FormValue("error") != "" {
+		log.Printf("got an error from the idp: %s", r.FormValue("error"))
+		http.Redirect(w, r, fmt.Sprintf("/error?%s", r.Form.Encode()), http.StatusFound)
+		return
+	}
+
+	var (
+		token      *oauth2.Token
+		idToken    *oidc.IDToken
+		err        error
+		idTokenRaw string
+		ok         bool
+	)
+
+	log.Println(r.URL)
+
+	// The state should be checked here in production
+	if token, err = oauth2Config.Exchange(
+		r.Context(),
+		r.URL.Query().Get("code"),
+		//  oauth2.SetAuthURLParam("client_id", oauth2Config.ClientID),
+		//  oauth2.SetAuthURLParam("client_secret", oauth2Config.ClientSecret),
+	); err != nil {
+		log.Println("Unable to exchange authorization code for tokens")
+		log.Println(err)
+		writeErr(w, err, "unable to exchange authorization code for tokens", http.StatusInternalServerError)
+		return
+	}
+
+	// Extract the ID Token from OAuth2 token.
+	if idTokenRaw, ok = token.Extra("id_token").(string); !ok {
+		log.Println("missing id token")
+		writeErr(w, nil, "missing id token", http.StatusInternalServerError)
+		return
+	}
+
+	// Parse and verify ID Token payload.
+	if idToken, err = verifier.Verify(r.Context(), idTokenRaw); err != nil {
+		log.Printf("unable to verify id token or token is invalid: %+v", idTokenRaw)
+		writeErr(w, err, "unable to verify id token or token is invalid", http.StatusInternalServerError)
+		return
+	}
+
+	// Extract custom claims
+	claimsIDToken := Claims{}
+
+	if err = idToken.Claims(&claimsIDToken); err != nil {
+		log.Printf("unable to decode id token claims: %+v", &claimsIDToken)
+		writeErr(w, err, "unable to decode id token claims", http.StatusInternalServerError)
+		return
+	}
+
+	var userinfo *oidc.UserInfo
+
+	if userinfo, err = provider.UserInfo(r.Context(), oauth2.StaticTokenSource(token)); err != nil {
+		log.Printf("unable to retreive userinfo claims")
+		writeErr(w, err, "unable to retrieve userinfo claims", http.StatusInternalServerError)
+		return
+	}
+
+	claimsUserInfo := Claims{}
+
+	if err = userinfo.Claims(&claimsUserInfo); err != nil {
+		log.Printf("unable to decode userinfo claims")
+		writeErr(w, err, "unable to decode userinfo claims", http.StatusInternalServerError)
+		return
+	}
+
+	var session *sessions.Session
+
+	if session, err = globalState.sessions.Get(r, options.CookieName); err != nil {
+		log.Printf("unable to get session from cookie")
+		writeErr(w, err, "unable to get session from cookie", http.StatusInternalServerError)
+		return
+	}
+
+	session.Values["id_token"] = claimsIDToken
+	session.Values["userinfo"] = claimsUserInfo
+	session.Values["logged"] = true
+	rawTokens[claimsIDToken.JWTIdentifier] = idTokenRaw
+	acURLs[claimsIDToken.JWTIdentifier] = r.URL
+
+	if err = session.Save(r, w); err != nil {
+		log.Printf("unable to save session")
+		writeErr(w, err, "unable to save session", http.StatusInternalServerError)
+		return
+	}
+
+	var redirectUrl string
+
+	if redirectUrl, ok = session.Values["redirect-url"].(string); ok {
+		log.Printf("all fine!")
+		http.Redirect(w, r, redirectUrl, http.StatusFound)
+		return
+	}
+
+	http.Redirect(w, r, "/", http.StatusFound)
+}
+
+func writeErr(w http.ResponseWriter, err error, msg string, statusCode int) {
+	switch {
+	case err == nil:
+		log.Println(msg)
+		http.Error(w, msg, statusCode)
+	default:
+		log.Println(msg)
+		log.Println(err)
+		http.Error(w, fmt.Errorf("%s: %w", msg, err).Error(), statusCode)
+	}
+}
diff --git a/nix/templates/goapp/frontend/src/init.go b/nix/templates/goapp/frontend/src/init.go
new file mode 100644
index 0000000..75fd87d
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/init.go
@@ -0,0 +1,95 @@
+package main
+
+import (
+	"context"
+	//  "crypto/tls"
+	"fmt"
+	"log"
+	//  "net/http"
+	"net/url"
+	"os"
+	"strings"
+
+	"github.com/coreos/go-oidc/v3/oidc"
+	"golang.org/x/oauth2"
+)
+
+func logInit() loggingMiddleware {
+	log.Println("[i] Setting up logging...")
+	logFile, err := os.OpenFile(options.LogFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0664)
+	if err != nil {
+		log.Fatal("Error opening the server.log file: ", err)
+	}
+	return loggingMiddleware{logFile}
+}
+
+func dbInit() {
+	log.Println("[i] Setting up Global State Struct...")
+	s, err := NewState()
+	if err != nil {
+		log.Fatal("Error creating the NewState(): ", err)
+	}
+	globalState = s
+}
+
+func sessionInit() {
+	log.Println("[i] Setting up Session Storage...")
+	session_key, err := os.ReadFile(options.SessionKeyPath)
+	if err != nil {
+		log.Println("Could not read Session key")
+		panic(err)
+	}
+	store, err := NewSqliteStore(
+		sessiondbPath,
+		"sessions",
+		"/",
+		3600,
+		session_key)
+	if err != nil {
+		panic(err)
+	}
+	globalState.sessions = store
+}
+
+func oauth2Init() (err error) {
+	log.Println("[i] Setting up oauth2...")
+	var redirectURL *url.URL
+	if _, redirectURL, err = getURLs(options.PublicURL); err != nil {
+		return fmt.Errorf("could not parse public url: %w", err)
+	}
+
+	log.Printf("[ ] provider_url: %s", options.Issuer)
+	log.Printf("[ ] redirect_url: %s", redirectURL.String())
+
+	if provider, err = oidc.NewProvider(context.Background(), options.Issuer); err != nil {
+		log.Println("Error init oidc provider: ", err)
+		return fmt.Errorf("error initializing oidc provider: %w", err)
+	}
+
+	verifier = provider.Verifier(&oidc.Config{ClientID: options.ClientID})
+
+	clientSecretBytes, err := os.ReadFile(options.ClientSecretPath)
+	if err != nil {
+		panic(err)
+	}
+	clientSecret := strings.TrimSpace(string(clientSecretBytes))
+
+	log.Printf("[ ] ClientID: %s", options.ClientID)
+	log.Printf("[ ] ClientSecret: %s", clientSecret)
+	log.Printf("[ ] redirectURL: %s", redirectURL.String())
+	log.Printf("[ ] providerEndpoint: %+v", provider.Endpoint())
+	log.Printf("[ ] Scopes: %s", options.Scopes)
+	log.Printf("[ ] Endpoint: %+v", provider.Endpoint())
+
+	oauth2Config = oauth2.Config{
+		ClientID:     options.ClientID,
+		ClientSecret: clientSecret,
+		RedirectURL:  redirectURL.String(),
+		Endpoint:     provider.Endpoint(),
+		Scopes:       strings.Split(options.Scopes, ","),
+	}
+
+	oauth2Config.Endpoint.AuthStyle = oauth2.AuthStyleInParams
+
+	return nil
+}
diff --git a/nix/templates/goapp/frontend/src/log.go b/nix/templates/goapp/frontend/src/log.go
new file mode 100644
index 0000000..5af719a
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/log.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+	"net/http"
+	"os"
+
+	"github.com/gorilla/handlers"
+)
+
+// Defines a middleware containing a logfile
+//
+// This is done to combine gorilla/handlers with gorilla/mux middlewares to
+// just use r.Use(logger.Middleware) once instead of adding this to all
+// handlers manually (Yes, I'm really missing macros in Go...)
+type loggingMiddleware struct {
+	logFile *os.File
+}
+
+func (l *loggingMiddleware) Middleware(next http.Handler) http.Handler {
+	return handlers.LoggingHandler(l.logFile, next)
+}
+
+func authMiddleware(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		session, _ := globalState.sessions.Get(r, "session")
+		username := session.Values["username"]
+
+		if username == nil {
+			http.Redirect(w, r, "/login", http.StatusSeeOther)
+		} else {
+			next.ServeHTTP(w, r)
+		}
+	})
+}
diff --git a/nix/templates/goapp/frontend/src/main.go b/nix/templates/goapp/frontend/src/main.go
new file mode 100644
index 0000000..72ec7ee
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/main.go
@@ -0,0 +1,99 @@
+package main
+
+import (
+	"crypto/tls"
+	"fmt"
+	"log"
+	"net/http"
+	"net/url"
+	"time"
+
+	"github.com/coreos/go-oidc/v3/oidc"
+	"github.com/gorilla/mux"
+	"github.com/spf13/cobra"
+	"golang.org/x/oauth2"
+)
+
+var (
+	host          string
+	port          int
+	databasePath  string
+	logFilePath   string
+	sessiondbPath string
+	templatesPath string
+	globalState   *State
+
+	options      Options
+	oauth2Config oauth2.Config
+	provider     *oidc.Provider
+	verifier     *oidc.IDTokenVerifier
+
+	rawTokens = make(map[string]string)
+	acURLs    = make(map[string]*url.URL)
+)
+
+func main() {
+
+	http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+
+	rootCmd := &cobra.Command{Use: "goapp", RunE: root}
+
+	rootCmd.Flags().StringVar(&options.Host, "host", "127.0.0.1", "Specifies the tcp host to listen on")
+	rootCmd.Flags().IntVar(&options.Port, "port", 8080, "Specifies the port to listen on")
+	rootCmd.Flags().StringVar(&options.PublicURL, "public-url", "http://localhost:8080/", "Specifies the root URL to generate the redirect URI")
+	rootCmd.Flags().StringVar(&options.ClientID, "id", "", "Specifies the OpenID Connect Client ID")
+	rootCmd.Flags().StringVarP(&options.ClientSecretPath, "oidc-secret-path", "s", "", "Specifies the OpenID Connect Client Secret path")
+	rootCmd.Flags().StringVarP(&options.Issuer, "issuer", "i", "", "Specifies the URL for the OpenID Connect OP")
+	rootCmd.Flags().StringVar(&options.Scopes, "scopes", "openid,profile,email,groups", "Specifies the OpenID Connect scopes to request")
+	rootCmd.Flags().StringVar(&options.CookieName, "cookie-name", "oidc-client", "Specifies the storage cookie name to use")
+	rootCmd.Flags().StringSliceVar(&options.Filters, "filters", []string{}, "If specified filters the specified text from html output (not json) out of the email addresses, display names, audience, etc")
+	rootCmd.Flags().StringSliceVar(&options.GroupsFilter, "groups-filter", []string{}, "If specified only shows the groups in this list")
+	rootCmd.Flags().StringVar(&options.LogFilePath, "logfilepath", "./server.log", "Specifies the path to store the server logs at")
+	rootCmd.Flags().StringVar(&options.TemplatesPath, "templatespath", "./templates", "Specifies the path to where the templates are stored")
+	rootCmd.Flags().StringVar(&options.DatabasePath, "databasepath", "./main.db", "Specifies the path to where the database is stored")
+	rootCmd.Flags().StringVar(&options.SessionDBPath, "sessiondbpath", "./sessions.db", "Specifies the path to where the session database is stored")
+	rootCmd.Flags().StringVar(&options.SessionKeyPath, "sessionkeypath", "", "Specifies the path to where the session key is stored")
+
+	_ = rootCmd.MarkFlagRequired("id")
+	_ = rootCmd.MarkFlagRequired("secret")
+	_ = rootCmd.MarkFlagRequired("issuer")
+
+	if err := rootCmd.Execute(); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func root(cmd *cobra.Command, args []string) (err error) {
+
+	logger := logInit()
+	oauth2Init()
+	dbInit()
+	sessionInit()
+
+	r := mux.NewRouter()
+	r.Use(logger.Middleware)
+	r.HandleFunc("/", indexHandler)
+	r.HandleFunc("/login", loginHandler)
+	//  r.HandleFunc("/logout", )
+	//  r.HandleFunc("/error", loginHandler)
+	r.HandleFunc("/oauth2/callback", oauthCallbackHandler)
+	//  r.HandleFunc("/json", loginHandler)
+	//  r.HandleFunc("/jwt.json", loginHandler)
+
+	// endpoints with auth needed
+	auth_needed := r.PathPrefix("/").Subrouter()
+	auth_needed.Use(authMiddleware)
+	auth_needed.HandleFunc("/logout", logoutHandler)
+
+	serverAddress := fmt.Sprintf("%s:%d", options.Host, options.Port)
+	srv := &http.Server{
+		Handler:      r,
+		Addr:         serverAddress,
+		WriteTimeout: 15 * time.Second,
+		ReadTimeout:  15 * time.Second,
+	}
+
+	log.Printf("[i] Running the server on %s", serverAddress)
+	log.Fatal(srv.ListenAndServe())
+	return
+}
diff --git a/nix/templates/goapp/frontend/src/sqlitestore.go b/nix/templates/goapp/frontend/src/sqlitestore.go
new file mode 100644
index 0000000..34e31e4
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/sqlitestore.go
@@ -0,0 +1,285 @@
+/*
+	Gorilla Sessions backend for SQLite.
+
+Copyright (c) 2013 Contributors. See the list of contributors in the CONTRIBUTORS file for details.
+
+This software is licensed under a MIT style license available in the LICENSE file.
+*/
+package main
+
+import (
+	"database/sql"
+	"encoding/gob"
+	"errors"
+	"fmt"
+	"log"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/gorilla/securecookie"
+	"github.com/gorilla/sessions"
+	_ "modernc.org/sqlite"
+)
+
+type SqliteStore struct {
+	db         DB
+	stmtInsert *sql.Stmt
+	stmtDelete *sql.Stmt
+	stmtUpdate *sql.Stmt
+	stmtSelect *sql.Stmt
+
+	Codecs  []securecookie.Codec
+	Options *sessions.Options
+	table   string
+}
+
+type sessionRow struct {
+	id         string
+	data       string
+	createdOn  time.Time
+	modifiedOn time.Time
+	expiresOn  time.Time
+}
+
+type DB interface {
+	Exec(query string, args ...interface{}) (sql.Result, error)
+	Prepare(query string) (*sql.Stmt, error)
+	Close() error
+}
+
+func init() {
+	gob.Register(time.Time{})
+	gob.Register(Claims{})
+}
+
+func NewSqliteStore(endpoint string, tableName string, path string, maxAge int, keyPairs ...[]byte) (*SqliteStore, error) {
+	db, err := sql.Open("sqlite3", endpoint)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewSqliteStoreFromConnection(db, tableName, path, maxAge, keyPairs...)
+}
+
+func NewSqliteStoreFromConnection(db DB, tableName string, path string, maxAge int, keyPairs ...[]byte) (*SqliteStore, error) {
+	// Make sure table name is enclosed.
+	tableName = "`" + strings.Trim(tableName, "`") + "`"
+
+	cTableQ := "CREATE TABLE IF NOT EXISTS " +
+		tableName + " (id INTEGER PRIMARY KEY, " +
+		"session_data LONGBLOB, " +
+		"created_on TIMESTAMP DEFAULT 0, " +
+		"modified_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
+		"expires_on TIMESTAMP DEFAULT 0);"
+	if _, err := db.Exec(cTableQ); err != nil {
+		return nil, err
+	}
+
+	insQ := "INSERT INTO " + tableName +
+		"(id, session_data, created_on, modified_on, expires_on) VALUES (NULL, ?, ?, ?, ?)"
+	stmtInsert, stmtErr := db.Prepare(insQ)
+	if stmtErr != nil {
+		return nil, stmtErr
+	}
+
+	delQ := "DELETE FROM " + tableName + " WHERE id = ?"
+	stmtDelete, stmtErr := db.Prepare(delQ)
+	if stmtErr != nil {
+		return nil, stmtErr
+	}
+
+	updQ := "UPDATE " + tableName + " SET session_data = ?, created_on = ?, expires_on = ? " +
+		"WHERE id = ?"
+	stmtUpdate, stmtErr := db.Prepare(updQ)
+	if stmtErr != nil {
+		return nil, stmtErr
+	}
+
+	selQ := "SELECT id, session_data, created_on, modified_on, expires_on from " +
+		tableName + " WHERE id = ?"
+	stmtSelect, stmtErr := db.Prepare(selQ)
+	if stmtErr != nil {
+		return nil, stmtErr
+	}
+
+	return &SqliteStore{
+		db:         db,
+		stmtInsert: stmtInsert,
+		stmtDelete: stmtDelete,
+		stmtUpdate: stmtUpdate,
+		stmtSelect: stmtSelect,
+		Codecs:     securecookie.CodecsFromPairs(keyPairs...),
+		Options: &sessions.Options{
+			Path:   path,
+			MaxAge: maxAge,
+		},
+		table: tableName,
+	}, nil
+}
+
+func (m *SqliteStore) Close() {
+	m.stmtSelect.Close()
+	m.stmtUpdate.Close()
+	m.stmtDelete.Close()
+	m.stmtInsert.Close()
+	m.db.Close()
+}
+
+func (m *SqliteStore) Get(r *http.Request, name string) (*sessions.Session, error) {
+	return sessions.GetRegistry(r).Get(m, name)
+}
+
+func (m *SqliteStore) New(r *http.Request, name string) (*sessions.Session, error) {
+	session := sessions.NewSession(m, name)
+	session.Options = &sessions.Options{
+		Path:   m.Options.Path,
+		MaxAge: m.Options.MaxAge,
+	}
+	session.IsNew = true
+	var err error
+	if cook, errCookie := r.Cookie(name); errCookie == nil {
+		err = securecookie.DecodeMulti(name, cook.Value, &session.ID, m.Codecs...)
+		if err == nil {
+			err = m.load(session)
+			if err == nil {
+				session.IsNew = false
+			} else {
+				err = nil
+			}
+		}
+	}
+	return session, err
+}
+
+func (m *SqliteStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
+	var err error
+	if session.ID == "" {
+		if err = m.insert(session); err != nil {
+			return err
+		}
+	} else if err = m.save(session); err != nil {
+		return err
+	}
+	encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, m.Codecs...)
+	if err != nil {
+		return err
+	}
+	http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, session.Options))
+	return nil
+}
+
+func (m *SqliteStore) insert(session *sessions.Session) error {
+	var createdOn time.Time
+	var modifiedOn time.Time
+	var expiresOn time.Time
+	crOn := session.Values["created_on"]
+	if crOn == nil {
+		createdOn = time.Now()
+	} else {
+		createdOn = crOn.(time.Time)
+	}
+	modifiedOn = createdOn
+	exOn := session.Values["expires_on"]
+	if exOn == nil {
+		expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
+	} else {
+		expiresOn = exOn.(time.Time)
+	}
+	delete(session.Values, "created_on")
+	delete(session.Values, "expires_on")
+	delete(session.Values, "modified_on")
+
+	encoded, encErr := securecookie.EncodeMulti(session.Name(), session.Values, m.Codecs...)
+	if encErr != nil {
+		return encErr
+	}
+	res, insErr := m.stmtInsert.Exec(encoded, createdOn, modifiedOn, expiresOn)
+	if insErr != nil {
+		return insErr
+	}
+	lastInserted, lInsErr := res.LastInsertId()
+	if lInsErr != nil {
+		return lInsErr
+	}
+	session.ID = fmt.Sprintf("%d", lastInserted)
+	return nil
+}
+
+func (m *SqliteStore) Delete(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
+
+	// Set cookie to expire.
+	options := *session.Options
+	options.MaxAge = -1
+	http.SetCookie(w, sessions.NewCookie(session.Name(), "", &options))
+	// Clear session values.
+	for k := range session.Values {
+		delete(session.Values, k)
+	}
+
+	_, delErr := m.stmtDelete.Exec(session.ID)
+	if delErr != nil {
+		return delErr
+	}
+	return nil
+}
+
+func (m *SqliteStore) save(session *sessions.Session) error {
+	if session.IsNew == true {
+		return m.insert(session)
+	}
+	var createdOn time.Time
+	var expiresOn time.Time
+	crOn := session.Values["created_on"]
+	if crOn == nil {
+		createdOn = time.Now()
+	} else {
+		createdOn = crOn.(time.Time)
+	}
+
+	exOn := session.Values["expires_on"]
+	if exOn == nil {
+		expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
+		log.Print("nil")
+	} else {
+		expiresOn = exOn.(time.Time)
+		if expiresOn.Sub(time.Now().Add(time.Second*time.Duration(session.Options.MaxAge))) < 0 {
+			expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
+		}
+	}
+
+	delete(session.Values, "created_on")
+	delete(session.Values, "expires_on")
+	delete(session.Values, "modified_on")
+	encoded, encErr := securecookie.EncodeMulti(session.Name(), session.Values, m.Codecs...)
+	if encErr != nil {
+		return encErr
+	}
+	_, updErr := m.stmtUpdate.Exec(encoded, createdOn, expiresOn, session.ID)
+	if updErr != nil {
+		return updErr
+	}
+	return nil
+}
+
+func (m *SqliteStore) load(session *sessions.Session) error {
+	row := m.stmtSelect.QueryRow(session.ID)
+	sess := sessionRow{}
+	scanErr := row.Scan(&sess.id, &sess.data, &sess.createdOn, &sess.modifiedOn, &sess.expiresOn)
+	if scanErr != nil {
+		return scanErr
+	}
+	if sess.expiresOn.Sub(time.Now()) < 0 {
+		log.Printf("Session expired on %s, but it is %s now.", sess.expiresOn, time.Now())
+		return errors.New("Session expired")
+	}
+	err := securecookie.DecodeMulti(session.Name(), sess.data, &session.Values, m.Codecs...)
+	if err != nil {
+		return err
+	}
+	session.Values["created_on"] = sess.createdOn
+	session.Values["modified_on"] = sess.modifiedOn
+	session.Values["expires_on"] = sess.expiresOn
+	return nil
+
+}
diff --git a/nix/templates/goapp/frontend/src/templates.go b/nix/templates/goapp/frontend/src/templates.go
new file mode 100644
index 0000000..5ae9397
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/templates.go
@@ -0,0 +1,42 @@
+package main
+
+import (
+	"html/template"
+	"strings"
+)
+
+var (
+	templateFuncMap = template.FuncMap{
+		"stringsJoin":      strings.Join,
+		"stringsEqualFold": strings.EqualFold,
+		"isStringInSlice":  isStringInSlice,
+	}
+)
+
+type indexTplData struct {
+	Title, Description, RawToken string
+
+	Breadcrumbs []Breadcrumb
+	NextLinks   []Link
+
+	Error            string
+	LoggedIn         bool
+	Claims           tplClaims
+	Groups           []string
+	AuthorizeCodeURL string
+}
+
+type Link struct {
+	Name   string
+	Target string
+}
+
+type Breadcrumb struct {
+	Main    Link
+	Options []Link
+}
+
+type tplClaims struct {
+	IDToken  Claims
+	UserInfo Claims
+}
diff --git a/nix/templates/goapp/frontend/src/types.go b/nix/templates/goapp/frontend/src/types.go
new file mode 100644
index 0000000..97e0db5
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/types.go
@@ -0,0 +1,68 @@
+package main
+
+type Claims struct {
+	JWTIdentifier                       string   `json:"jti"`
+	Issuer                              string   `json:"iss"`
+	Subject                             string   `json:"sub"`
+	Nonce                               string   `json:"nonce"`
+	Expires                             int64    `json:"exp"`
+	IssueTime                           int64    `json:"iat"`
+	RequestedAt                         int64    `json:"rat"`
+	AuthorizeTime                       int64    `json:"auth_time"`
+	NotBefore                           int64    `json:"nbf"`
+	Audience                            []string `json:"aud"`
+	Scope                               []string `json:"scp"`
+	ScopeString                         string   `json:"scope"`
+	AccessTokenHash                     string   `json:"at_hash"`
+	CodeHash                            string   `json:"c_hash"`
+	AuthenticationContextClassReference string   `json:"acr"`
+	AuthenticationMethodsReference      []string `json:"amr"`
+
+	Name                string       `json:"name"`
+	GivenName           string       `json:"given_name"`
+	FamilyName          string       `json:"family_name"`
+	MiddleName          string       `json:"middle_name"`
+	Nickname            string       `json:"nickname"`
+	PreferredUsername   string       `json:"preferred_username"`
+	Profile             string       `jsoon:"profile"`
+	Picture             string       `json:"picture"`
+	Website             string       `json:"website"`
+	Gender              string       `json:"gender"`
+	Birthdate           string       `json:"birthdate"`
+	ZoneInfo            string       `json:"zoneinfo"`
+	Locale              string       `json:"locale"`
+	UpdatedAt           int64        `json:"updated_at"`
+	Email               string       `json:"email"`
+	EmailAlts           []string     `json:"alt_emails"`
+	EmailVerified       bool         `json:"email_verified"`
+	PhoneNumber         string       `json:"phone_number"`
+	PhoneNumberVerified bool         `json:"phone_number_verified"`
+	Address             ClamsAddress `json:"address"`
+	Groups              []string     `json:"groups"`
+}
+
+type ClamsAddress struct {
+	StreetAddress string `json:"street_address"`
+	Locality      string `json:"locality"`
+	Region        string `json:"region"`
+	PostalCode    string `json:"postal_code"`
+	Country       string `json:"country"`
+}
+
+type Options struct {
+	ClientID         string
+	ClientSecretPath string
+	CookieName       string
+	DatabasePath     string
+	Filters          []string
+	GroupsFilter     []string
+	Host             string
+	Issuer           string
+	LogFilePath      string
+	Port             int
+	PublicURL        string
+	Scopes           string
+	SessionDBPath    string
+	SessionKeyPath   string
+	TemplatesPath    string
+}
diff --git a/nix/templates/goapp/frontend/src/util.go b/nix/templates/goapp/frontend/src/util.go
new file mode 100644
index 0000000..89d28ba
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/util.go
@@ -0,0 +1,58 @@
+package main
+
+import (
+	"fmt"
+	"net/url"
+	"path"
+	"strings"
+)
+
+func isStringInSlice(s string, slice []string) bool {
+	for _, x := range slice {
+		if s == x {
+			return true
+		}
+	}
+
+	return false
+}
+
+func filterText(input string, filters []string) (output string) {
+	if len(filters) == 0 {
+		return input
+	}
+
+	for _, filter := range filters {
+		input = strings.Replace(input, filter, strings.Repeat("*", len(filter)), -1)
+	}
+
+	return input
+}
+
+func filterSliceOfText(input []string, filters []string) (output []string) {
+	for _, item := range input {
+		output = append(output, filterText(item, filters))
+	}
+
+	return output
+}
+
+func getURLs(rootURL string) (publicURL *url.URL, redirectURL *url.URL, err error) {
+	if publicURL, err = url.Parse(rootURL); err != nil {
+		return nil, nil, err
+	}
+
+	if publicURL.Scheme != "http" && publicURL.Scheme != "https" {
+		return nil, nil, fmt.Errorf("scheme must be http or https but it is '%s'", publicURL.Scheme)
+	}
+
+	if !strings.HasSuffix(publicURL.Path, "/") {
+		publicURL.Path += "/"
+	}
+
+	redirectURL = &url.URL{}
+	*redirectURL = *publicURL
+	redirectURL.Path = path.Join(redirectURL.Path, "/oauth2/callback")
+
+	return publicURL, redirectURL, nil
+}
diff --git a/nix/templates/goapp/frontend/templates/footer.html b/nix/templates/goapp/frontend/templates/footer.html
new file mode 100644
index 0000000..1899096
--- /dev/null
+++ b/nix/templates/goapp/frontend/templates/footer.html
@@ -0,0 +1,8 @@
+{{ define "footer" }}
+{{ if .asd }}
+<br><br><hr><br>
+{{ . }}
+{{ end }}
+</div>
+{{ end }}
+
diff --git a/nix/templates/goapp/frontend/templates/head.html b/nix/templates/goapp/frontend/templates/head.html
new file mode 100644
index 0000000..efc7ce3
--- /dev/null
+++ b/nix/templates/goapp/frontend/templates/head.html
@@ -0,0 +1,153 @@
+{{ define "head" }}
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>goapp</title>
+
+  <style>
+* { word-wrap:break-word; font-family: monospace; margin: 0; padding: 0; }
+
+/* light/darktheme specific foo */
+@media (prefers-color-scheme: light) {
+  html { background: #fafafa; color: #040404; }
+  a:hover { color: #fafafa; background: #040404 }
+  a:not([href*="webring.xxiivv.com"]):hover, nav a:active { color: #fafafa; background: #040404 }
+  a { color: #040404; background: #fafafa; text-decoration: none;}
+  nav a:hover, a:active { color: #fafafa; background: #040404 }
+  nav { margin: 1ex 0; background: #eeeeee; }
+  nav a { display:block; background: #eeeeee; }
+  h1 { margin: 3ex 0 1ex 0; width: 100%; background-color: #eeeeee}
+  h2 { margin: 2ex 0 1ex 0; width: 100%; background-color: #eeeeee}
+  h3 { margin: 1ex 0 1ex 0; width: 100%; font-size: 1em; background-color: #eeeeee}
+  h4 { margin: 1ex 0 1ex 0; width: 100%; font-size: 1em; /*background-color: #fafafa*/}
+  h5 { margin: 1ex 0 1ex 0; width: 100%; font-size: 1em; /*background-color: #fafafa*/}
+  .code { border-left: 1px solid #040404; margin-left: 2ex; padding-left: 1ex; }
+  .codeline:hover { background: #eeeeee; color: #040404; }
+  .trhover:hover { background: #c0c0c0; color: #040404; }
+
+  /* add an outline while hovering, the !important makes hovering on checked elements still visible */
+  .check-with-label:checked + .label-for-check { background-color: #040404; color: #eeeeee !important; }
+  .check-with-label:hover + .label-for-check { outline: 1px solid #040404; color: #040404; }
+
+  input, textarea {
+    outline: 1px solid #000000;
+    border: none;
+    background-color: #ffffff;
+    color: #000000;
+  }
+  button {
+    background-color: #ffffff; color: #000000; border: 1px solid #000000; border-style: solid;
+    margin-top: 1ex;
+  };
+}
+@media (prefers-color-scheme: dark) {
+  html { background: #040404; color: #c0c0c0; }
+  a:hover { color: #040404; background: #c0c0c0 }
+  body nav a:not([href*="webring.xxiivv.com"]):hover, nav a:active { color: #c0c0c0; background: #040404 }
+  a { color: #c0c0c0; background: #040404; text-decoration: none; }
+  nav a:hover, a:active { color: #040404; background: #c0c0c0 }
+  nav { margin: 1ex 0; background: #c0c0c0; }
+  nav a { display:block; background: #c0c0c0; }
+  h1 { margin: 3ex 0 1ex 0; width: 100%; background-color: #c0c0c0}
+  h2 { margin: 2ex 0 1ex 0; width: 100%; background-color: #c0c0c0}
+  h3 { margin: 1ex 0 1ex 0; width: 100%; font-size: 1em; background-color: #c0c0c0}
+  h4 { margin: 1ex 0 1ex 0; width: 100%; font-size: 1em; /*background-color: #c0c0c0*/}
+  h5 { margin: 1ex 0 1ex 0; width: 100%; font-size: 1em; /*background-color: #c0c0c0*/}
+  .code { border-left: 1px solid #c0c0c0; margin-left: 2ex;  padding-left: 1ex; }
+  .codeline:hover { background: #c0c0c0; color: #040404; }
+  .webring { -webkit-filter: invert(100%); filter: invert(100%); }
+  .trhover:hover { background: #c0c0c0; color: #040404; }
+
+  /* add an outline while hovering, the !important makes hovering on checked elements still visible */
+  .check-with-label:checked + .label-for-check { background-color: #c0c0c0; color: #040404 !important; }
+  .check-with-label:hover + .label-for-check { outline: 1px solid #c0c0c0; color: #c0c0c0; }
+
+  input, textarea {
+    outline: 1px solid #ffffff;
+    border: none;
+    background-color: #000000;
+    color: #ffffff;
+  }
+
+  button {
+    background-color: #000000; color: #ffffff;
+    border: 1px solid #ffffff;
+    border-style: solid;
+    margin-top: 1ex;
+  };
+}
+
+/* settings for mobile devices*/
+@media only screen and (max-width: 768px) {
+  body { margin: 1ex; width: calc(100% - 2ex) !important; }
+  img { max-width: 100% !important; max-height: 500px; }
+}
+
+/* only display the hover dropdown on non-mobile devices */
+@media only screen and (min-width: 768px) {
+  nav ul li:hover a + ul { display: inherit; white-space: nowrap; }
+}
+
+img { max-width: 100ex; max-height: 500px; }
+
+body { margin-left: auto; margin-right: auto; margin-top: 1ex; margin-bottom: 1ex; width: 100ex; }
+
+.webring { align: right; }
+a .webring { float: right; }
+
+/* display local links using [] and external links using {} */
+body a:not(h1 a, h2 a, h3 a,h4 a):not([href*="webring.xxiivv.com"]):not([class*="local"]):before { content: "["; }
+body a:not(h1 a, h2 a, h3 a,h4 a):not([href*="webring.xxiivv.com"]):not([class*="local"]):after { content: "]"; }
+a[href*="//"]:not([href*="r2wa.rs"]):not([class*="icon"]):before { content: '{'; }
+a[href*="//"]:not([href*="r2wa.rs"]):not([class*="icon"]):after { content: '}'; }
+
+/* table { width: 100ex; } */
+input, textarea { width: 100%; }
+textarea { padding: 0.5ex; }
+
+ul { list-style-type: none; }
+
+/* navigation bar magic */
+nav * { color: #040404; }
+nav ul { list-style: none; position: relative; display: inline-block; }
+nav ul li { display:inline-block; }
+nav ul ul { display: none; position: absolute; outline: 1px solid #040404; background-color: #ff0; }
+nav ul ul li { width: 100%; padding-right: 1ex; float:none; display:list-item; position: relative; }
+nav + ul li { display: inline-block;}
+
+/* nav bar spacing char */
+nav ul li > a::after { content: " /"; }
+nav ul li > a:only-child::after { content: ""; }
+nav ul li:last-of-type a::after { content: ""; }
+
+h1 a, h2 a, h3 a { padding-right: 1ex; }
+
+pre { white-space: pre-wrap; hyphens: auto; }
+pre.code { white-space: pre-wrap; hyphens: none; }
+
+/* display the list of folders in the current one as a vertical list, if the
+ * .vert class is present */
+nav + ul.vert li { display: block; }
+
+.w-100 { width: 100%; }
+
+.check-with-label { display: none; } /* checkbox with a label */
+
+/* In tables, make the first column fit the content and the reset be relaxed */
+body table tbody { width: 100%; word-wrap: break-word; }
+/* body table tbody tr>td { padding: 0.5ex 0 0.5ex !important; } */
+body table tbody tr td:nth-child(1) { width: auto; white-space: nowrap; padding-right: 1ex; }
+body table tbody tr td:not(:nth-child(1)) { width: 100%; max-width: 100%; word-wrap: anywhere; }
+
+tr { text-wrap: wrap;}
+
+input { padding-left: 0.5ex; }
+input:focus { outline-offset: 0px; }
+textarea:focus { outline-offset: 0px; }
+
+  </style>
+</head>
+{{ end }}
diff --git a/nix/templates/goapp/frontend/templates/index.html b/nix/templates/goapp/frontend/templates/index.html
new file mode 100644
index 0000000..e4693af
--- /dev/null
+++ b/nix/templates/goapp/frontend/templates/index.html
@@ -0,0 +1,83 @@
+{{ define "index" }}
+
+{{ template "head" . }}
+{{ template "nav" . }}
+
+<h1>goapp</h1>
+
+{{ . }}
+
+{{- if .LoggedIn }}
+<p id="welcome">Logged in as {{ or .Claims.UserInfo.PreferredUsername .Claims.IDToken.Subject "unknown" }}!</p>
+<p><a href="/logout" id="log-out">Log out</a></p>
+<p>Access Token Hash: <span id="claim-at_hash">{{ .Claims.IDToken.AccessTokenHash }}</span></p>
+<p>Code Hash: <span id="claim-c_hash">{{ .Claims.IDToken.CodeHash }}</span></p>
+<p>Authentication Context Class Reference: <span id="claim-acr">{{ .Claims.IDToken.AuthenticationContextClassReference }}</span></p>
+<p>Authentication Methods Reference: <span id="claim-amr">{{ stringsJoin .Claims.IDToken.AuthenticationMethodsReference ", " }}</span></p>
+<p>Audience: <span id="claim-aud">{{ stringsJoin .Claims.IDToken.Audience ", " }}</span></p>
+<p>Expires: <span id="claim-exp">{{ .Claims.IDToken.Expires }}</span></p>
+<p>Issue Time: <span id="claim-iat">{{ .Claims.IDToken.IssueTime }}</span></p>
+<p>Requested At: <span id="claim-rat">{{ .Claims.IDToken.RequestedAt }}</span></p>
+<p>Authorize Time: <span id="claim-auth_at">{{ .Claims.IDToken.AuthorizeTime }}</span></p>
+<p>Not Before: <span id="claim-nbf">{{ .Claims.IDToken.NotBefore }}</span></p>
+<p>Issuer: <span id="claim-iss">{{ .Claims.IDToken.Issuer }}</span></p>
+<p>JWT ID: <span id="claim-jti">{{ .Claims.IDToken.JWTIdentifier }}</span></p>
+<p>Subject: <span id="claim-sub">{{ .Claims.IDToken.Subject }}</span></p>
+<p>Nonce: <span id="claim-nonce">{{ .Claims.IDToken.Nonce }}</span></p>
+<p>Name: <span id="claim-name">{{ .Claims.UserInfo.Name }}</span></p>
+<p>Name (ID Token): <span id="claim-id-token-name">{{ .Claims.IDToken.Name }}</span></p>
+<p>Given Name: <span id="claim-given_name">{{ .Claims.UserInfo.GivenName }}</span></p>
+<p>Given Name (ID Token): <span id="claim-id-token-given_name">{{ .Claims.IDToken.GivenName }}</span></p>
+<p>Family Name: <span id="claim-family_name">{{ .Claims.UserInfo.FamilyName }}</span></p>
+<p>Family Name (ID Token): <span id="claim-id-token-family_name">{{ .Claims.IDToken.FamilyName }}</span></p>
+<p>Middle Name: <span id="claim-middle_name">{{ .Claims.UserInfo.MiddleName }}</span></p>
+<p>Middle Name (ID Token): <span id="claim-id-token-middle_name">{{ .Claims.IDToken.MiddleName }}</span></p>
+<p>Nickname: <span id="claim-nickname">{{ .Claims.UserInfo.Nickname }}</span></p>
+<p>Nickname (ID Token): <span id="claim-id-token-nickname">{{ .Claims.IDToken.Nickname }}</span></p>
+<p>Preferred Username: <span id="claim-preferred_username">{{ .Claims.UserInfo.PreferredUsername }}</span></p>
+<p>Preferred Username (ID Token): <span id="claim-id-token-preferred_username">{{ .Claims.IDToken.PreferredUsername }}</span></p>
+<p>Profile: <span id="claim-profile">{{ .Claims.UserInfo.Profile }}</span></p>
+<p>Profile (ID Token): <span id="claim-id-token-profile">{{ .Claims.IDToken.Profile }}</span></p>
+<p>Website: <span id="claim-website">{{ .Claims.UserInfo.Website }}</span></p>
+<p>Website (ID Token): <span id="claim-id-token-website">{{ .Claims.IDToken.Website }}</span></p>
+<p>Gender: <span id="claim-gender">{{ .Claims.UserInfo.Gender }}</span></p>
+<p>Gender (ID Token): <span id="claim-id-token-gender">{{ .Claims.IDToken.Gender }}</span></p>
+<p>Birthdate: <span id="claim-birthdate">{{ .Claims.UserInfo.Birthdate }}</span></p>
+<p>Birthdate (ID Token): <span id="claim-id-token-birthdate">{{ .Claims.IDToken.Birthdate }}</span></p>
+<p>ZoneInfo: <span id="claim-zoneinfo">{{ .Claims.UserInfo.ZoneInfo }}</span></p>
+<p>ZoneInfo (ID Token): <span id="claim-id-token-zoneinfo">{{ .Claims.IDToken.ZoneInfo }}</span></p>
+<p>Locale: <span id="claim-locale">{{ .Claims.UserInfo.Locale }}</span></p>
+<p>Locale (ID Token): <span id="claim-id-token-locale">{{ .Claims.IDToken.Locale }}</span></p>
+<p>Updated At: <span id="claim-updated_at">{{ .Claims.UserInfo.UpdatedAt }}</span></p>
+<p>Updated At (ID Token): <span id="claim-id-token-updated_at">{{ .Claims.IDToken.UpdatedAt }}</span></p>
+<p>Email: <span id="claim-email">{{ .Claims.UserInfo.Email }}</span></p>
+<p>Email (ID Token): <span id="claim-id-token-email">{{ .Claims.IDToken.Email }}</span></p>
+<p>Email Alts: <span id="claim-alt_emails">{{ .Claims.UserInfo.EmailAlts }}</span></p>
+<p>Email Alts (ID Token): <span id="claim-id-token-alt_emails">{{ .Claims.IDToken.EmailAlts }}</span></p>
+<p>Email Verified: <span id="claim-email_verified">{{ .Claims.UserInfo.EmailVerified }}</span></p>
+<p>Email Verified (ID Token): <span id="claim-id-token-email_verified">{{ .Claims.IDToken.EmailVerified }}</span></p>
+<p>Phone Number: <span id="claim-phone_number">{{ .Claims.UserInfo.PhoneNumber }}</span></p>
+<p>Phone Number (ID Token): <span id="claim-id-token-phone_number">{{ .Claims.IDToken.PhoneNumber }}</span></p>
+<p>Phone Number Verified: <span id="claim-phone_number_verified">{{ .Claims.UserInfo.PhoneNumberVerified }}</span></p>
+<p>Phone Number Verified (ID Token): <span id="claim-id-token-phone_number_verified">{{ .Claims.IDToken.PhoneNumberVerified }}</span></p>
+<p>Groups: <span id="claim-groups">{{ stringsJoin .Groups ", " }}</span></p>
+<p>Groups (ID Token): <span id="claim-id-token-groups">{{ stringsJoin .Groups ", " }}</span></p>
+<p>Raw: <span id="raw">{{ .RawToken }}</span></p>
+<p>Authorize Code URL: <span id="auth-code-url">{{ .AuthorizeCodeURL }}</span></p>
+{{- else }}
+<p>Not logged yet...</p> <a id="login-link" href="/login">Log in</a>
+{{- end }}
+
+<!--
+<body style="height: 100vh; position: relative;">
+  <div style="position: absolute; width: 50%; transform: scale(2);
+   transform-origin: 0 0;">
+    <form action="/submit" method="POST" id="sumbit">
+      <input type="text" id="target" name="target" required size="20" />
+    </form>
+  </div>
+</body>
+-->
+
+{{ end }}
+
diff --git a/nix/templates/goapp/frontend/templates/login.html b/nix/templates/goapp/frontend/templates/login.html
new file mode 100644
index 0000000..6e54781
--- /dev/null
+++ b/nix/templates/goapp/frontend/templates/login.html
@@ -0,0 +1,41 @@
+{{ define "login" }}
+
+{{ template "head" . }}
+<body>
+  {{ template "nav" . }}
+
+  <span id="login"></span>
+  <h1><a href="#login">Login</a></h1>
+
+  {{ if .err }}{{ .err }}{{ end }}
+  {{ if .logged_in }}
+  Already logged in! <a href="/">Return home</a>
+  {{ else }}
+  <form method="POST" action="/login">
+
+    <table>
+      <tr>
+        <td><label for="username">Name:</label></td>
+        <td><input class="border" type="text" id="username" name="username" autofocus></td>
+      </tr>
+      <tr>
+        <td><label for="password">Password:</label></td>
+        <td><input class="border" type="password" id="password" name="password"></td>
+      </tr>
+      <tr>
+        <td></td>
+        <td><input class="border" type="submit" value="Login"></td>
+      </tr>
+      <tr>
+        <td></td>
+        <td>{{ .res }}</td>
+      </tr>
+    </table>
+  </form>
+  <!-- Not registered yet? <a href="/register">Register Now!</a> -->
+  {{ end }}
+
+  
+</body>
+{{ template "footer" . }}
+{{end}}
diff --git a/nix/templates/goapp/frontend/templates/nav.html b/nix/templates/goapp/frontend/templates/nav.html
new file mode 100644
index 0000000..bf9820f
--- /dev/null
+++ b/nix/templates/goapp/frontend/templates/nav.html
@@ -0,0 +1,41 @@
+{{ define "nav" }}
+  <header>
+    <p style="margin: 1ex 0; display: block; width: 100%; background-color: #ffaa00; color: white;">
+      v0.0.1 EARLY BETA - Data can be deleted at random!
+    </p>
+  </header>
+  <nav>
+
+    <ul>
+      {{ range .Breadcrumbs }}
+        <li>
+          <a class="local" href="{{ .Main.Target }}">{{ .Main.Name }}</a>
+          {{ if .Options }}
+          <ul>
+          {{ range $opt := .Options }}
+            <li><a class="local" href="{{ $opt.Target }}">{{ $opt.Name }}</a></li>
+          {{ end }}
+          </ul>
+          {{ end }}
+        </li>
+      {{ end }}
+    </ul>
+
+  
+    <ul style="float: right">
+        <li>
+            <a href="https://github.com/HanEmile/hefe/tree/main/nix/templates/goapp">src</a>
+        </li>
+    </ul>
+  </nav>
+  <ul>
+    {{ if .NextLinks }}
+    {{ range .NextLinks }}
+      <li><a class="local" href="{{ .Target }}">{{ .Name }}</a></li>
+    {{ end }}
+    {{ end }}
+  </ul>
+	<br>
+{{ end }}
+
+
diff --git a/secret_create.sh b/secret_create.sh
index fcd05cb..e022cd9 100755
--- a/secret_create.sh
+++ b/secret_create.sh
@@ -2,6 +2,8 @@
 # $1 = hostname
 # $2 = secretname (with .age suffix)
 
+set -xe
+
 if [ $# -lt 2 ]; then
     # TODO: print usage
     echo "USAGE: ./secret_create.sh <hostname> <secretname>"