base16-schemes: unstable-2024-06-21 -> unstable-2024-11-12
[NixPkgs.git] / nixos / modules / services / web-apps / outline.nix
blobba426ce89bbf26326b0691ea7200be19ce67fc81
1 { config, lib, pkgs, ...}:
3 let
4   defaultUser = "outline";
5   cfg = config.services.outline;
6   inherit (lib) mkRemovedOptionModule;
7 in
9   imports = [
10     (mkRemovedOptionModule [ "services" "outline" "sequelizeArguments" ] "Database migration are run agains configurated database by outline directly")
11   ];
12   # See here for a reference of all the options:
13   #   https://github.com/outline/outline/blob/v0.67.0/.env.sample
14   #   https://github.com/outline/outline/blob/v0.67.0/app.json
15   #   https://github.com/outline/outline/blob/v0.67.0/server/env.ts
16   #   https://github.com/outline/outline/blob/v0.67.0/shared/types.ts
17   # The order is kept the same here to make updating easier.
18   options.services.outline = {
19     enable = lib.mkEnableOption "outline";
21     package = lib.mkOption {
22       default = pkgs.outline;
23       defaultText = lib.literalExpression "pkgs.outline";
24       type = lib.types.package;
25       example = lib.literalExpression ''
26         pkgs.outline.overrideAttrs (super: {
27           # Ignore the domain part in emails that come from OIDC. This is might
28           # be helpful if you want multiple users with different email providers
29           # to still land in the same team. Note that this effectively makes
30           # Outline a single-team instance.
31           patchPhase = ${"''"}
32             sed -i 's/const domain = parts\.length && parts\[1\];/const domain = "example.com";/g' plugins/oidc/server/auth/oidc.ts
33           ${"''"};
34         })
35       '';
36       description = "Outline package to use.";
37     };
39     user = lib.mkOption {
40       type = lib.types.str;
41       default = defaultUser;
42       description = ''
43         User under which the service should run. If this is the default value,
44         the user will be created, with the specified group as the primary
45         group.
46       '';
47     };
49     group = lib.mkOption {
50       type = lib.types.str;
51       default = defaultUser;
52       description = ''
53         Group under which the service should run. If this is the default value,
54         the group will be created.
55       '';
56     };
58     #
59     # Required options
60     #
62     secretKeyFile = lib.mkOption {
63       type = lib.types.str;
64       default = "/var/lib/outline/secret_key";
65       description = ''
66         File path that contains the application secret key. It must be 32
67         bytes long and hex-encoded. If the file does not exist, a new key will
68         be generated and saved here.
69       '';
70     };
72     utilsSecretFile = lib.mkOption {
73       type = lib.types.str;
74       default = "/var/lib/outline/utils_secret";
75       description = ''
76         File path that contains the utility secret key. If the file does not
77         exist, a new key will be generated and saved here.
78       '';
79     };
81     databaseUrl = lib.mkOption {
82       type = lib.types.str;
83       default = "local";
84       description = ''
85         URI to use for the main PostgreSQL database. If this needs to include
86         credentials that shouldn't be world-readable in the Nix store, set an
87         environment file on the systemd service and override the
88         `DATABASE_URL` entry. Pass the string
89         `local` to setup a database on the local server.
90       '';
91     };
93     redisUrl = lib.mkOption {
94       type = lib.types.str;
95       default = "local";
96       description = ''
97         Connection to a redis server. If this needs to include credentials
98         that shouldn't be world-readable in the Nix store, set an environment
99         file on the systemd service and override the
100         `REDIS_URL` entry. Pass the string
101         `local` to setup a local Redis database.
102       '';
103     };
105     publicUrl = lib.mkOption {
106       type = lib.types.str;
107       default = "http://localhost:3000";
108       description = "The fully qualified, publicly accessible URL";
109     };
111     port = lib.mkOption {
112       type = lib.types.port;
113       default = 3000;
114       description = "Listening port.";
115     };
117     storage = lib.mkOption {
118       description = ''
119         To support uploading of images for avatars and document attachments an
120         s3-compatible storage can be provided. AWS S3 is recommended for
121         redundancy however if you want to keep all file storage local an
122         alternative such as [minio](https://github.com/minio/minio)
123         can be used.
124         Local filesystem storage can also be used.
126         A more detailed guide on setting up storage is available
127         [here](https://docs.getoutline.com/s/hosting/doc/file-storage-N4M0T6Ypu7).
128       '';
129       example = lib.literalExpression ''
130         {
131           accessKey = "...";
132           secretKeyFile = "/somewhere";
133           uploadBucketUrl = "https://minio.example.com";
134           uploadBucketName = "outline";
135           region = "us-east-1";
136         }
137       '';
138       type = lib.types.submodule {
139         options = {
140           storageType = lib.mkOption {
141             type = lib.types.enum [ "local" "s3" ];
142             description = "File storage type, it can be local or s3.";
143             default = "s3";
144           };
145           localRootDir = lib.mkOption {
146             type = lib.types.str;
147             description = ''
148               If `storageType` is `local`, this sets the parent directory
149               under which all attachments/images go.
150             '';
151             default = "/var/lib/outline/data";
152           };
153           accessKey = lib.mkOption {
154             type = lib.types.str;
155             description = "S3 access key.";
156           };
157           secretKeyFile = lib.mkOption {
158             type = lib.types.path;
159             description = "File path that contains the S3 secret key.";
160           };
161           region = lib.mkOption {
162             type = lib.types.str;
163             default = "xx-xxxx-x";
164             description = "AWS S3 region name.";
165           };
166           uploadBucketUrl = lib.mkOption {
167             type = lib.types.str;
168             description = ''
169               URL endpoint of an S3-compatible API where uploads should be
170               stored.
171             '';
172           };
173           uploadBucketName = lib.mkOption {
174             type = lib.types.str;
175             description = "Name of the bucket where uploads should be stored.";
176           };
177           uploadMaxSize = lib.mkOption {
178             type = lib.types.int;
179             default = 26214400;
180             description = "Maxmium file size for uploads.";
181           };
182           forcePathStyle = lib.mkOption {
183             type = lib.types.bool;
184             default = true;
185             description = "Force S3 path style.";
186           };
187           acl = lib.mkOption {
188             type = lib.types.str;
189             default = "private";
190             description = "ACL setting.";
191           };
192         };
193       };
194     };
196     #
197     # Authentication
198     #
200     slackAuthentication = lib.mkOption {
201       description = ''
202         To configure Slack auth, you'll need to create an Application at
203         https://api.slack.com/apps
205         When configuring the Client ID, add a redirect URL under "OAuth & Permissions"
206         to `https://[publicUrl]/auth/slack.callback`.
207       '';
208       default = null;
209       type = lib.types.nullOr (lib.types.submodule {
210         options = {
211           clientId = lib.mkOption {
212             type = lib.types.str;
213             description = "Authentication key.";
214           };
215           secretFile = lib.mkOption {
216             type = lib.types.str;
217             description = "File path containing the authentication secret.";
218           };
219         };
220       });
221     };
223     googleAuthentication = lib.mkOption {
224       description = ''
225         To configure Google auth, you'll need to create an OAuth Client ID at
226         https://console.cloud.google.com/apis/credentials
228         When configuring the Client ID, add an Authorized redirect URI to
229         `https://[publicUrl]/auth/google.callback`.
230       '';
231       default = null;
232       type = lib.types.nullOr (lib.types.submodule {
233         options = {
234           clientId = lib.mkOption {
235             type = lib.types.str;
236             description = "Authentication client identifier.";
237           };
238           clientSecretFile = lib.mkOption {
239             type = lib.types.str;
240             description = "File path containing the authentication secret.";
241           };
242         };
243       });
244     };
246     azureAuthentication = lib.mkOption {
247       description = ''
248         To configure Microsoft/Azure auth, you'll need to create an OAuth
249         Client. See
250         [the guide](https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4)
251         for details on setting up your Azure App.
252       '';
253       default = null;
254       type = lib.types.nullOr (lib.types.submodule {
255         options = {
256           clientId = lib.mkOption {
257             type = lib.types.str;
258             description = "Authentication client identifier.";
259           };
260           clientSecretFile = lib.mkOption {
261             type = lib.types.str;
262             description = "File path containing the authentication secret.";
263           };
264           resourceAppId = lib.mkOption {
265             type = lib.types.str;
266             description = "Authentication application resource ID.";
267           };
268         };
269       });
270     };
272     oidcAuthentication = lib.mkOption {
273       description = ''
274         To configure generic OIDC auth, you'll need some kind of identity
275         provider. See the documentation for whichever IdP you use to fill out
276         all the fields. The redirect URL is
277         `https://[publicUrl]/auth/oidc.callback`.
278       '';
279       default = null;
280       type = lib.types.nullOr (lib.types.submodule {
281         options = {
282           clientId = lib.mkOption {
283             type = lib.types.str;
284             description = "Authentication client identifier.";
285           };
286           clientSecretFile = lib.mkOption {
287             type = lib.types.str;
288             description = "File path containing the authentication secret.";
289           };
290           authUrl = lib.mkOption {
291             type = lib.types.str;
292             description = "OIDC authentication URL endpoint.";
293           };
294           tokenUrl = lib.mkOption {
295             type = lib.types.str;
296             description = "OIDC token URL endpoint.";
297           };
298           userinfoUrl = lib.mkOption {
299             type = lib.types.str;
300             description = "OIDC userinfo URL endpoint.";
301           };
302           usernameClaim = lib.mkOption {
303             type = lib.types.str;
304             description = ''
305               Specify which claims to derive user information from. Supports any
306               valid JSON path with the JWT payload
307             '';
308             default = "preferred_username";
309           };
310           displayName = lib.mkOption {
311             type = lib.types.str;
312             description = "Display name for OIDC authentication.";
313             default = "OpenID";
314           };
315           scopes = lib.mkOption {
316             type = lib.types.listOf lib.types.str;
317             description = "OpenID authentication scopes.";
318             default = [ "openid" "profile" "email" ];
319           };
320         };
321       });
322     };
324     #
325     # Optional configuration
326     #
328     sslKeyFile = lib.mkOption {
329       type = lib.types.nullOr lib.types.str;
330       default = null;
331       description = ''
332         File path that contains the Base64-encoded private key for HTTPS
333         termination. This is only required if you do not use an external reverse
334         proxy. See
335         [the documentation](https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4).
336       '';
337     };
338     sslCertFile = lib.mkOption {
339       type = lib.types.nullOr lib.types.str;
340       default = null;
341       description = ''
342         File path that contains the Base64-encoded certificate for HTTPS
343         termination. This is only required if you do not use an external reverse
344         proxy. See
345         [the documentation](https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4).
346       '';
347     };
349     cdnUrl = lib.mkOption {
350       type = lib.types.str;
351       default = "";
352       description = ''
353         If using a Cloudfront/Cloudflare distribution or similar it can be set
354         using this option. This will cause paths to JavaScript files,
355         stylesheets and images to be updated to the hostname defined here. In
356         your CDN configuration the origin server should be set to public URL.
357       '';
358     };
360     forceHttps = lib.mkOption {
361       type = lib.types.bool;
362       default = true;
363       description = ''
364         Auto-redirect to HTTPS in production. The default is
365         `true` but you may set this to `false`
366         if you can be sure that SSL is terminated at an external loadbalancer.
367       '';
368     };
370     enableUpdateCheck = lib.mkOption {
371       type = lib.types.bool;
372       default = false;
373       description = ''
374         Have the installation check for updates by sending anonymized statistics
375         to the maintainers.
376       '';
377     };
379     concurrency = lib.mkOption {
380       type = lib.types.int;
381       default = 1;
382       description = ''
383         How many processes should be spawned. For a rough estimate, divide your
384         server's available memory by 512.
385       '';
386     };
388     maximumImportSize = lib.mkOption {
389       type = lib.types.int;
390       default = 5120000;
391       description = ''
392         The maximum size of document imports. Overriding this could be required
393         if you have especially large Word documents with embedded imagery.
394       '';
395     };
397     debugOutput = lib.mkOption {
398       type = lib.types.nullOr (lib.types.enum [ "http" ]);
399       default = null;
400       description = "Set this to `http` log HTTP requests.";
401     };
403     slackIntegration = lib.mkOption {
404       description = ''
405         For a complete Slack integration with search and posting to channels
406         this configuration is also needed. See here for details:
407         https://wiki.generaloutline.com/share/be25efd1-b3ef-4450-b8e5-c4a4fc11e02a
408       '';
409       default = null;
410       type = lib.types.nullOr (lib.types.submodule {
411         options = {
412           verificationTokenFile = lib.mkOption {
413             type = lib.types.str;
414             description = "File path containing the verification token.";
415           };
416           appId = lib.mkOption {
417             type = lib.types.str;
418             description = "Application ID.";
419           };
420           messageActions = lib.mkOption {
421             type = lib.types.bool;
422             default = true;
423             description = "Whether to enable message actions.";
424           };
425         };
426       });
427     };
429     googleAnalyticsId = lib.mkOption {
430       type = lib.types.nullOr lib.types.str;
431       default = null;
432       description = ''
433         Optionally enable Google Analytics to track page views in the knowledge
434         base.
435       '';
436     };
438     sentryDsn = lib.mkOption {
439       type = lib.types.nullOr lib.types.str;
440       default = null;
441       description = ''
442         Optionally enable [Sentry](https://sentry.io/) to
443         track errors and performance.
444       '';
445     };
447     sentryTunnel = lib.mkOption {
448       type = lib.types.nullOr lib.types.str;
449       default = null;
450       description = ''
451         Optionally add a
452         [Sentry proxy tunnel](https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option)
453         for bypassing ad blockers in the UI.
454       '';
455     };
457     logo = lib.mkOption {
458       type = lib.types.nullOr lib.types.str;
459       default = null;
460       description = ''
461         Custom logo displayed on the authentication screen. This will be scaled
462         to a height of 60px.
463       '';
464     };
466     smtp = lib.mkOption {
467       description = ''
468         To support sending outgoing transactional emails such as
469         "document updated" or "you've been invited" you'll need to provide
470         authentication for an SMTP server.
471       '';
472       default = null;
473       type = lib.types.nullOr (lib.types.submodule {
474         options = {
475           host = lib.mkOption {
476             type = lib.types.str;
477             description = "Host name or IP address of the SMTP server.";
478           };
479           port = lib.mkOption {
480             type = lib.types.port;
481             description = "TCP port of the SMTP server.";
482           };
483           username = lib.mkOption {
484             type = lib.types.str;
485             description = "Username to authenticate with.";
486           };
487           passwordFile = lib.mkOption {
488             type = lib.types.str;
489             description = ''
490               File path containing the password to authenticate with.
491             '';
492           };
493           fromEmail = lib.mkOption {
494             type = lib.types.str;
495             description = "Sender email in outgoing mail.";
496           };
497           replyEmail = lib.mkOption {
498             type = lib.types.str;
499             description = "Reply address in outgoing mail.";
500           };
501           tlsCiphers = lib.mkOption {
502             type = lib.types.str;
503             default = "";
504             description = "Override SMTP cipher configuration.";
505           };
506           secure = lib.mkOption {
507             type = lib.types.bool;
508             default = true;
509             description = "Use a secure SMTP connection.";
510           };
511         };
512       });
513     };
515     defaultLanguage = lib.mkOption {
516       type = lib.types.enum [
517          "da_DK"
518          "de_DE"
519          "en_US"
520          "es_ES"
521          "fa_IR"
522          "fr_FR"
523          "it_IT"
524          "ja_JP"
525          "ko_KR"
526          "nl_NL"
527          "pl_PL"
528          "pt_BR"
529          "pt_PT"
530          "ru_RU"
531          "sv_SE"
532          "th_TH"
533          "vi_VN"
534          "zh_CN"
535          "zh_TW"
536       ];
537       default = "en_US";
538       description = ''
539         The default interface language. See
540         [translate.getoutline.com](https://translate.getoutline.com/)
541         for a list of available language codes and their rough percentage
542         translated.
543       '';
544     };
546     rateLimiter.enable = lib.mkEnableOption "rate limiter for the application web server";
547     rateLimiter.requests = lib.mkOption {
548       type = lib.types.int;
549       default = 5000;
550       description = "Maximum number of requests in a throttling window.";
551     };
552     rateLimiter.durationWindow = lib.mkOption {
553       type = lib.types.int;
554       default = 60;
555       description = "Length of a throttling window.";
556     };
557   };
559   config = lib.mkIf cfg.enable {
560     users.users = lib.optionalAttrs (cfg.user == defaultUser) {
561       ${defaultUser} = {
562         isSystemUser = true;
563         group = cfg.group;
564       };
565     };
567     users.groups = lib.optionalAttrs (cfg.group == defaultUser) {
568       ${defaultUser} = { };
569     };
571     systemd.tmpfiles.rules = [
572       "f ${cfg.secretKeyFile} 0600 ${cfg.user} ${cfg.group} -"
573       "f ${cfg.utilsSecretFile} 0600 ${cfg.user} ${cfg.group} -"
574       (if (cfg.storage.storageType == "s3") then
575         "f ${cfg.storage.secretKeyFile} 0600 ${cfg.user} ${cfg.group} -"
576       else
577         "d ${cfg.storage.localRootDir} 0700 ${cfg.user} ${cfg.group} - -")
578     ];
580     services.postgresql = lib.mkIf (cfg.databaseUrl == "local") {
581       enable = true;
582       ensureUsers = [{
583         name = "outline";
584         ensureDBOwnership = true;
585       }];
586       ensureDatabases = [ "outline" ];
587     };
589     services.redis.servers.outline = lib.mkIf (cfg.redisUrl == "local") {
590       enable = true;
591       user = config.services.outline.user;
592       port = 0; # Disable the TCP listener
593     };
595     systemd.services.outline = let
596       localRedisUrl = "redis+unix:///run/redis-outline/redis.sock";
597       localPostgresqlUrl = "postgres://localhost/outline?host=/run/postgresql";
598     in {
599       description = "Outline wiki and knowledge base";
600       wantedBy = [ "multi-user.target" ];
601       after = [ "networking.target" ]
602         ++ lib.optional (cfg.databaseUrl == "local") "postgresql.service"
603         ++ lib.optional (cfg.redisUrl == "local") "redis-outline.service";
604       requires = lib.optional (cfg.databaseUrl == "local") "postgresql.service"
605         ++ lib.optional (cfg.redisUrl == "local") "redis-outline.service";
606       path = [
607         pkgs.openssl # Required by the preStart script
608       ];
611       environment = lib.mkMerge [
612         {
613           NODE_ENV = "production";
615           REDIS_URL = if cfg.redisUrl == "local" then localRedisUrl else cfg.redisUrl;
616           URL = cfg.publicUrl;
617           PORT = builtins.toString cfg.port;
619           CDN_URL = cfg.cdnUrl;
620           FORCE_HTTPS = builtins.toString cfg.forceHttps;
621           ENABLE_UPDATES = builtins.toString cfg.enableUpdateCheck;
622           WEB_CONCURRENCY = builtins.toString cfg.concurrency;
623           MAXIMUM_IMPORT_SIZE = builtins.toString cfg.maximumImportSize;
624           DEBUG = cfg.debugOutput;
625           GOOGLE_ANALYTICS_ID = lib.optionalString (cfg.googleAnalyticsId != null) cfg.googleAnalyticsId;
626           SENTRY_DSN = lib.optionalString (cfg.sentryDsn != null) cfg.sentryDsn;
627           SENTRY_TUNNEL = lib.optionalString (cfg.sentryTunnel != null) cfg.sentryTunnel;
628           TEAM_LOGO = lib.optionalString (cfg.logo != null) cfg.logo;
629           DEFAULT_LANGUAGE = cfg.defaultLanguage;
631           RATE_LIMITER_ENABLED = builtins.toString cfg.rateLimiter.enable;
632           RATE_LIMITER_REQUESTS = builtins.toString cfg.rateLimiter.requests;
633           RATE_LIMITER_DURATION_WINDOW = builtins.toString cfg.rateLimiter.durationWindow;
635           FILE_STORAGE = cfg.storage.storageType;
636           FILE_STORAGE_UPLOAD_MAX_SIZE = builtins.toString cfg.storage.uploadMaxSize;
637           FILE_STORAGE_LOCAL_ROOT_DIR = cfg.storage.localRootDir;
638         }
640         (lib.mkIf (cfg.storage.storageType == "s3") {
641           AWS_ACCESS_KEY_ID = cfg.storage.accessKey;
642           AWS_REGION = cfg.storage.region;
643           AWS_S3_UPLOAD_BUCKET_URL = cfg.storage.uploadBucketUrl;
644           AWS_S3_UPLOAD_BUCKET_NAME = cfg.storage.uploadBucketName;
645           AWS_S3_FORCE_PATH_STYLE = builtins.toString cfg.storage.forcePathStyle;
646           AWS_S3_ACL = cfg.storage.acl;
647         })
649         (lib.mkIf (cfg.slackAuthentication != null) {
650           SLACK_CLIENT_ID = cfg.slackAuthentication.clientId;
651         })
653         (lib.mkIf (cfg.googleAuthentication != null) {
654           GOOGLE_CLIENT_ID = cfg.googleAuthentication.clientId;
655         })
657         (lib.mkIf (cfg.azureAuthentication != null) {
658           AZURE_CLIENT_ID = cfg.azureAuthentication.clientId;
659           AZURE_RESOURCE_APP_ID = cfg.azureAuthentication.resourceAppId;
660         })
662         (lib.mkIf (cfg.oidcAuthentication != null) {
663           OIDC_CLIENT_ID = cfg.oidcAuthentication.clientId;
664           OIDC_AUTH_URI = cfg.oidcAuthentication.authUrl;
665           OIDC_TOKEN_URI = cfg.oidcAuthentication.tokenUrl;
666           OIDC_USERINFO_URI = cfg.oidcAuthentication.userinfoUrl;
667           OIDC_USERNAME_CLAIM = cfg.oidcAuthentication.usernameClaim;
668           OIDC_DISPLAY_NAME = cfg.oidcAuthentication.displayName;
669           OIDC_SCOPES = lib.concatStringsSep " " cfg.oidcAuthentication.scopes;
670         })
672         (lib.mkIf (cfg.slackIntegration != null) {
673           SLACK_APP_ID = cfg.slackIntegration.appId;
674           SLACK_MESSAGE_ACTIONS = builtins.toString cfg.slackIntegration.messageActions;
675         })
677         (lib.mkIf (cfg.smtp != null) {
678           SMTP_HOST = cfg.smtp.host;
679           SMTP_PORT = builtins.toString cfg.smtp.port;
680           SMTP_USERNAME = cfg.smtp.username;
681           SMTP_FROM_EMAIL = cfg.smtp.fromEmail;
682           SMTP_REPLY_EMAIL = cfg.smtp.replyEmail;
683           SMTP_TLS_CIPHERS = cfg.smtp.tlsCiphers;
684           SMTP_SECURE = builtins.toString cfg.smtp.secure;
685         })
686       ];
688       preStart = ''
689         if [ ! -s ${lib.escapeShellArg cfg.secretKeyFile} ]; then
690           openssl rand -hex 32 > ${lib.escapeShellArg cfg.secretKeyFile}
691         fi
692         if [ ! -s ${lib.escapeShellArg cfg.utilsSecretFile} ]; then
693           openssl rand -hex 32 > ${lib.escapeShellArg cfg.utilsSecretFile}
694         fi
696       '';
698       script = ''
699         export SECRET_KEY="$(head -n1 ${lib.escapeShellArg cfg.secretKeyFile})"
700         export UTILS_SECRET="$(head -n1 ${lib.escapeShellArg cfg.utilsSecretFile})"
701         ${lib.optionalString (cfg.storage.storageType == "s3") ''
702           export AWS_SECRET_ACCESS_KEY="$(head -n1 ${lib.escapeShellArg cfg.storage.secretKeyFile})"
703         ''}
704         ${lib.optionalString (cfg.slackAuthentication != null) ''
705           export SLACK_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.slackAuthentication.secretFile})"
706         ''}
707         ${lib.optionalString (cfg.googleAuthentication != null) ''
708           export GOOGLE_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.googleAuthentication.clientSecretFile})"
709         ''}
710         ${lib.optionalString (cfg.azureAuthentication != null) ''
711           export AZURE_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.azureAuthentication.clientSecretFile})"
712         ''}
713         ${lib.optionalString (cfg.oidcAuthentication != null) ''
714           export OIDC_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.oidcAuthentication.clientSecretFile})"
715         ''}
716         ${lib.optionalString (cfg.sslKeyFile != null) ''
717           export SSL_KEY="$(head -n1 ${lib.escapeShellArg cfg.sslKeyFile})"
718         ''}
719         ${lib.optionalString (cfg.sslCertFile != null) ''
720           export SSL_CERT="$(head -n1 ${lib.escapeShellArg cfg.sslCertFile})"
721         ''}
722         ${lib.optionalString (cfg.slackIntegration != null) ''
723           export SLACK_VERIFICATION_TOKEN="$(head -n1 ${lib.escapeShellArg cfg.slackIntegration.verificationTokenFile})"
724         ''}
725         ${lib.optionalString (cfg.smtp != null) ''
726           export SMTP_PASSWORD="$(head -n1 ${lib.escapeShellArg cfg.smtp.passwordFile})"
727         ''}
729         ${if (cfg.databaseUrl == "local") then ''
730           export DATABASE_URL=${lib.escapeShellArg localPostgresqlUrl}
731           export PGSSLMODE=disable
732         '' else ''
733           export DATABASE_URL=${lib.escapeShellArg cfg.databaseUrl}
734         ''}
736         ${cfg.package}/bin/outline-server
737       '';
739       serviceConfig = {
740         User = cfg.user;
741         Group = cfg.group;
742         Restart = "always";
743         ProtectSystem = "strict";
744         PrivateHome = true;
745         PrivateTmp = true;
746         UMask = "0007";
748         StateDirectory = "outline";
749         StateDirectoryMode = "0750";
750         RuntimeDirectory = "outline";
751         RuntimeDirectoryMode = "0750";
752         # This working directory is required to find stuff like the set of
753         # onboarding files:
754         WorkingDirectory = "${cfg.package}/share/outline";
755         # In case this directory is not in /var/lib/outline, it needs to be made writable explicitly
756         ReadWritePaths = lib.mkIf (cfg.storage.storageType == "local") [ cfg.storage.localRootDir ];
757       };
758     };
759   };