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

Message ID 87tt8udyg7.fsf_-_@zacchae.us
State New
Headers
Series [bug#75959,v10] services: syncthing: Add support for config file generation. |

Commit Message

Zacchaeus Scheffer Feb. 16, 2025, 9:35 a.m. UTC
  From a573fd78e6b8d10b32eb10a753423073c7bbaeef Mon Sep 17 00:00:00 2001
From: Zacchaeus <eikcaz@zacchae.us>
Date: Sun, 21 Jul 2024 00:54:25 -0700
Subject: [PATCH v10] 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
---

Last time I forgot to test the system service.  Chowning/chmodding now
works.  (Needed to add a getpw instead of passing string of user name.)

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

Comments

Leo Famulari Feb. 17, 2025, 6:14 a.m. UTC | #1
On Sun, Feb 16, 2025 at 04:35:20AM -0500, Zacchaeus Scheffer wrote:
> From a573fd78e6b8d10b32eb10a753423073c7bbaeef Mon Sep 17 00:00:00 2001
> From: Zacchaeus <eikcaz@zacchae.us>
> Date: Sun, 21 Jul 2024 00:54:25 -0700
> Subject: [PATCH v10] 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.

Pushed as 651f8765b657e35baf85ac74a1f6b09ff71691cb

Thank you for your contribution to Guix!
  
Ludovic Courtès Feb. 17, 2025, 10:26 a.m. UTC | #2
Hi,

Zacchaeus Scheffer <eikcaz@zacchae.us> skribis:

>>From a573fd78e6b8d10b32eb10a753423073c7bbaeef Mon Sep 17 00:00:00 2001
> From: Zacchaeus <eikcaz@zacchae.us>
> Date: Sun, 21 Jul 2024 00:54:25 -0700
> Subject: [PATCH v10] 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

This is a nice improvement!  Much better than fiddling with the GUI.
:-)

Sorry for not chiming in earlier; I just noticed a couple of stylistic
issues:

> +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

“Kebab-case”, really? :-)

> +@table @asis
> +@item @code{folders} (default: @var{(list (syncthing-folder (id "default") (label "Default Folder") (path "~/Sync")))}

Should be @code, not @var (throughout this table).

Could you send a patch fixing this?

BTW, ‘define-configuration’ would make it easier to generate this doc.

> +@item @code{gui-enabled} (default: @var{"true"})

The naming convention used in all of Guix is to add a final question
mark for Boolean values; this would be ‘gui-enabled?’.

As for the value itself, it would help a lot to use #t and #f instead of
the strings "true" and "false" (both of which have truth value in
Scheme).  It leads to a bit of extra work in the serializer, but I think
it’s worth it.  Because then we can also have type-checking of fields.

> +@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}.

I believe you want @itemx for all but the first item.

> +@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"})

Similar to what I wrote above: numbers should be numbers, not strings.

Last note: the convention throughout Guix it to avoid abbreviations.

I’m not sure about the way forward; maybe we can make those typing
changes (and perhaps some naming changes) and afford some
incompatibility because the service is young?  WDYT?

Ludo’.
  
Zacchaeus Scheffer Feb. 17, 2025, 8:32 p.m. UTC | #3
Ludovic Courtès <ludo@gnu.org> writes:

> Hi,
>
> Zacchaeus Scheffer <eikcaz@zacchae.us> skribis:
>
>>>From a573fd78e6b8d10b32eb10a753423073c7bbaeef Mon Sep 17 00:00:00 2001
>> From: Zacchaeus <eikcaz@zacchae.us>
>> Date: Sun, 21 Jul 2024 00:54:25 -0700
>> Subject: [PATCH v10] 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
>
> This is a nice improvement!  Much better than fiddling with the GUI.
> :-)

Thanks.  I'm glad you agree.  Great for managing swarms of devices

> Sorry for not chiming in earlier; I just noticed a couple of stylistic
> issues:
>
>> +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
>
> “Kebab-case”, really? :-)

