[bug#75959,v9] services: syncthing: Add support for config file generation.

Message ID 871pvyg0kd.fsf@zacchae.us
State New
Headers
Series [bug#75959,v9] services: syncthing: Add support for config file generation. |

Commit Message

Zacchaeus Scheffer Feb. 16, 2025, 1:06 a.m. UTC
  From 1f38aeb8e2b1c5384ac915a09bad7df1f97b2543 Mon Sep 17 00:00:00 2001
From: Zacchaeus <eikcaz@zacchae.us>
Date: Sun, 21 Jul 2024 00:54:25 -0700
Subject: [PATCH v9] services: syncthing: Add support for config file
 generation.

* gnu/services/syncthing.scm: (syncthing-config-file,
syncthing-folder, syncthing-device, syncthing-folder-device): New
records;  (syncthing-service-type): Add special-files-service-type
extension for the config file; (syncthing-files-service): Add service
to create config file.
* gnu/home/services/syncthing.scm: (home-syncthing-service-type):
Extend home-files-services-type and re-exported more things from
gnu/services/syncthing.scm.
* doc/guix.texi: (syncthing-service-type): Document changes.

Change-Id: I87eeba1ee1fdada8f29c2ee881fbc6bc4113dde9
---

I incorporated the changes suggested by lfam, except that I kept the
semi-colon.  I double-checked that it was gramatically correct, and I
think it makes to where "there" refers clear.  I converted chmod/chown
to guile procedures, and added documentation on some of the flags used
to launch Syncthing.

 doc/guix.texi                   | 334 ++++++++++++++++++++-
 gnu/home/services/syncthing.scm |  17 +-
 gnu/services/syncthing.scm      | 515 ++++++++++++++++++++++++++++++--
 3 files changed, 833 insertions(+), 33 deletions(-)
  

Patch

diff --git a/doc/guix.texi b/doc/guix.texi
index b1b6d98e74..d1fbe5ffd3 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -136,6 +136,7 @@  Copyright @copyright{} 2024 Troy Figiel@*
 Copyright @copyright{} 2024 Sharlatan Hellseher@*
 Copyright @copyright{} 2024 45mg@*
 Copyright @copyright{} 2025 Sören Tempel@*
+Copyright @copyright{} 2025 Zacchaeus@*
 
 Permission is granted to copy, distribute and/or modify this document
 under the terms of the GNU Free Documentation License, Version 1.3 or
@@ -22620,7 +22621,7 @@  client.
 The @code{(gnu services syncthing)} module provides the following services:
 @cindex syncthing
 
-You might want a syncthing daemon if you have files between two or more
+You might want a Syncthing daemon if you have files between two or more
 computers and want to sync them in real time, safely protected from
 prying eyes.
 
@@ -22666,12 +22667,339 @@  The group as which the Syncthing service is to be run.
 This assumes that the specified group exists.
 
 @item @code{home} (default: @var{#f})
-Common configuration and data directory.  The default configuration
-directory is @file{$HOME} of the specified Syncthing @code{user}.
+Sets the @code{HOME} variable for the Syncthing daemon.  The default is
+@file{$HOME} of the specified Syncthing @code{user}.
+
+@item @code{config-file} (default: @var{#f})
+Either a file-like object that resolves to a Syncthing configuration XML
+file, or a @code{syncthing-config-file} record (see below).  If set to
+@code{#f}, Guix will not try to generate a config file, and Syncthing
+will generate a default configuration which will not be touched on
+reconfigure.  Specifying this in a system service moves Syncthing's
+common configuration and data directory (@code{--home} in
+@uref{https://docs.syncthing.net/users/syncthing.html}) to
+@file{/var/lib/syncthing-<user>}.
+
+@end table
+@end deftp
+
+This section documents a subset of the Syncthing configuration
+options—specifically those related to Guix or those affecting how your
+computer will connect to other computers over the network (such as
+Syncthing relays or discovery servers).  The configuration is fully
+documented in the upstream
+@uref{https://docs.syncthing.net/users/config.html, Syncthing config
+documentation}; camelCase there is converted to kebab-case here.  If you
+are migrating from a Syncthing-managed configuration to one managed by
+Guix, you can check what changes were introduced by @code{diff}ing the
+respective @file{config.xml} files.  Note that you will need to add
+whitespace with 4-space indentation to the file generated by Guix, using
+the @code{xmllint} program from the @code{libxml2} package like so:
+
+@example
+XMLLINT_INDENT="    " xmllint --format /path/to/new/config.xml | diff /path/to/old/config.xml -
+@end example
+
+When generating a configuration file through Guix, you can still
+temporarily modify Syncthing from the GUI or through @code{introducer}
+and @code{autoAcceptFolders} mechanisms, but such changes will be reset
+on reconfigure.
+
+@deftp {Data Type} syncthing-config-file
+Data type representing the configuration file read by the Syncthing
+daemon.
+
+@table @asis
+@item @code{folders} (default: @var{(list (syncthing-folder (id "default") (label "Default Folder") (path "~/Sync")))}
+The default here is the same as Syncthing's default.  The value should
+be a list of @code{syncthing-folder}s.
+
+@item @code{devices} (default: @var{'()}
+This should be a list of @code{syncthing-device}s.  Guix will
+automatically add any devices specified in any `folders' to this list.
+There are instances when you want to connect to a device despite not
+(initially) sharing any folders (such as a device with
+autoAcceptFolders).  In such instances, you should specify those devices
+here.  If multiple versions of the same device (as determined by
+comparing device ID) are discovered, the one in this list is
+prioritized.  Otherwise, the first instance in the first folder is used.
+
+@item @code{gui-enabled} (default: @var{"true"})
+By default, any user on the computer can access the GUI and make changes
+to Syncthing.  If you leave this enabled, you should probably set
+@code{gui-user} and @code{gui-password} (see below).
+
+@item @code{gui-tls} (default: @var{"false"})
+@item @code{gui-debugging} (default: @var{"false"})
+@item @code{gui-send-basic-auth-prompt} (default: @var{"false"})
+@item @code{gui-address} (default: @var{"127.0.0.1:8384"})
+@item @code{gui-user} (default: @var{#f})
+@item @code{gui-password} (default: @var{#f})
+A bcrypt hash of the GUI password.  Remember that this will be globally
+exposed in @file{/gnu/store}.
+
+@item @code{gui-apikey} (default: @var{#f})
+You must specify this to use the Syncthing REST interface.  This key is
+kept in @file{/gnu/store} and is accessible to all users of the system.
+
+@item @code{gui-theme} (default: @var{"default"})
+@item @code{ldap-enabled} (default: @var{#f})
+@item @code{ldap-address} (default: @var{""})
+@item @code{ldap-bind-dn} (default: @var{""})
+@item @code{ldap-transport} (default: @var{""})
+@item @code{ldap-insecure-skip-verify} (default: @var{""})
+@item @code{ldap-search-base-dn} (default: @var{""})
+@item @code{ldap-search-filter} (default: @var{""})
+@item @code{listen-address} (default: @var{"default"})
+@item @code{global-announce-server} (default: @var{"default"})
+@item @code{global-announce-enabled} (default: @var{"true"})
+Global discovery servers can be used to help connect devices at unknown
+IP addresses by storing the last known IP address.
+
+@item @code{local-announce-enabled} (default: @var{"true"})
+This makes devices find each other very easily on the same LAN.  Often,
+this will allow you to just plug an Ethernet between two devices, or
+connect one device to the other's hotspot and start syncing.
+
+@item @code{local-announce-port} (default: @var{"21027"})
+@item @code{local-announce-mcaddr} (default: @var{"[ff12::8384]:21027"})
+@item @code{max-send-kbps} (default: @var{"0"})
+@item @code{max-recv-kbps} (default: @var{"0"})
+@item @code{reconnection-interval-s} (default: @var{"60"})
+@item @code{relays-enabled} (default: @var{"true"})
+This option allows your Syncthing instance to use a global network of
+@uref{https://docs.syncthing.net/users/relaying.html, relays} to enable
+syncing between devices when all other methods fail.  As always,
+Syncthing traffic is encrypted in transport and the relays are unable to
+decrypt it.
+
+@item @code{relay-reconnect-interval-m} (default: @var{"10"})
+@item @code{start-browser} (default: @var{"true"})
+@item @code{nat-enabled} (default: @var{"true"})
+@item @code{nat-lease-minutes} (default: @var{"60"})
+@item @code{nat-renewal-minutes} (default: @var{"30"})
+@item @code{nat-timeout-seconds} (default: @var{"10"})
+@item @code{ur-accepted} (default: @var{"0"})
+Options whose names begin with `ur-' control usage reporting.  Set to -1
+to disable, or to a positive value to enable.  The default (0) disables
+reporting, but causes a usage reporting consent prompt to be displayed
+in the Syncthing GUI.
+
+@item @code{ur-seen} (default: @var{"0"})
+@item @code{ur-unique-id} (default: @var{""})
+@item @code{ur-url} (default: @var{"https://data.syncthing.net/newdata"})
+@item @code{ur-post-insecurely} (default: @var{"false"})
+@item @code{ur-initial-delay-s} (default: @var{"1800"})
+@item @code{auto-upgrade-interval-h} (default: @var{"12"})
+@item @code{upgrade-to-pre-releases} (default: @var{"false"})
+@item @code{keep-temporaries-h} (default: @var{"24"})
+@item @code{cache-ignored-files} (default: @var{"false"})
+@item @code{progress-update-interval-s} (default: @var{"5"})
+@item @code{limit-bandwidth-in-lan} (default: @var{"false"})
+@item @code{min-home-disk-free-unit} (default: @var{"%"})
+@item @code{min-home-disk-free} (default: @var{"1"})
+@item @code{releases-url} (default: @var{"https://upgrades.syncthing.net/meta.json"})
+@item @code{overwrite-remote-device-names-on-connect} (default: @var{"false"})
+@item @code{temp-index-min-blocks} (default: @var{"10"})
+@item @code{unacked-notification-id} (default: @var{"authenticationUserAndPassword"})
+@item @code{traffic-class} (default: @var{"0"})
+@item @code{set-low-priority} (default: @var{"true"})
+@item @code{max-folder-concurrency} (default: @var{"0"})
+@item @code{crash-reporting-url} (default: @var{"https://crash.syncthing.net/newcrash"})
+@item @code{crash-reporting-enabled} (default: @var{"true"})
+@item @code{stun-keepalive-start-s} (default: @var{"180"})
+@item @code{stun-keepalive-min-s} (default: @var{"20"})
+@item @code{stun-server} (default: @var{"default"})
+@item @code{database-tuning} (default: @var{"auto"})
+@item @code{max-concurrent-incoming-request-kib} (default: @var{"0"})
+@item @code{announce-lan-addresses} (default: @var{"true"})
+@item @code{send-full-index-on-upgrade} (default: @var{"false"})
+@item @code{connection-limit-enough} (default: @var{"0"})
+@item @code{connection-limit-max} (default: @var{"0"})
+@item @code{insecure-allow-old-tls-versions} (default: @var{"false"})
+@item @code{connection-priority-tcp-lan} (default: @var{"10"})
+@item @code{connection-priority-quic-lan} (default: @var{"20"})
+@item @code{connection-priority-tcp-wan} (default: @var{"30"})
+@item @code{connection-priority-quic-wan} (default: @var{"40"})
+@item @code{connection-priority-relay} (default: @var{"50"})
+@item @code{connection-priority-upgrade-threshold} (default: @var{"0"})
+@item @code{default-folder} (default: @var{(syncthing-folder (label ""))})
+@item @code{default-device} (default: @var{(syncthing-device (id ""))})
+@item @code{default-ignores} (default: @var{"")})
+Options whose names begin with `default-' above do not affect folders
+and devices added through the Guix configuration interface.  They will,
+however, affect folders and devices that are added through the Syncthing
+GUI, by an @code{introducer}, or a device with
+@code{auto-accept-folders}.
+@end table
+@end deftp
+
+@deftp {Data Type} syncthing-folder
+Data type representing a folder to be synchronized.
+
+@table @asis
+@item @code{id} (default: @var{#f})
+This ID cannot match the ID of any other folder on this device.  If left
+unspecified, it will default to the label (see below).
+
+@item @code{label}
+A human readable label for the folder.
+
+@item @code{path}
+The path at which to store this folder.
+
+@item @code{type} (default: @var{"sendreceive"})
+@item @code{rescan-interval-s} (default: @var{"3600"})
+@item @code{fs-watcher-enabled} (default: @var{"true"})
+@item @code{fs-watcher-delay-s} (default: @var{"10"})
+@item @code{ignore-perms} (default: @var{"false"})
+@item @code{auto-normalize} (default: @var{"true"})
+@item @code{devices} (default: @var{'()})
+This should be a list of other Syncthing devices.  You do not need to
+specify the current device.  Each device can be listed as a a
+@code{syncthing-device} record or a @code{syncthing-folder-device}
+record if you want files to be encrypted on disk.  See below.
+
+@item @code{filesystem-type} (default: @var{"basic"})
+@item @code{min-disk-free-unit} (default: @var{"%"})
+@item @code{min-disk-free} (default: @var{"1"})
+@item @code{versioning-type} (default: @var{#f})
+@item @code{versioning-fs-path} (default: @var{""})
+@item @code{versioning-fs-type} (default: @var{"basic"})
+@item @code{versioning-cleanup-interval-s} (default: @var{"3600"})
+@item @code{versioning-cleanout-days} (default: @var{#f})
+@item @code{versioning-keep} (default: @var{#f})
+@item @code{versioning-max-age} (default: @var{#f})
+@item @code{versioning-command} (default: @var{#f})
+@item @code{copiers} (default: @var{"0"})
+@item @code{puller-max-pending-kib} (default: @var{"0"})
+@item @code{hashers} (default: @var{"0"})
+@item @code{order} (default: @var{"random"})
+@item @code{ignore-delete} (default: @var{"false"})
+@item @code{scan-progress-interval-s} (default: @var{"0"})
+@item @code{puller-pause-s} (default: @var{"0"})
+@item @code{max-conflicts} (default: @var{"10"})
+@item @code{disable-sparse-files} (default: @var{"false"})
+@item @code{disable-temp-indexes} (default: @var{"false"})
+@item @code{paused} (default: @var{"false"})
+@item @code{weak-hash-threshold-pct} (default: @var{"25"})
+@item @code{marker-name} (default: @var{".stfolder"})
+@item @code{copy-ownership-from-parent} (default: @var{"false"})
+@item @code{mod-time-window-s} (default: @var{"0"})
+@item @code{max-concurrent-writes} (default: @var{"2"})
+@item @code{disable-fsync} (default: @var{"false"})
+@item @code{block-pull-order} (default: @var{"standard"})
+@item @code{copy-range-method} (default: @var{"standard"})
+@item @code{case-sensitive-fs} (default: @var{"false"})
+@item @code{junctions-as-dirs} (default: @var{"false"})
+@item @code{sync-ownership} (default: @var{"false"})
+@item @code{send-ownership} (default: @var{"false"})
+@item @code{sync-xattrs} (default: @var{"false"})
+@item @code{send-xattrs} (default: @var{"false"})
+@item @code{xattr-filter-max-single-entry-size} (default: @var{"1024"})
+@item @code{xattr-filter-max-total-size} (default: @var{"4096")})
+@end table
+@end deftp
+
+@deftp {Data Type} syncthing-device
+Data type representing a device to synchronize folders with.
+
+@table @asis
+@item @code{id}
+A long hash representing the keys generated by Syncthing on the first
+launch.  You can obtain this from the Syncthing GUI or by inspecting an
+existing Syncthing configuration file.
+
+@item @code{name} (default: @var{""})
+A human readable device name for viewing in the GUI or in Scheme.
+
+@item @code{compression} (default: @var{"metadata"})
+@item @code{introducer} (default: @var{"false"})
+@item @code{skip-introduction-removals} (default: @var{"false"})
+@item @code{introduced-by} (default: @var{""})
+@item @code{addresses} (default: @var{'("dynamic")})
+List of addresses at which to search for this device.  When the special
+value ``dynamic'' is included, Syncthing will search for the device
+locally as well as via the Syncthing project's
+@uref{https://docs.syncthing.net/users/security.html#global-discovery,
+global discovery} servers.
+
+@item @code{paused} (default: @var{"false"})
+@item @code{auto-accept-folders} (default: @var{"false"})
+@item @code{max-send-kbps} (default: @var{"0"})
+@item @code{max-recv-kbps} (default: @var{"0"})
+@item @code{max-request-kib} (default: @var{"0"})
+@item @code{untrusted} (default: @var{"false"})
+@item @code{remote-gui-port} (default: @var{"0"})
+@item @code{num-connections} (default: @var{"0")})
+
+@end table
+@end deftp
+
+@deftp {Data Type} syncthing-folder-device
+This data type offers two folder-specific device options.  First, it
+offers @code{introduced-by}, which is a record of Syncthing
+@uref{https://docs.syncthing.net/users/introducer.html, introductions}.
+Second, it offers @code{encryption-password}, by which you can set the
+password used to encrypt data that is synced with
+@uref{https://docs.syncthing.net/users/untrusted.html, untrusted
+devices}.
+
+@code{syncthing-folder-device} corresponds to the
+@uref{https://docs.syncthing.net/users/config.html#config-option-folder.device,
+`device'} option in the upstream `folder' element.
+
+If you don't need to use these options, then you can just specify
+@code{syncthing-device}s instead of @code{syncthing-folder-device}s in a
+@code{syncthing-folder}'s @code{devices} field.
+
+@table @asis
+@item @code{device}
+The @code{syncthing-device} for which this configuration applies.
+
+@item @code{introduced-by} (default: @var{""})
+@item @code{encryption-password} (default: @var{""})
+Beware: specifying this field will include this password as plain text
+(not encrypted) and globally visible in @file{/gnu/store/}.  If the
+encryption-password is non-empty, then it will be used as a password to
+encrypt file chunks as they are synchronized to untrusted devices.  For
+more information on syncing to devices you don't totally trust, see
+Syncthing's documentation on
+@uref{https://docs.syncthing.net/users/untrusted.html, Untrusted
+(Encrypted) Devices}.  Note that data transfer is always encrypted while
+in transport ("end-to-end encryption"), regardless of this setting.
 
 @end table
 @end deftp
 
+Here is a more complex example configuration for illustrative purposes:
+
+@lisp
+(service syncthing-service-type
+         (let ((laptop (syncthing-device (id "VHOD2D6-...-7XRMDEN")))
+               (desktop (syncthing-device (id "64SAZ37-...-FZJ5GUA")
+                                          (addresses '("tcp://example.com"))))
+               (bob-desktop (syncthing-device (id "KYIMEGO-...-FT77EAO"))))
+           (syncthing-configuration
+            (user "alice")
+            (config-file
+             (syncthing-config-file
+               (folders (list (syncthing-folder
+                               (label "some-files")
+                               (path "~/data")
+                               (devices (list desktop laptop)))
+                              (syncthing-folder
+                               (label "critical-files")
+                               (path "~/secrets")
+                               (devices
+                                (list desktop
+                                      laptop
+                                      (syncthing-folder-device
+                                       (device bob-desktop)
+                                       (encryption-password "mypassword"))))))))))))
+@end lisp
+
+
 Furthermore, @code{(gnu services ssh)} provides the following services.
 @cindex SSH
 @cindex SSH server
diff --git a/gnu/home/services/syncthing.scm b/gnu/home/services/syncthing.scm
index 8d66a167ce..dd6c752ee4 100644
--- a/gnu/home/services/syncthing.scm
+++ b/gnu/home/services/syncthing.scm
@@ -1,5 +1,6 @@ 
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2023 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2025 Zacchaeus <eikcaz@zacchae.us>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -24,9 +25,23 @@  (define-module (gnu home services syncthing)
   #:use-module (gnu home services shepherd)
   #:export (home-syncthing-service-type)
   #:re-export (syncthing-configuration
-               syncthing-configuration?))
+               syncthing-configuration?
+               syncthing-config-file
+               syncthing-config-file?
+               syncthing-device
+               syncthing-device?
+               syncthing-folder
+               syncthing-folder?
+               syncthing-folder-device
+               syncthing-folder-device?))
 
 (define home-syncthing-service-type
   (service-type
    (inherit (system->home-service-type syncthing-service-type))
+   ;; system->home-service-type does not convert special-files-service-type to
+   ;; home-files-service-type, so redefine extensios
+   (extensions (list (service-extension home-files-service-type
+                                        syncthing-files-service)
+                     (service-extension home-shepherd-service-type
+                                        syncthing-shepherd-service)))
    (default-value (for-home (syncthing-configuration)))))
diff --git a/gnu/services/syncthing.scm b/gnu/services/syncthing.scm
index a7a9c6aadd..cc88bf7925 100644
--- a/gnu/services/syncthing.scm
+++ b/gnu/services/syncthing.scm
@@ -1,6 +1,7 @@ 
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2021 Oleg Pykhalov <go.wigust@gmail.com>
 ;;; Copyright © 2023 Justin Veilleux <terramorpha@cock.li>
+;;; Copyright © 2025 Zacchaeus <eikcaz@zacchae.us>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -25,9 +26,20 @@  (define-module (gnu services syncthing)
   #:use-module (guix records)
   #:use-module (ice-9 match)
   #:use-module (srfi srfi-1)
+  #:use-module (sxml simple)
   #:export (syncthing-configuration
             syncthing-configuration?
-            syncthing-service-type))
+            syncthing-device
+            syncthing-device?
+            syncthing-config-file
+            syncthing-config-file?
+            syncthing-folder-device
+            syncthing-folder-device?
+            syncthing-folder
+            syncthing-folder?
+            syncthing-service-type
+            syncthing-shepherd-service
+            syncthing-files-service))
 
 ;;; Commentary:
 ;;;
@@ -35,6 +47,414 @@  (define-module (gnu services syncthing)
 ;;;
 ;;; Code:
 
+(define-record-type* <syncthing-device>
+  syncthing-device make-syncthing-device
+  syncthing-device?
+  (id syncthing-device-id)
+  (name syncthing-device-name (default ""))
+  (compression syncthing-device-compression (default "metadata"))
+  (introducer syncthing-device-introducer (default "false"))
+  (skip-introduction-removals syncthing-device-skip-introduction-removals (default "false"))
+  (introduced-by syncthing-device-introduced-by (default ""))
+  (addresses syncthing-device-addresses (default '("dynamic")))
+  (paused syncthing-device-paused (default "false"))
+  (auto-accept-folders syncthing-device-auto-accept-folders (default "false"))
+  (max-send-kbps syncthing-device-max-send-kbps (default "0"))
+  (max-recv-kbps syncthing-device-max-recv-kbps (default "0"))
+  (max-request-kib syncthing-device-max-request-kib (default "0"))
+  (untrusted syncthing-device-untrusted (default "false"))
+  (remote-gui-port syncthing-device-remote-gui-port (default "0"))
+  (num-connections syncthing-device-num-connections (default "0")))
+
+(define syncthing-device->sxml
+  (match-record-lambda <syncthing-device>
+      (id
+       name compression introducer skip-introduction-removals introduced-by
+       addresses paused auto-accept-folders max-send-kbps max-recv-kbps
+       max-request-kib untrusted remote-gui-port num-connections)
+    `(device (@ (id ,id)
+                (name ,name)
+                (compression ,compression)
+                (introducer ,introducer)
+                (skipIntroductionRemovals ,skip-introduction-removals)
+                (introducedBy ,introduced-by))
+             ,@(map (lambda (address) `(address ,address)) addresses)
+             (paused ,paused)
+             (autoAcceptFolders ,auto-accept-folders)
+             (maxSendKbps ,max-send-kbps)
+             (maxRecvKbps ,max-recv-kbps)
+             (maxRequestKiB ,max-request-kib)
+             (untrusted ,untrusted)
+             (remoteGUIPort ,remote-gui-port)
+             (numConnections ,num-connections))))
+
+(define-record-type* <syncthing-folder-device>
+  syncthing-folder-device make-syncthing-folder-device
+  syncthing-folder-device?
+  (device syncthing-folder-device-device)
+  (introduced-by syncthing-folder-device-introduced-by (default (syncthing-device (id ""))))
+  (encryption-password syncthing-folder-device-encryption-password (default "")))
+
+(define syncthing-folder-device->sxml
+  (match-record-lambda <syncthing-folder-device>
+      (device introduced-by encryption-password)
+    `(device (@ (id ,(syncthing-device-id device))
+                (introducedBy ,(syncthing-device-id introduced-by)))
+             (encryptionPassword ,encryption-password))))
+
+(define-record-type* <syncthing-folder>
+  syncthing-folder make-syncthing-folder
+  syncthing-folder?
+  (id syncthing-folder-id (default #f))
+  (label syncthing-folder-label)
+  (path syncthing-folder-path)
+  (type syncthing-folder-type (default "sendreceive"))
+  (rescan-interval-s syncthing-folder-rescan-interval-s (default "3600"))
+  (fs-watcher-enabled syncthing-folder-fs-watcher-enabled (default "true"))
+  (fs-watcher-delay-s syncthing-folder-fs-watcher-delay-s (default "10"))
+  (fs-watcher-timeout-s syncthing-folder-fs-watcher-timeout-s (default "0"))
+  (ignore-perms syncthing-folder-ignore-perms (default "false"))
+  (auto-normalize syncthing-folder-auto-normalize (default "true"))
+  (devices syncthing-folder-devices (default '())
+           (sanitize (lambda (folder-device-list)
+                       (map (lambda (device)
+                              (if (syncthing-folder-device? device)
+                                  device
+                                  (syncthing-folder-device (device device))))
+                            folder-device-list))))
+  (filesystem-type syncthing-folder-filesystem-type (default "basic"))
+  (min-disk-free-unit syncthing-folder-min-disk-free-unit (default "%"))
+  (min-disk-free syncthing-folder-min-disk-free (default "1"))
+  (versioning-type syncthing-folder-versioning-type (default #f))
+  (versioning-fs-path syncthing-folder-versioning-fs-path (default ""))
+  (versioning-fs-type syncthing-folder-versioning-fs-type (default "basic"))
+  (versioning-cleanup-interval-s syncthing-folder-versioning-cleanup-interval-s (default "3600"))
+  (versioning-cleanout-days syncthing-folder-versioning-cleanout-days (default #f))
+  (versioning-keep syncthing-folder-versioning-keep (default #f))
+  (versioning-max-age syncthing-folder-versioning-max-age (default #f))
+  (versioning-command syncthing-folder-versioning-command (default #f))
+  (copiers syncthing-folder-copiers (default "0"))
+  (puller-max-pending-kib syncthing-folder-puller-max-pending-kib (default "0"))
+  (hashers syncthing-folder-hashers (default "0"))
+  (order syncthing-folder-order (default "random"))
+  (ignore-delete syncthing-folder-ignore-delete (default "false"))
+  (scan-progress-interval-s syncthing-folder-scan-progress-interval-s (default "0"))
+  (puller-pause-s syncthing-folder-puller-pause-s (default "0"))
+  (max-conflicts syncthing-folder-max-conflicts (default "10"))
+  (disable-sparse-files syncthing-folder-disable-sparse-files (default "false"))
+  (disable-temp-indexes syncthing-folder-disable-temp-indexes (default "false"))
+  (paused syncthing-folder-paused (default "false"))
+  (weak-hash-threshold-pct syncthing-folder-weak-hash-threshold-pct (default "25"))
+  (marker-name syncthing-folder-marker-name (default ".stfolder"))
+  (copy-ownership-from-parent syncthing-folder-copy-ownership-from-parent (default "false"))
+  (mod-time-window-s syncthing-folder-mod-time-window-s (default "0"))
+  (max-concurrent-writes syncthing-folder-max-concurrent-writes (default "2"))
+  (disable-fsync syncthing-folder-disable-fsync (default "false"))
+  (block-pull-order syncthing-folder-block-pull-order (default "standard"))
+  (copy-range-method syncthing-folder-copy-range-method (default "standard"))
+  (case-sensitive-fs syncthing-folder-case-sensitive-fs (default "false"))
+  (junctions-as-dirs syncthing-folder-junctions-as-dirs (default "false"))
+  (sync-ownership syncthing-folder-sync-ownership (default "false"))
+  (send-ownership syncthing-folder-send-ownership (default "false"))
+  (sync-xattrs syncthing-folder-sync-xattrs (default "false"))
+  (send-xattrs syncthing-folder-send-xattrs (default "false"))
+  (xattr-filter-max-single-entry-size syncthing-folder-xattr-filter-max-single-entry-size (default "1024"))
+  (xattr-filter-max-total-size syncthing-folder-xattr-filter-max-total-size (default "4096")))
+
+;; Some parameters, when empty, are fully omitted from the config file.  It is
+;; unknown if this causes a functional difference, but stick to the normal
+;; program's behavior to be safe.
+(define (maybe-param symbol value)
+  (if value `((param (@ (key ,(symbol->string symbol)) (val ,value)) "")) '()))
+
+(define syncthing-folder->sxml
+  (match-record-lambda <syncthing-folder>
+      (id
+       label path type rescan-interval-s fs-watcher-enabled fs-watcher-delay-s
+       fs-watcher-timeout-s ignore-perms auto-normalize devices filesystem-type
+       min-disk-free-unit min-disk-free versioning-type versioning-fs-path
+       versioning-fs-type versioning-cleanup-interval-s versioning-cleanout-days
+       versioning-keep versioning-max-age versioning-command copiers
+       puller-max-pending-kib hashers order ignore-delete scan-progress-interval-s
+       puller-pause-s max-conflicts disable-sparse-files disable-temp-indexes paused
+       weak-hash-threshold-pct marker-name copy-ownership-from-parent mod-time-window-s
+       max-concurrent-writes disable-fsync block-pull-order copy-range-method
+       case-sensitive-fs junctions-as-dirs sync-ownership send-ownership sync-xattrs
+       send-xattrs xattr-filter-max-single-entry-size xattr-filter-max-total-size)
+    `(folder (@ (id ,(if id id label))
+                (label ,label)
+                (path ,path)
+                (type ,type)
+                (rescanIntervalS ,rescan-interval-s)
+                (fsWatcherEnabled ,fs-watcher-enabled)
+                (fsWatcherDelayS ,fs-watcher-delay-s)
+                (fsWatcherTimeoutS ,fs-watcher-timeout-s)
+                (ignorePerms ,ignore-perms)
+                (autoNormalize ,auto-normalize))
+             (filesystemType ,filesystem-type)
+             ,@(map syncthing-folder-device->sxml
+                    devices)
+             (minDiskFree (@ (unit ,min-disk-free-unit))
+                          ,min-disk-free)
+             (versioning ,@(if versioning-type
+                               `((@ (type ,versioning-type)))
+                               '())
+                         ,@(maybe-param 'cleanoutDays versioning-cleanout-days)
+                         ,@(maybe-param 'keep versioning-keep)
+                         ,@(maybe-param 'maxAge versioning-max-age)
+                         ,@(maybe-param 'command versioning-command)
+                         (cleanupIntervalS ,versioning-cleanup-interval-s)
+                         (fsPath ,versioning-fs-path)
+                         (fsType ,versioning-fs-type))
+             (copiers ,copiers)
+             (pullerMaxPendingKiB ,puller-max-pending-kib)
+             (hashers ,hashers)
+             (order ,order)
+             (ignoreDelete ,ignore-delete)
+             (scanProgressIntervalS ,scan-progress-interval-s)
+             (pullerPauseS ,puller-pause-s)
+             (maxConflicts ,max-conflicts)
+             (disableSparseFiles ,disable-sparse-files)
+             (disableTempIndexes ,disable-temp-indexes)
+             (paused ,paused)
+             (weakHashThresholdPct ,weak-hash-threshold-pct)
+             (markerName ,marker-name)
+             (copyOwnershipFromParent ,copy-ownership-from-parent)
+             (modTimeWindowS ,mod-time-window-s)
+             (maxConcurrentWrites ,max-concurrent-writes)
+             (disableFsync ,disable-fsync)
+             (blockPullOrder ,block-pull-order)
+             (copyRangeMethod ,copy-range-method)
+             (caseSensitiveFS ,case-sensitive-fs)
+             (junctionsAsDirs ,junctions-as-dirs)
+             (syncOwnership ,sync-ownership)
+             (sendOwnership ,send-ownership)
+             (syncXattrs ,sync-xattrs)
+             (sendXattrs ,send-xattrs)
+             (xattrFilter (maxSingleEntrySize ,xattr-filter-max-single-entry-size)
+                          (maxTotalSize ,xattr-filter-max-total-size)))))
+
+(define-record-type* <syncthing-config-file>
+  syncthing-config-file make-syncthing-config-file
+  syncthing-config-file?
+  (folders syncthing-config-folders
+           ; this matches syncthing's default
+           (default (list (syncthing-folder (id "default")
+                                            (label "Default Folder")
+                                            (path "~/Sync")))))
+  (devices syncthing-config-devices
+           (default '()))
+  (gui-enabled syncthing-config-gui-enabled (default "true"))
+  (gui-tls syncthing-config-gui-tls (default "false"))
+  (gui-debugging syncthing-config-gui-debugging (default "false"))
+  (gui-send-basic-auth-prompt syncthing-config-gui-send-basic-auth-prompt (default "false"))
+  (gui-address syncthing-config-gui-address (default "127.0.0.1:8384"))
+  (gui-user syncthing-config-gui-user (default #f))
+  (gui-password syncthing-config-gui-password (default #f))
+  (gui-apikey syncthing-config-gui-apikey (default #f))
+  (gui-theme syncthing-config-gui-theme (default "default"))
+  (ldap-enabled syncthing-config-ldap-enabled (default #f))
+  (ldap-address syncthing-config-ldap-address (default ""))
+  (ldap-bind-dn syncthing-config-ldap-bind-dn (default ""))
+  (ldap-transport syncthing-config-ldap-transport (default ""))
+  (ldap-insecure-skip-verify syncthing-config-ldap-insecure-skip-verify (default ""))
+  (ldap-search-base-dn syncthing-config-ldap-search-base-dn (default ""))
+  (ldap-search-filter syncthing-config-ldap-search-filter (default ""))
+  (listen-address syncthing-config-listen-address (default "default"))
+  (global-announce-server syncthing-config-global-announce-server (default "default"))
+  (global-announce-enabled syncthing-config-global-announce-enabled (default "true"))
+  (local-announce-enabled syncthing-config-local-announce-enabled (default "true"))
+  (local-announce-port syncthing-config-local-announce-port (default "21027"))
+  (local-announce-mcaddr syncthing-config-local-announce-mcaddr (default "[ff12::8384]:21027"))
+  (max-send-kbps syncthing-config-max-send-kbps (default "0"))
+  (max-recv-kbps syncthing-config-max-recv-kbps (default "0"))
+  (reconnection-interval-s syncthing-config-reconnection-interval-s (default "60"))
+  (relays-enabled syncthing-config-relays-enabled (default "true"))
+  (relay-reconnect-interval-m syncthing-config-relay-reconnect-interval-m (default "10"))
+  (start-browser syncthing-config-start-browser (default "true"))
+  (nat-enabled syncthing-config-nat-enabled (default "true"))
+  (nat-lease-minutes syncthing-config-nat-lease-minutes (default "60"))
+  (nat-renewal-minutes syncthing-config-nat-renewal-minutes (default "30"))
+  (nat-timeout-seconds syncthing-config-nat-timeout-seconds (default "10"))
+  (ur-accepted syncthing-config-ur-accepted (default "0"))
+  (ur-seen syncthing-config-ur-seen (default "0"))
+  (ur-unique-id syncthing-config-ur-unique-id (default ""))
+  (ur-url syncthing-config-ur-url (default "https://data.syncthing.net/newdata"))
+  (ur-post-insecurely syncthing-config-ur-post-insecurely (default "false"))
+  (ur-initial-delay-s syncthing-config-ur-initial-delay-s (default "1800"))
+  (auto-upgrade-interval-h syncthing-config-auto-upgrade-interval-h (default "12"))
+  (upgrade-to-pre-releases syncthing-config-upgrade-to-pre-releases (default "false"))
+  (keep-temporaries-h syncthing-config-keep-temporaries-h (default "24"))
+  (cache-ignored-files syncthing-config-cache-ignored-files (default "false"))
+  (progress-update-interval-s syncthing-config-progress-update-interval-s (default "5"))
+  (limit-bandwidth-in-lan syncthing-config-limit-bandwidth-in-lan (default "false"))
+  (min-home-disk-free-unit syncthing-config-min-home-disk-free-unit (default "%"))
+  (min-home-disk-free syncthing-config-min-home-disk-free (default "1"))
+  (releases-url syncthing-config-releases-url (default "https://upgrades.syncthing.net/meta.json"))
+  (overwrite-remote-device-names-on-connect syncthing-config-overwrite-remote-device-names-on-connect (default "false"))
+  (temp-index-min-blocks syncthing-config-temp-index-min-blocks (default "10"))
+  (unacked-notification-id syncthing-config-unacked-notification-id (default "authenticationUserAndPassword"))
+  (traffic-class syncthing-config-traffic-class (default "0"))
+  (set-low-priority syncthing-config-set-low-priority (default "true"))
+  (max-folder-concurrency syncthing-config-max-folder-concurrency (default "0"))
+  (crash-reporting-url syncthing-config-crash-reporting-url (default "https://crash.syncthing.net/newcrash"))
+  (crash-reporting-enabled syncthing-config-crash-reporting-enabled (default "true"))
+  (stun-keepalive-start-s syncthing-config-stun-keepalive-start-s (default "180"))
+  (stun-keepalive-min-s syncthing-config-stun-keepalive-min-s (default "20"))
+  (stun-server syncthing-config-stun-server (default "default"))
+  (database-tuning syncthing-config-database-tuning (default "auto"))
+  (max-concurrent-incoming-request-kib syncthing-config-max-concurrent-incoming-request-kib (default "0"))
+  (announce-lan-addresses syncthing-config-announce-lan-addresses (default "true"))
+  (send-full-index-on-upgrade syncthing-config-send-full-index-on-upgrade (default "false"))
+  (connection-limit-enough syncthing-config-connection-limit-enough (default "0"))
+  (connection-limit-max syncthing-config-connection-limit-max (default "0"))
+  (insecure-allow-old-tlsVersions syncthing-config-insecure-allow-old-tlsVersions (default "false"))
+  (connection-priority-tcp-lan syncthing-config-connection-priority-tcp-lan (default "10"))
+  (connection-priority-quic-lan syncthing-config-connection-priority-quic-lan (default "20"))
+  (connection-priority-tcp-wan syncthing-config-connection-priority-tcp-wan (default "30"))
+  (connection-priority-quic-wan syncthing-config-connection-priority-quic-wan (default "40"))
+  (connection-priority-relay syncthing-config-connection-priority-relay (default "50"))
+  (connection-priority-upgrade-threshold syncthing-config-connection-priority-upgrade-threshold (default "0"))
+  (default-folder syncthing-config-default-folder
+    (default (syncthing-folder (label "") (path "~"))))
+  (default-device syncthing-config-default-device
+    (default (syncthing-device (id ""))))
+  (default-ignores syncthing-config-default-ignores (default "")))
+
+(define syncthing-config-file->sxml
+  (match-record-lambda <syncthing-config-file>
+      (folders
+       devices gui-enabled gui-tls gui-debugging gui-send-basic-auth-prompt
+       gui-address gui-user gui-password gui-apikey gui-theme ldap-enabled
+       ldap-address ldap-bind-dn ldap-transport ldap-insecure-skip-verify
+       ldap-search-base-dn ldap-search-filter listen-address global-announce-server
+       global-announce-enabled local-announce-enabled local-announce-port
+       local-announce-mcaddr max-send-kbps max-recv-kbps reconnection-interval-s
+       relays-enabled relay-reconnect-interval-m start-browser nat-enabled
+       nat-lease-minutes nat-renewal-minutes nat-timeout-seconds ur-accepted
+       ur-seen ur-unique-id ur-url ur-post-insecurely ur-initial-delay-s
+       auto-upgrade-interval-h upgrade-to-pre-releases keep-temporaries-h
+       cache-ignored-files progress-update-interval-s limit-bandwidth-in-lan
+       min-home-disk-free-unit min-home-disk-free releases-url
+       overwrite-remote-device-names-on-connect temp-index-min-blocks
+       unacked-notification-id traffic-class set-low-priority max-folder-concurrency
+       crash-reporting-url crash-reporting-enabled stun-keepalive-start-s
+       stun-keepalive-min-s stun-server database-tuning
+       max-concurrent-incoming-request-kib announce-lan-addresses
+       send-full-index-on-upgrade connection-limit-enough connection-limit-max
+       insecure-allow-old-tlsVersions connection-priority-tcp-lan
+       connection-priority-quic-lan connection-priority-tcp-wan
+       connection-priority-quic-wan connection-priority-relay
+       connection-priority-upgrade-threshold default-folder default-device
+       default-ignores)
+    `(configuration (@ (version "37"))
+                    ,@(map syncthing-folder->sxml
+                           folders)
+                    ;; collect any devices in any folders, as well as any
+                    ;; devices explicitly added.
+                    ,@(map syncthing-device->sxml
+                           (delete-duplicates
+                            (append devices
+                                    (apply append
+                                           (map (lambda (folder)
+                                                  (map syncthing-folder-device-device
+                                                       (syncthing-folder-devices folder)))
+                                                folders)))
+                            ;; devices are the same if their id's are equal
+                            (lambda (device1 device2)
+                              (string= (syncthing-device-id device1)
+                                       (syncthing-device-id device2)))))
+                    (gui (@ (enabled ,gui-enabled)
+                            (tls ,gui-tls)
+                            (debugging ,gui-debugging)
+                            (sendBasicAuthPrompt ,gui-send-basic-auth-prompt))
+                         (address ,gui-address)
+                         ,@(if gui-user `((user ,gui-user)) '())
+                         ,@(if gui-password `((password ,gui-password)) '())
+                         ,@(if gui-apikey `((apikey ,gui-apikey)) '())
+                         (theme ,gui-theme))
+                    (ldap ,(if ldap-enabled
+                               `((address ,ldap-address)
+                                 (bindDN ,ldap-bind-dn)
+                                 ,@(if ldap-transport
+                                       `((transport ,ldap-transport))
+                                       '())
+                                 ,@(if ldap-insecure-skip-verify
+                                       `((insecureSkipVerify ,ldap-insecure-skip-verify))
+                                       '())
+                                 ,@(if ldap-search-base-dn
+                                       `((searchBaseDN ,ldap-search-base-dn))
+                                       '())
+                                 ,@(if ldap-search-filter
+                                       `((searchFilter ,ldap-search-filter))
+                                       '()))
+                               ""))
+                    (options (listenAddress ,listen-address)
+                             (globalAnnounceServer ,global-announce-server)
+                             (globalAnnounceEnabled ,global-announce-enabled)
+                             (localAnnounceEnabled ,local-announce-enabled)
+                             (localAnnouncePort ,local-announce-port)
+                             (localAnnounceMCAddr ,local-announce-mcaddr)
+                             (maxSendKbps ,max-send-kbps)
+                             (maxRecvKbps ,max-recv-kbps)
+                             (reconnectionIntervalS ,reconnection-interval-s)
+                             (relaysEnabled ,relays-enabled)
+                             (relayReconnectIntervalM ,relay-reconnect-interval-m)
+                             (startBrowser ,start-browser)
+                             (natEnabled ,nat-enabled)
+                             (natLeaseMinutes ,nat-lease-minutes)
+                             (natRenewalMinutes ,nat-renewal-minutes)
+                             (natTimeoutSeconds ,nat-timeout-seconds)
+                             (urAccepted ,ur-accepted)
+                             (urSeen ,ur-seen)
+                             (urUniqueID ,ur-unique-id)
+                             (urURL ,ur-url)
+                             (urPostInsecurely ,ur-post-insecurely)
+                             (urInitialDelayS ,ur-initial-delay-s)
+                             (autoUpgradeIntervalH ,auto-upgrade-interval-h)
+                             (upgradeToPreReleases ,upgrade-to-pre-releases)
+                             (keepTemporariesH ,keep-temporaries-h)
+                             (cacheIgnoredFiles ,cache-ignored-files)
+                             (progressUpdateIntervalS ,progress-update-interval-s)
+                             (limitBandwidthInLan ,limit-bandwidth-in-lan)
+                             (minHomeDiskFree (@ (unit ,min-home-disk-free-unit))
+                                              ,min-home-disk-free)
+                             (releasesURL ,releases-url)
+                             (overwriteRemoteDeviceNamesOnConnect ,overwrite-remote-device-names-on-connect)
+                             (tempIndexMinBlocks ,temp-index-min-blocks)
+                             (unackedNotificationID ,unacked-notification-id)
+                             (trafficClass ,traffic-class)
+                             (setLowPriority ,set-low-priority)
+                             (maxFolderConcurrency ,max-folder-concurrency)
+                             (crashReportingURL ,crash-reporting-url)
+                             (crashReportingEnabled ,crash-reporting-enabled)
+                             (stunKeepaliveStartS ,stun-keepalive-start-s)
+                             (stunKeepaliveMinS ,stun-keepalive-min-s)
+                             (stunServer ,stun-server)
+                             (databaseTuning ,database-tuning)
+                             (maxConcurrentIncomingRequestKiB ,max-concurrent-incoming-request-kib)
+                             (announceLANAddresses ,announce-lan-addresses)
+                             (sendFullIndexOnUpgrade ,send-full-index-on-upgrade)
+                             (connectionLimitEnough ,connection-limit-enough)
+                             (connectionLimitMax ,connection-limit-max)
+                             (insecureAllowOldTLSVersions ,insecure-allow-old-tlsVersions)
+                             (connectionPriorityTcpLan ,connection-priority-tcp-lan)
+                             (connectionPriorityQuicLan ,connection-priority-quic-lan)
+                             (connectionPriorityTcpWan ,connection-priority-tcp-wan)
+                             (connectionPriorityQuicWan ,connection-priority-quic-wan)
+                             (connectionPriorityRelay ,connection-priority-relay)
+                             (connectionPriorityUpgradeThreshold ,connection-priority-upgrade-threshold))
+                    (defaults
+                      ,(syncthing-folder->sxml default-folder)
+                      ,(syncthing-device->sxml default-device)
+                      (ignores ,default-ignores)))))
+
+
+(define (serialize-syncthing-config-file config)
+  (with-output-to-string
+    (lambda ()
+      (sxml->xml (cons '*TOP* (list (syncthing-config-file->sxml config)))))))
+
 (define-record-type* <syncthing-configuration>
   syncthing-configuration make-syncthing-configuration
   syncthing-configuration?
@@ -50,12 +470,14 @@  (define-record-type* <syncthing-configuration>
              (default "users"))
   (home      syncthing-configuration-home      ;string
              (default #f))
+  (config-file syncthing-configuration-config-file
+                         (default #f))         ; syncthing-config-file or file-like
   (home-service? syncthing-configuration-home-service?
                  (default for-home?) (innate)))
 
 (define syncthing-shepherd-service
   (match-record-lambda <syncthing-configuration>
-      (syncthing arguments logflags user group home home-service?)
+      (syncthing arguments logflags user group home home-service? config-file)
     (list
      (shepherd-service
       (provision (if home-service?
@@ -64,39 +486,74 @@  (define syncthing-shepherd-service
                             (string-append "syncthing-" user)))))
       (documentation "Run syncthing.")
       (requirement (if home-service? '() '(loopback user-processes)))
-      (start #~(make-forkexec-constructor
-                (append (list (string-append #$syncthing "/bin/syncthing")
-                              "--no-browser"
-                              "--no-restart"
-                              (string-append "--logflags=" (number->string #$logflags)))
-                        '#$arguments)
-                #:user #$(and (not home-service?) user)
-                #:group #$(and (not home-service?) group)
-                #:environment-variables
-                (append
-                 (list
-                  (string-append "HOME="
-                                 (or #$home
-                                     (passwd:dir
-                                      (getpw (if (and #$home-service?
-                                                      (not #$user))
-                                                 (getuid)
-                                                 #$user)))))
-                              "SSL_CERT_DIR=/etc/ssl/certs"
-                              "SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt")
-                        (filter (negate       ;XXX: 'remove' is not in (guile)
-                                 (lambda (str)
-                                   (or (string-prefix? "HOME=" str)
-                                       (string-prefix? "SSL_CERT_DIR=" str)
-                                       (string-prefix? "SSL_CERT_FILE=" str))))
-                                (environ)))))
+      (start #~(lambda _
+                 ;; If we are managing the config, and it's not a home
+                 ;; service, then exepect the config file at
+                 ;; /var/lib/syncthing-<user>.  This makes sure the ownership
+                 ;; is correct
+                 (unless (or #$(not config-file) #$home-service?)
+                   (chown (string-append "/var/lib/syncthing-" #$user)
+                          (passwd:uid #$user)
+                          (passwd:gid #$user))
+                   (chmod (string-append "/var/lib/syncthing-" #$user) #o700))
+                 (make-forkexec-constructor
+                  (append (list (string-append #$syncthing "/bin/syncthing")
+                                ;; Do not try to try to lauch a browser on startup.
+                                "--no-browser"
+                                ;; If syncthing crashes, let the service fail.
+                                "--no-restart"
+                                (string-append "--logflags=" (number->string #$logflags)))
+                          ;; Optionally move data and configuration home to
+                          ;; /var/lib/syncthing-<user>.
+                          (if (or #$(not config-file) #$home-service?) '()
+                              (list (string-append "--home=/var/lib/syncthing-" #$user)))
+                          '#$arguments)
+                  #:user #$(and (not home-service?) user)
+                  #:group #$(and (not home-service?) group)
+                  #:environment-variables
+                  (append
+                   (list
+                    (string-append "HOME="
+                                   (or #$home
+                                       (passwd:dir
+                                        (getpw (if (and #$home-service?
+                                                        (not #$user))
+                                                   (getuid)
+                                                   #$user)))))
+                    "SSL_CERT_DIR=/etc/ssl/certs"
+                    "SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt")
+                   (filter (negate       ;XXX: 'remove' is not in (guile)
+                            (lambda (str)
+                              (or (string-prefix? "HOME=" str)
+                                  (string-prefix? "SSL_CERT_DIR=" str)
+                                  (string-prefix? "SSL_CERT_FILE=" str))))
+                           (environ))))))
       (respawn? #f)
       (stop #~(make-kill-destructor))))))
 
+
+(define syncthing-files-service
+  (match-record-lambda <syncthing-configuration> (config-file user home home-service?)
+    (if config-file
+        ;; When used as a system service, this service might be executed
+        ;; before a user's home even exists, causing it to be owned by root,
+        ;; and the skeletons to never be applied to that user's home.  In such
+        ;; cases, put the config at /var/lib/syncthnig-<user>/config.xml
+        `((,(if home-service?
+                ".config/syncthing/config.xml"
+                (string-append "/var/lib/syncthing-" user "/config.xml"))
+           ,(if (file-like? config-file)
+                config-file
+                (plain-file "syncthin-config.xml" (serialize-syncthing-config-file
+                                                   config-file)))))
+        '())))
+
 (define syncthing-service-type
   (service-type (name 'syncthing)
                 (extensions (list (service-extension shepherd-root-service-type
-                                                     syncthing-shepherd-service)))
+                                                     syncthing-shepherd-service)
+                                  (service-extension special-files-service-type
+                                                     syncthing-files-service)))
                 (description
                  "Run @uref{https://github.com/syncthing/syncthing, Syncthing}
 decentralized continuous file system synchronization.")))