An easy-to-host PDS on the ATProtocol, iPhone and MacOS. Maintain control of your keys and data, always.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Human Test Plan: MM-135 NixOS Module for Relay Deployment#

Generated from test-analyst review of implementation plan docs/implementation-plans/2026-03-09-MM-135/.

Automated coverage: 18/18 acceptance criteria verified by nix eval smoke tests and structural checks.

This plan covers: Manual verification steps for criteria that require a Linux builder, runtime testing, or human code review.


Prerequisites#

  • NixOS system or VM available for runtime testing (Linux required for E2E phases)
  • Development shell activated: nix develop --impure --accept-flake-config
  • All Phase 3 smoke tests passing (run commands from docs/implementation-plans/2026-03-09-MM-135/phase_03.md Tasks 1–5)
  • just nix-check exits 0

Phase 1: TOML Content Verification (Linux Only)#

Step Action Expected
1.1 On a Linux builder, run: nix eval --impure --accept-flake-config --raw --expr 'let flake = builtins.getFlake (builtins.toString ./.); evalNixpkgs = builtins.getFlake "nixpkgs"; sys = evalNixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ flake.nixosModules.default { services.ezpds.enable = true; services.ezpds.settings.public_url = "https://relay.example.com"; } ]; }; execStart = sys.config.systemd.services.ezpds.serviceConfig.ExecStart; configPath = builtins.elemAt (builtins.match ".* --config (.*)" execStart) 0; in builtins.readFile configPath' Output is valid TOML containing exactly: bind_address = "0.0.0.0", data_dir = "/var/lib/ezpds", port = 8080, public_url = "https://relay.example.com". No database_url key. No [blobs], [oauth], or [iroh] sections.
1.2 Repeat step 1.1 but add services.ezpds.settings.database_url = "sqlite:///var/lib/ezpds/custom.db"; to the module config Output TOML additionally contains database_url = "sqlite:///var/lib/ezpds/custom.db".

Phase 2: Group and StateDirectory Eval Verification#

Step Action Expected
2.1 Run: nix eval --impure --accept-flake-config --json --expr 'let flake = builtins.getFlake (builtins.toString ./.); evalNixpkgs = builtins.getFlake "nixpkgs"; sys = evalNixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ flake.nixosModules.default { services.ezpds.enable = true; services.ezpds.settings.public_url = "https://relay.example.com"; } ]; }; in builtins.hasAttr "ezpds" sys.config.users.groups' Output: true
2.2 Run: nix eval --impure --accept-flake-config --raw --expr 'let flake = builtins.getFlake (builtins.toString ./.); evalNixpkgs = builtins.getFlake "nixpkgs"; sys = evalNixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ flake.nixosModules.default { services.ezpds.enable = true; services.ezpds.settings.public_url = "https://relay.example.com"; } ]; }; in sys.config.systemd.services.ezpds.serviceConfig.StateDirectory' Output: ezpds

Phase 3: Bare Module Success Path#

Step Action Expected
3.1 Run: nix eval --impure --accept-flake-config --raw --expr 'let evalNixpkgs = builtins.getFlake "nixpkgs"; pkgs = import evalNixpkgs { system = "x86_64-linux"; }; sys = evalNixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ (import ./nix/module.nix) { services.ezpds.enable = true; services.ezpds.package = pkgs.hello; services.ezpds.settings.public_url = "https://relay.example.com"; } ]; }; in sys.config.systemd.services.ezpds.serviceConfig.ExecStart' Eval succeeds (exit code 0). Output is a string containing /bin/relay --config /nix/store/...-relay.toml. Confirms the bare module is importable when package is explicitly set.

Phase 4: Scope Boundaries Code Review#

Step Action Expected
4.1 Open nix/module.nix and search for blobs, oauth, iroh Zero matches. No options for out-of-scope config sections exist.
4.2 Review the options.services.ezpds block and confirm it declares exactly: enable, package, configFile, settings.bind_address, settings.port, settings.data_dir, settings.public_url, settings.database_url Eight options total. No additional stub options for future sections.

Phase 5: Systemd Hardening Code Review#

