From c1943f573e148d618cf53134168e417186cdeb8d Mon Sep 17 00:00:00 2001 From: pimkoter Date: Mon, 20 Jul 2026 22:06:22 +0200 Subject: [PATCH 1/6] fix(nix) fixed legacy xorg syntax --- packaging/nix/package-desktop.nix | 272 +++++++++++++++--------------- 1 file changed, 140 insertions(+), 132 deletions(-) diff --git a/packaging/nix/package-desktop.nix b/packaging/nix/package-desktop.nix index c4375470..cdfc3dc6 100644 --- a/packaging/nix/package-desktop.nix +++ b/packaging/nix/package-desktop.nix @@ -7,7 +7,6 @@ wrapGAppsHook3, copyDesktopItems, makeDesktopItem, - # Electron / Chromium runtime libraries alsa-lib, at-spi2-atk, @@ -34,137 +33,146 @@ pango, systemd, wayland, - xorg, - + libX11, + libXcomposite, + libXcursor, + libXdamage, + libXext, + libXfixes, + libXi, + libXrandr, + libXrender, + libXScrnSaver, + libXtst, + libxcb, commandLineArgs ? "", -}: -let +}: let releaseData = lib.importJSON ./release-data.json; in -# Packaged from the official prebuilt linux-x64 release tarball rather than built -# from source, so `nix run github:ZenNotes/zennotes` downloads the same signed -# binary the AppImage/deb/AUR ship instead of compiling Electron locally. Bump -# `version` + `desktopHash` in release-data.json per release (see README.md). -stdenv.mkDerivation (finalAttrs: { - pname = "zennotes-desktop"; - inherit (releaseData) version; - - src = fetchurl { - url = "https://github.com/ZenNotes/zennotes/releases/download/v${finalAttrs.version}/ZenNotes-${finalAttrs.version}-linux-x64.tar.gz"; - hash = releaseData.desktopHash; - }; - - sourceRoot = "ZenNotes-${finalAttrs.version}-linux-x64"; - - nativeBuildInputs = [ - autoPatchelfHook - makeWrapper - wrapGAppsHook3 - copyDesktopItems - ]; - - buildInputs = [ - alsa-lib - at-spi2-atk - at-spi2-core - atk - cairo - cups - dbus - expat - fontconfig - freetype - gdk-pixbuf - glib - gtk3 - libdrm - libgbm - libuuid - libxkbcommon - nspr - nss - pango - stdenv.cc.cc # libstdc++ - xorg.libX11 - xorg.libXcomposite - xorg.libXcursor - xorg.libXdamage - xorg.libXext - xorg.libXfixes - xorg.libXi - xorg.libXrandr - xorg.libXrender - xorg.libXScrnSaver - xorg.libXtst - xorg.libxcb - ]; - - # dlopen'd at runtime (not in DT_NEEDED), so keep them on the wrapper's path. - runtimeDependencies = [ - (lib.getLib systemd) - libGL - libnotify - libpulseaudio - wayland - ]; - - dontConfigure = true; - dontBuild = true; - # We invoke makeWrapper manually and splice in gappsWrapperArgs ourselves. - dontWrapGApps = true; - - installPhase = '' - runHook preInstall - - # The SUID chrome-sandbox can't be made setuid in the Nix store; drop it so - # Electron falls back to the user-namespace sandbox. - rm -f chrome-sandbox - - mkdir -p $out/share/zennotes - cp -r . $out/share/zennotes - - # Icons + desktop entry ship inside the tarball's arch-extras tree. - for icon in $out/share/zennotes/resources/arch-extras/icons/*.png; do - size="$(basename "$icon" .png)" - install -Dm644 "$icon" "$out/share/icons/hicolor/$size/apps/${finalAttrs.pname}.png" - done - - makeWrapper $out/share/zennotes/ZenNotes $out/bin/${finalAttrs.pname} \ - "''${gappsWrapperArgs[@]}" \ - --add-flags "\''${NIXOS_OZONE_WL:+\''${WAYLAND_DISPLAY:+--ozone-platform-hint=auto}}" \ - ${lib.optionalString (commandLineArgs != "") "--add-flags ${lib.escapeShellArg commandLineArgs}"} - - runHook postInstall - ''; - - desktopItems = [ - (makeDesktopItem { - name = finalAttrs.pname; - desktopName = "ZenNotes"; - exec = "${finalAttrs.pname} %U"; - icon = finalAttrs.pname; - comment = "Keyboard-first local Markdown notes"; - categories = [ - "Office" - "Utility" - "TextEditor" - ]; - startupWMClass = "ZenNotes"; - mimeTypes = [ - "text/markdown" - "x-scheme-handler/zennotes" - ]; - }) - ]; - - meta = { - description = "Keyboard-first local Markdown notes with Vim motions, diagrams, and MCP integration (prebuilt binary)"; - homepage = "https://zennotes.org/"; - changelog = "https://github.com/ZenNotes/zennotes/releases/tag/v${finalAttrs.version}"; - license = lib.licenses.mit; - maintainers = with lib.maintainers; [ justkrysteq ]; - mainProgram = finalAttrs.pname; - sourceProvenance = with lib.sourceTypes; [ binaryNativeCode ]; - platforms = [ "x86_64-linux" ]; - }; -}) + # Packaged from the official prebuilt linux-x64 release tarball rather than built + # from source, so `nix run github:ZenNotes/zennotes` downloads the same signed + # binary the AppImage/deb/AUR ship instead of compiling Electron locally. Bump + # `version` + `desktopHash` in release-data.json per release (see README.md). + stdenv.mkDerivation (finalAttrs: { + pname = "zennotes-desktop"; + inherit (releaseData) version; + + src = fetchurl { + url = "https://github.com/ZenNotes/zennotes/releases/download/v${finalAttrs.version}/ZenNotes-${finalAttrs.version}-linux-x64.tar.gz"; + hash = releaseData.desktopHash; + }; + + sourceRoot = "ZenNotes-${finalAttrs.version}-linux-x64"; + + nativeBuildInputs = [ + autoPatchelfHook + makeWrapper + wrapGAppsHook3 + copyDesktopItems + ]; + + buildInputs = [ + alsa-lib + at-spi2-atk + at-spi2-core + atk + cairo + cups + dbus + expat + fontconfig + freetype + gdk-pixbuf + glib + gtk3 + libdrm + libgbm + libuuid + libxkbcommon + nspr + nss + pango + stdenv.cc.cc # libstdc++ + libX11 + libXcomposite + libXcursor + libXdamage + libXext + libXfixes + libXi + libXrandr + libXrender + libXScrnSaver + libXtst + libxcb + ]; + + # dlopen'd at runtime (not in DT_NEEDED), so keep them on the wrapper's path. + runtimeDependencies = [ + (lib.getLib systemd) + libGL + libnotify + libpulseaudio + wayland + ]; + + dontConfigure = true; + dontBuild = true; + # We invoke makeWrapper manually and splice in gappsWrapperArgs ourselves. + dontWrapGApps = true; + + installPhase = '' + runHook preInstall + + # The SUID chrome-sandbox can't be made setuid in the Nix store; drop it so + # Electron falls back to the user-namespace sandbox. + rm -f chrome-sandbox + + mkdir -p $out/share/zennotes + cp -r . $out/share/zennotes + + # Icons + desktop entry ship inside the tarball's arch-extras tree. + for icon in $out/share/zennotes/resources/arch-extras/icons/*.png; do + size="$(basename "$icon" .png)" + install -Dm644 "$icon" "$out/share/icons/hicolor/$size/apps/${finalAttrs.pname}.png" + done + + makeWrapper $out/share/zennotes/ZenNotes $out/bin/${finalAttrs.pname} \ + "''${gappsWrapperArgs[@]}" \ + --add-flags "\''${NIXOS_OZONE_WL:+\''${WAYLAND_DISPLAY:+--ozone-platform-hint=auto}}" \ + ${lib.optionalString (commandLineArgs != "") "--add-flags ${lib.escapeShellArg commandLineArgs}"} + + runHook postInstall + ''; + + desktopItems = [ + (makeDesktopItem { + name = finalAttrs.pname; + desktopName = "ZenNotes"; + exec = "${finalAttrs.pname} %U"; + icon = finalAttrs.pname; + comment = "Keyboard-first local Markdown notes"; + categories = [ + "Office" + "Utility" + "TextEditor" + ]; + startupWMClass = "ZenNotes"; + mimeTypes = [ + "text/markdown" + "x-scheme-handler/zennotes" + ]; + }) + ]; + + meta = { + description = "Keyboard-first local Markdown notes with Vim motions, diagrams, and MCP integration (prebuilt binary)"; + homepage = "https://zennotes.org/"; + changelog = "https://github.com/ZenNotes/zennotes/releases/tag/v${finalAttrs.version}"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [justkrysteq]; + mainProgram = finalAttrs.pname; + sourceProvenance = with lib.sourceTypes; [binaryNativeCode]; + platforms = ["x86_64-linux"]; + }; + }) From 61bb3193f4734e9f8eba7ff05f9f1b7ce5f456b0 Mon Sep 17 00:00:00 2001 From: pimkoter Date: Tue, 21 Jul 2026 12:01:19 +0200 Subject: [PATCH 2/6] feature(nix-server) implemented services.zennotes added zennotes.nixosModules.server --- flake.nix | 55 +++++++++-------- packaging/nix/server-module.nix | 104 ++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 28 deletions(-) create mode 100644 packaging/nix/server-module.nix diff --git a/flake.nix b/flake.nix index 83a3b224..520ff6f5 100644 --- a/flake.nix +++ b/flake.nix @@ -5,39 +5,35 @@ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; }; - outputs = { nixpkgs, ... }: - let - systems = nixpkgs.lib.platforms.linux ++ nixpkgs.lib.platforms.darwin; + outputs = {nixpkgs, ...}: let + systems = nixpkgs.lib.platforms.linux ++ nixpkgs.lib.platforms.darwin; - forAllSystems = nixpkgs.lib.genAttrs systems; - in - { - packages = forAllSystems (system: - let - pkgs = nixpkgs.legacyPackages.${system}; - zennotes-server = pkgs.callPackage ./packaging/nix/package-server.nix { }; - in - { inherit zennotes-server; } + forAllSystems = nixpkgs.lib.genAttrs systems; + in { + packages = forAllSystems ( + system: let + pkgs = nixpkgs.legacyPackages.${system}; + zennotes-server = pkgs.callPackage ./packaging/nix/package-server.nix {}; + in + {inherit zennotes-server;} # The desktop package wraps the prebuilt linux-x64 release tarball, so it # only exists on x86_64-linux; elsewhere the server is the default. // ( - if system == "x86_64-linux" then - let - zennotes-desktop = pkgs.callPackage ./packaging/nix/package-desktop.nix { }; - in - { - inherit zennotes-desktop; - default = zennotes-desktop; - } - else - { default = zennotes-server; } + if system == "x86_64-linux" + then let + zennotes-desktop = pkgs.callPackage ./packaging/nix/package-desktop.nix {}; + in { + inherit zennotes-desktop; + default = zennotes-desktop; + } + else {default = zennotes-server;} ) - ); + ); - devShell = forAllSystems (system: - let - pkgs = nixpkgs.legacyPackages.${system}; - in + devShell = forAllSystems ( + system: let + pkgs = nixpkgs.legacyPackages.${system}; + in pkgs.mkShell { buildInputs = with pkgs; [ go @@ -50,6 +46,9 @@ export ELECTRON_SKIP_BINARY_DOWNLOAD=1 ''; } - ); + ); + nixosModules = { + server = import ./packaging/nix/server-module.nix; }; + }; } diff --git a/packaging/nix/server-module.nix b/packaging/nix/server-module.nix new file mode 100644 index 00000000..adad172a --- /dev/null +++ b/packaging/nix/server-module.nix @@ -0,0 +1,104 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; let + cfg = config.services.zennotes; +in { + options.services.zennotes = { + enable = mkEnableOption "ZenNotes self-hosted server"; + + package = mkOption { + type = types.package; + default = pkgs.callPackage ./package-server.nix {}; + description = "The zennotes server package to use."; + }; + + dataDir = mkOption { + type = types.str; + default = "/var/lib/zennotes"; + description = "Directory to store server data and auth tokens."; + }; + + vaultPath = mkOption { + type = types.str; + default = "/var/lib/zennotes/vault"; + description = "Path to the markdown vault directory."; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Whether to open the port in the firewall."; + }; + + port = mkOption { + type = types.port; + default = 7878; + description = "Port the ZenNotes server listens on."; + }; + + bindAddress = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Address the server binds to."; + }; + + extraEnvironment = mkOption { + type = types.attrsOf types.str; + default = {}; + description = "Extra environment variables for the ZenNotes server."; + }; + }; + + config = mkIf cfg.enable { + # Ensure state directories exist with proper permissions + systemd.tmpfiles.rules = [ + "d '${cfg.dataDir}' 0750 zennotes zennotes -" + "d '${cfg.vaultPath}' 0770 zennotes zennotes -" + ]; + + users.users.zennotes = { + isSystemUser = true; + group = "zennotes"; + description = "ZenNotes server user"; + home = cfg.dataDir; + }; + users.groups.zennotes = {}; + + systemd.services.zennotes = { + description = "ZenNotes Self-Hosted Markdown Server"; + wantedBy = ["multi-user.target"]; + after = ["network.target"]; + + serviceConfig = { + Type = "simple"; + User = "zennotes"; + Group = "zennotes"; + WorkingDirectory = cfg.dataDir; + # Make sure 'zennotes-server' matches the actual binary name output by your package-server.nix + ExecStart = "${cfg.package}/bin/zennotes-server"; + Restart = "on-failure"; + + # Security hardening options + ProtectSystem = "strict"; + ProtectHome = true; + NoNewPrivileges = true; + ReadWritePaths = [cfg.dataDir cfg.vaultPath]; + }; + + environment = + { + PORT = toString cfg.port; + ZENNOTES_BIND = "${cfg.bindAddress}:${toString cfg.port}"; + ZENNOTES_DEFAULT_VAULT_PATH = cfg.vaultPath; + ZENNOTES_CONFIG_PATH = "${cfg.dataDir}/config.toml"; + } + // cfg.extraEnvironment; + }; + + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [cfg.port]; + }; +} From 23c24075f8445fca967e168272bcc9cf34b622f6 Mon Sep 17 00:00:00 2001 From: pimkoter Date: Tue, 21 Jul 2026 12:23:52 +0200 Subject: [PATCH 3/6] fix(nix) fixed wrong hash in ./packaging/nix/release-data.json --- packaging/nix/release-data.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/nix/release-data.json b/packaging/nix/release-data.json index 4e411a2b..22bd6159 100644 --- a/packaging/nix/release-data.json +++ b/packaging/nix/release-data.json @@ -1,6 +1,6 @@ { "version": "2.14.0", - "hash": "sha256-PdTD3oWxbqJWPwiZTjuYSvnWRn66d8wC73VXWMjQHm8=", + "hash": "sha256-C+doF20/PoCyCtueH9VI3Pb9CA6jOeQP1mGA1Lwd2mQ=", "npmDepsHash": "sha256-7dchbcGAZm+PlVsES76sYD9NOqeCulEKC7S0zLERvvY=", "vendorHash": "sha256-ZdOHC2JldvnKSDUFnBUJrKD4F1IWfvYJBksgeDnU9cw=", "desktopHash": "sha256-eL9UL5+BD4+kNfLBsagSbMAX5mrqlWI6Up2HbM1uvHA=" From 9aa7d9deb4068a58df4f9df0f976b26602ef43d9 Mon Sep 17 00:00:00 2001 From: pimkoter Date: Tue, 21 Jul 2026 12:52:57 +0200 Subject: [PATCH 4/6] feature(nix-server) added additional config features for services.zennotes fixed startup issues --- packaging/nix/server-module.nix | 115 +++++++++++++++++++------------- 1 file changed, 70 insertions(+), 45 deletions(-) diff --git a/packaging/nix/server-module.nix b/packaging/nix/server-module.nix index adad172a..5d6885a0 100644 --- a/packaging/nix/server-module.nix +++ b/packaging/nix/server-module.nix @@ -51,54 +51,79 @@ in { default = {}; description = "Extra environment variables for the ZenNotes server."; }; - }; - config = mkIf cfg.enable { - # Ensure state directories exist with proper permissions - systemd.tmpfiles.rules = [ - "d '${cfg.dataDir}' 0750 zennotes zennotes -" - "d '${cfg.vaultPath}' 0770 zennotes zennotes -" - ]; - - users.users.zennotes = { - isSystemUser = true; - group = "zennotes"; - description = "ZenNotes server user"; - home = cfg.dataDir; + settings = mkOption { + type = types.submodule { + freeformType = (pkgs.formats.toml {}).type; + }; + default = {}; + description = '' + Declarative settings for ZenNotes. Translated directly into config.toml. + ''; }; - users.groups.zennotes = {}; - - systemd.services.zennotes = { - description = "ZenNotes Self-Hosted Markdown Server"; - wantedBy = ["multi-user.target"]; - after = ["network.target"]; - - serviceConfig = { - Type = "simple"; - User = "zennotes"; - Group = "zennotes"; - WorkingDirectory = cfg.dataDir; - # Make sure 'zennotes-server' matches the actual binary name output by your package-server.nix - ExecStart = "${cfg.package}/bin/zennotes-server"; - Restart = "on-failure"; - - # Security hardening options - ProtectSystem = "strict"; - ProtectHome = true; - NoNewPrivileges = true; - ReadWritePaths = [cfg.dataDir cfg.vaultPath]; + }; + + config = mkIf cfg.enable ( + let + configFile = (pkgs.formats.toml {}).generate "config.toml" cfg.settings; + in { + # Ensure state directories exist with proper permissions + systemd.tmpfiles.rules = [ + "d '${cfg.dataDir}' 0750 zennotes zennotes -" + "d '${cfg.vaultPath}' 0770 zennotes zennotes -" + ]; + + users.users.zennotes = { + isSystemUser = true; + group = "zennotes"; + description = "ZenNotes server user"; + home = cfg.dataDir; }; + users.groups.zennotes = {}; - environment = - { - PORT = toString cfg.port; - ZENNOTES_BIND = "${cfg.bindAddress}:${toString cfg.port}"; - ZENNOTES_DEFAULT_VAULT_PATH = cfg.vaultPath; - ZENNOTES_CONFIG_PATH = "${cfg.dataDir}/config.toml"; - } - // cfg.extraEnvironment; - }; + systemd.services.zennotes = { + description = "ZenNotes Self-Hosted Markdown Server"; + wantedBy = ["multi-user.target"]; + after = ["network.target"]; - networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [cfg.port]; - }; + serviceConfig = { + Type = "simple"; + User = "zennotes"; + Group = "zennotes"; + WorkingDirectory = cfg.dataDir; + ExecStart = "${cfg.package}/bin/zennotes-server"; + Restart = "on-failure"; + + # Systemd automatically creates these directories as root, + # assigns them to user/group zennotes, and sets permissions BEFORE dropping privileges. + StateDirectory = "zennotes"; # Creates /var/lib/zennotes (0750) + + # Security hardening options + ProtectSystem = "strict"; + ProtectHome = true; + NoNewPrivileges = true; + ReadWritePaths = [cfg.dataDir cfg.vaultPath]; + }; + + # Safe preStart: No root actions (chown/mkdir) required! + preStart = '' + if [ ! -f ${cfg.dataDir}/config.toml ]; then + cp ${configFile} ${cfg.dataDir}/config.toml + chmod 0640 ${cfg.dataDir}/config.toml + fi + ''; + + environment = + { + PORT = toString cfg.port; + ZENNOTES_BIND = "${cfg.bindAddress}:${toString cfg.port}"; + ZENNOTES_DEFAULT_VAULT_PATH = cfg.vaultPath; + ZENNOTES_CONFIG_PATH = "${cfg.dataDir}/config.toml"; + } + // cfg.extraEnvironment; + }; + + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [cfg.port]; + } + ); } From df19d5172a092313f0a054308bb245bfdf4c5254 Mon Sep 17 00:00:00 2001 From: pimkoter Date: Tue, 21 Jul 2026 20:15:05 +0200 Subject: [PATCH 5/6] added zennotes/vault to initdir --- packaging/nix/server-module.nix | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packaging/nix/server-module.nix b/packaging/nix/server-module.nix index 5d6885a0..e8b9c1d7 100644 --- a/packaging/nix/server-module.nix +++ b/packaging/nix/server-module.nix @@ -96,7 +96,10 @@ in { # Systemd automatically creates these directories as root, # assigns them to user/group zennotes, and sets permissions BEFORE dropping privileges. - StateDirectory = "zennotes"; # Creates /var/lib/zennotes (0750) + StateDirectory = [ + "zennotes" + "zennotes/vault" + ]; # Security hardening options ProtectSystem = "strict"; From bb4cc20eb4cb2706ea7d7533f19442f98e6a6ea0 Mon Sep 17 00:00:00 2001 From: pimkoter Date: Tue, 21 Jul 2026 20:21:38 +0200 Subject: [PATCH 6/6] fix --- packaging/nix/server-module.nix | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packaging/nix/server-module.nix b/packaging/nix/server-module.nix index e8b9c1d7..d11c003b 100644 --- a/packaging/nix/server-module.nix +++ b/packaging/nix/server-module.nix @@ -67,12 +67,6 @@ in { let configFile = (pkgs.formats.toml {}).generate "config.toml" cfg.settings; in { - # Ensure state directories exist with proper permissions - systemd.tmpfiles.rules = [ - "d '${cfg.dataDir}' 0750 zennotes zennotes -" - "d '${cfg.vaultPath}' 0770 zennotes zennotes -" - ]; - users.users.zennotes = { isSystemUser = true; group = "zennotes"; @@ -94,8 +88,8 @@ in { ExecStart = "${cfg.package}/bin/zennotes-server"; Restart = "on-failure"; - # Systemd automatically creates these directories as root, - # assigns them to user/group zennotes, and sets permissions BEFORE dropping privileges. + # Automatically manages /var/lib/zennotes and its subdirectories + # creating them with correct permissions before applying systemd isolation. StateDirectory = [ "zennotes" "zennotes/vault" @@ -105,14 +99,19 @@ in { ProtectSystem = "strict"; ProtectHome = true; NoNewPrivileges = true; - ReadWritePaths = [cfg.dataDir cfg.vaultPath]; + + # Allow full read/write access to both the data directory and vault path + ReadWritePaths = [ + cfg.dataDir + cfg.vaultPath + ]; }; - # Safe preStart: No root actions (chown/mkdir) required! + # Use cp --no-preserve=mode to make sure the copied file isn't read-only like the store original preStart = '' - if [ ! -f ${cfg.dataDir}/config.toml ]; then - cp ${configFile} ${cfg.dataDir}/config.toml - chmod 0640 ${cfg.dataDir}/config.toml + if [ ! -f "${cfg.dataDir}/config.toml" ]; then + cp --no-preserve=mode ${configFile} "${cfg.dataDir}/config.toml" + chmod 0640 "${cfg.dataDir}/config.toml" fi '';