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