{ config, lib, pkgs, homeyConfig, ... }: # Ntfy — self-hosted push notification server. # # Mobile app (Android/iOS) connects to https://ntfy.zakobar.com with a token # and subscribes to the "alerts" topic. Uptime Kuma and Grafana send alerts # to that topic when services go down. # # Auth model: # - Web UI: public-facing but ntfy enforces its own auth (deny-all by default) # - Caddy does NOT put forward_auth here; ntfy has native token/password auth # so the mobile app can connect without Authelia SSO complications. # # Setup after first deploy: # 1. Visit https://ntfy.zakobar.com — log in with the admin password from sops. # 2. Create an access token for your phone (Admin → Users & Tokens). # 3. In the Ntfy app: server = https://ntfy.zakobar.com, token = . # 4. Subscribe to the "alerts" topic. # # Volume layout: # /ntfy/auth.db ← user/token database # /ntfy/cache.db ← message cache (for missed messages) # /ntfy/attachments/ ← file attachments # # Secrets consumed from sops: # ntfy/admin_password let cfg = config.homey.ntfy; dataDir = config.homey.storage.mountPoint; domain = homeyConfig.domain; in { options.homey.ntfy = { enable = lib.mkEnableOption "Ntfy push notification server"; port = lib.mkOption { type = lib.types.port; default = 2586; description = "Host port ntfy listens on (bound to 127.0.0.1)."; }; }; config = lib.mkIf cfg.enable { # ----------------------------------------------------------------------- # Secrets # ----------------------------------------------------------------------- sops.secrets."ntfy/admin_password" = { owner = "root"; }; # ----------------------------------------------------------------------- # ntfy-sh native NixOS service # ----------------------------------------------------------------------- services.ntfy-sh = { enable = true; settings = { # Bind to localhost; Caddy reverse-proxies it listen-http = "127.0.0.1:${toString cfg.port}"; base-url = "https://ntfy.${domain}"; # Require auth on all topics — deny unauthenticated access entirely auth-default-access = "deny-all"; # Persistent state on external HD auth-file = "${dataDir}/ntfy/auth.db"; cache-file = "${dataDir}/ntfy/cache.db"; attachment-root = "${dataDir}/ntfy/attachments"; # Keep messages for 12 hours so the app catches up if offline cache-duration = "12h"; # Attachment limits attachment-total-size-limit = "5G"; attachment-file-size-limit = "15M"; attachment-expiry-duration = "3h"; }; }; # ----------------------------------------------------------------------- # Create the admin user on first start (idempotent) # ----------------------------------------------------------------------- systemd.services.ntfy-sh-setup = { description = "Create Ntfy admin user"; wantedBy = [ "multi-user.target" ]; after = [ "ntfy-sh.service" "mnt-data.mount" ]; requires = [ "ntfy-sh.service" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; LoadCredential = "ntfy_admin_password:${config.sops.secrets."ntfy/admin_password".path}"; ExecStart = pkgs.writeShellScript "ntfy-create-admin" '' set -euo pipefail # Wait until ntfy HTTP endpoint is ready (max 60 s) for i in $(seq 1 30); do if ${pkgs.curl}/bin/curl -sf http://127.0.0.1:${toString cfg.port}/v1/health > /dev/null 2>&1; then break fi sleep 2 done PASS=$(cat "$CREDENTIALS_DIRECTORY/ntfy_admin_password") # ntfy user commands need the config file to find the auth database. # The NixOS ntfy-sh module writes config to /etc/ntfy-sh/server.yml. NTFY="${pkgs.ntfy-sh}/bin/ntfy user --config /etc/ntfy-sh/server.yml" # ntfy user list exits non-zero if the user DB is empty/doesn't exist; # grep exits non-zero if the pattern is missing. Either means no admin. if $NTFY list 2>/dev/null | grep -qE "^admin\b"; then echo "ntfy-sh-setup: admin user already exists" else echo "$PASS" | $NTFY add --role=admin admin echo "ntfy-sh-setup: admin user created" fi ''; }; }; # Ensure ntfy-sh starts after the external HD is mounted systemd.services.ntfy-sh = { after = lib.mkAfter [ "mnt-data.mount" ]; requires = lib.mkAfter [ "mnt-data.mount" ]; }; # ----------------------------------------------------------------------- # Uptime Kuma monitor for this service # ----------------------------------------------------------------------- homey.monitoring.monitors = [{ name = "Ntfy"; url = "https://ntfy.${domain}/v1/health"; interval = 60; }]; }; }