I believe that is an accurate term:
https://en.wikipedia.org/wiki/Naming_convention_(programming)#Delimiter-separated_words

>> +@table @asis
>> +@item @code{folders} (default: @var{(list (syncthing-folder (id "default") (label "Default Folder") (path "~/Sync")))}
>
> Should be @code, not @var (throughout this table).
>
> Could you send a patch fixing this?

Sure thing.  I assume I can send it to the same issue even though it is
closed?

> BTW, ‘define-configuration’ would make it easier to generate this doc.

I originally used define-record because that is what the original
syncthing service implementation used.  If there are reasons to change
this I could update this as well, but it seems fine.

>> +@item @code{gui-enabled} (default: @var{"true"})
>
> The naming convention used in all of Guix is to add a final question
> mark for Boolean values; this would be ‘gui-enabled?’.
>
> As for the value itself, it would help a lot to use #t and #f instead of
> the strings "true" and "false" (both of which have truth value in
> Scheme).  It leads to a bit of extra work in the serializer, but I think
> it’s worth it.  Because then we can also have type-checking of fields.

This would be easiest to implement as a sanitizer as in:

(define (bool-string bool)
  (if bool "true" "false"))
(define-record-type* <syncthing-config-file>
...
  (gui-enabled <accessor> (default "true")
                          (sanitizer bool-string))
...

But then the guix docs would technically be wrong reporting a default
value of #t instead of the actual default value of "true".  I think this
would yield a more helpful type-checking error though, so I'll go this
direction unless you think otherwise.

>> +@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}.
>
> I believe you want @itemx for all but the first item.

Does it matter that the describing text only applies to gui-password,
not to the whole block?  For now, I'll assume not and change all the
consecutive items with no specific documentation throughout to itemx.

>> +@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"})
>
> Similar to what I wrote above: numbers should be numbers, not strings.

I'll figure this out as well, using sanitizers (or leaving it as
numbers.  It seems sxml allows the body of a tag to be a number, but not
values of parameters of tags, so most the time it just works.)

> Last note: the convention throughout Guix it to avoid abbreviations.

My goal was to, as much as possible, avoid needing to maintain Syncthing
documentation in Guix.  My original patch even had camelcase for record
names so the user could easily search the Syncthing documentation
knowing what keyword to search.  Since you can't directly search anymore
anyway, maybe it does make sense to expand out some names.  I'll do that
for these:

auth - authorization
s - seconds
m - minutes
h - hours
fs - file-system
pct - percentage
mcaddr - mac-address
recv - recieve
ur - usage-reporting

> I’m not sure about the way forward; maybe we can make those typing
> changes (and perhaps some naming changes) and afford some
> incompatibility because the service is young?  WDYT?
>
> Ludo’.

I think it's unlikely that the service update will have major adoption
in the couple days between patches, so I think a bit of incompatibility
is fine.

eikcaz-
  
Zacchaeus Scheffer Feb. 17, 2025, 11:41 p.m. UTC | #4
Hi all,

Dispite what I said, it is a separate patch, so see my submission at
#76379.  The patch annotation + commit message says it all.

eikcaz-
  
Ludovic Courtès Feb. 21, 2025, 11:05 a.m. UTC | #5
Hi,

Zacchaeus Scheffer <eikcaz@zacchae.us> skribis:

> I believe that is an accurate term:
> https://en.wikipedia.org/wiki/Naming_convention_(programming)#Delimiter-separated_words

Woow, TIL.  :-)

> Sure thing.  I assume I can send it to the same issue even though it is
> closed?

Yes, but maybe open a new issue, for clarity.

