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: nix module
lewis.moe
4 weeks ago
3e4760c2
91cfc536
+803
-6
3 changed files
expand all
collapse all
unified
split
flake.nix
module.nix
test.nix
+21
-6
flake.nix
···
7
7
# for now we important that PR as well purely for its fetchDenoDeps
8
8
nixpkgs-fetch-deno.url = "github:aMOPel/nixpkgs/feat/fetchDenoDeps";
9
9
};
10
10
-
11
11
-
outputs = { self, nixpkgs, ... } @ inputs : let
12
12
-
forAllSystems =
13
13
-
function:
10
10
+
11
11
+
outputs = {
12
12
+
self,
13
13
+
nixpkgs,
14
14
+
...
15
15
+
} @ inputs: let
16
16
+
forAllSystems = function:
14
17
nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed (
15
18
system: (function system nixpkgs.legacyPackages.${system})
16
19
);
17
20
in {
18
21
packages = forAllSystems (system: pkgs: {
19
19
-
tranquil-pds = pkgs.callPackage ./default.nix { };
22
22
+
tranquil-pds = pkgs.callPackage ./default.nix {};
20
23
tranquil-frontend = pkgs.callPackage ./frontend.nix {
21
24
inherit (inputs.nixpkgs-fetch-deno.legacyPackages.${system}) fetchDenoDeps;
22
25
};
···
24
27
});
25
28
26
29
devShells = forAllSystems (system: pkgs: {
27
27
-
default = pkgs.callPackage ./shell.nix { };
30
30
+
default = pkgs.callPackage ./shell.nix {};
28
31
});
32
32
+
33
33
+
nixosModules.default = import ./module.nix;
34
34
+
35
35
+
checks.x86_64-linux.integration = import ./test.nix {
36
36
+
pkgs = nixpkgs.legacyPackages.x86_64-linux;
37
37
+
inherit self;
38
38
+
};
39
39
+
40
40
+
checks.aarch64-linux.integration = import ./test.nix {
41
41
+
pkgs = nixpkgs.legacyPackages.aarch64-linux;
42
42
+
inherit self;
43
43
+
};
29
44
};
30
45
}
+707
module.nix
···
1
1
+
{
2
2
+
config,
3
3
+
lib,
4
4
+
pkgs,
5
5
+
...
6
6
+
}: let
7
7
+
cfg = config.services.tranquil-pds;
8
8
+
9
9
+
optionalStr = lib.types.nullOr lib.types.str;
10
10
+
optionalInt = lib.types.nullOr lib.types.int;
11
11
+
optionalBool = lib.types.nullOr lib.types.bool;
12
12
+
optionalPath = lib.types.nullOr lib.types.str;
13
13
+
optionalPort = lib.types.nullOr lib.types.port;
14
14
+
15
15
+
filterNulls = lib.filterAttrs (_: v: v != null);
16
16
+
17
17
+
boolToStr = b:
18
18
+
if b == true
19
19
+
then "true"
20
20
+
else if b == false
21
21
+
then "false"
22
22
+
else null;
23
23
+
24
24
+
settingsToEnv = settings: let
25
25
+
raw = {
26
26
+
SERVER_HOST = settings.server.host;
27
27
+
SERVER_PORT = settings.server.port;
28
28
+
PDS_HOSTNAME = settings.server.pdsHostname;
29
29
+
30
30
+
DATABASE_URL = settings.database.url;
31
31
+
DATABASE_MAX_CONNECTIONS = settings.database.maxConnections;
32
32
+
DATABASE_MIN_CONNECTIONS = settings.database.minConnections;
33
33
+
DATABASE_ACQUIRE_TIMEOUT_SECS = settings.database.acquireTimeoutSecs;
34
34
+
35
35
+
BLOB_STORAGE_BACKEND = settings.storage.blobBackend;
36
36
+
BLOB_STORAGE_PATH = settings.storage.blobPath;
37
37
+
S3_ENDPOINT = settings.storage.s3Endpoint;
38
38
+
AWS_REGION = settings.storage.awsRegion;
39
39
+
S3_BUCKET = settings.storage.s3Bucket;
40
40
+
41
41
+
BACKUP_ENABLED = boolToStr settings.backup.enabled;
42
42
+
BACKUP_STORAGE_BACKEND = settings.backup.backend;
43
43
+
BACKUP_STORAGE_PATH = settings.backup.path;
44
44
+
BACKUP_S3_BUCKET = settings.backup.s3Bucket;
45
45
+
BACKUP_RETENTION_COUNT = settings.backup.retentionCount;
46
46
+
BACKUP_INTERVAL_SECS = settings.backup.intervalSecs;
47
47
+
48
48
+
VALKEY_URL = settings.cache.valkeyUrl;
49
49
+
50
50
+
TRANQUIL_PDS_ALLOW_INSECURE_SECRETS = boolToStr settings.security.allowInsecureSecrets;
51
51
+
52
52
+
PLC_DIRECTORY_URL = settings.plc.directoryUrl;
53
53
+
PLC_TIMEOUT_SECS = settings.plc.timeoutSecs;
54
54
+
PLC_CONNECT_TIMEOUT_SECS = settings.plc.connectTimeoutSecs;
55
55
+
PLC_ROTATION_KEY = settings.plc.rotationKey;
56
56
+
57
57
+
DID_CACHE_TTL_SECS = settings.did.cacheTtlSecs;
58
58
+
59
59
+
CRAWLERS = settings.relay.crawlers;
60
60
+
61
61
+
FIREHOSE_BUFFER_SIZE = settings.firehose.bufferSize;
62
62
+
FIREHOSE_MAX_LAG = settings.firehose.maxLag;
63
63
+
64
64
+
NOTIFICATION_BATCH_SIZE = settings.notifications.batchSize;
65
65
+
NOTIFICATION_POLL_INTERVAL_MS = settings.notifications.pollIntervalMs;
66
66
+
MAIL_FROM_ADDRESS = settings.notifications.mailFromAddress;
67
67
+
MAIL_FROM_NAME = settings.notifications.mailFromName;
68
68
+
SENDMAIL_PATH = settings.notifications.sendmailPath;
69
69
+
SIGNAL_CLI_PATH = settings.notifications.signalCliPath;
70
70
+
SIGNAL_SENDER_NUMBER = settings.notifications.signalSenderNumber;
71
71
+
72
72
+
MAX_BLOB_SIZE = settings.limits.maxBlobSize;
73
73
+
74
74
+
ACCEPTING_REPO_IMPORTS = boolToStr settings.import.accepting;
75
75
+
MAX_IMPORT_SIZE = settings.import.maxSize;
76
76
+
MAX_IMPORT_BLOCKS = settings.import.maxBlocks;
77
77
+
SKIP_IMPORT_VERIFICATION = boolToStr settings.import.skipVerification;
78
78
+
79
79
+
INVITE_CODE_REQUIRED = boolToStr settings.registration.inviteCodeRequired;
80
80
+
AVAILABLE_USER_DOMAINS = settings.registration.availableUserDomains;
81
81
+
ENABLE_SELF_HOSTED_DID_WEB = boolToStr settings.registration.enableSelfHostedDidWeb;
82
82
+
83
83
+
PRIVACY_POLICY_URL = settings.metadata.privacyPolicyUrl;
84
84
+
TERMS_OF_SERVICE_URL = settings.metadata.termsOfServiceUrl;
85
85
+
CONTACT_EMAIL = settings.metadata.contactEmail;
86
86
+
87
87
+
DISABLE_RATE_LIMITING = boolToStr settings.rateLimiting.disable;
88
88
+
89
89
+
SCHEDULED_DELETE_CHECK_INTERVAL_SECS = settings.scheduling.deleteCheckIntervalSecs;
90
90
+
91
91
+
REPORT_SERVICE_URL = settings.moderation.reportServiceUrl;
92
92
+
REPORT_SERVICE_DID = settings.moderation.reportServiceDid;
93
93
+
94
94
+
PDS_AGE_ASSURANCE_OVERRIDE = boolToStr settings.misc.ageAssuranceOverride;
95
95
+
ALLOW_HTTP_PROXY = boolToStr settings.misc.allowHttpProxy;
96
96
+
97
97
+
SSO_GITHUB_ENABLED = boolToStr settings.sso.github.enabled;
98
98
+
SSO_GITHUB_CLIENT_ID = settings.sso.github.clientId;
99
99
+
100
100
+
SSO_DISCORD_ENABLED = boolToStr settings.sso.discord.enabled;
101
101
+
SSO_DISCORD_CLIENT_ID = settings.sso.discord.clientId;
102
102
+
103
103
+
SSO_GOOGLE_ENABLED = boolToStr settings.sso.google.enabled;
104
104
+
SSO_GOOGLE_CLIENT_ID = settings.sso.google.clientId;
105
105
+
106
106
+
SSO_GITLAB_ENABLED = boolToStr settings.sso.gitlab.enabled;
107
107
+
SSO_GITLAB_CLIENT_ID = settings.sso.gitlab.clientId;
108
108
+
SSO_GITLAB_ISSUER = settings.sso.gitlab.issuer;
109
109
+
110
110
+
SSO_OIDC_ENABLED = boolToStr settings.sso.oidc.enabled;
111
111
+
SSO_OIDC_CLIENT_ID = settings.sso.oidc.clientId;
112
112
+
SSO_OIDC_ISSUER = settings.sso.oidc.issuer;
113
113
+
SSO_OIDC_NAME = settings.sso.oidc.name;
114
114
+
115
115
+
SSO_APPLE_ENABLED = boolToStr settings.sso.apple.enabled;
116
116
+
SSO_APPLE_CLIENT_ID = settings.sso.apple.clientId;
117
117
+
SSO_APPLE_TEAM_ID = settings.sso.apple.teamId;
118
118
+
SSO_APPLE_KEY_ID = settings.sso.apple.keyId;
119
119
+
};
120
120
+
in
121
121
+
lib.mapAttrs (_: v: toString v) (filterNulls raw);
122
122
+
in {
123
123
+
options.services.tranquil-pds = {
124
124
+
enable = lib.mkEnableOption "tranquil-pds AT Protocol personal data server";
125
125
+
126
126
+
package = lib.mkPackageOption pkgs "tranquil-pds" {};
127
127
+
128
128
+
user = lib.mkOption {
129
129
+
type = lib.types.str;
130
130
+
default = "tranquil-pds";
131
131
+
description = "User under which tranquil-pds runs";
132
132
+
};
133
133
+
134
134
+
group = lib.mkOption {
135
135
+
type = lib.types.str;
136
136
+
default = "tranquil-pds";
137
137
+
description = "Group under which tranquil-pds runs";
138
138
+
};
139
139
+
140
140
+
dataDir = lib.mkOption {
141
141
+
type = lib.types.str;
142
142
+
default = "/var/lib/tranquil-pds";
143
143
+
description = "Directory for tranquil-pds data (blobs, backups)";
144
144
+
};
145
145
+
146
146
+
secretsFile = lib.mkOption {
147
147
+
type = lib.types.nullOr lib.types.path;
148
148
+
default = null;
149
149
+
description = ''
150
150
+
Path to a file containing secrets in EnvironmentFile format.
151
151
+
Should contain: JWT_SECRET, DPOP_SECRET, MASTER_KEY
152
152
+
May also contain: DISCORD_BOT_TOKEN, TELEGRAM_BOT_TOKEN,
153
153
+
TELEGRAM_WEBHOOK_SECRET, SSO_*_CLIENT_SECRET, SSO_APPLE_PRIVATE_KEY,
154
154
+
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
155
155
+
'';
156
156
+
};
157
157
+
158
158
+
settings = {
159
159
+
server = {
160
160
+
host = lib.mkOption {
161
161
+
type = lib.types.str;
162
162
+
default = "127.0.0.1";
163
163
+
description = "Address to bind the server to";
164
164
+
};
165
165
+
166
166
+
port = lib.mkOption {
167
167
+
type = lib.types.port;
168
168
+
default = 3000;
169
169
+
description = "Port to bind the server to";
170
170
+
};
171
171
+
172
172
+
pdsHostname = lib.mkOption {
173
173
+
type = lib.types.str;
174
174
+
description = "Public-facing hostname of the PDS (used in DID documents, JWTs, etc)";
175
175
+
};
176
176
+
};
177
177
+
178
178
+
database = {
179
179
+
url = lib.mkOption {
180
180
+
type = lib.types.str;
181
181
+
description = "PostgreSQL connection string";
182
182
+
};
183
183
+
184
184
+
maxConnections = lib.mkOption {
185
185
+
type = optionalInt;
186
186
+
default = null;
187
187
+
description = "Maximum database connections";
188
188
+
};
189
189
+
190
190
+
minConnections = lib.mkOption {
191
191
+
type = optionalInt;
192
192
+
default = null;
193
193
+
description = "Minimum database connections";
194
194
+
};
195
195
+
196
196
+
acquireTimeoutSecs = lib.mkOption {
197
197
+
type = optionalInt;
198
198
+
default = null;
199
199
+
description = "Connection acquire timeout in seconds";
200
200
+
};
201
201
+
};
202
202
+
203
203
+
storage = {
204
204
+
blobBackend = lib.mkOption {
205
205
+
type = lib.types.enum ["filesystem" "s3"];
206
206
+
default = "filesystem";
207
207
+
description = "Backend for blob storage";
208
208
+
};
209
209
+
210
210
+
blobPath = lib.mkOption {
211
211
+
type = optionalPath;
212
212
+
default = null;
213
213
+
description = "Path for filesystem blob storage";
214
214
+
};
215
215
+
216
216
+
s3Endpoint = lib.mkOption {
217
217
+
type = optionalStr;
218
218
+
default = null;
219
219
+
description = "S3 endpoint URL (for object storage)";
220
220
+
};
221
221
+
222
222
+
awsRegion = lib.mkOption {
223
223
+
type = optionalStr;
224
224
+
default = null;
225
225
+
description = "Region for objsto";
226
226
+
};
227
227
+
228
228
+
s3Bucket = lib.mkOption {
229
229
+
type = optionalStr;
230
230
+
default = null;
231
231
+
description = "Bucket name for objsto";
232
232
+
};
233
233
+
};
234
234
+
235
235
+
backup = {
236
236
+
enabled = lib.mkOption {
237
237
+
type = optionalBool;
238
238
+
default = null;
239
239
+
description = "Enable automatic repo backups";
240
240
+
};
241
241
+
242
242
+
backend = lib.mkOption {
243
243
+
type = lib.types.enum ["filesystem" "s3"];
244
244
+
default = "filesystem";
245
245
+
description = "Backend for backup storage";
246
246
+
};
247
247
+
248
248
+
path = lib.mkOption {
249
249
+
type = optionalPath;
250
250
+
default = null;
251
251
+
description = "Path for filesystem backup storage";
252
252
+
};
253
253
+
254
254
+
s3Bucket = lib.mkOption {
255
255
+
type = optionalStr;
256
256
+
default = null;
257
257
+
description = "Object storage bucket name for backups";
258
258
+
};
259
259
+
260
260
+
retentionCount = lib.mkOption {
261
261
+
type = optionalInt;
262
262
+
default = null;
263
263
+
description = "Number of backups to retain";
264
264
+
};
265
265
+
266
266
+
intervalSecs = lib.mkOption {
267
267
+
type = optionalInt;
268
268
+
default = null;
269
269
+
description = "Backup interval in seconds";
270
270
+
};
271
271
+
};
272
272
+
273
273
+
cache = {
274
274
+
valkeyUrl = lib.mkOption {
275
275
+
type = optionalStr;
276
276
+
default = null;
277
277
+
description = "Valkey URL for caching";
278
278
+
};
279
279
+
};
280
280
+
281
281
+
security = {
282
282
+
allowInsecureSecrets = lib.mkOption {
283
283
+
type = optionalBool;
284
284
+
default = null;
285
285
+
description = "Allow default/weak secrets (development only, NEVER in production ofc)";
286
286
+
};
287
287
+
};
288
288
+
289
289
+
plc = {
290
290
+
directoryUrl = lib.mkOption {
291
291
+
type = optionalStr;
292
292
+
default = null;
293
293
+
description = "PLC directory URL";
294
294
+
};
295
295
+
296
296
+
timeoutSecs = lib.mkOption {
297
297
+
type = optionalInt;
298
298
+
default = null;
299
299
+
description = "PLC request timeout in seconds";
300
300
+
};
301
301
+
302
302
+
connectTimeoutSecs = lib.mkOption {
303
303
+
type = optionalInt;
304
304
+
default = null;
305
305
+
description = "PLC connection timeout in seconds";
306
306
+
};
307
307
+
308
308
+
rotationKey = lib.mkOption {
309
309
+
type = optionalStr;
310
310
+
default = null;
311
311
+
description = "Rotation key for PLC operations (did:key:xyz)";
312
312
+
};
313
313
+
};
314
314
+
315
315
+
did = {
316
316
+
cacheTtlSecs = lib.mkOption {
317
317
+
type = optionalInt;
318
318
+
default = null;
319
319
+
description = "DID document cache TTL in seconds";
320
320
+
};
321
321
+
};
322
322
+
323
323
+
relay = {
324
324
+
crawlers = lib.mkOption {
325
325
+
type = optionalStr;
326
326
+
default = null;
327
327
+
description = "Comma-separated list of relay URLs to notify via requestCrawl";
328
328
+
};
329
329
+
};
330
330
+
331
331
+
firehose = {
332
332
+
bufferSize = lib.mkOption {
333
333
+
type = optionalInt;
334
334
+
default = null;
335
335
+
description = "Firehose broadcast channel buffer size";
336
336
+
};
337
337
+
338
338
+
maxLag = lib.mkOption {
339
339
+
type = optionalInt;
340
340
+
default = null;
341
341
+
description = "Disconnect slow consumers after this many events of lag";
342
342
+
};
343
343
+
};
344
344
+
345
345
+
notifications = {
346
346
+
batchSize = lib.mkOption {
347
347
+
type = optionalInt;
348
348
+
default = null;
349
349
+
description = "Notification queue batch size";
350
350
+
};
351
351
+
352
352
+
pollIntervalMs = lib.mkOption {
353
353
+
type = optionalInt;
354
354
+
default = null;
355
355
+
description = "Notification queue poll interval in ms";
356
356
+
};
357
357
+
358
358
+
mailFromAddress = lib.mkOption {
359
359
+
type = optionalStr;
360
360
+
default = null;
361
361
+
description = "Email from address for notifications";
362
362
+
};
363
363
+
364
364
+
mailFromName = lib.mkOption {
365
365
+
type = optionalStr;
366
366
+
default = null;
367
367
+
description = "Email from name for notifications";
368
368
+
};
369
369
+
370
370
+
sendmailPath = lib.mkOption {
371
371
+
type = optionalPath;
372
372
+
default = null;
373
373
+
description = "Path to sendmail binary";
374
374
+
};
375
375
+
376
376
+
signalCliPath = lib.mkOption {
377
377
+
type = optionalPath;
378
378
+
default = null;
379
379
+
description = "Path to signal-cli binary";
380
380
+
};
381
381
+
382
382
+
signalSenderNumber = lib.mkOption {
383
383
+
type = optionalStr;
384
384
+
default = null;
385
385
+
description = "Signal sender phone number";
386
386
+
};
387
387
+
};
388
388
+
389
389
+
limits = {
390
390
+
maxBlobSize = lib.mkOption {
391
391
+
type = optionalInt;
392
392
+
default = null;
393
393
+
description = "Maximum blob size in bytes";
394
394
+
};
395
395
+
};
396
396
+
397
397
+
import = {
398
398
+
accepting = lib.mkOption {
399
399
+
type = optionalBool;
400
400
+
default = null;
401
401
+
description = "Accept repository imports";
402
402
+
};
403
403
+
404
404
+
maxSize = lib.mkOption {
405
405
+
type = optionalInt;
406
406
+
default = null;
407
407
+
description = "Maximum import size in bytes";
408
408
+
};
409
409
+
410
410
+
maxBlocks = lib.mkOption {
411
411
+
type = optionalInt;
412
412
+
default = null;
413
413
+
description = "Maximum blocks per import";
414
414
+
};
415
415
+
416
416
+
skipVerification = lib.mkOption {
417
417
+
type = optionalBool;
418
418
+
default = null;
419
419
+
description = "Skip verification during import (testing only)";
420
420
+
};
421
421
+
};
422
422
+
423
423
+
registration = {
424
424
+
inviteCodeRequired = lib.mkOption {
425
425
+
type = optionalBool;
426
426
+
default = null;
427
427
+
description = "Require invite codes for registration";
428
428
+
};
429
429
+
430
430
+
availableUserDomains = lib.mkOption {
431
431
+
type = optionalStr;
432
432
+
default = null;
433
433
+
description = "Comma-separated list of available user domains";
434
434
+
};
435
435
+
436
436
+
enableSelfHostedDidWeb = lib.mkOption {
437
437
+
type = optionalBool;
438
438
+
default = null;
439
439
+
description = "Enable self-hosted did:web identities";
440
440
+
};
441
441
+
};
442
442
+
443
443
+
metadata = {
444
444
+
privacyPolicyUrl = lib.mkOption {
445
445
+
type = optionalStr;
446
446
+
default = null;
447
447
+
description = "Privacy policy URL";
448
448
+
};
449
449
+
450
450
+
termsOfServiceUrl = lib.mkOption {
451
451
+
type = optionalStr;
452
452
+
default = null;
453
453
+
description = "Terms of service URL";
454
454
+
};
455
455
+
456
456
+
contactEmail = lib.mkOption {
457
457
+
type = optionalStr;
458
458
+
default = null;
459
459
+
description = "Contact email address";
460
460
+
};
461
461
+
};
462
462
+
463
463
+
rateLimiting = {
464
464
+
disable = lib.mkOption {
465
465
+
type = optionalBool;
466
466
+
default = null;
467
467
+
description = "Disable rate limiting (testing only, NEVER in production you naughty!)";
468
468
+
};
469
469
+
};
470
470
+
471
471
+
scheduling = {
472
472
+
deleteCheckIntervalSecs = lib.mkOption {
473
473
+
type = optionalInt;
474
474
+
default = null;
475
475
+
description = "Scheduled deletion check interval in seconds";
476
476
+
};
477
477
+
};
478
478
+
479
479
+
moderation = {
480
480
+
reportServiceUrl = lib.mkOption {
481
481
+
type = optionalStr;
482
482
+
default = null;
483
483
+
description = "Moderation report service URL (like ozone)";
484
484
+
};
485
485
+
486
486
+
reportServiceDid = lib.mkOption {
487
487
+
type = optionalStr;
488
488
+
default = null;
489
489
+
description = "Moderation report service DID";
490
490
+
};
491
491
+
};
492
492
+
493
493
+
misc = {
494
494
+
ageAssuranceOverride = lib.mkOption {
495
495
+
type = optionalBool;
496
496
+
default = null;
497
497
+
description = "Override age assurance checks";
498
498
+
};
499
499
+
500
500
+
allowHttpProxy = lib.mkOption {
501
501
+
type = optionalBool;
502
502
+
default = null;
503
503
+
description = "Allow HTTP for proxy requests (development only)";
504
504
+
};
505
505
+
};
506
506
+
507
507
+
sso = {
508
508
+
github = {
509
509
+
enabled = lib.mkOption {
510
510
+
type = optionalBool;
511
511
+
default = null;
512
512
+
description = "Enable GitHub SSO";
513
513
+
};
514
514
+
515
515
+
clientId = lib.mkOption {
516
516
+
type = optionalStr;
517
517
+
default = null;
518
518
+
description = "GitHub OAuth client ID";
519
519
+
};
520
520
+
};
521
521
+
522
522
+
discord = {
523
523
+
enabled = lib.mkOption {
524
524
+
type = optionalBool;
525
525
+
default = null;
526
526
+
description = "Enable Discord SSO";
527
527
+
};
528
528
+
529
529
+
clientId = lib.mkOption {
530
530
+
type = optionalStr;
531
531
+
default = null;
532
532
+
description = "Discord OAuth client ID";
533
533
+
};
534
534
+
};
535
535
+
536
536
+
google = {
537
537
+
enabled = lib.mkOption {
538
538
+
type = optionalBool;
539
539
+
default = null;
540
540
+
description = "Enable Google SSO";
541
541
+
};
542
542
+
543
543
+
clientId = lib.mkOption {
544
544
+
type = optionalStr;
545
545
+
default = null;
546
546
+
description = "Google OAuth client ID";
547
547
+
};
548
548
+
};
549
549
+
550
550
+
gitlab = {
551
551
+
enabled = lib.mkOption {
552
552
+
type = optionalBool;
553
553
+
default = null;
554
554
+
description = "Enable GitLab SSO";
555
555
+
};
556
556
+
557
557
+
clientId = lib.mkOption {
558
558
+
type = optionalStr;
559
559
+
default = null;
560
560
+
description = "GitLab OAuth client ID";
561
561
+
};
562
562
+
563
563
+
issuer = lib.mkOption {
564
564
+
type = optionalStr;
565
565
+
default = null;
566
566
+
description = "GitLab issuer URL";
567
567
+
};
568
568
+
};
569
569
+
570
570
+
oidc = {
571
571
+
enabled = lib.mkOption {
572
572
+
type = optionalBool;
573
573
+
default = null;
574
574
+
description = "Enable generic OIDC SSO";
575
575
+
};
576
576
+
577
577
+
clientId = lib.mkOption {
578
578
+
type = optionalStr;
579
579
+
default = null;
580
580
+
description = "OIDC client ID";
581
581
+
};
582
582
+
583
583
+
issuer = lib.mkOption {
584
584
+
type = optionalStr;
585
585
+
default = null;
586
586
+
description = "OIDC issuer URL";
587
587
+
};
588
588
+
589
589
+
name = lib.mkOption {
590
590
+
type = optionalStr;
591
591
+
default = null;
592
592
+
description = "OIDC provider display name";
593
593
+
};
594
594
+
};
595
595
+
596
596
+
apple = {
597
597
+
enabled = lib.mkOption {
598
598
+
type = optionalBool;
599
599
+
default = null;
600
600
+
description = "Enable Apple Sign-in";
601
601
+
};
602
602
+
603
603
+
clientId = lib.mkOption {
604
604
+
type = optionalStr;
605
605
+
default = null;
606
606
+
description = "Apple Services ID";
607
607
+
};
608
608
+
609
609
+
teamId = lib.mkOption {
610
610
+
type = optionalStr;
611
611
+
default = null;
612
612
+
description = "Apple Team ID";
613
613
+
};
614
614
+
615
615
+
keyId = lib.mkOption {
616
616
+
type = optionalStr;
617
617
+
default = null;
618
618
+
description = "Apple Key ID";
619
619
+
};
620
620
+
};
621
621
+
};
622
622
+
};
623
623
+
};
624
624
+
625
625
+
config = let
626
626
+
effectiveBlobPath =
627
627
+
if cfg.settings.storage.blobPath != null
628
628
+
then cfg.settings.storage.blobPath
629
629
+
else "${cfg.dataDir}/blobs";
630
630
+
effectiveBackupPath =
631
631
+
if cfg.settings.backup.path != null
632
632
+
then cfg.settings.backup.path
633
633
+
else "${cfg.dataDir}/backups";
634
634
+
envVars =
635
635
+
(settingsToEnv cfg.settings)
636
636
+
// {
637
637
+
BLOB_STORAGE_PATH = effectiveBlobPath;
638
638
+
BACKUP_STORAGE_PATH = effectiveBackupPath;
639
639
+
};
640
640
+
in
641
641
+
lib.mkIf cfg.enable {
642
642
+
assertions = [
643
643
+
{
644
644
+
assertion = config.systemd.enable or true;
645
645
+
message = "services.tranquil-pds requires systemd";
646
646
+
}
647
647
+
];
648
648
+
649
649
+
users.users.${cfg.user} = {
650
650
+
isSystemUser = true;
651
651
+
inherit (cfg) group;
652
652
+
home = cfg.dataDir;
653
653
+
};
654
654
+
655
655
+
users.groups.${cfg.group} = {};
656
656
+
657
657
+
systemd.tmpfiles.rules = [
658
658
+
"d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} -"
659
659
+
"d ${effectiveBlobPath} 0750 ${cfg.user} ${cfg.group} -"
660
660
+
"d ${effectiveBackupPath} 0750 ${cfg.user} ${cfg.group} -"
661
661
+
];
662
662
+
663
663
+
systemd.services.tranquil-pds = {
664
664
+
description = "Tranquil PDS - AT Protocol Personal Data Server";
665
665
+
after = ["network.target" "postgresql.service"];
666
666
+
wants = ["network.target"];
667
667
+
wantedBy = ["multi-user.target"];
668
668
+
669
669
+
environment = envVars;
670
670
+
671
671
+
serviceConfig = {
672
672
+
Type = "exec";
673
673
+
User = cfg.user;
674
674
+
Group = cfg.group;
675
675
+
ExecStart = "${cfg.package}/bin/tranquil-pds";
676
676
+
Restart = "on-failure";
677
677
+
RestartSec = 5;
678
678
+
679
679
+
WorkingDirectory = cfg.dataDir;
680
680
+
StateDirectory = "tranquil-pds";
681
681
+
682
682
+
EnvironmentFile = lib.mkIf (cfg.secretsFile != null) cfg.secretsFile;
683
683
+
684
684
+
NoNewPrivileges = true;
685
685
+
ProtectSystem = "strict";
686
686
+
ProtectHome = true;
687
687
+
PrivateTmp = true;
688
688
+
PrivateDevices = true;
689
689
+
ProtectKernelTunables = true;
690
690
+
ProtectKernelModules = true;
691
691
+
ProtectControlGroups = true;
692
692
+
RestrictAddressFamilies = ["AF_INET" "AF_INET6" "AF_UNIX"];
693
693
+
RestrictNamespaces = true;
694
694
+
LockPersonality = true;
695
695
+
MemoryDenyWriteExecute = true;
696
696
+
RestrictRealtime = true;
697
697
+
RestrictSUIDSGID = true;
698
698
+
RemoveIPC = true;
699
699
+
700
700
+
ReadWritePaths = [
701
701
+
effectiveBlobPath
702
702
+
effectiveBackupPath
703
703
+
];
704
704
+
};
705
705
+
};
706
706
+
};
707
707
+
}
+75
test.nix
···
1
1
+
{
2
2
+
pkgs,
3
3
+
self,
4
4
+
...
5
5
+
}:
6
6
+
pkgs.testers.nixosTest {
7
7
+
name = "tranquil-pds";
8
8
+
9
9
+
nodes.server = {
10
10
+
config,
11
11
+
pkgs,
12
12
+
...
13
13
+
}: {
14
14
+
imports = [self.nixosModules.default];
15
15
+
16
16
+
services.postgresql = {
17
17
+
enable = true;
18
18
+
ensureDatabases = ["tranquil"];
19
19
+
ensureUsers = [
20
20
+
{
21
21
+
name = "tranquil";
22
22
+
ensureDBOwnership = true;
23
23
+
}
24
24
+
];
25
25
+
authentication = ''
26
26
+
local all all trust
27
27
+
host all all 127.0.0.1/32 trust
28
28
+
host all all ::1/128 trust
29
29
+
'';
30
30
+
};
31
31
+
32
32
+
services.tranquil-pds = {
33
33
+
enable = true;
34
34
+
package = self.packages.${pkgs.stdenv.hostPlatform.system}.tranquil-pds;
35
35
+
secretsFile = pkgs.writeText "tranquil-secrets" ''
36
36
+
JWT_SECRET=test-jwt-secret-must-be-32-chars-long
37
37
+
DPOP_SECRET=test-dpop-secret-must-be-32-chars-long
38
38
+
MASTER_KEY=test-master-key-must-be-32-chars-long
39
39
+
'';
40
40
+
41
41
+
settings = {
42
42
+
server.pdsHostname = "test.local";
43
43
+
server.host = "0.0.0.0";
44
44
+
45
45
+
database.url = "postgres://tranquil@localhost/tranquil";
46
46
+
47
47
+
storage.blobBackend = "filesystem";
48
48
+
backup.backend = "filesystem";
49
49
+
};
50
50
+
};
51
51
+
52
52
+
networking.firewall.allowedTCPPorts = [3000];
53
53
+
};
54
54
+
55
55
+
testScript = ''
56
56
+
server.wait_for_unit("postgresql.service")
57
57
+
server.wait_for_unit("tranquil-pds.service")
58
58
+
server.wait_for_open_port(3000)
59
59
+
60
60
+
with subtest("service is running"):
61
61
+
status = server.succeed("systemctl is-active tranquil-pds")
62
62
+
assert "active" in status
63
63
+
64
64
+
with subtest("blob storage directory exists"):
65
65
+
server.succeed("test -d /var/lib/tranquil-pds/blobs")
66
66
+
server.succeed("test -d /var/lib/tranquil-pds/backups")
67
67
+
68
68
+
with subtest("healthcheck responds"):
69
69
+
server.succeed("curl -sf http://localhost:3000/xrpc/_health")
70
70
+
71
71
+
with subtest("describeServer returns valid response"):
72
72
+
result = server.succeed("curl -sf http://localhost:3000/xrpc/com.atproto.server.describeServer")
73
73
+
assert "availableUserDomains" in result
74
74
+
'';
75
75
+
}