Our Personal Data Server from scratch!
tranquil.farm
oauth
atproto
pds
rust
postgresql
objectstorage
fun
1self: {
2 lib,
3 pkgs,
4 config,
5 ...
6}: let
7 cfg = config.services.tranquil-pds;
8
9 inherit (lib) types mkOption;
10
11 settingsFormat = pkgs.formats.toml { };
12in {
13 _class = "nixos";
14
15 options.services.tranquil-pds = {
16 enable = lib.mkEnableOption "tranquil-pds AT Protocol personal data server";
17
18 package = mkOption {
19 type = types.package;
20 default = self.packages.${pkgs.stdenv.hostPlatform.system}.tranquil-pds;
21 defaultText = lib.literalExpression "self.packages.\${pkgs.stdenv.hostPlatform.system}.tranquil-pds";
22 description = "The tranquil-pds package to use";
23 };
24
25 user = mkOption {
26 type = types.str;
27 default = "tranquil-pds";
28 description = "User under which tranquil-pds runs";
29 };
30
31 group = mkOption {
32 type = types.str;
33 default = "tranquil-pds";
34 description = "Group under which tranquil-pds runs";
35 };
36
37 dataDir = mkOption {
38 type = types.str;
39 default = "/var/lib/tranquil-pds";
40 description = "Working directory for tranquil-pds. Also expected to be used for data (blobs, backups)";
41 };
42
43 environmentFiles = mkOption {
44 type = types.listOf types.path;
45 default = [ ];
46 description = ''
47 File to load environment variables from. Loaded variables override
48 values set in {option}`environment`.
49
50 Use it to set values of `JWT_SECRET`, `DPOP_SECRET` and `MASTER_KEY`.
51
52 Generate these with:
53 ```
54 openssl rand -base64 48
55 ```
56 '';
57 };
58
59 database.createLocally = mkOption {
60 type = types.bool;
61 default = false;
62 description = ''
63 Create the postgres database and user on the local host.
64 '';
65 };
66
67 settings = mkOption {
68 type = types.submodule {
69 freeformType = settingsFormat.type;
70
71 options = {
72 server = {
73 host = mkOption {
74 type = types.str;
75 default = "127.0.0.1";
76 description = "Host for tranquil-pds to listen on";
77 };
78
79 port = mkOption {
80 type = types.int;
81 default = 3000;
82 description = "Port for tranquil-pds to listen on";
83 };
84
85 hostname = mkOption {
86 type = types.str;
87 default = "";
88 example = "pds.example.com";
89 description = "The public-facing hostname of the PDS";
90 };
91
92 max_blob_size = mkOption {
93 type = types.int;
94 default = 10737418240; # 10 GiB
95 description = "Maximum allowed blob size in bytes.";
96 };
97 };
98
99 frontend = {
100 enabled = lib.mkEnableOption "serving the frontend from the backend. Disable to serve the frontend manually"
101 // { default = true; };
102
103 dir = mkOption {
104 type = types.nullOr types.package;
105 default = self.packages.${pkgs.stdenv.hostPlatform.system}.tranquil-frontend;
106 defaultText = lib.literalExpression "self.packages.\${pkgs.stdenv.hostPlatform.system}.tranquil-frontend";
107 description = "Frontend package to be served by the backend";
108 };
109 };
110
111 storage = {
112 path = mkOption {
113 type = types.path;
114 default = "/var/lib/tranquil-pds/blobs";
115 description = "Directory for storing blobs";
116 };
117 };
118
119 backup = {
120 path = mkOption {
121 type = types.path;
122 default = "/var/lib/tranquil-pds/backups";
123 description = "Directory for storing backups";
124 };
125 };
126
127 email = {
128 sendmail_path = mkOption {
129 type = types.path;
130 default = lib.getExe pkgs.system-sendmail;
131 description = "Path to the sendmail executable to use for sending emails.";
132 };
133 };
134
135 signal = {
136 cli_path = mkOption {
137 type = types.path;
138 default = lib.getExe pkgs.signal-cli;
139 description = "Path to the signal-cli executable to use for sending Signal notifications.";
140 };
141 };
142 };
143 };
144
145 description = ''
146 Configuration options to set for the service. Secrets should be
147 specified using {option}`environmentFile`.
148
149 Refer to <https://tangled.org/tranquil.farm/tranquil-pds/blob/main/example.toml>
150 for available configuration options.
151 '';
152 };
153 };
154
155 config = lib.mkIf cfg.enable (
156 lib.mkMerge [
157 (lib.mkIf cfg.database.createLocally {
158 services.postgresql = {
159 enable = true;
160 ensureDatabases = [ cfg.user ];
161 ensureUsers = [
162 {
163 name = cfg.user;
164 ensureDBOwnership = true;
165 }
166 ];
167 };
168
169 services.tranquil-pds.settings.database.url =
170 lib.mkDefault "postgresql:///${cfg.user}?host=/run/postgresql";
171
172 systemd.services.tranquil-pds = {
173 requires = [ "postgresql.service" ];
174 after = [ "postgresql.service" ];
175 };
176 })
177
178 {
179 users.users.${cfg.user} = {
180 isSystemUser = true;
181 inherit (cfg) group;
182 home = cfg.dataDir;
183 };
184
185 users.groups.${cfg.group} = { };
186
187 systemd.tmpfiles.settings."tranquil-pds" =
188 lib.genAttrs
189 [
190 cfg.dataDir
191 cfg.settings.storage.path
192 cfg.settings.backup.path
193 ]
194 (_: {
195 d = {
196 mode = "0750";
197 inherit (cfg) user group;
198 };
199 });
200
201 environment.etc = {
202 "tranquil-pds/config.toml".source = settingsFormat.generate "tranquil-pds.toml" cfg.settings;
203 };
204
205 systemd.services.tranquil-pds = {
206 description = "Tranquil PDS - AT Protocol Personal Data Server";
207 after = [ "network-online.target" ];
208 wants = [ "network-online.target" ];
209 wantedBy = [ "multi-user.target" ];
210
211 serviceConfig = {
212 User = cfg.user;
213 Group = cfg.group;
214 ExecStart = lib.getExe cfg.package;
215 Restart = "on-failure";
216 RestartSec = 5;
217
218 WorkingDirectory = cfg.dataDir;
219 StateDirectory = "tranquil-pds";
220
221 EnvironmentFile = cfg.environmentFiles;
222
223 NoNewPrivileges = true;
224 ProtectSystem = "strict";
225 ProtectHome = true;
226 PrivateTmp = true;
227 PrivateDevices = true;
228 ProtectKernelTunables = true;
229 ProtectKernelModules = true;
230 ProtectControlGroups = true;
231 RestrictAddressFamilies = [
232 "AF_INET"
233 "AF_INET6"
234 "AF_UNIX"
235 ];
236 RestrictNamespaces = true;
237 LockPersonality = true;
238 MemoryDenyWriteExecute = true;
239 RestrictRealtime = true;
240 RestrictSUIDSGID = true;
241 RemoveIPC = true;
242
243 ReadWritePaths = [
244 cfg.settings.storage.path
245 cfg.settings.backup.path
246 ];
247 };
248 };
249 }
250 ]
251 );
252}