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