1 { config, pkgs, lib, ... }:
5 cfg = config.services.sourcehut;
6 rcfg = config.services.redis;
8 settingsFormat = pkgs.formats.ini {
11 else generators.mkKeyValueDefault {
13 if v == true then "yes"
14 else if v == false then "no"
15 else generators.mkValueStringDefault {} v;
18 commonServiceSettings = service: {
20 description = "URL ${service}.sr.ht is being served at (protocol://domain)";
22 default = "https://${service}.${cfg.originBase}";
24 debug-host = mkOption {
25 description = "Address to bind the debug server to.";
29 debug-port = mkOption {
30 description = "Port to bind the debug server to.";
32 default = cfg.${service}.port;
34 connection-string = mkOption {
35 description = "SQLAlchemy connection string for the database.";
37 default = "postgresql:///${cfg.${service}.database}?user=${cfg.${service}.user}&host=/var/run/postgresql";
39 migrate-on-upgrade = mkEnableOption "automatic migrations on package upgrade";
40 oauth-client-id = mkOptionNullOrStr "OAUTH client id for meta.sr.ht.";
41 oauth-client-secret = mkOptionNullOrStr "OAUTH client secret for meta.sr.ht.";
44 # Specialized python containing all the modules
45 python = pkgs.sourcehut.python.withPackages (ps: with ps; [
61 mkOptionNullOrStr = description: mkOption {
63 type = with types; nullOr str;
80 (mkRemovedOptionModule [ "services" "sourcehut" "nginx" "enable" ] ''
81 The sourcehut module supports `nginx` as a local reverse-proxy by default and doesn't
82 support other reverse-proxies officially.
84 However it's possible to use an alternative reverse-proxy by
87 * adjusting the relevant settings for server addresses and ports directly
89 Further details about this can be found in the `Sourcehut`-section of the NixOS-manual.
93 options.services.sourcehut = {
94 enable = mkEnableOption ''
95 sourcehut - git hosting, continuous integration, mailing list, ticket tracking,
96 task dispatching, wiki and account management services
100 type = types.nonEmptyListOf (types.enum [ "builds" "dispatch" "git" "hub" "hg" "lists" "man" "meta" "paste" "todo" ]);
101 default = [ "man" "meta" "paste" ];
102 example = [ "builds" "dispatch" "git" "hub" "hg" "lists" "man" "meta" "paste" "todo" ];
104 Services to enable on the sourcehut network.
108 originBase = mkOption {
110 default = with config.networking; hostName + lib.optionalString (domain != null) ".${domain}";
112 Host name used by reverse-proxy and for default settings. Will host services at git."''${originBase}". For example: git.sr.ht
118 default = "127.0.0.1";
126 type = types.package;
129 The python package to use. It should contain references to the *srht modules and also
134 statePath = mkOption {
136 default = "/var/lib/sourcehut";
138 Root state path for the sourcehut network. If left as the default value
139 this directory will automatically be created before the sourcehut server
140 starts, otherwise the sysadmin is responsible for ensuring the
141 directory exists with appropriate ownership and permissions.
145 settings = mkOption {
146 type = lib.types.submodule {
147 freeformType = settingsFormat.type;
149 environment = mkOption {
150 description = "Values other than \"production\" adds a banner to each page.";
151 type = types.enum [ "development" "production" ];
152 default = "development";
154 global-domain = mkOptionNullOrStr "Global domain name.";
155 owner-email = mkOption {
156 description = "Owner's email.";
158 default = "contact@example.com";
160 owner-name = mkOption {
161 description = "Owner's name.";
163 default = "John Doe";
165 secret-key = mkOptionNullOrStr "Secret key to encrypt session cookies with.";
166 site-blurb = mkOption {
167 description = "Blurb for your site.";
169 default = "the hacker's forge";
171 site-info = mkOption {
172 description = "The top-level info page for your site.";
174 default = "https://sourcehut.org";
176 site-name = mkOption {
177 description = "The name of your network of sr.ht-based sites.";
179 default = "sourcehut";
181 source-url = mkOption {
182 description = "The source code for your fork of sr.ht.";
184 default = "https://git.sr.ht/~sircmpwn/srht";
188 smtp-host = mkOptionNullOrStr "Outgoing SMTP host.";
189 smtp-port = mkOption {
190 description = "Outgoing SMTP port.";
191 type = with types; nullOr port;
194 smtp-user = mkOptionNullOrStr "Outgoing SMTP user.";
195 smtp-password = mkOptionNullOrStr "Outgoing SMTP password.";
196 smtp-from = mkOptionNullOrStr "Outgoing SMTP FROM.";
197 error-to = mkOptionNullOrStr "Address receiving application exceptions";
198 error-from = mkOptionNullOrStr "Address sending application exceptions";
199 pgp-privkey = mkOptionNullOrStr ''
202 Your PGP key information (DO NOT mix up pub and priv here)
203 You must remove the password from your secret key, if present.
204 You can do this with <code>gpg --edit-key [key-id]</code>, then use the <code>passwd</code> command and do not enter a new password.
206 pgp-pubkey = mkOptionNullOrStr "OpenPGP public key.";
207 pgp-key-id = mkOptionNullOrStr "OpenPGP key identifier.";
210 private-key = mkOptionNullOrStr ''
211 base64-encoded Ed25519 key for signing webhook payloads.
212 This should be consistent for all *.sr.ht sites,
213 as this key will be used to verify signatures
214 from other sites in your network.
215 Use the <code>srht-webhook-keygen</code> command to generate a key.
218 options."dispatch.sr.ht" = commonServiceSettings "dispatch";
219 options."dispatch.sr.ht::github" = {
220 oauth-client-id = mkOptionNullOrStr "OAUTH client id.";
221 oauth-client-secret = mkOptionNullOrStr "OAUTH client secret.";
223 options."dispatch.sr.ht::gitlab" = {
224 enabled = mkEnableOption "GitLab integration";
225 canonical-upstream = mkOption {
227 description = "Canonical upstream.";
228 default = "gitlab.com";
230 repo-cache = mkOption {
232 description = "Repository cache directory.";
233 default = "./repo-cache";
235 "gitlab.com" = mkOption {
237 description = "GitLab id and secret.";
239 example = "GitLab:application id:secret";
242 options."builds.sr.ht" = commonServiceSettings "builds" // {
244 description = "The redis connection used for the celery worker.";
246 default = "redis://${rcfg.bind}:${toString rcfg.port}/3";
249 description = "The shell used for ssh.";
251 default = "runner-shell";
254 options."git.sr.ht" = commonServiceSettings "git" // {
255 outgoing-domain = mkOption {
256 description = "Outgoing domain.";
258 default = "http://git.${cfg.originBase}";
260 post-update-script = mkOption {
261 description = "A post-update script which is installed in every git repo.";
263 default = "${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
266 description = "Path to git repositories on disk.";
268 default = "/var/lib/git";
270 webhooks = mkOption {
271 description = "The redis connection used for the webhooks worker.";
273 default = "redis://${rcfg.bind}:${toString rcfg.port}/1";
276 options."hg.sr.ht" = commonServiceSettings "hg" // {
277 changegroup-script = mkOption {
278 description = "A post-update script which is installed in every mercurial repo..";
280 default = "${cfg.python}/bin/hgsrht-hook-changegroup";
283 description = "Path to mercurial repositories on disk.";
285 default = "/var/lib/hg";
287 srhtext = mkOptionNullOrStr ''
288 Path to the srht mercurial extension
289 (defaults to where the hgsrht code is)
291 clone_bundle_threshold = mkOption {
292 description = ".hg/store size (in MB) past which the nightly job generates clone bundles.";
293 type = types.ints.unsigned;
297 description = "Path to hg-ssh (if not in $PATH).";
299 default = "${pkgs.mercurial}/bin/hg-ssh";
301 webhooks = mkOption {
302 description = "The redis connection used for the webhooks worker.";
304 default = "redis://${rcfg.bind}:${toString rcfg.port}/1";
307 options."hub.sr.ht" = commonServiceSettings "hub" // {
309 options."lists.sr.ht" = commonServiceSettings "lists" // {
310 allow-new-lists = mkEnableOption "Allow creation of new lists.";
311 network-key = mkOptionNullOrStr "Network key.";
312 notify-from = mkOption {
313 description = "Outgoing email for notifications generated by users.";
315 default = "lists-notify@${cfg.originBase}";
317 posting-domain = mkOption {
318 description = "Posting domain.";
320 default = "lists.${cfg.originBase}";
323 description = "The redis connection used for the celery worker.";
325 default = "redis://${rcfg.bind}:${toString rcfg.port}/4";
327 webhooks = mkOption {
328 description = "The redis connection used for the webhooks worker.";
330 default = "redis://${rcfg.bind}:${toString rcfg.port}/2";
333 options."lists.sr.ht::worker" = {
334 reject-mimetypes = mkOption {
335 type = with types; listOf str;
336 default = ["text/html"];
338 reject-url = mkOption {
339 description = "Reject URL.";
340 default = "https://man.sr.ht/lists.sr.ht/etiquette.md";
345 Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
346 Alternatively, specify IP:PORT and an SMTP server will be run instead.
349 default = "/tmp/lists.sr.ht-lmtp.sock";
351 sock-group = mkOption {
353 The lmtp daemon will make the unix socket group-read/write
354 for users in this group.
360 options."man.sr.ht" = commonServiceSettings "man" // {
362 options."meta.sr.ht" = commonServiceSettings "meta" // {
363 api-origin = mkOption {
364 description = "Origin URL for API, 100 more than web.";
366 default = "http://localhost:5100";
368 webhooks = mkOption {
369 description = "The redis connection used for the webhooks worker.";
371 default = "redis://${rcfg.bind}:${toString rcfg.port}/6";
373 welcome-emails = mkEnableOption "sending stock sourcehut welcome emails after signup";
375 options."meta.sr.ht::settings" = {
376 registration = mkEnableOption "public registration";
377 onboarding-redirect = mkOption {
378 description = "Where to redirect new users upon registration.";
380 default = "https://meta.${cfg.originBase}";
382 user-invites = mkOption {
384 How many invites each user is issued upon registration
385 (only applicable if open registration is disabled).
387 type = types.ints.unsigned;
391 options."meta.sr.ht::aliases" = mkOption {
392 description = "Aliases for the client IDs of commonly used OAuth clients.";
393 type = with types; attrsOf int;
395 example = { "git.sr.ht" = 12345; };
397 options."meta.sr.ht::billing" = {
398 enabled = mkEnableOption "the billing system";
399 stripe-public-key = mkOptionNullOrStr "Public key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys";
400 stripe-secret-key = mkOptionNullOrStr "Secret key for Stripe. Get your keys at https://dashboard.stripe.com/account/apikeys";
402 options."paste.sr.ht" = commonServiceSettings "paste" // {
403 webhooks = mkOption {
405 default = "redis://${rcfg.bind}:${toString rcfg.port}/5";
408 options."todo.sr.ht" = commonServiceSettings "todo" // {
409 network-key = mkOptionNullOrStr "Network key.";
410 notify-from = mkOption {
411 description = "Outgoing email for notifications generated by users.";
413 default = "todo-notify@${cfg.originBase}";
415 webhooks = mkOption {
416 description = "The redis connection used for the webhooks worker.";
418 default = "redis://${rcfg.bind}:${toString rcfg.port}/7";
421 options."todo.sr.ht::mail" = {
422 posting-domain = mkOption {
423 description = "Posting domain.";
425 default = "todo.${cfg.originBase}";
429 Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
430 Alternatively, specify IP:PORT and an SMTP server will be run instead.
433 default = "/tmp/todo.sr.ht-lmtp.sock";
435 sock-group = mkOption {
437 The lmtp daemon will make the unix socket group-read/write
438 for users in this group.
447 The configuration for the sourcehut network.
452 config = mkIf cfg.enable {
456 assertion = with cfgIni.webhooks; private-key != null && stringLength private-key == 44;
457 message = "The webhook's private key must be defined and of a 44 byte length.";
461 assertion = hasAttrByPath [ "meta.sr.ht" "origin" ] cfgIni && cfgIni."meta.sr.ht".origin != null;
462 message = "meta.sr.ht's origin must be defined.";
466 environment.etc."sr.ht/config.ini".source =
467 settingsFormat.generate "sourcehut-config.ini"
468 # Disabled services must be removed from the config
469 # to be effectively disabled.
471 let srv = builtins.match "^([a-z]*)\\.sr\\.ht(::.*)?$" k; in
472 srv == null || elem (head srv) cfg.services
475 environment.systemPackages = [ pkgs.sourcehut.coresrht ];
478 services.postgresql.enable = mkOverride 999 true;
480 services.postfix.enable = mkOverride 999 true;
482 services.cron.enable = mkOverride 999 true;
484 services.redis.enable = mkOverride 999 true;
485 services.redis.bind = mkOverride 999 "127.0.0.1";
488 meta.doc = ./sourcehut.xml;
489 meta.maintainers = with maintainers; [ tomberek ];