>> As for the value itself, it would help a lot to use #t and #f instead of
>> the strings "true" and "false" (both of which have truth value in
>> Scheme).  It leads to a bit of extra work in the serializer, but I think
>> it’s worth it.  Because then we can also have type-checking of fields.
>
> This would be easiest to implement as a sanitizer as in:
>
> (define (bool-string bool)
>   (if bool "true" "false"))
> (define-record-type* <syncthing-config-file>
> ...
>   (gui-enabled <accessor> (default "true")
>                           (sanitizer bool-string))
> ...
>
> But then the guix docs would technically be wrong reporting a default
> value of #t instead of the actual default value of "true".  I think this
> would yield a more helpful type-checking error though, so I'll go this
> direction unless you think otherwise.

I would advise against using sanitizer in this way because it would lead
to inconsistencies: users would provide a boolean, but they’d get a
string when calling the accessor.

Instead the record should contain records of the “right” type;
converting to the relevant string should be left to whatever serializes
the record to the config file.

(This is something ‘define-configuration’ really helps with; I think
it’s worth looking into it!)

>>> +@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}.
>>
>> I believe you want @itemx for all but the first item.
>
> Does it matter that the describing text only applies to gui-password,
> not to the whole block?

It does yes, so my suggestion is actually bogus, sorry!

Normally each field would be documented, not just ‘gui-password’.

> My goal was to, as much as possible, avoid needing to maintain Syncthing
> documentation in Guix.  My original patch even had camelcase for record
> names so the user could easily search the Syncthing documentation
> knowing what keyword to search.  Since you can't directly search anymore
> anyway, maybe it does make sense to expand out some names.  I'll do that
> for these:
>
> auth - authorization
> s - seconds
> m - minutes
> h - hours
> fs - file-system
> pct - percentage
> mcaddr - mac-address
> recv - recieve
> ur - usage-reporting

Sounds good to me.

Thanks!

Ludo’.
  
Zacchaeus Scheffer Feb. 21, 2025, 8:26 p.m. UTC | #6
Hi all,

Ludovic Courtès <ludo@gnu.org> writes:

> Hi,
>
> Zacchaeus Scheffer <eikcaz@zacchae.us> skribis:
>> Sure thing.  I assume I can send it to the same issue even though it is
>> closed?
>
> Yes, but maybe open a new issue, for clarity.

I have opened a new issue at #76379

