tangled
alpha
login
or
join now
tranquil.farm
/
tranquil-pds
149
fork
atom
Our Personal Data Server from scratch!
tranquil.farm
oauth
atproto
pds
rust
postgresql
objectstorage
fun
149
fork
atom
overview
issues
19
pulls
2
pipelines
feat: actual good config from Isabel
lewis.moe
4 weeks ago
20bbe735
ee0f599e
+214
-689
4 changed files
expand all
collapse all
unified
split
default.nix
flake.nix
module.nix
test.nix
+1
default.nix
···
36
36
37
37
meta = {
38
38
license = lib.licenses.agpl3Plus;
39
39
+
mainProgram = "tranquil-pds";
39
40
};
40
41
}
+7
-1
flake.nix
···
30
30
default = pkgs.callPackage ./shell.nix {};
31
31
});
32
32
33
33
-
nixosModules.default = import ./module.nix self;
33
33
+
nixosModules = {
34
34
+
default = self.nixosModules.tranquil-pds;
35
35
+
tranquil-pds = {
36
36
+
_file = "${self.outPath}/flake.nix#nixosModules.tranquil-pds";
37
37
+
imports = [(import ./module.nix self)];
38
38
+
};
39
39
+
};
34
40
35
41
checks.x86_64-linux.integration = import ./test.nix {
36
42
pkgs = nixpkgs.legacyPackages.x86_64-linux;
+198
-678
module.nix
···
1
1
-
self: {
2
2
-
config,
1
1
+
self:
2
2
+
{
3
3
lib,
4
4
pkgs,
5
5
+
config,
5
6
...
6
6
-
}: let
7
7
+
}:
8
8
+
let
7
9
cfg = config.services.tranquil-pds;
8
10
9
9
-
optionalStr = lib.types.nullOr lib.types.str;
10
10
-
optionalInt = lib.types.nullOr lib.types.int;
11
11
-
optionalPath = lib.types.nullOr lib.types.str;
12
12
-
13
13
-
filterNulls = lib.filterAttrs (_: v: v != null);
14
14
-
15
15
-
boolToStr = b:
16
16
-
if b
17
17
-
then "true"
18
18
-
else "false";
11
11
+
inherit (lib) types mkOption;
19
12
20
20
-
backendUrl = "http://127.0.0.1:${toString cfg.settings.server.port}";
13
13
+
backendUrl = "http://127.0.0.1:${toString cfg.settings.SERVER_PORT}";
21
14
22
15
useACME = cfg.nginx.enableACME && cfg.nginx.useACMEHost == null;
23
16
hasSSL = useACME || cfg.nginx.useACMEHost != null;
24
24
-
25
25
-
settingsToEnv = settings: let
26
26
-
raw = {
27
27
-
SERVER_HOST = settings.server.host;
28
28
-
SERVER_PORT = settings.server.port;
29
29
-
PDS_HOSTNAME = settings.server.pdsHostname;
30
30
-
31
31
-
DATABASE_URL = settings.database.url;
32
32
-
DATABASE_MAX_CONNECTIONS = settings.database.maxConnections;
33
33
-
DATABASE_MIN_CONNECTIONS = settings.database.minConnections;
34
34
-
DATABASE_ACQUIRE_TIMEOUT_SECS = settings.database.acquireTimeoutSecs;
35
35
-
36
36
-
BLOB_STORAGE_BACKEND = settings.storage.blobBackend;
37
37
-
BLOB_STORAGE_PATH = settings.storage.blobPath;
38
38
-
S3_ENDPOINT = settings.storage.s3Endpoint;
39
39
-
AWS_REGION = settings.storage.awsRegion;
40
40
-
S3_BUCKET = settings.storage.s3Bucket;
41
41
-
42
42
-
BACKUP_ENABLED = boolToStr settings.backup.enable;
43
43
-
BACKUP_STORAGE_BACKEND = settings.backup.backend;
44
44
-
BACKUP_STORAGE_PATH = settings.backup.path;
45
45
-
BACKUP_S3_BUCKET = settings.backup.s3Bucket;
46
46
-
BACKUP_RETENTION_COUNT = settings.backup.retentionCount;
47
47
-
BACKUP_INTERVAL_SECS = settings.backup.intervalSecs;
48
48
-
49
49
-
VALKEY_URL = settings.cache.valkeyUrl;
50
50
-
51
51
-
TRANQUIL_PDS_ALLOW_INSECURE_SECRETS = boolToStr settings.security.allowInsecureSecrets;
52
52
-
53
53
-
PLC_DIRECTORY_URL = settings.plc.directoryUrl;
54
54
-
PLC_TIMEOUT_SECS = settings.plc.timeoutSecs;
55
55
-
PLC_CONNECT_TIMEOUT_SECS = settings.plc.connectTimeoutSecs;
56
56
-
PLC_ROTATION_KEY = settings.plc.rotationKey;
57
57
-
58
58
-
DID_CACHE_TTL_SECS = settings.did.cacheTtlSecs;
59
59
-
60
60
-
CRAWLERS = settings.relay.crawlers;
61
61
-
62
62
-
FIREHOSE_BUFFER_SIZE = settings.firehose.bufferSize;
63
63
-
FIREHOSE_MAX_LAG = settings.firehose.maxLag;
64
64
-
65
65
-
NOTIFICATION_BATCH_SIZE = settings.notifications.batchSize;
66
66
-
NOTIFICATION_POLL_INTERVAL_MS = settings.notifications.pollIntervalMs;
67
67
-
MAIL_FROM_ADDRESS = settings.notifications.mailFromAddress;
68
68
-
MAIL_FROM_NAME = settings.notifications.mailFromName;
69
69
-
SENDMAIL_PATH = settings.notifications.sendmailPath;
70
70
-
SIGNAL_CLI_PATH = settings.notifications.signalCliPath;
71
71
-
SIGNAL_SENDER_NUMBER = settings.notifications.signalSenderNumber;
72
72
-
73
73
-
MAX_BLOB_SIZE = settings.limits.maxBlobSize;
74
74
-
75
75
-
ACCEPTING_REPO_IMPORTS = boolToStr settings.import.accepting;
76
76
-
MAX_IMPORT_SIZE = settings.import.maxSize;
77
77
-
MAX_IMPORT_BLOCKS = settings.import.maxBlocks;
78
78
-
SKIP_IMPORT_VERIFICATION = boolToStr settings.import.skipVerification;
79
79
-
80
80
-
INVITE_CODE_REQUIRED = boolToStr settings.registration.inviteCodeRequired;
81
81
-
AVAILABLE_USER_DOMAINS = settings.registration.availableUserDomains;
82
82
-
ENABLE_SELF_HOSTED_DID_WEB = boolToStr settings.registration.enableSelfHostedDidWeb;
83
83
-
84
84
-
PRIVACY_POLICY_URL = settings.metadata.privacyPolicyUrl;
85
85
-
TERMS_OF_SERVICE_URL = settings.metadata.termsOfServiceUrl;
86
86
-
CONTACT_EMAIL = settings.metadata.contactEmail;
87
87
-
88
88
-
DISABLE_RATE_LIMITING = boolToStr settings.rateLimiting.disable;
89
89
-
90
90
-
SCHEDULED_DELETE_CHECK_INTERVAL_SECS = settings.scheduling.deleteCheckIntervalSecs;
91
91
-
92
92
-
REPORT_SERVICE_URL = settings.moderation.reportServiceUrl;
93
93
-
REPORT_SERVICE_DID = settings.moderation.reportServiceDid;
94
94
-
95
95
-
PDS_AGE_ASSURANCE_OVERRIDE = boolToStr settings.misc.ageAssuranceOverride;
96
96
-
ALLOW_HTTP_PROXY = boolToStr settings.misc.allowHttpProxy;
97
97
-
98
98
-
SSO_GITHUB_ENABLED = boolToStr settings.sso.github.enable;
99
99
-
SSO_GITHUB_CLIENT_ID = settings.sso.github.clientId;
100
100
-
101
101
-
SSO_DISCORD_ENABLED = boolToStr settings.sso.discord.enable;
102
102
-
SSO_DISCORD_CLIENT_ID = settings.sso.discord.clientId;
103
103
-
104
104
-
SSO_GOOGLE_ENABLED = boolToStr settings.sso.google.enable;
105
105
-
SSO_GOOGLE_CLIENT_ID = settings.sso.google.clientId;
106
106
-
107
107
-
SSO_GITLAB_ENABLED = boolToStr settings.sso.gitlab.enable;
108
108
-
SSO_GITLAB_CLIENT_ID = settings.sso.gitlab.clientId;
109
109
-
SSO_GITLAB_ISSUER = settings.sso.gitlab.issuer;
17
17
+
in
18
18
+
{
19
19
+
_class = "nixos";
110
20
111
111
-
SSO_OIDC_ENABLED = boolToStr settings.sso.oidc.enable;
112
112
-
SSO_OIDC_CLIENT_ID = settings.sso.oidc.clientId;
113
113
-
SSO_OIDC_ISSUER = settings.sso.oidc.issuer;
114
114
-
SSO_OIDC_NAME = settings.sso.oidc.name;
115
115
-
116
116
-
SSO_APPLE_ENABLED = boolToStr settings.sso.apple.enable;
117
117
-
SSO_APPLE_CLIENT_ID = settings.sso.apple.clientId;
118
118
-
SSO_APPLE_TEAM_ID = settings.sso.apple.teamId;
119
119
-
SSO_APPLE_KEY_ID = settings.sso.apple.keyId;
120
120
-
};
121
121
-
in
122
122
-
lib.mapAttrs (_: v: toString v) (filterNulls raw);
123
123
-
in {
124
21
options.services.tranquil-pds = {
125
22
enable = lib.mkEnableOption "tranquil-pds AT Protocol personal data server";
126
23
127
127
-
package = lib.mkOption {
128
128
-
type = lib.types.package;
24
24
+
package = mkOption {
25
25
+
type = types.package;
129
26
default = self.packages.${pkgs.stdenv.hostPlatform.system}.tranquil-pds;
130
27
defaultText = lib.literalExpression "self.packages.\${pkgs.stdenv.hostPlatform.system}.tranquil-pds";
131
28
description = "The tranquil-pds package to use";
132
29
};
133
30
134
134
-
user = lib.mkOption {
135
135
-
type = lib.types.str;
31
31
+
user = mkOption {
32
32
+
type = types.str;
136
33
default = "tranquil-pds";
137
34
description = "User under which tranquil-pds runs";
138
35
};
139
36
140
140
-
group = lib.mkOption {
141
141
-
type = lib.types.str;
37
37
+
group = mkOption {
38
38
+
type = types.str;
142
39
default = "tranquil-pds";
143
40
description = "Group under which tranquil-pds runs";
144
41
};
145
42
146
146
-
dataDir = lib.mkOption {
147
147
-
type = lib.types.str;
43
43
+
dataDir = mkOption {
44
44
+
type = types.str;
148
45
default = "/var/lib/tranquil-pds";
149
46
description = "Directory for tranquil-pds data (blobs, backups)";
150
47
};
151
48
152
152
-
secretsFile = lib.mkOption {
153
153
-
type = lib.types.nullOr lib.types.path;
154
154
-
default = null;
49
49
+
environmentFiles = mkOption {
50
50
+
type = types.listOf types.path;
51
51
+
default = [ ];
155
52
description = ''
156
156
-
Path to a file containing secrets in EnvironmentFile format.
157
157
-
Should contain: JWT_SECRET, DPOP_SECRET, MASTER_KEY
158
158
-
May also contain: DISCORD_BOT_TOKEN, TELEGRAM_BOT_TOKEN,
159
159
-
TELEGRAM_WEBHOOK_SECRET, SSO_*_CLIENT_SECRET, SSO_APPLE_PRIVATE_KEY,
160
160
-
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
53
53
+
File to load environment variables from. Loaded variables override
54
54
+
values set in {option}`environment`.
55
55
+
56
56
+
Use it to set values of `JWT_SECRET`, `DPOP_SECRET` and `MASTER_KEY`.
57
57
+
58
58
+
Generate these with:
59
59
+
```
60
60
+
openssl rand --hex 32
61
61
+
```
161
62
'';
162
63
};
163
64
164
164
-
database.createLocally = lib.mkOption {
165
165
-
type = lib.types.bool;
65
65
+
database.createLocally = mkOption {
66
66
+
type = types.bool;
166
67
default = false;
167
68
description = ''
168
69
Create the postgres database and user on the local host.
169
70
'';
170
71
};
171
72
172
172
-
frontend.package = lib.mkOption {
173
173
-
type = lib.types.nullOr lib.types.package;
73
73
+
frontend.package = mkOption {
74
74
+
type = types.nullOr types.package;
174
75
default = self.packages.${pkgs.stdenv.hostPlatform.system}.tranquil-frontend;
175
76
defaultText = lib.literalExpression "self.packages.\${pkgs.stdenv.hostPlatform.system}.tranquil-frontend";
176
77
description = "Frontend package to serve via nginx (set null to disable frontend)";
···
179
80
nginx = {
180
81
enable = lib.mkEnableOption "nginx reverse proxy for tranquil-pds";
181
82
182
182
-
enableACME = lib.mkOption {
183
183
-
type = lib.types.bool;
83
83
+
enableACME = mkOption {
84
84
+
type = types.bool;
184
85
default = true;
185
86
description = "Enable ACME for the pds domain";
186
87
};
187
88
188
188
-
useACMEHost = lib.mkOption {
189
189
-
type = lib.types.nullOr lib.types.str;
89
89
+
useACMEHost = mkOption {
90
90
+
type = types.nullOr types.str;
190
91
default = null;
191
92
description = ''
192
93
Use a pre-configured ACME certificate instead of generating one.
193
94
Set this to the cert name from security.acme.certs for wildcard setups.
95
95
+
194
96
REMEMBER: Handle subdomains (*.pds.example.com) require a wildcard cert via DNS-01.
195
97
'';
196
98
};
197
197
-
198
198
-
openFirewall = lib.mkOption {
199
199
-
type = lib.types.bool;
200
200
-
default = true;
201
201
-
description = "Open ports 80 and 443 in the firewall";
202
202
-
};
203
99
};
204
100
205
205
-
settings = {
206
206
-
server = {
207
207
-
host = lib.mkOption {
208
208
-
type = lib.types.str;
209
209
-
default = "127.0.0.1";
210
210
-
description = "Address to bind the server to";
211
211
-
};
212
212
-
213
213
-
port = lib.mkOption {
214
214
-
type = lib.types.port;
215
215
-
default = 3000;
216
216
-
description = "Port to bind the server to";
217
217
-
};
218
218
-
219
219
-
pdsHostname = lib.mkOption {
220
220
-
type = lib.types.str;
221
221
-
description = "Public-facing hostname of the PDS (used in DID documents, JWTs, etc)";
222
222
-
};
223
223
-
};
224
224
-
225
225
-
database = {
226
226
-
url = lib.mkOption {
227
227
-
type = lib.types.str;
228
228
-
description = "PostgreSQL connection string";
229
229
-
};
230
230
-
231
231
-
maxConnections = lib.mkOption {
232
232
-
type = lib.types.int;
233
233
-
default = 100;
234
234
-
description = "Maximum database connections";
235
235
-
};
236
236
-
237
237
-
minConnections = lib.mkOption {
238
238
-
type = lib.types.int;
239
239
-
default = 10;
240
240
-
description = "Minimum database connections";
241
241
-
};
242
242
-
243
243
-
acquireTimeoutSecs = lib.mkOption {
244
244
-
type = lib.types.int;
245
245
-
default = 10;
246
246
-
description = "Connection acquire timeout in seconds";
247
247
-
};
248
248
-
};
249
249
-
250
250
-
storage = {
251
251
-
blobBackend = lib.mkOption {
252
252
-
type = lib.types.enum ["filesystem" "s3"];
253
253
-
default = "filesystem";
254
254
-
description = "Backend for blob storage";
255
255
-
};
256
256
-
257
257
-
blobPath = lib.mkOption {
258
258
-
type = lib.types.str;
259
259
-
default = "${cfg.dataDir}/blobs";
260
260
-
defaultText = lib.literalExpression ''"''${cfg.dataDir}/blobs"'';
261
261
-
description = "Path for filesystem blob storage";
262
262
-
};
263
263
-
264
264
-
s3Endpoint = lib.mkOption {
265
265
-
type = optionalStr;
266
266
-
default = null;
267
267
-
description = "S3 endpoint URL (for object storage)";
268
268
-
};
269
269
-
270
270
-
awsRegion = lib.mkOption {
271
271
-
type = optionalStr;
272
272
-
default = null;
273
273
-
description = "Region for objsto";
274
274
-
};
275
275
-
276
276
-
s3Bucket = lib.mkOption {
277
277
-
type = optionalStr;
278
278
-
default = null;
279
279
-
description = "Bucket name for objsto";
280
280
-
};
281
281
-
};
282
282
-
283
283
-
backup = {
284
284
-
enable = lib.mkEnableOption "automatic repo backups";
285
285
-
286
286
-
backend = lib.mkOption {
287
287
-
type = lib.types.enum ["filesystem" "s3"];
288
288
-
default = "filesystem";
289
289
-
description = "Backend for backup storage";
290
290
-
};
291
291
-
292
292
-
path = lib.mkOption {
293
293
-
type = lib.types.str;
294
294
-
default = "${cfg.dataDir}/backups";
295
295
-
defaultText = lib.literalExpression ''"''${cfg.dataDir}/backups"'';
296
296
-
description = "Path for filesystem backup storage";
297
297
-
};
298
298
-
299
299
-
s3Bucket = lib.mkOption {
300
300
-
type = optionalStr;
301
301
-
default = null;
302
302
-
description = "Object storage bucket name for backups";
303
303
-
};
304
304
-
305
305
-
retentionCount = lib.mkOption {
306
306
-
type = lib.types.int;
307
307
-
default = 7;
308
308
-
description = "Number of backups to retain";
309
309
-
};
310
310
-
311
311
-
intervalSecs = lib.mkOption {
312
312
-
type = lib.types.int;
313
313
-
default = 86400;
314
314
-
description = "Backup interval in seconds";
315
315
-
};
316
316
-
};
317
317
-
318
318
-
cache = {
319
319
-
valkeyUrl = lib.mkOption {
320
320
-
type = optionalStr;
321
321
-
default = null;
322
322
-
description = "Valkey URL for caching";
323
323
-
};
324
324
-
};
325
325
-
326
326
-
security = {
327
327
-
allowInsecureSecrets = lib.mkOption {
328
328
-
type = lib.types.bool;
329
329
-
default = false;
330
330
-
description = "Allow default/weak secrets (development only, NEVER in production ofc)";
331
331
-
};
332
332
-
};
333
333
-
334
334
-
plc = {
335
335
-
directoryUrl = lib.mkOption {
336
336
-
type = lib.types.str;
337
337
-
default = "https://plc.directory";
338
338
-
description = "PLC directory URL";
339
339
-
};
340
340
-
341
341
-
timeoutSecs = lib.mkOption {
342
342
-
type = lib.types.int;
343
343
-
default = 10;
344
344
-
description = "PLC request timeout in seconds";
345
345
-
};
346
346
-
347
347
-
connectTimeoutSecs = lib.mkOption {
348
348
-
type = lib.types.int;
349
349
-
default = 5;
350
350
-
description = "PLC connection timeout in seconds";
351
351
-
};
352
352
-
353
353
-
rotationKey = lib.mkOption {
354
354
-
type = optionalStr;
355
355
-
default = null;
356
356
-
description = "Rotation key for PLC operations (did:key:xyz)";
357
357
-
};
358
358
-
};
359
359
-
360
360
-
did = {
361
361
-
cacheTtlSecs = lib.mkOption {
362
362
-
type = lib.types.int;
363
363
-
default = 300;
364
364
-
description = "DID document cache TTL in seconds";
365
365
-
};
366
366
-
};
367
367
-
368
368
-
relay = {
369
369
-
crawlers = lib.mkOption {
370
370
-
type = optionalStr;
371
371
-
default = null;
372
372
-
description = "Comma-separated list of relay URLs to notify via requestCrawl";
373
373
-
};
374
374
-
};
375
375
-
376
376
-
firehose = {
377
377
-
bufferSize = lib.mkOption {
378
378
-
type = lib.types.int;
379
379
-
default = 10000;
380
380
-
description = "Firehose broadcast channel buffer size";
381
381
-
};
382
382
-
383
383
-
maxLag = lib.mkOption {
384
384
-
type = optionalInt;
385
385
-
default = null;
386
386
-
description = "Disconnect slow consumers after this many events of lag";
387
387
-
};
388
388
-
};
389
389
-
390
390
-
notifications = {
391
391
-
batchSize = lib.mkOption {
392
392
-
type = lib.types.int;
393
393
-
default = 100;
394
394
-
description = "Notification queue batch size";
395
395
-
};
396
396
-
397
397
-
pollIntervalMs = lib.mkOption {
398
398
-
type = lib.types.int;
399
399
-
default = 1000;
400
400
-
description = "Notification queue poll interval in ms";
401
401
-
};
402
402
-
403
403
-
mailFromAddress = lib.mkOption {
404
404
-
type = optionalStr;
405
405
-
default = null;
406
406
-
description = "Email from address for notifications";
407
407
-
};
408
408
-
409
409
-
mailFromName = lib.mkOption {
410
410
-
type = optionalStr;
411
411
-
default = null;
412
412
-
description = "Email from name for notifications";
413
413
-
};
414
414
-
415
415
-
sendmailPath = lib.mkOption {
416
416
-
type = optionalPath;
417
417
-
default = null;
418
418
-
description = "Path to sendmail binary";
419
419
-
};
420
420
-
421
421
-
signalCliPath = lib.mkOption {
422
422
-
type = optionalPath;
423
423
-
default = null;
424
424
-
description = "Path to signal-cli binary";
425
425
-
};
426
426
-
427
427
-
signalSenderNumber = lib.mkOption {
428
428
-
type = optionalStr;
429
429
-
default = null;
430
430
-
description = "Signal sender phone number";
431
431
-
};
432
432
-
};
433
433
-
434
434
-
limits = {
435
435
-
maxBlobSize = lib.mkOption {
436
436
-
type = lib.types.int;
437
437
-
default = 10737418240;
438
438
-
description = "Maximum blob size in bytes";
439
439
-
};
440
440
-
};
441
441
-
442
442
-
import = {
443
443
-
accepting = lib.mkOption {
444
444
-
type = lib.types.bool;
445
445
-
default = true;
446
446
-
description = "Accept repository imports";
447
447
-
};
448
448
-
449
449
-
maxSize = lib.mkOption {
450
450
-
type = lib.types.int;
451
451
-
default = 1073741824;
452
452
-
description = "Maximum import size in bytes";
453
453
-
};
454
454
-
455
455
-
maxBlocks = lib.mkOption {
456
456
-
type = lib.types.int;
457
457
-
default = 500000;
458
458
-
description = "Maximum blocks per import";
459
459
-
};
460
460
-
461
461
-
skipVerification = lib.mkOption {
462
462
-
type = lib.types.bool;
463
463
-
default = false;
464
464
-
description = "Skip verification during import (testing only)";
465
465
-
};
466
466
-
};
467
467
-
468
468
-
registration = {
469
469
-
inviteCodeRequired = lib.mkOption {
470
470
-
type = lib.types.bool;
471
471
-
default = false;
472
472
-
description = "Require invite codes for registration";
473
473
-
};
474
474
-
475
475
-
availableUserDomains = lib.mkOption {
476
476
-
type = optionalStr;
477
477
-
default = null;
478
478
-
description = "Comma-separated list of available user domains";
479
479
-
};
480
480
-
481
481
-
enableSelfHostedDidWeb = lib.mkOption {
482
482
-
type = lib.types.bool;
483
483
-
default = true;
484
484
-
description = "Enable self-hosted did:web identities";
485
485
-
};
486
486
-
};
487
487
-
488
488
-
metadata = {
489
489
-
privacyPolicyUrl = lib.mkOption {
490
490
-
type = optionalStr;
491
491
-
default = null;
492
492
-
description = "Privacy policy URL";
493
493
-
};
494
494
-
495
495
-
termsOfServiceUrl = lib.mkOption {
496
496
-
type = optionalStr;
497
497
-
default = null;
498
498
-
description = "Terms of service URL";
499
499
-
};
500
500
-
501
501
-
contactEmail = lib.mkOption {
502
502
-
type = optionalStr;
503
503
-
default = null;
504
504
-
description = "Contact email address";
505
505
-
};
506
506
-
};
507
507
-
508
508
-
rateLimiting = {
509
509
-
disable = lib.mkOption {
510
510
-
type = lib.types.bool;
511
511
-
default = false;
512
512
-
description = "Disable rate limiting (testing only, NEVER in production you naughty!)";
513
513
-
};
514
514
-
};
515
515
-
516
516
-
scheduling = {
517
517
-
deleteCheckIntervalSecs = lib.mkOption {
518
518
-
type = lib.types.int;
519
519
-
default = 3600;
520
520
-
description = "Scheduled deletion check interval in seconds";
521
521
-
};
522
522
-
};
101
101
+
settings = mkOption {
102
102
+
type = types.submodule {
103
103
+
freeformType = types.attrsOf (
104
104
+
types.nullOr (
105
105
+
types.oneOf [
106
106
+
types.str
107
107
+
types.path
108
108
+
types.int
109
109
+
]
110
110
+
)
111
111
+
);
523
112
524
524
-
moderation = {
525
525
-
reportServiceUrl = lib.mkOption {
526
526
-
type = optionalStr;
527
527
-
default = null;
528
528
-
description = "Moderation report service URL (like ozone)";
529
529
-
};
530
530
-
531
531
-
reportServiceDid = lib.mkOption {
532
532
-
type = optionalStr;
533
533
-
default = null;
534
534
-
description = "Moderation report service DID";
535
535
-
};
536
536
-
};
537
537
-
538
538
-
misc = {
539
539
-
ageAssuranceOverride = lib.mkOption {
540
540
-
type = lib.types.bool;
541
541
-
default = false;
542
542
-
description = "Override age assurance checks";
543
543
-
};
544
544
-
545
545
-
allowHttpProxy = lib.mkOption {
546
546
-
type = lib.types.bool;
547
547
-
default = false;
548
548
-
description = "Allow HTTP for proxy requests (development only)";
549
549
-
};
550
550
-
};
551
551
-
552
552
-
sso = {
553
553
-
github = {
554
554
-
enable = lib.mkOption {
555
555
-
type = lib.types.bool;
556
556
-
default = false;
557
557
-
description = "Enable GitHub SSO";
113
113
+
options = {
114
114
+
SERVER_HOST = mkOption {
115
115
+
type = types.str;
116
116
+
default = "127.0.0.1";
117
117
+
description = "Host for tranquil-pds to listen on";
558
118
};
559
119
560
560
-
clientId = lib.mkOption {
561
561
-
type = optionalStr;
562
562
-
default = null;
563
563
-
description = "GitHub OAuth client ID";
564
564
-
};
565
565
-
};
566
566
-
567
567
-
discord = {
568
568
-
enable = lib.mkOption {
569
569
-
type = lib.types.bool;
570
570
-
default = false;
571
571
-
description = "Enable Discord SSO";
120
120
+
SERVER_PORT = mkOption {
121
121
+
type = types.int;
122
122
+
default = 3000;
123
123
+
description = "Port for tranquil-pds to listen on";
572
124
};
573
125
574
574
-
clientId = lib.mkOption {
575
575
-
type = optionalStr;
126
126
+
PDS_HOSTNAME = mkOption {
127
127
+
type = types.nullOr types.str;
576
128
default = null;
577
577
-
description = "Discord OAuth client ID";
129
129
+
example = "pds.example.com";
130
130
+
description = "The public-facing hostname of the PDS";
578
131
};
579
579
-
};
580
132
581
581
-
google = {
582
582
-
enable = lib.mkOption {
583
583
-
type = lib.types.bool;
584
584
-
default = false;
585
585
-
description = "Enable Google SSO";
133
133
+
BLOB_STORAGE_PATH = mkOption {
134
134
+
type = types.path;
135
135
+
default = "/var/lib/tranquil-pds/blobs";
136
136
+
description = "Directory for storing blobs";
586
137
};
587
138
588
588
-
clientId = lib.mkOption {
589
589
-
type = optionalStr;
590
590
-
default = null;
591
591
-
description = "Google OAuth client ID";
139
139
+
BACKUP_STORAGE_PATH = mkOption {
140
140
+
type = types.path;
141
141
+
default = "/var/lib/tranquil-pds/backups";
142
142
+
description = "Directory for storing backups";
592
143
};
593
593
-
};
594
144
595
595
-
gitlab = {
596
596
-
enable = lib.mkOption {
597
597
-
type = lib.types.bool;
598
598
-
default = false;
599
599
-
description = "Enable GitLab SSO";
600
600
-
};
601
601
-
602
602
-
clientId = lib.mkOption {
603
603
-
type = optionalStr;
145
145
+
MAIL_FROM_ADDRESS = mkOption {
146
146
+
type = types.nullOr types.str;
604
147
default = null;
605
605
-
description = "GitLab OAuth client ID";
148
148
+
description = "Email address to use in the From header when sending emails.";
606
149
};
607
150
608
608
-
issuer = lib.mkOption {
609
609
-
type = optionalStr;
151
151
+
SENDMAIL_PATH = mkOption {
152
152
+
type = types.nullOr types.path;
610
153
default = null;
611
611
-
description = "GitLab issuer URL";
154
154
+
description = "Path to the sendmail executable to use for sending emails.";
612
155
};
613
613
-
};
614
156
615
615
-
oidc = {
616
616
-
enable = lib.mkOption {
617
617
-
type = lib.types.bool;
618
618
-
default = false;
619
619
-
description = "Enable generic OIDC SSO";
620
620
-
};
621
621
-
622
622
-
clientId = lib.mkOption {
623
623
-
type = optionalStr;
157
157
+
SIGNAL_SENDER_NUMBER = mkOption {
158
158
+
type = types.nullOr types.str;
624
159
default = null;
625
625
-
description = "OIDC client ID";
160
160
+
description = "Phone number (in international format) to use for sending Signal notifications.";
626
161
};
627
162
628
628
-
issuer = lib.mkOption {
629
629
-
type = optionalStr;
163
163
+
SIGNAL_CLI_PATH = mkOption {
164
164
+
type = types.nullOr types.path;
630
165
default = null;
631
631
-
description = "OIDC issuer URL";
166
166
+
description = "Path to the signal-cli executable to use for sending Signal notifications.";
632
167
};
633
168
634
634
-
name = lib.mkOption {
635
635
-
type = optionalStr;
636
636
-
default = null;
637
637
-
description = "OIDC provider display name";
169
169
+
MAX_BLOB_SIZE = mkOption {
170
170
+
type = types.int;
171
171
+
default = 10737418240; # 10 GiB
172
172
+
description = "Maximum allowed blob size in bytes.";
638
173
};
639
174
};
175
175
+
};
640
176
641
641
-
apple = {
642
642
-
enable = lib.mkOption {
643
643
-
type = lib.types.bool;
644
644
-
default = false;
645
645
-
description = "Enable Apple Sign-in";
646
646
-
};
647
647
-
648
648
-
clientId = lib.mkOption {
649
649
-
type = optionalStr;
650
650
-
default = null;
651
651
-
description = "Apple Services ID";
652
652
-
};
653
653
-
654
654
-
teamId = lib.mkOption {
655
655
-
type = optionalStr;
656
656
-
default = null;
657
657
-
description = "Apple Team ID";
658
658
-
};
177
177
+
description = ''
178
178
+
Environment variables to set for the service. Secrets should be
179
179
+
specified using {option}`environmentFile`.
659
180
660
660
-
keyId = lib.mkOption {
661
661
-
type = optionalStr;
662
662
-
default = null;
663
663
-
description = "Apple Key ID";
664
664
-
};
665
665
-
};
666
666
-
};
181
181
+
Refer to <https://tangled.org/tranquil.farm/tranquil-pds/blob/main/.env.example>
182
182
+
available environment variables.
183
183
+
'';
667
184
};
668
185
};
669
186
670
670
-
config = lib.mkIf cfg.enable (lib.mkMerge [
671
671
-
(lib.mkIf (cfg.settings.notifications.mailFromAddress != null) {
672
672
-
services.tranquil-pds.settings.notifications.sendmailPath =
673
673
-
lib.mkDefault "/run/wrappers/bin/sendmail";
674
674
-
})
187
187
+
config = lib.mkIf cfg.enable (
188
188
+
lib.mkMerge [
189
189
+
(lib.mkIf cfg.database.createLocally {
190
190
+
services.postgresql = {
191
191
+
enable = true;
192
192
+
ensureDatabases = [ cfg.user ];
193
193
+
ensureUsers = [
194
194
+
{
195
195
+
name = cfg.user;
196
196
+
ensureDBOwnership = true;
197
197
+
}
198
198
+
];
199
199
+
};
675
200
676
676
-
(lib.mkIf (cfg.settings.notifications.signalSenderNumber != null) {
677
677
-
services.tranquil-pds.settings.notifications.signalCliPath =
678
678
-
lib.mkDefault (lib.getExe pkgs.signal-cli);
679
679
-
})
201
201
+
services.tranquil-pds.settings.DATABASE_URL = lib.mkDefault "postgresql:///${cfg.user}?host=/run/postgresql";
680
202
681
681
-
(lib.mkIf cfg.database.createLocally {
682
682
-
services.postgresql = {
683
683
-
enable = true;
684
684
-
ensureDatabases = [cfg.user];
685
685
-
ensureUsers = [
686
686
-
{
687
687
-
name = cfg.user;
688
688
-
ensureDBOwnership = true;
689
689
-
}
690
690
-
];
691
691
-
};
692
692
-
693
693
-
services.tranquil-pds.settings.database.url =
694
694
-
lib.mkDefault "postgresql:///${cfg.user}?host=/run/postgresql";
695
695
-
696
696
-
systemd.services.tranquil-pds = {
697
697
-
requires = ["postgresql.service"];
698
698
-
after = ["postgresql.service"];
699
699
-
};
700
700
-
})
203
203
+
systemd.services.tranquil-pds = {
204
204
+
requires = [ "postgresql.service" ];
205
205
+
after = [ "postgresql.service" ];
206
206
+
};
207
207
+
})
701
208
702
702
-
(lib.mkIf cfg.nginx.enable (lib.mkMerge [
703
703
-
{
209
209
+
(lib.mkIf cfg.nginx.enable {
704
210
services.nginx = {
705
211
enable = true;
706
706
-
recommendedProxySettings = lib.mkDefault true;
707
707
-
recommendedTlsSettings = lib.mkDefault true;
708
708
-
recommendedGzipSettings = lib.mkDefault true;
709
709
-
recommendedOptimisation = lib.mkDefault true;
710
212
711
711
-
virtualHosts.${cfg.settings.server.pdsHostname} = {
712
712
-
serverAliases = ["*.${cfg.settings.server.pdsHostname}"];
213
213
+
virtualHosts.${cfg.settings.PDS_HOSTNAME} = {
214
214
+
serverAliases = [ "*.${cfg.settings.PDS_HOSTNAME}" ];
713
215
forceSSL = hasSSL;
714
216
enableACME = useACME;
715
217
useACMEHost = cfg.nginx.useACMEHost;
716
218
717
717
-
root = lib.mkIf (cfg.frontend.package != null) "${cfg.frontend.package}";
219
219
+
root = lib.mkIf (cfg.frontend.package != null) cfg.frontend.package;
718
220
719
719
-
extraConfig = "client_max_body_size ${toString cfg.settings.limits.maxBlobSize};";
221
221
+
extraConfig = "client_max_body_size ${toString cfg.settings.MAX_BLOB_SIZE};";
720
222
721
721
-
locations = let
722
722
-
proxyLocations = {
223
223
+
locations = lib.mkMerge [
224
224
+
{
723
225
"/xrpc/" = {
724
226
proxyPass = backendUrl;
725
227
proxyWebsockets = true;
···
766
268
"~ ^/u/[^/]+/did\\.json$" = {
767
269
proxyPass = backendUrl;
768
270
};
769
769
-
};
271
271
+
}
770
272
771
771
-
frontendLocations = lib.optionalAttrs (cfg.frontend.package != null) {
273
273
+
(lib.optionalAttrs (cfg.frontend.package != null) {
772
274
"= /oauth/client-metadata.json" = {
773
275
root = "${cfg.frontend.package}";
774
276
extraConfig = ''
···
780
282
};
781
283
782
284
"/assets/" = {
285
285
+
# TODO: use `add_header_inherit` when nixpkgs updates to nginx 1.29.3+
783
286
extraConfig = ''
784
287
expires 1y;
785
288
add_header Cache-Control "public, immutable";
···
799
302
tryFiles = "$uri $uri/ /index.html";
800
303
priority = 9999;
801
304
};
802
802
-
};
803
803
-
in
804
804
-
proxyLocations // frontendLocations;
305
305
+
})
306
306
+
];
805
307
};
806
308
};
807
807
-
}
309
309
+
})
808
310
809
809
-
(lib.mkIf cfg.nginx.openFirewall {
810
810
-
networking.firewall.allowedTCPPorts = [80 443];
811
811
-
})
812
812
-
]))
311
311
+
{
312
312
+
services.tranquil-pds.settings = {
313
313
+
SENDMAIL_PATH = lib.mkDefault (
314
314
+
if cfg.settings.MAIL_FROM_ADDRESS != null then (lib.getExe pkgs.system-sendmail) else null
315
315
+
);
813
316
814
814
-
{
815
815
-
users.users.${cfg.user} = {
816
816
-
isSystemUser = true;
817
817
-
inherit (cfg) group;
818
818
-
home = cfg.dataDir;
819
819
-
};
317
317
+
SIGNAL_CLI_PATH = lib.mkDefault (
318
318
+
if cfg.settings.SIGNAL_SENDER_NUMBER != null then (lib.getExe pkgs.signal-cli) else null
319
319
+
);
320
320
+
};
820
321
821
821
-
users.groups.${cfg.group} = {};
322
322
+
users.users.${cfg.user} = {
323
323
+
isSystemUser = true;
324
324
+
inherit (cfg) group;
325
325
+
home = cfg.dataDir;
326
326
+
};
822
327
823
823
-
systemd.tmpfiles.rules = [
824
824
-
"d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} -"
825
825
-
"d ${cfg.settings.storage.blobPath} 0750 ${cfg.user} ${cfg.group} -"
826
826
-
"d ${cfg.settings.backup.path} 0750 ${cfg.user} ${cfg.group} -"
827
827
-
];
328
328
+
users.groups.${cfg.group} = { };
828
329
829
829
-
systemd.services.tranquil-pds = {
830
830
-
description = "Tranquil PDS - AT Protocol Personal Data Server";
831
831
-
after = ["network.target" "postgresql.service"];
832
832
-
wants = ["network.target"];
833
833
-
wantedBy = ["multi-user.target"];
330
330
+
systemd.tmpfiles.settings."tranquil-pds" =
331
331
+
lib.genAttrs
332
332
+
[
333
333
+
cfg.dataDir
334
334
+
cfg.settings.BLOB_STORAGE_PATH
335
335
+
cfg.settings.BACKUP_STORAGE_PATH
336
336
+
]
337
337
+
(_: {
338
338
+
d = {
339
339
+
mode = "0750";
340
340
+
inherit (cfg) user group;
341
341
+
};
342
342
+
});
834
343
835
835
-
environment = settingsToEnv cfg.settings;
344
344
+
systemd.services.tranquil-pds = {
345
345
+
description = "Tranquil PDS - AT Protocol Personal Data Server";
346
346
+
after = [ "network-online.target" ];
347
347
+
wants = [ "network-online.target" ];
348
348
+
wantedBy = [ "multi-user.target" ];
836
349
837
837
-
serviceConfig = {
838
838
-
Type = "exec";
839
839
-
User = cfg.user;
840
840
-
Group = cfg.group;
841
841
-
ExecStart = "${cfg.package}/bin/tranquil-pds";
842
842
-
Restart = "on-failure";
843
843
-
RestartSec = 5;
350
350
+
serviceConfig = {
351
351
+
User = cfg.user;
352
352
+
Group = cfg.group;
353
353
+
ExecStart = lib.getExe cfg.package;
354
354
+
Restart = "on-failure";
355
355
+
RestartSec = 5;
844
356
845
845
-
WorkingDirectory = cfg.dataDir;
846
846
-
StateDirectory = "tranquil-pds";
357
357
+
WorkingDirectory = cfg.dataDir;
358
358
+
StateDirectory = "tranquil-pds";
847
359
848
848
-
EnvironmentFile = lib.mkIf (cfg.secretsFile != null) cfg.secretsFile;
360
360
+
EnvironmentFile = cfg.environmentFiles;
361
361
+
Environment = lib.mapAttrsToList (k: v: "${k}=${if builtins.isInt v then toString v else v}") (
362
362
+
lib.filterAttrs (_: v: v != null) cfg.settings
363
363
+
);
849
364
850
850
-
NoNewPrivileges = true;
851
851
-
ProtectSystem = "strict";
852
852
-
ProtectHome = true;
853
853
-
PrivateTmp = true;
854
854
-
PrivateDevices = true;
855
855
-
ProtectKernelTunables = true;
856
856
-
ProtectKernelModules = true;
857
857
-
ProtectControlGroups = true;
858
858
-
RestrictAddressFamilies = ["AF_INET" "AF_INET6" "AF_UNIX"];
859
859
-
RestrictNamespaces = true;
860
860
-
LockPersonality = true;
861
861
-
MemoryDenyWriteExecute = true;
862
862
-
RestrictRealtime = true;
863
863
-
RestrictSUIDSGID = true;
864
864
-
RemoveIPC = true;
365
365
+
NoNewPrivileges = true;
366
366
+
ProtectSystem = "strict";
367
367
+
ProtectHome = true;
368
368
+
PrivateTmp = true;
369
369
+
PrivateDevices = true;
370
370
+
ProtectKernelTunables = true;
371
371
+
ProtectKernelModules = true;
372
372
+
ProtectControlGroups = true;
373
373
+
RestrictAddressFamilies = [
374
374
+
"AF_INET"
375
375
+
"AF_INET6"
376
376
+
"AF_UNIX"
377
377
+
];
378
378
+
RestrictNamespaces = true;
379
379
+
LockPersonality = true;
380
380
+
MemoryDenyWriteExecute = true;
381
381
+
RestrictRealtime = true;
382
382
+
RestrictSUIDSGID = true;
383
383
+
RemoveIPC = true;
865
384
866
866
-
ReadWritePaths = [
867
867
-
cfg.settings.storage.blobPath
868
868
-
cfg.settings.backup.path
869
869
-
];
385
385
+
ReadWritePaths = [
386
386
+
cfg.settings.BLOB_STORAGE_PATH
387
387
+
cfg.settings.BACKUP_STORAGE_PATH
388
388
+
];
389
389
+
};
870
390
};
871
871
-
};
872
872
-
}
873
873
-
]);
391
391
+
}
392
392
+
]
393
393
+
);
874
394
}
+8
-10
test.nix
···
16
16
services.tranquil-pds = {
17
17
enable = true;
18
18
database.createLocally = true;
19
19
-
secretsFile = pkgs.writeText "tranquil-secrets" ''
20
20
-
JWT_SECRET=test-jwt-secret-must-be-32-chars-long
21
21
-
DPOP_SECRET=test-dpop-secret-must-be-32-chars-long
22
22
-
MASTER_KEY=test-master-key-must-be-32-chars-long
23
23
-
'';
24
19
25
20
nginx = {
26
21
enable = true;
···
28
23
};
29
24
30
25
settings = {
31
31
-
server.pdsHostname = "test.local";
32
32
-
server.host = "0.0.0.0";
26
26
+
PDS_HOSTNAME = "test.local";
27
27
+
SERVER_HOST = "0.0.0.0";
33
28
34
34
-
storage.blobBackend = "filesystem";
35
35
-
rateLimiting.disable = true;
36
36
-
security.allowInsecureSecrets = true;
29
29
+
DISABLE_RATE_LIMITING = 1;
30
30
+
TRANQUIL_PDS_ALLOW_INSECURE_SECRETS = 1;
31
31
+
32
32
+
JWT_SECRET="test-jwt-secret-must-be-32-chars-long";
33
33
+
DPOP_SECRET="test-dpop-secret-must-be-32-chars-long";
34
34
+
MASTER_KEY="test-master-key-must-be-32-chars-long";
37
35
};
38
36
};
39
37
};