An easy-to-host PDS on the ATProtocol, iPhone and MacOS. Maintain control of your keys and data, always.
1{ lib, pkgs, config, ... }:
2
3let
4 cfg = config.services.ezpds;
5
6 # Build the TOML attrset, omitting any null values (currently only
7 # database_url can be null). When null, the relay binary derives the
8 # database path from data_dir.
9 settingsToml = lib.filterAttrs (_: v: v != null) {
10 inherit (cfg.settings) bind_address port data_dir public_url database_url;
11 };
12
13 generatedConfigFile = (pkgs.formats.toml { }).generate "relay.toml" settingsToml;
14
15 # When configFile is set, bypass the Nix-store-generated TOML entirely.
16 # This is the escape hatch for secret injection via agenix or sops-nix.
17 activeConfigFile =
18 if cfg.configFile != null then cfg.configFile else generatedConfigFile;
19
20in
21{
22 options.services.ezpds = {
23 enable = lib.mkEnableOption "ezpds relay server";
24
25 package = lib.mkOption {
26 type = lib.types.package;
27 description = "The ezpds relay package to use.";
28 };
29
30 configFile = lib.mkOption {
31 type = lib.types.nullOr lib.types.str;
32 default = null;
33 description = ''
34 Path to a relay.toml configuration file.
35 When set, all settings.* options are ignored and this path is
36 passed directly to --config. Use with agenix or sops-nix to
37 keep secrets outside the world-readable Nix store.
38
39 When using agenix or sops-nix, ensure the secrets service runs
40 before ezpds to avoid a startup race:
41 systemd.services.ezpds.after = [ "agenix.service" ];
42 systemd.services.ezpds.wants = [ "agenix.service" ];
43 '';
44 };
45
46 settings = {
47 bind_address = lib.mkOption {
48 type = lib.types.str;
49 default = "0.0.0.0";
50 description = "IP address to bind the relay HTTP server to.";
51 };
52
53 port = lib.mkOption {
54 type = lib.types.port;
55 default = 8080;
56 description = "TCP port to bind the relay HTTP server to.";
57 };
58
59 data_dir = lib.mkOption {
60 type = lib.types.str;
61 default = "/var/lib/ezpds";
62 description = ''
63 Path to the relay data directory. Must be writable by the ezpds user.
64 Uses lib.types.str (not lib.types.path) to preserve the value as a
65 literal string and avoid Nix store coercion of runtime paths.
66 '';
67 };
68
69 public_url = lib.mkOption {
70 type = lib.types.str;
71 description = ''
72 Public URL where this relay is reachable (e.g. https://relay.example.com).
73 Required — Nix evaluation fails if this option is not set.
74 '';
75 };
76
77 database_url = lib.mkOption {
78 type = lib.types.nullOr lib.types.str;
79 default = null;
80 description = ''
81 SQLite database URL. When null (the default), the relay derives
82 the database path from data_dir. Omitted from the generated
83 relay.toml when null.
84 '';
85 };
86 };
87 };
88
89 config = lib.mkIf cfg.enable {
90 users.users.ezpds = {
91 isSystemUser = true;
92 group = "ezpds";
93 description = "ezpds relay service user";
94 };
95
96 users.groups.ezpds = { };
97
98 systemd.services.ezpds = {
99 description = "ezpds relay server";
100 wantedBy = [ "multi-user.target" ];
101 after = [ "network.target" ];
102
103 serviceConfig = {
104 User = "ezpds";
105 Group = "ezpds";
106 ExecStart = "${cfg.package}/bin/relay --config '${activeConfigFile}'";
107 StateDirectory = "ezpds";
108 StateDirectoryMode = "0750";
109 # Extend write access to custom data_dir paths. When data_dir is the
110 # default (/var/lib/ezpds), StateDirectory already covers it and this
111 # is a no-op. For any other path, ProtectSystem=strict would otherwise
112 # block all writes at runtime.
113 ReadWritePaths = [ cfg.settings.data_dir ];
114 Restart = "on-failure";
115 PrivateTmp = true;
116 ProtectSystem = "strict";
117 ProtectHome = true;
118 NoNewPrivileges = true;
119 };
120 };
121 };
122}