Initial commit: Ultimate Notetaking, Sync & Backup System

A NixOS-based system for managing personal data across three tiers:
- Tier 1: Configuration (shareable via git)
- Tier 2: Syncable data (nb + Syncthing)
- Tier 3: Large data (self-hosted services + backup)

Includes:
- NixOS modules for nb, Syncthing, backup (restic)
- Server modules for Forgejo, Immich, Jellyfin
- Helper scripts (usync, ustatus)
- Comprehensive documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 01:44:00 -05:00
commit b40ac99524
17 changed files with 3151 additions and 0 deletions

269
modules/syncthing.nix Normal file
View File

@@ -0,0 +1,269 @@
# Syncthing Module
#
# Declarative Syncthing configuration for NixOS.
# This wraps the built-in syncthing module with sensible defaults.
#
# Usage:
# services.syncthing-managed.enable = true;
# services.syncthing-managed.user = "youruser";
# services.syncthing-managed.devices.laptop.id = "DEVICE-ID-HERE";
# services.syncthing-managed.folders.documents.path = "~/Documents";
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.syncthing-managed;
in {
options.services.syncthing-managed = {
enable = mkEnableOption "managed Syncthing file synchronization";
user = mkOption {
type = types.str;
description = "User to run Syncthing as.";
example = "alice";
};
group = mkOption {
type = types.str;
default = "users";
description = "Group to run Syncthing as.";
};
dataDir = mkOption {
type = types.str;
default = "/home/${cfg.user}";
defaultText = literalExpression ''"/home/''${cfg.user}"'';
description = "Default directory for Syncthing data.";
};
configDir = mkOption {
type = types.str;
default = "/home/${cfg.user}/.config/syncthing";
defaultText = literalExpression ''"/home/''${cfg.user}/.config/syncthing"'';
description = "Directory for Syncthing configuration.";
};
guiAddress = mkOption {
type = types.str;
default = "127.0.0.1:8384";
description = "Address for the Syncthing web GUI.";
};
openFirewall = mkOption {
type = types.bool;
default = true;
description = "Whether to open the firewall for Syncthing.";
};
devices = mkOption {
type = types.attrsOf (types.submodule {
options = {
id = mkOption {
type = types.str;
description = "Device ID (from syncthing CLI or GUI).";
example = "XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX";
};
name = mkOption {
type = types.nullOr types.str;
default = null;
description = "Friendly name for the device.";
};
addresses = mkOption {
type = types.listOf types.str;
default = [ "dynamic" ];
description = "Addresses to connect to this device.";
};
autoAcceptFolders = mkOption {
type = types.bool;
default = false;
description = "Automatically accept shared folders from this device.";
};
};
});
default = {};
description = "Syncthing devices to connect to.";
example = literalExpression ''
{
laptop = {
id = "XXXXXXX-...";
name = "My Laptop";
};
server = {
id = "YYYYYYY-...";
addresses = [ "tcp://server.local:22000" ];
};
}
'';
};
folders = mkOption {
type = types.attrsOf (types.submodule ({ name, ... }: {
options = {
path = mkOption {
type = types.str;
description = "Local path to sync.";
example = "~/Documents";
};
devices = mkOption {
type = types.listOf types.str;
default = attrNames cfg.devices;
defaultText = literalExpression "attrNames cfg.devices";
description = "Devices to share this folder with.";
};
id = mkOption {
type = types.str;
default = name;
description = "Unique folder ID.";
};
type = mkOption {
type = types.enum [ "sendreceive" "sendonly" "receiveonly" ];
default = "sendreceive";
description = ''
Folder type:
- sendreceive: Full two-way sync (default)
- sendonly: Only send changes, ignore remote changes
- receiveonly: Only receive changes, don't send local changes
'';
};
versioning = mkOption {
type = types.nullOr (types.submodule {
options = {
type = mkOption {
type = types.enum [ "simple" "staggered" "trashcan" "external" ];
default = "simple";
description = "Versioning type.";
};
params = mkOption {
type = types.attrsOf types.str;
default = { keep = "5"; };
description = "Versioning parameters.";
};
};
});
default = null;
description = "Versioning configuration for this folder.";
example = literalExpression ''
{
type = "staggered";
params = {
cleanInterval = "3600";
maxAge = "31536000";
};
}
'';
};
ignorePerms = mkOption {
type = types.bool;
default = false;
description = "Ignore permission changes.";
};
rescanInterval = mkOption {
type = types.int;
default = 3600;
description = "How often to rescan the folder (seconds). 0 to disable.";
};
fsWatcherEnabled = mkOption {
type = types.bool;
default = true;
description = "Use filesystem watcher for real-time sync.";
};
};
}));
default = {};
description = "Folders to synchronize.";
example = literalExpression ''
{
documents = {
path = "~/Documents";
devices = [ "laptop" "desktop" ];
versioning = { type = "simple"; params.keep = "5"; };
};
music = {
path = "~/Music";
type = "receiveonly"; # Don't upload changes
};
}
'';
};
extraOptions = mkOption {
type = types.attrs;
default = {};
description = "Extra options to pass to services.syncthing.settings.";
};
};
config = mkIf cfg.enable {
services.syncthing = {
enable = true;
user = cfg.user;
group = cfg.group;
dataDir = cfg.dataDir;
configDir = cfg.configDir;
guiAddress = cfg.guiAddress;
overrideDevices = true; # Declarative device management
overrideFolders = true; # Declarative folder management
settings = {
devices = mapAttrs (name: device: {
inherit (device) id addresses autoAcceptFolders;
name = if device.name != null then device.name else name;
}) cfg.devices;
folders = mapAttrs (name: folder: {
inherit (folder) path id type ignorePerms rescanInterval fsWatcherEnabled;
devices = folder.devices;
versioning = if folder.versioning != null then folder.versioning else {};
}) cfg.folders;
options = {
urAccepted = -1; # Disable usage reporting
relaysEnabled = true;
globalAnnounceEnabled = true;
localAnnounceEnabled = true;
} // cfg.extraOptions;
};
};
# Open firewall if requested
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ 22000 ]; # Syncthing protocol
allowedUDPPorts = [ 22000 21027 ]; # Syncthing + discovery
};
# Convenience alias
environment.systemPackages = [
(pkgs.writeShellScriptBin "st" ''
# Syncthing shortcut
case "$1" in
status)
${pkgs.syncthing}/bin/syncthing cli show system
;;
scan)
${pkgs.syncthing}/bin/syncthing cli scan --all
;;
errors)
${pkgs.syncthing}/bin/syncthing cli errors
;;
*)
echo "Usage: st {status|scan|errors}"
;;
esac
'')
];
};
}