# ntfy self-hosted notification module — design Date: 2026-07-05 Target machine: jupiter ## Purpose Add a NixOS profile module that runs and configures a self-hosted [ntfy](https://ntfy.sh) notification server on jupiter, with authentication required, a homepage dashboard tile, and out-of-the-box plumbing so Home Assistant automations can publish push notifications. ## Scope decisions (from brainstorming) - **Service:** ntfy (self-hosted server), not Apprise/Gotify/HA-only. - **Access control:** authentication required (`auth-default-access = deny-all`). - **Home Assistant:** the module wires an HA `rest_command` so automations can send notifications out of the box. - **Credentials:** fully manual. The module runs the server and wires the HA plumbing, but does **not** seed users or store any secret. The operator provisions ntfy users/passwords by hand, and the only secret lives in Home Assistant's `secrets.yaml` — never in the world-readable Nix store. ## Module structure New module at `modules/environments/ntfy/default.nix` following the repo's standard profile pattern (`my.profiles.` with `options` + `config = lib.mkIf cfg.enable { ... }`). Registration: 1. Add `./ntfy` to the `imports` list in `modules/environments/default.nix`. 2. Enable via `my.profiles.ntfy.enable = true;` in `machines/jupiter/environments.nix`. ### Options ```nix options.my.profiles.ntfy = with lib; { enable = mkEnableOption "ntfy notification server"; port = mkOption { type = types.port; default = 2586; description = "HTTP port ntfy listens on."; }; topic = mkOption { type = types.str; default = "ha"; description = "Topic Home Assistant publishes notifications to."; }; haIntegration.enable = mkOption { type = types.bool; default = true; description = "Wire a Home Assistant rest_command that publishes to ntfy."; }; }; ``` ## Server configuration ```nix config = lib.mkIf cfg.enable { services.ntfy-sh = { enable = true; settings = { base-url = "http://${hostName}:${toString cfg.port}"; listen-http = ":${toString cfg.port}"; auth-file = "/var/lib/ntfy-sh/user.db"; auth-default-access = "deny-all"; }; }; networking.firewall.allowedTCPPorts = [ cfg.port ]; my.homepage.services = [ { group = "Services"; name = "ntfy"; description = "Push notifications"; href = "http://${hostName}:${toString cfg.port}"; icon = "ntfy.svg"; } ]; }; ``` `hostName` is bound from `config.networking.hostName`, matching the pattern in the home-assistant and paperless modules. ## Home Assistant wiring Applied only when ntfy, the HA integration flag, and the home-assistant profile are all enabled: ```nix lib.mkIf (cfg.enable && cfg.haIntegration.enable && config.my.profiles.home-assistant.enable) { services.home-assistant.config.rest_command.ntfy_send = { url = "http://${hostName}:${toString cfg.port}/${cfg.topic}"; method = "POST"; payload = "{{ message }}"; content_type = "text/plain"; username = "homeassistant"; password = "!secret ntfy_password"; headers = { Title = "{{ title | default('Home Assistant') }}"; Priority = "{{ priority | default('default') }}"; }; }; }; ``` Notes: - `services.home-assistant.config` is an attrset that NixOS merges, so adding `rest_command.ntfy_send` from this module composes with the config the home-assistant module already defines. - `password = "!secret ntfy_password"` is a whole-value `!secret` reference. The upstream home-assistant module unquotes such values when rendering the config (the same mechanism the repo already relies on for `!include`), so the secret resolves from `/var/lib/hass/secrets.yaml` at runtime and never enters the Nix store. See `reference_nixos_ha_yaml_includes`. - `rest_command` is chosen over the `notify` REST platform because ntfy's per-topic URL path plus header-based metadata map cleanly onto rest_command, whereas the notify platform's fixed JSON payload fights ntfy's format. ## Manual provisioning (operator runbook) Because credentials are fully manual, after the first `nixos-rebuild switch` run these once on jupiter: ```bash # Dedicated publisher for Home Assistant, scoped to the ha topic ntfy user add homeassistant # prompts for a password ntfy access homeassistant ha write-only # Admin account for the app / web UI ntfy user add --role=admin admin ``` Then add the homeassistant password to Home Assistant's secrets: ```yaml # /var/lib/hass/secrets.yaml ntfy_password: ``` Restart Home Assistant. Automations can then publish with: ```yaml service: rest_command.ntfy_send data: message: "Garage door left open" title: "Alert" priority: high ``` Subscribers (phone/desktop ntfy app) log in as `admin` (or another user granted read access) to receive messages. ## Testing / verification - Build check: `nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel'` must succeed with the module enabled. - `nixfmt-rfc-style` clean on the new file. - Post-deploy manual verification (documented, not automated): create the users above, publish a test message from HA, confirm it reaches a subscribed client. ## Out of scope - No automated user/token seeding (explicitly deferred to manual provisioning). - No sops-nix setup (repo has dangling `config.sops.secrets` references, but wiring up sops is a separate change and not required here). - No reverse-proxy / TLS termination (`behind-proxy` left default; LAN-only via firewall).