Nix flake configuration for my various machines (clients and homelab servers)
0

Configure Feed

Select the types of activity you want to include in your feed.

mkSelfHostedFeature calls mkFeature

Parthiv Krishna (Mar 5, 2026, 5:45 AM UTC) e47eecca f7257403

+109 -141
+109 -141
lib/features.nix
··· 1 1 { lib, customLib, ... }: 2 - { 3 - # mkSelfHostedFeature: Create a selfhosted service feature 4 - # 5 - # This creates a feature at custom.features.selfhosted.<name> that: 6 - # - Registers metadata (homepage, oidcClient) always (for cross-machine config) 7 - # - Only runs the actual service when enabled on the current host 8 - # 9 - # Arguments: 10 - # name: service name (also used for option path) 11 - # subdomain: subdomain for reverse proxy (defaults to name) 12 - # port: service port 13 - # extraOptions: additional options beyond `enable` 14 - # serviceConfig: function (cfg: moduleArgs: { ... }) returning NixOS service config 15 - # homepage: optional { category, description, icon } for homepage dashboard 16 - # oidcClient: optional OIDC client config for Authelia 17 - # backupServices: list of systemd services to stop during backups 18 - # persistentDirectories: directories to persist (for impermanence) 19 - # extraConfig: additional NixOS config to merge (always applied when enabled) 20 - mkSelfHostedFeature = 21 - { 22 - name, 23 - subdomain ? name, 24 - port, 25 - extraOptions ? { }, 26 - serviceConfig ? _cfg: _moduleArgs: { }, 27 - homepage ? null, 28 - oidcClient ? null, 29 - backupServices ? [ ], 30 - persistentDirectories ? [ ], 31 - extraConfig ? { }, 32 - }: 33 - let 34 - optionPath = [ 35 - "custom" 36 - "features" 37 - "selfhosted" 38 - name 39 - ]; 40 - featureName = "selfhosted.${name}"; 41 - 42 - optionsDef = { 43 - enable = lib.mkEnableOption "the ${featureName} service"; 44 - } 45 - // extraOptions; 46 - 47 - # Convert strings to attribute sets for persistent directories 48 - processPersistentDir = dir: if lib.isString dir then { directory = dir; } else dir; 49 - in 50 - { 51 - nixos = 52 - { 53 - config, 54 - lib, 55 - # deadnix: skip 56 - pkgs, 57 - ... 58 - }@moduleArgs: 59 - let 60 - cfg = lib.getAttrFromPath optionPath config; 61 - currentHost = config.networking.hostName; 62 - 63 - # FQDNs for reverse proxy 64 - fqdn = { 65 - internal = lib.custom.mkInternalFqdn config.constants subdomain currentHost; 66 - public = lib.custom.mkPublicFqdn config.constants subdomain; 67 - }; 68 - 69 - # Caddy virtual host config 70 - virtualHostConfig = logName: { 71 - logFormat = '' 72 - output file ${config.services.caddy.logDir}/access-${logName}.log { 73 - roll_size 10MB 74 - roll_keep 5 75 - roll_keep_for 14d 76 - mode 0640 77 - } 78 - level DEBUG 79 - ''; 80 - extraConfig = '' 81 - tls { 82 - dns cloudflare {env.CF_API_TOKEN} 83 - } 84 - reverse_proxy localhost:${toString port} 85 - ''; 86 - }; 87 - 88 - # Persistent directory configs 89 - processedPersistentDirs = map processPersistentDir persistentDirectories; 90 - persistentDirConfigs = map customLib.custom.mkPersistentSystemDir processedPersistentDirs; 91 - 92 - # Homepage entry (always registered for cross-machine visibility) 93 - # Static metadata only - no host-specific info needed 94 - homepageEntry = lib.mkIf (homepage != null) { 95 - custom.features.selfhosted.homepageServices.${name} = { 96 - inherit (homepage) category description icon; 97 - inherit name subdomain; 98 - status = homepage.status or null; 99 - }; 100 - }; 101 - 102 - # OIDC entry (always registered so Authelia sees all clients) 103 - oidcEntry = lib.mkIf (oidcClient != null) { 104 - custom.features.selfhosted.oidcClients.${name} = oidcClient // { 105 - inherit subdomain; 106 - }; 107 - }; 108 - 109 - # Enabled config (service, reverse proxy, persistence, backups) 110 - enabledConfig = lib.mkIf cfg.enable ( 111 - lib.mkMerge ( 112 - [ 113 - (serviceConfig cfg moduleArgs) 114 - { 115 - custom.features.selfhosted.enableReverseProxy = true; 116 - services.caddy.virtualHosts = { 117 - "${fqdn.internal}" = virtualHostConfig fqdn.internal; 118 - "${fqdn.public}" = virtualHostConfig fqdn.public; 119 - }; 120 - } 121 - extraConfig 122 - ] 123 - ++ persistentDirConfigs 124 - ++ (lib.optional (backupServices != [ ]) { 125 - custom.features.selfhosted.backupServices = backupServices; 126 - }) 127 - ) 128 - ); 129 - in 130 - { 131 - options = lib.setAttrByPath optionPath optionsDef; 132 - config = lib.mkMerge [ 133 - homepageEntry 134 - oidcEntry 135 - enabledConfig 136 - ]; 137 - }; 138 - 139 - # Selfhosted features don't have home-manager config 140 - home = _: { }; 141 - }; 142 - 2 + rec { 143 3 # mkFeature: Create a feature module that works in both NixOS and standalone home-manager 144 4 # 145 5 # Arguments: ··· 148 8 # systemConfig: function (cfg: moduleArgs: { ... }) returning NixOS config 149 9 # homeConfig: function (cfg: moduleArgs: { ... }) returning home-manager config 150 10 # homeImports: list of paths to import in the home module (for complex features like nixvim) 11 + # nixosExtraConfig: unconditional NixOS config (not wrapped in mkIf cfg.enable) 151 12 mkFeature = 152 13 { 153 14 path, ··· 156 17 systemConfig ? null, 157 18 homeConfig ? null, 158 19 homeImports ? [ ], 20 + nixosExtraConfig ? { }, 159 21 }: 160 22 let 161 23 optionPath = [ ··· 188 50 options = lib.setAttrByPath optionPath optionsDef; 189 51 190 52 config = lib.mkMerge [ 53 + nixosExtraConfig 191 54 ( 192 55 if homeConfig != null || homeImports != [ ] then 193 56 { ··· 291 154 in 292 155 { 293 156 imports = modules; 157 + }; 158 + 159 + # mkSelfHostedFeature: Create a selfhosted service feature 160 + # 161 + # This creates a feature at custom.features.selfhosted.<name> that: 162 + # - Registers metadata (homepage, oidcClient) always (for cross-machine config) 163 + # - Only runs the actual service when enabled on the current host 164 + # 165 + # Arguments: 166 + # name: service name (also used for option path) 167 + # subdomain: subdomain for reverse proxy (defaults to name) 168 + # port: service port 169 + # extraOptions: additional options beyond `enable` 170 + # serviceConfig: function (cfg: moduleArgs: { ... }) returning NixOS service config 171 + # homepage: optional { category, description, icon } for homepage dashboard 172 + # oidcClient: optional OIDC client config for Authelia 173 + # backupServices: list of systemd services to stop during backups 174 + # persistentDirectories: directories to persist (for impermanence) 175 + # extraConfig: additional NixOS config to merge (always applied when enabled) 176 + mkSelfHostedFeature = 177 + { 178 + name, 179 + subdomain ? name, 180 + port, 181 + extraOptions ? { }, 182 + serviceConfig ? _cfg: _moduleArgs: { }, 183 + homepage ? null, 184 + oidcClient ? null, 185 + backupServices ? [ ], 186 + persistentDirectories ? [ ], 187 + extraConfig ? { }, 188 + }: 189 + mkFeature { 190 + path = [ 191 + "selfhosted" 192 + name 193 + ]; 194 + inherit extraOptions; 195 + 196 + # Unconditional config: homepage/oidc entries visible to all hosts 197 + nixosExtraConfig = lib.mkMerge [ 198 + (lib.optionalAttrs (homepage != null) { 199 + custom.features.selfhosted.homepageServices.${name} = { 200 + inherit (homepage) category description icon; 201 + inherit name subdomain; 202 + status = homepage.status or null; 203 + }; 204 + }) 205 + (lib.optionalAttrs (oidcClient != null) { 206 + custom.features.selfhosted.oidcClients.${name} = oidcClient // { 207 + inherit subdomain; 208 + }; 209 + }) 210 + ]; 211 + 212 + systemConfig = 213 + cfg: 214 + { config, lib, ... }@moduleArgs: 215 + let 216 + currentHost = config.networking.hostName; 217 + 218 + fqdn = { 219 + internal = lib.custom.mkInternalFqdn config.constants subdomain currentHost; 220 + public = lib.custom.mkPublicFqdn config.constants subdomain; 221 + }; 222 + 223 + virtualHostConfig = logName: { 224 + logFormat = '' 225 + output file ${config.services.caddy.logDir}/access-${logName}.log { 226 + roll_size 10MB 227 + roll_keep 5 228 + roll_keep_for 14d 229 + mode 0640 230 + } 231 + level DEBUG 232 + ''; 233 + extraConfig = '' 234 + tls { 235 + dns cloudflare {env.CF_API_TOKEN} 236 + } 237 + reverse_proxy localhost:${toString port} 238 + ''; 239 + }; 240 + 241 + persistentDirConfigs = map ( 242 + dir: customLib.custom.mkPersistentSystemDir (if lib.isString dir then { directory = dir; } else dir) 243 + ) persistentDirectories; 244 + in 245 + lib.mkMerge ( 246 + [ 247 + (serviceConfig cfg moduleArgs) 248 + { 249 + custom.features.selfhosted.enableReverseProxy = true; 250 + services.caddy.virtualHosts = { 251 + "${fqdn.internal}" = virtualHostConfig fqdn.internal; 252 + "${fqdn.public}" = virtualHostConfig fqdn.public; 253 + }; 254 + } 255 + extraConfig 256 + ] 257 + ++ persistentDirConfigs 258 + ++ (lib.optional (backupServices != [ ]) { 259 + custom.features.selfhosted.backupServices = backupServices; 260 + }) 261 + ); 294 262 }; 295 263 }