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