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