Step Action Expected
5.1 Open nix/module.nix and review the serviceConfig block Block contains: ProtectSystem = "strict", PrivateTmp = true, ProtectHome = true, NoNewPrivileges = true, StateDirectoryMode = "0750", Restart = "on-failure"

End-to-End: NixOS VM Runtime Test#

Purpose: Validate that the module produces a functioning systemd service on a live NixOS system, covering the gap between eval-time correctness and runtime behavior.

Step Action Expected
E2E.1 Create a NixOS configuration that imports nixosModules.default with services.ezpds.enable = true and services.ezpds.settings.public_url = "https://relay.example.com". Build a VM: nixos-rebuild build-vm --flake .#<config-name> VM image builds successfully.
E2E.2 Boot the VM and run systemctl status ezpds Service is loaded and attempted to start.
E2E.3 Run id ezpds inside the VM User exists, is a system user, member of group ezpds.
E2E.4 Run stat /var/lib/ezpds inside the VM Directory exists, owned by ezpds:ezpds, mode 0750.
E2E.5 Run systemd-analyze security ezpds inside the VM Confirms hardening directives active: NoNewPrivileges, ProtectSystem=strict, ProtectHome=yes, PrivateTmp=yes.
E2E.6 Run cat $(systemctl show ezpds -p ExecStart --value | grep -oP '/nix/store/[^ ]+relay\.toml') inside the VM TOML file contents match expected defaults with the configured public_url.

End-to-End: configFile Escape Hatch Runtime Test#

Purpose: Validate that configFile properly bypasses generated TOML at runtime, critical for secret injection workflows (agenix/sops-nix).

Step Action Expected
CF.1 In a NixOS config, set services.ezpds.configFile = "/etc/ezpds/relay.toml" and create that file with custom contents (e.g., public_url = "https://custom.example.com", port = 9090). Config file created at the specified path.
CF.2 Run systemctl show ezpds -p ExecStart ExecStart ends with --config /etc/ezpds/relay.toml (not a Nix store path).
CF.3 Start the service and check relay logs for the port binding Relay attempts to bind on port 9090 (from the custom config), not 8080 (the NixOS module default).

Traceability#

Acceptance Criterion Automated Test Manual Step
MM-135.AC1.1 (file exists) git ls-files nix/module.nix
MM-135.AC1.2 (options declared) nix eval --impure --expr 'builtins.typeOf ...' + Phase 3 Task 2 Step 1 Phase 4 step 4.2
MM-135.AC1.3 (defaults match) Phase 3 Task 2 Step 1 + Task 4 Steps 1–2 Phase 1 step 1.1
MM-135.AC1.4 (missing public_url fails) Phase 3 Task 3 Step 1
MM-135.AC1.5 (missing package fails) Phase 3 Task 3 Step 2
MM-135.AC2.1 (TOML keys present) Phase 3 Task 4 Step 2 Phase 1 step 1.1
MM-135.AC2.2 (database_url absent when null) Phase 3 Task 4 Step 1 Phase 1 step 1.1
MM-135.AC2.3 (database_url present when set) Phase 3 Task 4 Step 3 Phase 1 step 1.2
MM-135.AC2.4 (ExecStart --config) Phase 3 Task 2 Step 1 E2E.6
MM-135.AC3.1 (configFile overrides) Phase 3 Task 5 Step 1 CF.2
MM-135.AC3.2 (settings isolation) Phase 3 Task 5 Step 2 CF.3
MM-135.AC4.1 (system user) Phase 3 Task 2 Step 2 E2E.3
MM-135.AC4.2 (group defined) Structural (source review) Phase 2 step 2.1
MM-135.AC4.3 (StateDirectory) Structural (source review) Phase 2 step 2.2, E2E.4
MM-135.AC5.1 (flake output) nix flake check + nix eval .#nixosModules --apply builtins.attrNames
MM-135.AC5.2 (package default) Phase 3 Task 2 Step 1
MM-135.AC5.3 (bare module importable) Phase 3 Task 3 Step 2 (inverse proof) Phase 3 step 3.1
MM-135.AC6.1 (no stub sections) Structural (grep returns no matches) Phase 4 steps 4.1–4.2