>>> As for the value itself, it would help a lot to use #t and #f instead of
>>> the strings "true" and "false" (both of which have truth value in
>>> Scheme).  It leads to a bit of extra work in the serializer, but I think
>>> it’s worth it.  Because then we can also have type-checking of fields.
>>
>> This would be easiest to implement as a sanitizer as in:
>>
>> (define (bool-string bool)
>>   (if bool "true" "false"))
>> (define-record-type* <syncthing-config-file>
>> ...
>>   (gui-enabled <accessor> (default "true")
>>                           (sanitizer bool-string))
>> ...
>>
>> But then the guix docs would technically be wrong reporting a default
>> value of #t instead of the actual default value of "true".  I think this
>> would yield a more helpful type-checking error though, so I'll go this
>> direction unless you think otherwise.
>
> I would advise against using sanitizer in this way because it would lead
> to inconsistencies: users would provide a boolean, but they’d get a
> string when calling the accessor.
>
> Instead the record should contain records of the “right” type;
> converting to the relevant string should be left to whatever serializes
> the record to the config file.
>
> (This is something ‘define-configuration’ really helps with; I think
> it’s worth looking into it!)

Alright, a suggestion from the Ludo made twice should not be so easily
dismissed.  I should mention my original assertion here is slightly
wrong; to work, I should set (default #t), but, as you mention, the
accessor will still retrieve a string instead of a bool.  I originally
tried to do my serialization through 'define-configuration', but most of
that work evaporated when I discovered sxml (which is the right way to
do it).  Still, there may be some room for improvment translating to
'define-configuration'.

I will investigate how to best translate 'define-record-type*' to
'define-configuration', but in the meantime I think we should prioritize
getting my new patch (#76379) through.  I renamed fields and changed
data types per your suggestions, so my new patch is backwards
incompatible with the first.

>>>> +@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}.
>>>
>>> I believe you want @itemx for all but the first item.
>>
>> Does it matter that the describing text only applies to gui-password,
>> not to the whole block?
>
> It does yes, so my suggestion is actually bogus, sorry!

Alright, I reverted (most of) my itemx changes in the v2 patch submitted
at #76379.

> Normally each field would be documented, not just ‘gui-password’.

So here's the problem: Syncthing has a LOT of documentation for a LOT of
fields.  Do we really want to maintain all that documentation in Guix?
I omitted documenting the majority of fields, pointing to Syncthing
documentation instead, out of caution for future Guix maintainers, not
laziness.  I opted for just documenting (1) any feature that is specific
to Guix configuration and (2) anything that controls to which other
devices your device connects.  For anything else, the user should
consult Syncthing documentation.  Is my premise flawed, and I should
document everything?  Maybe a one-liner for each is unlikely to be
invalidated by future Syncthing updates. WDYT?


eikcaz-
  
Gabriel Santos Feb. 22, 2025, 11:16 a.m. UTC | #7
Greetings,

I appreciate your efforts on this, but this completly broke
home-syncthing-service-type for me, as it's resetting my configuration,
giving my device another ID.

--
Gabriel Santos
  
Zacchaeus Scheffer Feb. 22, 2025, 8:49 p.m. UTC | #8
Hi,

Gabriel Santos <gabrielsantosdesouza@disroot.org> writes:

> Greetings,
>
> I appreciate your efforts on this, but this completly broke
> home-syncthing-service-type for me, as it's resetting my configuration,
> giving my device another ID.
>
> --
> Gabriel Santos

I think I know why this is.  At some point Syncthing changed the default
config directory from ~/.config/syncthing to ~/.local/state/syncthing.
If you move your syncthing .pem files from ~/.local/state/syncthing to
~/.config/syncthing, it will work.  Since it seems Syncthing has changed
the default, this service should really do so as well.  I'll update it
on the other patch that I'm submitting.

-Zacchae
  
Rodion Goritskov Feb. 22, 2025, 11:29 p.m. UTC | #9
Zacchaeus Scheffer <eikcaz@zacchae.us> writes:

> Hi,
>
> Gabriel Santos <gabrielsantosdesouza@disroot.org> writes:
>
>> Greetings,
>>
>> I appreciate your efforts on this, but this completly broke
>> home-syncthing-service-type for me, as it's resetting my configuration,
>> giving my device another ID.
>>
>> --
>> Gabriel Santos
>
> I think I know why this is.  At some point Syncthing changed the default
> config directory from ~/.config/syncthing to ~/.local/state/syncthing.
> If you move your syncthing .pem files from ~/.local/state/syncthing to
> ~/.config/syncthing, it will work.  Since it seems Syncthing has changed
> the default, this service should really do so as well.  I'll update it
> on the other patch that I'm submitting.
>
> -Zacchae

Hi!

I also have a problem (cannot say if it the same as Gabriel's) - after upgrade my Syncthing
instances stopped working with the odd error "remote device missing in
cluster config".

Moving config didn't work - and, as far as I understand, Syncthing still
read the config in .config if it is present.

However, it looks like it is not because the service itself, but because
of the syncthing upgrade to 1.29.2 in commit
06d37f38606fabbace21e55ec7f2546b3ae5214f.

I checked it by inheriting the Syncthing with the older version:

> (define-public syncthing-old
>  (package
>    (inherit syncthing)
>    (name "syncthing-old")
>    (version "1.28.1")
>    (source (origin
>              (method url-fetch)
>              (uri (string-append "https://github.com/syncthing/syncthing"
>                                  "/releases/download/v" version
>                                  "/syncthing-source-v" version ".tar.gz"))
>              (sha256
>               (base32
>                "16j5w6hdr1x2231hw0zsxm53sw34wxcs4ijjjcnzcg1vz9drjrg9"))))))

And passing it to the server:

> (service syncthing-with-vpn-service-type
> 		 (syncthing-configuration
> 		  (syncthing syncthing-old)
> 		  (user "rodion")))

After that everything started working as before.
Will try to investigate the issue further - couldn't find anything
related neither in Syncthing form nor in the Github issues.
  
Zacchaeus Scheffer Feb. 22, 2025, 11:56 p.m. UTC | #10
Rodion Goritskov <rodion@goritskov.com> writes:

>> (service syncthing-with-vpn-service-type
>> 		 (syncthing-configuration
>> 		  (syncthing syncthing-old)
>> 		  (user "rodion")))
>
> After that everything started working as before.
> Will try to investigate the issue further - couldn't find anything
> related neither in Syncthing form nor in the Github issues.

If you aren't passing a non-#f value for config-file in
syncthing-configuration, then my update should have no affect on your
system, so the issue should be somewhere else, probably in the syncthing
version update you mentioned.

Still, the other issue of syncthing configuration directory location is
one I will address.

-Zacchae
  
Leo Famulari Feb. 25, 2025, 2:20 a.m. UTC | #11
On Sun, Feb 23, 2025 at 12:29:25AM +0100, Rodion Goritskov wrote:
> I also have a problem (cannot say if it the same as Gabriel's) - after upgrade my Syncthing
> instances stopped working with the odd error "remote device missing in
> cluster config".

I see this too. I don't use the Guix service for Syncthing so it has
nothing to do with that.

> However, it looks like it is not because the service itself, but because
> of the syncthing upgrade to 1.29.2 in commit
> 06d37f38606fabbace21e55ec7f2546b3ae5214f.

Yes, seems that way.

> After that everything started working as before.
> Will try to investigate the issue further - couldn't find anything
> related neither in Syncthing form nor in the Github issues.

Let's try to debug a bit before reporting this upstream.
  
Leo Famulari Feb. 25, 2025, 3:08 a.m. UTC | #12
On Sun, Feb 23, 2025 at 12:29:25AM +0100, Rodion Goritskov wrote:
> However, it looks like it is not because the service itself, but because
> of the syncthing upgrade to 1.29.2 in commit
> 06d37f38606fabbace21e55ec7f2546b3ae5214f.

We can see here that Syncthing v1.29 requires Go 1.22 or newer:

https://github.com/syncthing/syncthing/blob/v1.29.2/go.mod#L3

However, the default Go version in Guix is 1.21:

https://git.savannah.gnu.org/cgit/guix.git/tree/gnu/packages/golang.scm?id=68cd38756b51d4abd8c796a5bcbbb9ea053eafde#n1045

I fixed the bug on our end by building Syncthing with Go 1.23:

https://git.savannah.gnu.org/cgit/guix.git/commit/?id=68cd38756b51d4abd8c796a5bcbbb9ea053eafde

Please let me know if you still experience this problem.
  
Gabriel Santos Feb. 25, 2025, 3:14 a.m. UTC | #13
>I see this too. I don't use the Guix service for Syncthing so it has
>nothing to do with that.
>
>> However, it looks like it is not because the service itself, but because
>> of the syncthing upgrade to 1.29.2 in commit
>> 06d37f38606fabbace21e55ec7f2546b3ae5214f.
>
>Yes, seems that way.

Since this isn't related to the service, but rather the version,
I think it would be best to look at the
diff between the tags:

<https://github.com/syncthing/syncthing/compare/v1.28.1...v1.29.2>

--
Gabriel Santos
  
Gabriel Santos Feb. 25, 2025, 11:37 a.m. UTC | #14
>I fixed the bug on our end by building Syncthing with Go 1.23:
>
>https://git.savannah.gnu.org/cgit/guix.git/commit/?id=68cd38756b51d4abd8c796a5bcbbb9ea053eafde
>
>Please let me know if you still experience this problem.

Thank you, that was it. Updating solved the issue for me.

--
Gabriel Santos
  
Rodion Goritskov Feb. 25, 2025, 11:14 p.m. UTC | #15
Hi!

Syncthing 1.29.2 works great now!

Thank you for the fix, Leo.
  

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..5cc7eadd57 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,75 @@  (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?)
+                   (let ((user-pw (getpw #$user)))
+                     (chown (string-append "/var/lib/syncthing-" #$user)
+                            (passwd:uid user-pw)
+                            (passwd:gid user-pw)))
+                   (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.")))