Services¶
Source directory: modules/services/
default.nix¶
modules/services/appimage/default.nix
No option declarations; see source for implementation.
arr-suite-mcp.nix¶
modules/services/arr-suite-mcp.nix
arr-suite MCP server — exposed as an HTTP/SSE daemon.
arr-suite-mcp (shaktech786) is a stdio-only Python MCP server. To make it a tailnet-reachable daemon we wrap it with mcp-proxy (stdio→SSE):
mcp-proxy --host 0.0.0.0 --port
Clients connect to the SSE endpoint: http://
The *arr API keys are supplied via an agenix EnvironmentFile
(SONARR/RADARR/PROWLARR/OVERSEERR_API_KEY). Hosts/ports default to
localhost:
- Enable option: arr-suite MCP server (SSE daemon via mcp-proxy)
Options declaration (Nix)
options.features.arr-suite-mcp = {
enable = lib.mkEnableOption "arr-suite MCP server (SSE daemon via mcp-proxy)";
port = lib.mkOption {
type = lib.types.port;
default = 3011;
description = "Port the SSE bridge binds to (loopback always; tailnet + LAN via firewall).";
};
environmentFile = lib.mkOption {
type = lib.types.path;
default = config.age.secrets."arr-suite-mcp-env".path;
defaultText = lib.literalExpression ''config.age.secrets."arr-suite-mcp-env".path'';
description = "EnvironmentFile with the *arr API keys (KEY=VALUE per line).";
};
listenLanInterface = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "eno1";
description = ''
LAN interface to open the port on, in addition to tailscale0 and
loopback. null exposes the daemon only via Tailscale. (No effect on
hosts where the firewall is disabled.)
'';
};
}
default.nix¶
modules/services/atuin/default.nix
No option declarations; see source for implementation.
audible-sync.nix¶
modules/services/audible-sync.nix
audible-sync — download + decrypt your Audible library to local .m4b files.
UX: one command, audible-sync, after a one-time interactive login.
Pipeline: library export → bulk download (.aaxc/.aax) → decrypt to .m4b
→ organise into one folder per book under outputDir. Re-runnable;
already-downloaded books and already-decrypted files are skipped.
Per-host wiring (currently p620 only):
features.audibleSync = { enable = true; outputDir = "~/audiobooks/audible"; # default };
One-time setup (run interactively on p620 AFTER deploy): 1. audible quickstart # picks marketplace, handles 2FA in browser 2. audible library list # confirm your books are visible 3. audible-sync # downloads + decrypts everything
Legal note: stripping DRM violates Audible's ToS. Personal-use only.
- Enable option: audible-sync — download + decrypt Audible library to .m4b
Options declaration (Nix)
options.features.audibleSync = {
enable = lib.mkEnableOption "audible-sync — download + decrypt Audible library to .m4b";
outputDir = lib.mkOption {
type = lib.types.str;
default = "~/audiobooks/audible";
description = ''
Destination for decrypted, organised .m4b files (one folder per book).
Tilde is expanded at runtime against the invoking user's $HOME.
'';
};
}
audiobook-import.nix¶
modules/services/audiobook-import.nix
audiobook-import — completed-download → Audiobookshelf import pipeline.
A timer-driven reconciler (modules/services/audiobook-import.py) that scans
the audiobook download dir(s) for stable, completed folders, uses the local
Ollama to parse the release name into structured metadata, optionally merges
multi-file books into a chaptered M4B via m4b-tool, and hardlinks/places the
result into the Audiobookshelf library with a metadata.json. Idempotent via
a .imported marker; sources are left intact so torrent seeding continues.
- Enable option: audiobook download → Audiobookshelf import pipeline
Options declaration (Nix)
options.features.audiobook-import = {
enable = lib.mkEnableOption "audiobook download → Audiobookshelf import pipeline";
watchDirs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "/mnt/media/downloads/torrents/audiobooks" ];
description = "Download directories scanned for completed audiobook folders.";
};
libraryDir = lib.mkOption {
type = lib.types.str;
default = "/mnt/media/Media/Audiobooks";
description = "Audiobookshelf library root to place imported books into.";
};
model = lib.mkOption {
type = lib.types.str;
default = "qwen2.5:7b";
description = "Ollama model used for metadata extraction (strict JSON).";
};
ollamaUrl = lib.mkOption {
type = lib.types.str;
default = "http://127.0.0.1:11434";
description = "Base URL of the local Ollama server.";
};
mergeToM4b = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Merge multi-file audiobooks into a single chaptered M4B via m4b-tool.";
};
stableSeconds = lib.mkOption {
type = lib.types.int;
default = 120;
description = "Skip folders modified more recently than this (still downloading).";
};
interval = lib.mkOption {
type = lib.types.str;
default = "*:0/5";
description = "systemd OnCalendar expression for the import scan (default every 5 min).";
};
user = lib.mkOption {
type = lib.types.str;
default = "olafkfreund";
description = "User to run the import as (must own the library + read downloads).";
};
group = lib.mkOption {
type = lib.types.str;
default = "users";
description = "Group to run the import as.";
};
}
audiobook-mcp.nix¶
modules/services/audiobook-mcp.nix
audiobook-mcp — audiobook acquisition + library MCP server, as an SSE daemon.
audiobook-mcp (pkgs.customPkgs.audiobook-mcp) is a stdio FastMCP server. To make it tailnet-reachable we wrap it with mcp-proxy (stdio→SSE):
mcp-proxy --host 0.0.0.0 --port
Clients connect to the SSE endpoint: http://
Backend URLs default to the local services on p510 and are passed as plain env; the API keys (PROWLARR/SABNZBD/ABS) come from an agenix EnvironmentFile read by systemd as root before dropping to the DynamicUser.
- Enable option: audiobook MCP server (SSE daemon via mcp-proxy)
Options declaration (Nix)
options.features.audiobook-mcp = {
enable = lib.mkEnableOption "audiobook MCP server (SSE daemon via mcp-proxy)";
port = lib.mkOption {
type = lib.types.port;
default = 3012;
description = "Port the SSE bridge binds to (loopback always; tailnet + LAN via firewall).";
};
abbAppUrl = lib.mkOption {
type = lib.types.str;
default = "http://127.0.0.1:5078";
description = "audiobookbay-automated base URL (used to add ABB releases).";
};
prowlarrUrl = lib.mkOption {
type = lib.types.str;
default = "http://127.0.0.1:9696";
description = "Prowlarr base URL (Usenet/torrent indexer search).";
};
sabnzbdUrl = lib.mkOption {
type = lib.types.str;
default = "http://127.0.0.1:8080";
description = "SABnzbd base URL (Usenet grabs).";
};
audiobookshelfUrl = lib.mkOption {
type = lib.types.str;
default = "http://127.0.0.1:13378";
description = "Audiobookshelf base URL (library lookups).";
};
ollamaUrl = lib.mkOption {
type = lib.types.str;
default = "http://127.0.0.1:11434";
description = "Local Ollama base URL (recommend_bestsellers tool).";
};
ollamaModel = lib.mkOption {
type = lib.types.str;
default = "qwen2.5:7b";
description = "Ollama model used for bestseller suggestions.";
};
environmentFile = lib.mkOption {
type = lib.types.path;
default = config.age.secrets."audiobook-mcp-env".path;
defaultText = lib.literalExpression ''config.age.secrets."audiobook-mcp-env".path'';
description = "EnvironmentFile with backend API keys (PROWLARR/SABNZBD/ABS).";
};
listenLanInterface = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "eno1";
description = ''
LAN interface to open the port on, in addition to tailscale0 and
loopback. null exposes the daemon only via Tailscale.
'';
};
}
audiobookbay-automated.nix¶
modules/services/audiobookbay-automated.nix
audiobookbay-automated — AudioBookBay search → Transmission web app.
A small Flask app (pkgs.customPkgs.audiobookbay-automated) that searches
AudioBookBay and sends the chosen release's magnet to the existing
Transmission daemon on p510. Each download is saved to
The app only talks to Transmission's RPC (127.0.0.1:9091, auth disabled on p510) and to AudioBookBay over HTTPS — it writes nothing to disk itself, so the unit runs fully sandboxed (DynamicUser + ProtectSystem=strict).
Note: AudioBookBay distributes copyrighted material without authorization. abbHostname is configurable; the same app works against any compatible host.
- Enable option: AudioBookBay search → Transmission web app
Options declaration (Nix)
options.features.audiobookbay-automated = {
enable = lib.mkEnableOption "AudioBookBay search → Transmission web app";
port = lib.mkOption {
type = lib.types.port;
default = 5078;
description = "Port the Flask UI binds to (loopback always; tailnet + LAN via firewall).";
};
abbHostname = lib.mkOption {
type = lib.types.str;
default = "audiobookbay.lu";
description = "AudioBookBay host to search against.";
};
transmissionHost = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Transmission RPC host.";
};
transmissionPort = lib.mkOption {
type = lib.types.port;
default = 9091;
description = "Transmission RPC port.";
};
savePathBase = lib.mkOption {
type = lib.types.str;
default = "/mnt/media/downloads/torrents/audiobooks";
description = ''
Base directory passed to Transmission as the per-torrent download
location (each book lands in <savePathBase>/<Title>/). Must be
writable by the Transmission service user; watched by the
audiobook-import pipeline.
'';
};
category = lib.mkOption {
type = lib.types.str;
default = "Audiobookbay-Audiobooks";
description = "Download category/label tag.";
};
listenLanInterface = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "eno1";
description = ''
LAN interface to open the port on, in addition to tailscale0 and
loopback. null exposes the UI only via Tailscale.
'';
};
}
backstage.nix¶
modules/services/backstage.nix
Backstage developer portal.
Runs olafkfreund/backstage (a customised Spotify Backstage app) on p510 alongside a sibling Postgres container. Image is consumed from ghcr.io/olafkfreund/backstage, pinned to a SHA digest (NEVER :latest — that's a supply-chain risk: a leaked GHCR token could quietly swap the running image).
Wiring overview:
┌─────────────────────────┐ ┌──────────────────────────┐ │ podman-backstage-postgres│◀────│ podman-backstage │ │ 127.0.0.1:5435 → 5432 │ │ 127.0.0.1:7007 → 7007 │ └─────────────────────────┘ └──────────────────────────┘ ▲ │ Tailscale Serve │ /backstage path ▼ https://p510.tail833f7.ts.net/backstage
Secrets (from agenix, loaded at runtime — never in the Nix store): backstage-postgres-password → POSTGRES_PASSWORD backstage-github-token → GITHUB_TOKEN (catalog integration) backstage-github-oauth-client-id → AUTH_GITHUB_CLIENT_ID backstage-github-oauth-client-secret → AUTH_GITHUB_CLIENT_SECRET
The secret-to-env bridge: a one-shot systemd unit (backstage-env-setup) reads /run/agenix/backstage-* and writes /run/backstage/env-{postgres, backstage}, consumed by the container services as environmentFiles. /run/backstage is tmpfs (cleared on every boot — secrets re-emitted each time the unit runs).
This module is intentionally disabled by default. Flip features.backstage.enable = true on p510 only AFTER: 1. Phase 1 image is in ghcr.io with a real SHA digest 2. Phase 2 agenix secrets exist and have been rekeyed 3. Phase 4 Tailscale Serve route is added See olafkfreund/nixos_config epic #731.
- Enable option: Backstage developer portal
Options declaration (Nix)
options.features.backstage = {
enable = lib.mkEnableOption "Backstage developer portal";
image = lib.mkOption {
type = lib.types.str;
default = "ghcr.io/olafkfreund/backstage@sha256:33a836eb6a7b8d45e5ef240e973ba2dd1856e377a31b1c952fa244b2d3dd70bc";
example = "ghcr.io/olafkfreund/backstage@sha256:abc123...";
description = ''
OCI image to pull for the Backstage backend. MUST be pinned to a
SHA256 digest (the @sha256:... form). Do NOT use :latest — updates
should be explicit nixos commits so a leaked GHCR token can't
quietly swap the running image.
'';
};
postgresImage = lib.mkOption {
type = lib.types.str;
default = "docker.io/postgres:16-alpine";
description = "OCI image for the Postgres sidecar.";
};
port = lib.mkOption {
type = lib.types.port;
default = 7007;
description = "Localhost port the Backstage backend binds to.";
};
pgPort = lib.mkOption {
type = lib.types.port;
default = 5435;
description = ''
Localhost port for Backstage's Postgres. 5435 avoids colliding with
skill-pool's 5434 on p620 (in case that ever migrates to p510) and
with a typical host Postgres on 5432.
'';
};
pgDatabase = lib.mkOption {
type = lib.types.str;
default = "backstage";
description = "Postgres database name.";
};
pgUser = lib.mkOption {
type = lib.types.str;
default = "backstage";
description = "Postgres user.";
};
publicUrl = lib.mkOption {
type = lib.types.str;
default = "https://p510.tail833f7.ts.net/backstage";
description = ''
Public-facing base URL Backstage uses for app.baseUrl,
backend.baseUrl, CORS origin, and OAuth callbacks. If you rename
your tailnet or move Backstage to a different host, update this
AND the GitHub OAuth App's authorization callback URL (which
must match exactly).
'';
};
memoryHigh = lib.mkOption {
type = lib.types.str;
default = "2G";
description = ''
Soft memory cap on the Backstage container (passed as --memory to
podman). Caps blast radius if Backstage leaks memory while Plex
transcode + Ollama are running. Epic #731 risk #3.
'';
};
}
bazarr.nix¶
Bazarr — subtitle manager for Sonarr/Radarr/Lidarr on p510.
Thin wrapper over nixpkgs's services.bazarr. We add a feature flag for
consistency with the other media services, plus narrow the firewall
opening to tailscale0 (and optionally a named LAN interface) instead
of using openFirewall = true which would expose the port globally.
Storage: nixpkgs default /var/lib/bazarr (small config + SQLite db).
Subtitle .srt files are written next to the video files in
/mnt/media, not into Bazarr's own data dir — no extra config needed.
First-deploy UX (one-time, in the Bazarr web UI at http://p510:6767): 1. Settings → Sonarr → add: localhost:8989 + SONARR_API_KEY 2. Settings → Radarr → add: localhost:7878 + RADARR_API_KEY (API keys can be found in arr-suite-mcp-env.age — pasted via UI; Bazarr stores them in its own DB after that) 3. Settings → Languages → enable Norwegian Bokmål (nb) + English (en) 4. Settings → Languages → Default Profile: a. Norwegian Bokmål (forced=False) b. English (forced=False) 5. Settings → Providers → enable OpenSubtitles.com (anonymous works; authenticate later for higher daily quota) 6. Tick "use embedded subs" if present (saves a download when the release already has subs muxed in)
Phase 2 candidate: declarative initial-config via Bazarr's REST API at first deploy, similar to the *arr webhook wiring for media-bot.
- Enable option: Bazarr subtitle manager
Options declaration (Nix)
options.features.bazarr = {
enable = lib.mkEnableOption "Bazarr subtitle manager";
listenLanInterface = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "eno1";
description = ''
LAN interface to also open the Bazarr port on, in addition to
tailscale0. null exposes the service only via Tailscale (the
recommended setting; Bazarr's UI is fine over the tailnet).
'';
};
}
bluetooth.nix¶
modules/services/bluetooth/bluetooth.nix
No option declarations; see source for implementation.
citrix-workspace.nix¶
modules/services/citrix-workspace.nix
- Enable option: Citrix Workspace
Options: enable, package, acceptLicense
Options declaration (Nix)
options.services.citrix-workspace = {
enable = mkEnableOption "Citrix Workspace";
package = mkOption {
type = types.package;
default = pkgs.citrix_workspace;
description = "Citrix Workspace package to use";
};
acceptLicense = mkOption {
type = types.bool;
default = false;
description = ''
Accept the Citrix Workspace End User License Agreement.
WARNING: By setting this to true, you accept the Citrix EULA.
You must manually download the tarball from Citrix if required.
'';
};
}
cloudflared.nix¶
modules/services/cloudflared.nix
Cloudflare Tunnel — public ingress for p510 services from behind Starlink CGNAT.
Outbound-only tunnel from p510 to Cloudflare's edge. No port forwards, no
public IP needed. Each route maps a hostname under freundcloud.org.uk
(Cloudflare-managed zone) to a local URL on p510.
Thin feature-flag wrapper over upstream services.cloudflared. The
credentials.json from cloudflared tunnel create lives in agenix
(cloudflared-credentials.age); the cert.pem from cloudflared login
lives in agenix too (cloudflared-cert.age). Routes are declarative —
add a new entry to cfg.ingress, deploy, optionally run
cloudflared tunnel route dns <tunnel> <hostname> once to add the
Cloudflare DNS CNAME (or do it via dashboard).
One-time bootstrap (run on a workstation with browser access — NOT p510): 1. nix-shell -p cloudflared 2. cloudflared login # → opens browser; saves ~/.cloudflared/cert.pem 3. cloudflared tunnel create p510-home
→ prints tunnel UUID, saves ~/.cloudflared/.json¶
- Copy cert.pem +
.json into agenix via manage-secrets.sh (one secret each: cloudflared-cert, cloudflared-credentials) - Set features.cloudflared.tunnelId to the UUID and deploy
Adding a Cloudflare DNS record for a hostname (one-time per hostname): cloudflared tunnel route dns p510-home argocd.freundcloud.org.uk
or click-route in the Cloudflare Zero Trust dashboard¶
References: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/ https://nixos.wiki/wiki/Cloudflare_tunnel
- Enable option: Cloudflare Tunnel client — public ingress from behind Starlink CGNAT
Options declaration (Nix)
options.features.cloudflared = {
enable = lib.mkEnableOption "Cloudflare Tunnel client — public ingress from behind Starlink CGNAT";
tunnelId = lib.mkOption {
type = lib.types.str;
description = ''
Tunnel UUID issued by `cloudflared tunnel create`. The credentials
file in agenix MUST match this UUID — they are paired.
'';
example = "deadbeef-1234-5678-9abc-def012345678";
};
ingress = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = lib.literalExpression ''
{
"argocd.freundcloud.org.uk" = "http://localhost:80";
"backstage.freundcloud.org.uk" = "http://localhost:7007";
}
'';
description = ''
Map of public hostnames to local service URLs that cloudflared will
proxy. Each hostname MUST also have a Cloudflare DNS CNAME pointing
at <tunnelId>.cfargotunnel.com — created once via:
cloudflared tunnel route dns p510-home <hostname>
or via the Cloudflare Zero Trust dashboard.
Default fallback (`services.cloudflared.tunnels.<id>.default`) is
set below to `http_status:404` so any miss returns a clean 404
rather than leaking that the tunnel exists.
'';
};
keepalive = {
enable = lib.mkEnableOption ''
Periodic GET against each ingress origin to keep apps warm.
Cloudflare opens a fresh TCP connection to the origin per request,
so any app behind the tunnel that idles aggressively (Node SPAs,
gunicorn workers, JVMs, podman containers without --keepalive)
will cold-start on the first hit and the SPA may render blank
while it boots. A 2-minute heartbeat sidesteps this entirely.
Hits the LOCAL origin URLs directly — does not exercise the
cloudflared edge path, just the origin app, which is what needs
keeping warm.
'';
interval = lib.mkOption {
type = lib.types.str;
default = "2min";
example = "5min";
description = ''
systemd `OnUnitActiveSec` interval between keepalive runs.
Default 2min is comfortably under typical idle-recycle windows
(Node `keepAliveTimeout`, gunicorn worker recycling, k8s HPA
scale-to-zero) without generating meaningful load.
'';
};
timeout = lib.mkOption {
type = lib.types.int;
default = 10;
description = ''
Per-origin curl timeout in seconds. Kept short so a single
slow origin doesn't delay the rest of the sweep.
'';
};
};
}
cron.nix¶
modules/services/cron/cron.nix
No option declarations; see source for implementation.
default.nix¶
No option declarations; see source for implementation.
secure-dns.nix¶
modules/services/dns/secure-dns.nix
- Enable option: Secure DNS with enhanced stability
Options: enable, dnssec, useStubResolver, fallbackProviders, cacheSize, dnsOverTls, networkManagerIntegration
Options declaration (Nix)
options.services.secure-dns = {
enable = mkEnableOption "Secure DNS with enhanced stability";
dnssec = mkOption {
type = types.enum [ "true" "false" "allow-downgrade" ];
default = "true";
description = "Whether to enable DNSSEC validation";
example = "allow-downgrade";
};
useStubResolver = mkOption {
type = types.bool;
default = true;
description = "Use systemd-resolved's stub resolver";
example = true;
};
fallbackProviders = mkOption {
type = types.listOf types.str;
default = [
"1.1.1.1#cloudflare-dns.com"
"8.8.8.8#dns.google"
];
description = "List of fallback DNS providers to use";
example = [ "9.9.9.9#dns.quad9.net" ];
};
cacheSize = mkOption {
type = types.int;
default = 4096;
description = "Size of DNS cache in entries";
example = 8192;
};
dnsOverTls = mkOption {
type = types.bool;
default = true;
description = "Whether to enable DNS-over-TLS";
example = true;
};
networkManagerIntegration = mkOption {
type = types.bool;
default = true;
description = "Whether to integrate with NetworkManager";
example = true;
};
}
default.nix¶
modules/services/flaresolverr/default.nix
FlareSolverr Configuration Module A proxy server to bypass Cloudflare protection for web scraping applications
- Enable option: FlareSolverr proxy server
Options: enable, package, port, host, logLevel, logHtml, captchaSolver, testUrl, sessionTtl, headless, browserTimeout, user, group, dataDir, extraEnvironment, openFirewall
Options declaration (Nix)
options.services.flaresolverr = {
enable = mkEnableOption "FlareSolverr proxy server";
package = mkOption {
type = types.package;
default = pkgs.flaresolverr;
defaultText = literalExpression "pkgs.flaresolverr";
description = "The FlareSolverr package to use";
};
port = mkOption {
type = types.port;
default = 8191;
description = "Port on which FlareSolverr will listen";
};
host = mkOption {
type = types.str;
default = "0.0.0.0";
description = "Host address to bind to";
};
logLevel = mkOption {
type = types.enum [ "debug" "info" "warning" "error" ];
default = "info";
description = "Log level for FlareSolverr";
};
logHtml = mkOption {
type = types.bool;
default = false;
description = "Whether to log HTML content";
};
captchaSolver = mkOption {
type = types.enum [ "none" "hcaptcha-solver" "harvester" ];
default = "none";
description = "CAPTCHA solver to use";
};
testUrl = mkOption {
type = types.str;
default = "https://www.google.com";
description = "URL to test browser functionality";
};
sessionTtl = mkOption {
type = types.int;
default = 600000;
description = "Session time-to-live in milliseconds";
};
headless = mkOption {
type = types.bool;
default = true;
description = "Run browser in headless mode";
};
browserTimeout = mkOption {
type = types.int;
default = 40000;
description = "Browser timeout in milliseconds";
};
user = mkOption {
type = types.str;
default = "flaresolverr";
description = "User to run FlareSolverr as";
};
group = mkOption {
type = types.str;
default = "flaresolverr";
description = "Group to run FlareSolverr as";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/flaresolverr";
description = "Data directory for FlareSolverr";
};
extraEnvironment = mkOption {
type = types.attrs;
default = { };
description = "Extra environment variables for FlareSolverr";
example = {
PROMETHEUS_ENABLED = "true";
PROMETHEUS_PORT = "8192";
};
# … truncated — see source link above
flatpak.nix¶
modules/services/flatpak/flatpak.nix
Flatpak Application Management Module Enables Flatpak with automatic Flathub repository setup
- Enable option: Flatpak application management
Options: enable, autoAddFlathub, maxRetries, retryDelay
Options declaration (Nix)
options.modules.services.flatpak = {
enable = mkEnableOption "Flatpak application management";
autoAddFlathub = mkOption {
type = types.bool;
default = true;
description = ''Automatically add Flathub repository on system startup'';
example = false;
};
repositorySetup = {
maxRetries = mkOption {
type = types.int;
default = 3;
description = ''Maximum number of retry attempts for repository setup'';
example = 5;
};
retryDelay = mkOption {
type = types.int;
default = 5;
description = ''Base delay in seconds between retry attempts'';
example = 10;
};
};
}
default.nix¶
modules/services/geforcenow/default.nix
NVIDIA GeForce NOW Cloud Gaming Module Enables GeForce NOW native Linux client via Flatpak
- Enable option: NVIDIA GeForce NOW cloud gaming client
Options: enable, autoInstall, waylandFix, maxRetries, retryDelay
Options declaration (Nix)
options.modules.services.geforcenow = {
enable = mkEnableOption "NVIDIA GeForce NOW cloud gaming client";
autoInstall = mkOption {
type = types.bool;
default = true;
description = ''
Automatically install GeForce NOW Flatpak on system startup.
If disabled, the remote will be added but installation must be done manually.
'';
example = false;
};
waylandFix = mkOption {
type = types.bool;
default = false;
description = ''
Apply Wayland fix by disabling Wayland socket for GeForce NOW.
Enable this if the application window doesn't open on Wayland.
'';
example = true;
};
remoteSetup = {
maxRetries = mkOption {
type = types.int;
default = 3;
description = ''Maximum number of retry attempts for repository setup'';
example = 5;
};
retryDelay = mkOption {
type = types.int;
default = 10;
description = ''Base delay in seconds between retry attempts'';
example = 15;
};
};
}
github-runner.nix¶
modules/services/github/github-runner.nix
No option declarations; see source for implementation.
gitlab-runner.nix¶
modules/services/gitlab-runner.nix
- Enable option: GitLab Runner for local CI/CD
Options: enable, registrationConfigFile, concurrent, checkInterval, services, name, url, executor, dockerImage, dockerPrivileged, dockerVolumes, tagList, runUntagged, limit
Options declaration (Nix)
options.services.gitlab-runner-local = {
enable = mkEnableOption "GitLab Runner for local CI/CD";
registrationConfigFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to file containing GitLab Runner registration token.
This file should contain the registration token from your GitLab instance.
Example file content:
CI_SERVER_URL=https://gitlab.com
REGISTRATION_TOKEN=your-registration-token-here
'';
example = "/run/agenix/gitlab-runner-token";
};
concurrent = mkOption {
type = types.int;
default = 4;
description = "Maximum number of concurrent jobs";
};
checkInterval = mkOption {
type = types.int;
default = 0;
description = "Check interval for new jobs (in seconds, 0 = default)";
};
services = mkOption {
type = types.listOf (types.submodule {
options = {
name = mkOption {
type = types.str;
description = "Name of the runner service";
example = "docker-runner";
};
url = mkOption {
type = types.str;
default = "https://gitlab.com";
description = "GitLab instance URL";
};
executor = mkOption {
type = types.enum [ "shell" "docker" "docker+machine" "kubernetes" ];
default = "docker";
description = "Executor type for running jobs";
};
dockerImage = mkOption {
type = types.str;
default = "alpine:latest";
description = "Default Docker image for docker executor";
};
dockerPrivileged = mkOption {
type = types.bool;
default = false;
description = "Run Docker containers in privileged mode";
};
dockerVolumes = mkOption {
type = types.listOf types.str;
default = [ "/cache" ];
description = "Docker volumes to mount";
};
tagList = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Tags for this runner";
example = [ "docker" "linux" "nix" ];
};
runUntagged = mkOption {
type = types.bool;
default = false;
description = "Run jobs without tags";
};
limit = mkOption {
type = types.int;
default = 0;
description = "Maximum number of jobs for this runner (0 = unlimited)";
};
};
});
default = [ ];
description = "GitLab Runner service configurations";
# … truncated — see source link above
gnome-services.nix¶
modules/services/gnome/gnome-services.nix
No option declarations; see source for implementation.
greetd.nix¶
modules/services/greetd/greetd.nix
No option declarations; see source for implementation.
home-assistant.nix¶
modules/services/home-assistant.nix
- Enable option: Home Assistant home automation platform
Options: enable, port, enableCloud, enableCLI, extraComponents, tailscaleIntegration, dashboards, title, icon, showInSidebar, yaml
Options declaration (Nix)
options.features.homeAssistant = {
enable = mkEnableOption "Home Assistant home automation platform";
port = mkOption {
type = types.port;
default = 8123;
description = "Port for Home Assistant web interface";
};
enableCloud = mkOption {
type = types.bool;
default = true;
description = "Enable Home Assistant Cloud (Nabu Casa) integration";
};
enableCLI = mkOption {
type = types.bool;
default = true;
description = "Install Home Assistant CLI tool";
};
extraComponents = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Additional Home Assistant components to enable";
};
tailscaleIntegration = mkOption {
type = types.bool;
default = true;
description = "Configure trusted proxies for Tailscale access";
};
dashboards = mkOption {
type = types.attrsOf (types.submodule {
options = {
title = mkOption {
type = types.str;
description = "Sidebar display title";
};
icon = mkOption {
type = types.str;
default = "mdi:view-dashboard";
description = "Material Design Icons name for the sidebar entry";
};
showInSidebar = mkOption {
type = types.bool;
default = true;
description = "Show this dashboard in the HA sidebar";
};
yaml = mkOption {
type = types.lines;
description = ''
Raw Lovelace dashboard YAML. Must define `title:` and `views:`.
Written to /etc/home-assistant/dashboards/<name>.yaml at activation;
referenced from configuration.yaml via lovelace.dashboards.
'';
};
};
});
default = { };
description = ''
Declarative Lovelace dashboards. Each attr key becomes the URL slug
(/<key>) and the corresponding YAML is rendered into a read-only file
under /etc/home-assistant/dashboards/. The default Overview dashboard
remains in storage mode and is unaffected.
'';
};
}
intune-portal.nix¶
modules/services/intune-portal.nix
- Enable option: Microsoft Intune Company Portal
Options: enable, package, autoStart, enableDesktopIntegration
Options declaration (Nix)
options.features.intune = {
enable = mkEnableOption "Microsoft Intune Company Portal";
package = mkOption {
type = types.package;
default = pkgs.intune-portal;
defaultText = literalExpression "pkgs.intune-portal";
description = ''
The Microsoft Intune Company Portal package to use.
This defaults to our custom-built package with manual version control.
Change the version by updating pkgs/intune-portal/default.nix and
rebuilding the system.
'';
example = literalExpression "pkgs.intune-portal";
};
autoStart = mkOption {
type = types.bool;
default = false;
description = ''
Whether to automatically start the Intune Portal service on login.
When enabled, the intune-portal service will start with the user session.
When disabled, you must manually launch intune-portal from the application menu.
'';
};
enableDesktopIntegration = mkOption {
type = types.bool;
default = true;
description = ''
Whether to install desktop integration files (.desktop files, system tray support).
This makes Intune Portal available in application menus and provides
system tray integration for enrollment status and notifications.
'';
};
}
default.nix¶
modules/services/kometa/default.nix
Kometa (was Plex Meta Manager) — collections + metadata + posters for Plex.
Runs as a podman OCI container on p510 sharing the host network namespace
so it can reach localhost:32400 (Plex) directly. Container is always-on;
Kometa's internal scheduler fires per the schedule: key in config.yml.
Phase 1a (this commit): dry-run mode + IMDb Top 250 only. Once the
dry-run output looks right, flip dry_run: false in
modules/services/kometa/config.yml and redeploy.
Pattern mirrors modules/services/skill-pool.nix (oci-containers + podman).
Config flow: modules/services/kometa/config.yml ← repo source of truth; contains ${TMDB_API_KEY} + ${PLEX_TOKEN} shell-style placeholders. kometa-config-render.service ← systemd oneshot; envsubst's the template into /var/lib/kometa/config.yml with agenix-provided values. podman-kometa.service ← depends on the oneshot; mounts /var/lib/kometa as /config.
- Enable option: Kometa (Plex Meta Manager)
Options declaration (Nix)
default.nix¶
modules/services/libinput/default.nix
No option declarations; see source for implementation.
litellm-router.nix¶
modules/services/litellm-router.nix
LiteLLM proxy — Anthropic-compat router that fronts the local Ollama service. Lets Claude Code (which speaks the Anthropic API natively) reach local Ollama models by setting ANTHROPIC_BASE_URL per repo.
Architecture: Claude Code → http(s)://p620.../router (LiteLLM) → 127.0.0.1:11434 (Ollama)
Model aliases: claude-sonnet-4-6 → qwen3:14b (default coding model — primary) claude-opus-4-6 → gemma4:e4b (light/fast on-demand) qwen3 → qwen3:14b (native name passthrough) qwen3.6 → qwen3:14b (backward compatibility alias) qwen2.5-coder → qwen2.5-coder:14b (previous default, still pulled) gemma4 → gemma4:e4b (backward compatibility alias)
Authentication: a single master bearer key loaded at runtime from agenix (/run/agenix/litellm-master-key). Per-host clients hold the same plaintext under their own .age files (api-router-p620, api-router-razer — Phase 3).
Network: binds 0.0.0.0:4000 but firewall opens it only on tailscale0 and the configured LAN interface — never globally reachable.
See docs/plans/2026-05-22-ollama-p620-litellm-design.md §5 for full design.
- Enable option: LiteLLM proxy fronting the local Ollama service
Options declaration (Nix)
options.features.litellm-router = {
enable = lib.mkEnableOption "LiteLLM proxy fronting the local Ollama service";
port = lib.mkOption {
type = lib.types.port;
default = 4000;
description = "Port LiteLLM binds to (loopback always; tailnet + LAN via firewall).";
};
listenLanInterface = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "enp1s0";
description = ''
LAN interface to open the port on (in addition to tailscale0 and
loopback). Set to the host's actual LAN NIC; confirm with `ip link`.
Set to null to expose only via Tailscale.
'';
};
}
default.nix¶
modules/services/logind/default.nix
No option declarations; see source for implementation.
default.nix¶
modules/services/mandb/default.nix
No option declarations; see source for implementation.
media-bot.nix¶
modules/services/media-bot.nix
media-bot — household Telegram bot for the *arr stack on p510.
Phase 1 surface (spec: docs/plans/2026-05-30-media-bot-design.md, mirrored at ~/.claude/plans/stateless-enchanting-beaver.md during brainstorm): • menu commands (/search /add /queue /status /recent /wanted) • aiohttp webhook receiver on cfg.port ingesting Sonarr / Radarr / Overseerr / audiobook-import events; replies to Telegram with inline action buttons (Quiet event set — wins only). • Ollama-backed natural-language fallback (qwen2.5:7b by default, override via OLLAMA_MODEL in the env file).
Required secrets (both agenix-encrypted, host-keyed for p510): • media-bot-env.age — TELEGRAM_BOT_TOKEN + arr API keys + OLLAMA_ • media-bot-users.age — YAML user whitelist (telegram_id, plex_user, name)
Pattern mirrors modules/services/arr-suite-mcp.nix: DynamicUser, full systemd hardening, tailscale-only firewall by default.
- Enable option: household media Telegram bot
Options declaration (Nix)
options.features.media-bot = {
enable = lib.mkEnableOption "household media Telegram bot";
port = lib.mkOption {
type = lib.types.port;
default = 8090;
description = ''
Port the aiohttp webhook receiver listens on. Loopback is always
available; the firewall opens this port on `tailscale0` (and
optionally on a named LAN interface) so Sonarr / Radarr / Overseerr
and audiobook-import.service can POST event payloads to it.
'';
};
environmentFile = lib.mkOption {
type = lib.types.path;
default = config.age.secrets."media-bot-env".path;
defaultText = lib.literalExpression
''config.age.secrets."media-bot-env".path'';
description = ''
EnvironmentFile with TELEGRAM_BOT_TOKEN, *arr API keys, OLLAMA_*
endpoint + model (KEY=VALUE per line). The bot reads these on
startup; restart the service to pick up changes.
'';
};
usersFile = lib.mkOption {
type = lib.types.path;
default = config.age.secrets."media-bot-users".path;
defaultText = lib.literalExpression
''config.age.secrets."media-bot-users".path'';
description = ''
YAML file listing whitelisted Telegram users and their Plex
usernames. Reloadable at runtime: `systemctl reload media-bot`
sends SIGHUP, the bot rereads this file, no restart needed.
'';
};
listenLanInterface = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "eno1";
description = ''
LAN interface to additionally open `cfg.port` on. `null` exposes
the webhook receiver only via tailscale0 — recommended, since
Sonarr / Radarr / Overseerr / audiobook-import all run on the
same host as the bot and reach it via 127.0.0.1.
'';
};
}
meeting-transcribe.nix¶
modules/services/meeting-transcribe.nix
meeting-transcribe — one-button meeting recording + transcription + summary.
UX: SUPER+SHIFT+M to start, again to stop. After stop, a background job transcribes (whisperX + diarization) and summarizes (Ollama, mistral-small3.1) the audio. ~2-5 min later, notify-send fires with a markdown brief at ~/meetings/YYYY-MM-DD-HHMM.md containing TL;DR, your action items, decisions, flagged keywords, topic timeline, and the full diarized transcript.
Topology: razer records locally + offloads heavy work to p620 over Tailscale SSH. p620 records AND processes locally. Per-host wiring:
razer (client only)¶
features.meetingTranscribe = { enable = true; processHost = "p620"; installProcessor = false; userName = "Olaf"; userEmail = "olaf@freundcloud.com"; };
p620 (client + processor)¶
features.meetingTranscribe = { enable = true; processHost = "local"; installProcessor = true; huggingfaceTokenFile = config.age.secrets."api-huggingface".path; ollamaUrl = "http://localhost:11434"; userName = "Olaf"; userEmail = "olaf@freundcloud.com"; };
Setup (one-time, post-deploy on p620): 1. Sign up at https://huggingface.co/join (free). 2. Accept terms at https://huggingface.co/pyannote/speaker-diarization-3.1 (and the dependency https://huggingface.co/pyannote/segmentation-3.0). 3. Create a read token at https://huggingface.co/settings/tokens. 4. ./scripts/manage-secrets.sh create api-huggingface (paste token). 5. just quick-deploy p620 && just quick-deploy razer.
- Enable option: Meeting recording, transcription, and summarization
Options declaration (Nix)
options.features.meetingTranscribe = {
enable = lib.mkEnableOption "Meeting recording, transcription, and summarization";
processHost = lib.mkOption {
type = lib.types.str;
default = "local";
example = "p620";
description = ''
Where transcription + summarization runs. "local" means same host
(requires installProcessor = true). Otherwise an SSH-reachable host
name (typically a Tailscale node) where the processor is installed.
'';
};
installProcessor = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Install processor-side dependencies (whisperX, meet-process helper).
Set true on the host that runs the heavy lifting (typically p620).
'';
};
huggingfaceTokenFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = lib.literalExpression ''config.age.secrets."api-huggingface".path'';
description = ''
Path to a HuggingFace token file (required by whisperX for the
pyannote diarization model). Required on the processor host. Read
at runtime, never embedded in the store.
'';
};
ollamaUrl = lib.mkOption {
type = lib.types.str;
default = "http://p620:11434";
description = ''
Ollama API base URL for summarization. On p620, override to
http://localhost:11434.
'';
};
ollamaModel = lib.mkOption {
type = lib.types.str;
default = "mistral-small3.1";
description = "Ollama model name for summarization (must be pulled on the host).";
};
whisperModel = lib.mkOption {
type = lib.types.str;
default = "large-v3";
description = "whisperX model size: tiny | base | small | medium | large-v3.";
};
language = lib.mkOption {
type = lib.types.str;
default = "en";
description = "Language code for whisperX (e.g. en, no, da).";
};
outputDir = lib.mkOption {
type = lib.types.str;
default = "~/meetings";
description = "Where finished meeting briefs land (per-user; tilde expanded at runtime).";
};
userName = lib.mkOption {
type = lib.types.str;
example = "Olaf";
description = ''
Your display name. Used in the Ollama prompt to identify "your action
items" vs others'.
'';
};
userEmail = lib.mkOption {
type = lib.types.str;
example = "olaf@freundcloud.com";
description = "Your email. Helps the LLM identify you in the transcript.";
};
flagKeywords = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "blocker" "deadline" "urgent" "incident" "risk" "escalate" ];
description = "Keywords extracted with timestamps into the 'Flagged' section of the brief.";
};
}
mtr.nix¶
No option declarations; see source for implementation.
n8n.nix¶
n8n — local workflow-automation runtime.
Stands up the upstream services.n8n (which already runs hardened under
DynamicUser + ProtectSystem=strict and loads any _FILE env var via systemd
credentials) behind a feature flag. The only NixOS-managed secret is the n8n
encryption key (agenix); all workflow/service API keys (Overseerr, Tautulli,
Home Assistant, …) live as n8n *credentials, encrypted by that key inside
n8n's own store and entered at runtime in the n8n UI.
Network: binds 127.0.0.1 only (firewall untouched). On p510 every consumer (Tautulli, Overseerr, Lidarr, ollama) is co-located, so loopback suffices.
Used by the "just-finished" media-recommendation workflow. See docs/plans/2026-05-26-plex-llm-recommendations-design.md.
- Enable option: n8n workflow-automation runtime (localhost, agenix-keyed)
Options declaration (Nix)
options.features.n8n = {
enable = lib.mkEnableOption "n8n workflow-automation runtime (localhost, agenix-keyed)";
port = lib.mkOption {
type = lib.types.port;
default = 5678;
description = "Loopback HTTP port n8n listens on. Never exposed to the network (no firewall opening).";
};
publicUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "https://n8n.example.com";
description = ''
External base URL when fronting n8n with a reverse proxy (cloudflared,
Tailscale Serve, Caddy, …). When set, n8n is told its public hostname
and protocol so webhook URLs, OAuth callbacks, and Secure cookies all
reference the proxy address instead of localhost. The listen address
stays loopback — wire the proxy separately.
Leave null for loopback-only operation (the original module behavior).
'';
};
}
network-stability.nix¶
modules/services/network-stability.nix
- Enable option: Comprehensive network stability improvements
Options: enable, interval, providers, improve, switchDelayMs, startDelay, restartSec, scriptPath
Options declaration (Nix)
options.services.network-stability = {
enable = mkEnableOption "Comprehensive network stability improvements";
monitoring = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable network monitoring";
example = false;
};
interval = mkOption {
type = types.int;
default = 30;
description = "Monitoring interval in seconds";
example = 60;
};
};
secureDns = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable secure DNS configuration";
example = false;
};
providers = mkOption {
type = types.listOf types.str;
default = [
"1.1.1.1#cloudflare-dns.com"
"8.8.8.8#dns.google"
];
description = "List of DNS providers to use";
example = [ "9.9.9.9#dns.quad9.net" ];
};
};
electron = {
improve = mkOption {
type = types.bool;
default = true;
description = "Enable Electron app network stability improvements";
example = false;
};
};
connectionStability = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable network connection stability enhancements";
example = false;
};
switchDelayMs = mkOption {
type = types.int;
default = 5000;
description = "Delay in milliseconds before switching network interfaces";
example = 3000;
};
};
# Add helper service configuration options
helperService = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable the network stability helper service";
example = false;
};
startDelay = mkOption {
type = types.int;
default = 5;
description = "Delay in seconds before starting the network stability service";
example = 10;
};
restartSec = mkOption {
type = types.int;
default = 30;
description = "Time in seconds to wait before restarting the service on failure";
example = 60;
};
};
# Script path option required by network-stability-service.nix
scriptPath = mkOption {
type = types.path;
# … truncated — see source link above
default.nix¶
modules/services/nixos-update-checker/default.nix
- Enable option: NixOS update checker service
Options: enable, flakeDir, checkInterval, enableMotd, user, group
Options declaration (Nix)
options.services.nixos-update-checker = {
enable = mkEnableOption "NixOS update checker service";
flakeDir = mkOption {
type = types.path;
default = "/home/olafkfreund/.config/nixos";
description = "Path to the flake directory to check for updates";
};
checkInterval = mkOption {
type = types.str;
default = "monthly";
example = "weekly";
description = ''
How often to check for updates. Uses systemd timer format.
Common values: daily, weekly, monthly
'';
};
enableMotd = mkOption {
type = types.bool;
default = true;
description = "Enable MOTD (Message of the Day) notifications for available updates";
};
user = mkOption {
type = types.str;
default = "nixos-update-checker";
description = "User to run the update checker service as";
};
group = mkOption {
type = types.str;
default = "nixos-update-checker";
description = "Group for the update checker service";
};
}
ntfy.nix¶
ntfy-sh — self-hosted push notification server.
Wraps the upstream nixpkgs services.ntfy-sh module with a feature flag and injects the agenix-decrypted environment file for auth configuration.
Quick-start after first deploy:
ssh p510 -- sudo ntfy user add --role=admin
Then subscribe on mobile/desktop via https://ntfy.freundcloud.org.uk¶
Sending a notification (example): curl -u user:pass https://ntfy.freundcloud.org.uk/alerts -d "Hello!"
Secrets required (edit before deploy): agenix -e secrets/ntfy-env.age Content: NTFY_AUTH_DEFAULT_ACCESS=deny-all
Reference: https://ntfy.sh/docs/config/
- Enable option: ntfy-sh push notification server
Options declaration (Nix)
options.features.ntfy = {
enable = lib.mkEnableOption "ntfy-sh push notification server";
baseUrl = lib.mkOption {
type = lib.types.str;
example = "https://ntfy.freundcloud.org.uk";
description = "Public-facing base URL (required for attachments and iOS push).";
};
port = lib.mkOption {
type = lib.types.port;
default = 2586;
description = "Local port ntfy-sh listens on (loopback only).";
};
attachmentSizeLimit = lib.mkOption {
type = lib.types.str;
default = "15M";
description = "Maximum size of a single attachment.";
};
attachmentTotalLimit = lib.mkOption {
type = lib.types.str;
default = "2G";
description = "Total attachment cache size on disk.";
};
}
ollama.nix¶
Ollama coding-model server (services.ollama wrapper).
Designed for p620's RX 7900 XTX (gfx1100, 24GB VRAM, ROCm). The single GPU comfortably fits each default model individually (qwen3.6:27b ~17GB, gemma4:26b MoE ~18GB) but NOT both at once (~35GB > 24GB), so MAX_LOADED_MODELS=1 forces deterministic evict-then-load on switch.
Default model choices (May 2026): Persistent: qwen3.6:27b — strong agentic tool calling (Qwen RL-trained on 1M agentic envs), good for Claude Code's tool-use loops. On-demand: gemma4:26b — MoE with ~3.8B active params, very fast (~80-100 tok/s) for raw code-gen bursts.
Bind address is configurable via host (default 127.0.0.1). Set to
"0.0.0.0" to expose on all interfaces — note Ollama has no built-in
auth, so restrict access via firewall / tailnet ACLs when bound wider.
OLLAMA_ORIGINS="*" is set so browser UIs from any origin can call the
API; this only matters once the bind is non-loopback.
See docs/plans/2026-05-22-ollama-p620-litellm-design.md for the full design and the dual-tier model + GPU-contention rationale.
- Enable option: Ollama coding-model server (loopback only)
Options declaration (Nix)
options.features.ollama-server = {
enable = lib.mkEnableOption "Ollama coding-model server (loopback only)";
package = lib.mkOption {
type = lib.types.package;
default = pkgs.ollama-rocm;
defaultText = lib.literalExpression "pkgs.ollama-rocm";
description = ''
Ollama package. Defaults to `pkgs.ollama-rocm` for AMD GPUs (RDNA3
/ gfx1100 on p620). Switch to `pkgs.ollama-cuda` for NVIDIA hosts.
'';
};
persistentModels = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "qwen3.6:27b" ];
description = ''
Models pulled at activation and used as the default coding model.
Listed first in the load priority. Default qwen3.6:27b (~17GB,
strong agentic tool calling).
'';
};
onDemandModels = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "gemma4:26b" ];
description = ''
Alternate models pulled at activation but only loaded into VRAM on
request. Auto-evicted after `keepAlive` of idle. Default gemma4:26b
(~18GB MoE, ~3.8B active params, very fast raw code-gen).
'';
};
modelsDir = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/mnt/data/ollama/models";
description = ''
Override for where Ollama stores model blobs. Set this to a path
on a large filesystem (~100GB+ recommended) — each Q4 model is
~17-20GB and multiple are typically pulled. When null, NixOS uses
the default under /var/lib/ollama.
'';
};
keepAlive = lib.mkOption {
type = lib.types.str;
default = "5m";
description = ''
Auto-unload models after this idle time. On a workstation host,
keep this low so the GPU is released for desktop work (Blender,
games, video editing) when not actively coding. Use "-1" for
always-loaded if Ollama is the only GPU consumer.
'';
};
rocrVisibleDevices = lib.mkOption {
type = lib.types.str;
default = "0";
description = ''
Comma-separated indices of ROCm-visible devices. Defaults to the
first discrete GPU only; prevents accidental fallthrough to an
integrated GPU on hybrid-graphics systems.
'';
};
host = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
example = "0.0.0.0";
description = ''
Bind address for Ollama's HTTP API. Default 127.0.0.1 (loopback
only). Set to "0.0.0.0" to expose on all interfaces — Ollama has
no auth, so combine with firewall / tailnet ACLs when widening.
'';
};
origins = lib.mkOption {
type = lib.types.str;
default = "*";
description = ''
Value for OLLAMA_ORIGINS — comma-separated list of allowed
browser origins for CORS. Defaults to "*" so any local or remote
web UI can call the API. Tighten if you want browser-origin
restriction (network exposure is controlled by `host`, not this).
'';
};
cloudApiKeyFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
# … truncated — see source link above
openssh.nix¶
modules/services/openssh/openssh.nix
No option declarations; see source for implementation.
default.nix¶
modules/services/plex-auto-languages/default.nix
Plex-Auto-Languages (PAL) — per-show audio + subtitle track preference memorization for Plex.
Watches Plex for play events and scan events (via the Plex websocket API, no Plex Pass needed for that path — only the optional webhook integration requires Pass). When a user plays an episode, PAL records the audio/subtitle track choice; future episodes of the same series automatically get those tracks selected on play.
Phase 1: tracking mode, all libraries with shows, no filter labels.
Pattern mirrors modules/services/kometa/default.nix exactly (oci- containers + envsubst-rendered config + agenix env file). The lessons from the Kometa journey are baked in: no fictional systemd deps, real env-var substitution via envsubst-not-Kometa-magic, repo template as source of truth + rendered file under /var/lib for container mount.
- Enable option: Plex-Auto-Languages
Options declaration (Nix)
plex-mcp.nix¶
Plex MCP server — niavasha/plex-mcp-server run as an SSE daemon.
Exposes the local Plex Media Server (and optionally Sonarr/Radarr) to AI
clients over the Model Context Protocol. Clients connect to:
http://
plex-mcp-server's native --transport http uses a single shared session for the process lifetime, which wedges when a client reconnects. We instead run it in stdio mode behind mcp-proxy, which spawns a fresh stdio child per session — robust across reconnects, and consistent with features.arr-suite-mcp.
Network: binds 0.0.0.0:
Secret: the Plex auth token is loaded at runtime from agenix via LoadCredential (never in the Nix store), exported into the wrapper, and passed to the stdio child by mcp-proxy --pass-environment. PLEX_URL is non-secret and set as a plain unit Environment value.
- Enable option: Plex MCP server (HTTP transport daemon)
Options declaration (Nix)
options.features.plex-mcp = {
enable = lib.mkEnableOption "Plex MCP server (HTTP transport daemon)";
port = lib.mkOption {
type = lib.types.port;
default = 3010;
description = "Port the MCP server binds to (loopback always; tailnet + LAN via firewall).";
};
plexUrl = lib.mkOption {
type = lib.types.str;
default = "http://127.0.0.1:32400";
description = "URL of the Plex Media Server the MCP server talks to.";
};
tokenFile = lib.mkOption {
type = lib.types.path;
default = config.age.secrets."plex-token".path;
defaultText = lib.literalExpression ''config.age.secrets."plex-token".path'';
description = ''
Path to a file containing ONLY the Plex auth token. Loaded into the
unit at runtime via LoadCredential. Defaults to the agenix secret
declared by this module.
'';
};
enableMutativeOps = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Enable Plex write/mutative tools (PLEX_ENABLE_MUTATIVE_OPS). Disabled
by default for safety — read-only tools only.
'';
};
listenLanInterface = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "eno1";
description = ''
LAN interface to open the port on, in addition to tailscale0 and
loopback. Set to the host's actual LAN NIC (confirm with `ip link`).
null exposes the server only via Tailscale.
'';
};
}
default.nix¶
modules/services/power/default.nix
No option declarations; see source for implementation.
default.nix¶
modules/services/print/default.nix
Options: enable
Options declaration (Nix)
rescreenshot-mcp.nix¶
modules/services/rescreenshot-mcp.nix
- Enable option: rescreenshot-mcp MCP server for Claude Desktop
Options: enable, package, user, logLevel, autoConfigureClaudeDesktop
Options declaration (Nix)
options.services.rescreenshot-mcp = {
enable = mkEnableOption "rescreenshot-mcp MCP server for Claude Desktop";
package = mkOption {
type = types.package;
default = pkgs.callPackage ../../pkgs/rescreenshot-mcp { };
description = "The rescreenshot-mcp package to use";
};
user = mkOption {
type = types.str;
default = "olafkfreund";
description = "User to configure Claude Desktop for";
};
logLevel = mkOption {
type = types.str;
default = "info";
example = "debug";
description = "Log level for rescreenshot-mcp (trace, debug, info, warn, error)";
};
autoConfigureClaudeDesktop = mkOption {
type = types.bool;
default = true;
description = "Automatically configure Claude Desktop with rescreenshot-mcp";
};
}
mdatp.nix¶
modules/services/security/mdatp.nix
- Enable option: Microsoft Defender for Endpoint
Options: enable, package, onboardingFile, managedSettings, logLevel, autoUpdate, enableNetworkProtection
Options declaration (Nix)
options.services.mdatp = {
enable = mkEnableOption "Microsoft Defender for Endpoint";
package = mkOption {
type = types.package;
default = pkgs.mdatp;
defaultText = literalExpression "pkgs.mdatp";
description = ''
The Microsoft Defender for Endpoint package to use.
'';
};
onboardingFile = mkOption {
type = types.nullOr types.path;
default = null;
example = literalExpression "/run/agenix/mdatp-onboarding.json";
description = ''
Path to the Microsoft Defender onboarding JSON file.
This file must be obtained from the Microsoft Defender portal
(https://security.microsoft.com) under Settings > Endpoints > Onboarding.
It is recommended to store this file using agenix for security:
```nix
age.secrets."mdatp-onboarding" = {
file = ../secrets/mdatp-onboarding.json.age;
path = "/etc/opt/microsoft/mdatp/mdatp_onboard.json";
mode = "0600";
};
```
'';
};
managedSettings = mkOption {
type = types.nullOr (types.attrsOf types.anything);
default = null;
example = literalExpression ''
{
antivirusEngine = {
enforcementLevel = "real_time";
scanAfterDefinitionUpdate = true;
scanArchives = true;
maximumOnDemandScanThreads = 2;
};
cloudService = {
enabled = true;
diagnosticLevel = "optional";
automaticDefinitionUpdateEnabled = true;
};
}
'';
description = ''
Managed configuration settings for Microsoft Defender.
These settings will be written to `/etc/opt/microsoft/mdatp/managed/mdatp_managed.json`.
See the official documentation for available options:
https://learn.microsoft.com/en-us/defender-endpoint/linux-preferences
'';
};
logLevel = mkOption {
type = types.enum [ "error" "warning" "info" "verbose" "debug" ];
default = "info";
description = ''
Logging level for Microsoft Defender service.
Available levels:
- error: Only critical errors
- warning: Errors and warnings
- info: Normal operation information (default)
- verbose: Detailed operation logs
- debug: Full debugging information
'';
};
autoUpdate = mkOption {
type = types.bool;
default = true;
description = ''
Enable automatic definition updates.
When enabled, Microsoft Defender will automatically download
and install the latest threat definitions.
'';
};
enableNetworkProtection = mkOption {
type = types.bool;
default = true;
# … truncated — see source link above
sound.nix¶
modules/services/sound/sound.nix
Audio System Configuration Module Configures PipeWire audio system with Bluetooth support
- Enable option: PipeWire audio system
Options: enable, enableJack, enable32BitSupport, enableAdvancedCodecs, enableHardwareVolume
Options declaration (Nix)
options.modules.services.sound = {
enable = mkEnableOption "PipeWire audio system";
pipewire = {
enableJack = mkOption {
type = types.bool;
default = false;
description = ''Enable JACK support in PipeWire'';
example = true;
};
enable32BitSupport = mkOption {
type = types.bool;
default = true;
description = ''Enable 32-bit ALSA support'';
example = false;
};
};
bluetooth = {
enableAdvancedCodecs = mkOption {
type = types.bool;
default = true;
description = ''Enable advanced Bluetooth audio codecs (SBC-XQ, mSBC)'';
example = false;
};
enableHardwareVolume = mkOption {
type = types.bool;
default = true;
description = ''Enable hardware volume control for Bluetooth devices'';
example = false;
};
};
}
syncthing.nix¶
modules/services/syncthing.nix
- Enable option: Syncthing file synchronization
Options: enable, user, syncHosts, deviceIds, masterHost, syncClaude, syncGemini, guiAddress, openFirewall
Options declaration (Nix)
options.features.syncthing = {
enable = mkEnableOption "Syncthing file synchronization";
user = mkOption {
type = types.str;
default = "olafkfreund";
description = "User to run Syncthing as";
};
syncHosts = mkOption {
type = types.listOf types.str;
default = [ "p620" "razer" "p510" ];
description = "List of hosts to sync with";
};
deviceIds = mkOption {
type = types.attrsOf types.str;
default = { };
description = ''
Device IDs for each host. Get these by running `syncthing --device-id`
on each host after initial Syncthing setup.
'';
example = {
p620 = "ABCDEFG-HIJKLMN-OPQRSTU-VWXYZ12-3456789-ABCDEFG-HIJKLMN-OPQRSTU";
};
};
masterHost = mkOption {
type = types.str;
default = "p620";
description = "Primary host for conflict resolution (introducer)";
};
syncClaude = mkOption {
type = types.bool;
default = true;
description = "Sync ~/.claude directory";
};
syncGemini = mkOption {
type = types.bool;
default = true;
description = "Sync ~/.gemini directory";
};
guiAddress = mkOption {
type = types.str;
default = "127.0.0.1:8384";
description = "Address for Syncthing Web UI";
};
openFirewall = mkOption {
type = types.bool;
default = true;
description = "Open firewall ports for Syncthing";
};
}
default.nix¶
modules/services/sysprof/default.nix
No option declarations; see source for implementation.
default.nix¶
modules/services/system/default.nix
No option declarations; see source for implementation.
default.nix¶
modules/services/systemd/default.nix
No option declarations; see source for implementation.
default.nix¶
modules/services/tabby/default.nix
No option declarations; see source for implementation.
whatsapp-bridge.nix¶
modules/services/whatsapp-bridge.nix
WhatsApp Bridge Systemd Service Persistent background service for WhatsApp Web API connection Follows docs/NIXOS-ANTI-PATTERNS.md security patterns
No option declarations; see source for implementation.
whisper-server.nix¶
modules/services/whisper-server.nix
whisper-server — local Whisper transcription HTTP API
Wraps whisper-server from pkgs.whisper-cpp. Used by voice-input clients
on razer + p620 to convert speech-to-text via a hold-to-talk hotkey.
Default deployment: p620 hosts the server (it has the CPU/GPU budget), razer reaches it over the tailnet. p510 is irrelevant — headless.
Service surface: POST http://p620:9300/inference multipart file=@audio.wav → transcript
Designed to be cheap to run idle: the binary memory-maps the model and only burns CPU when a request comes in.
- Enable option: Whisper.cpp HTTP transcription server
Options declaration (Nix)
options.features.whisper-server = {
enable = lib.mkEnableOption "Whisper.cpp HTTP transcription server";
port = lib.mkOption {
type = lib.types.port;
default = 9300;
description = "TCP port for the HTTP inference endpoint.";
};
model = lib.mkOption {
type = lib.types.str;
default = "base.en";
example = "small.en";
description = ''
ggml model name (passed to whisper-cpp-download-ggml-model). Sizes:
tiny.en ≈ 40 MB, fastest, lowest accuracy
base.en ≈ 150 MB, good balance for short dictation
small.en ≈ 500 MB, more accurate, slower
medium.en ≈ 1.5 GB
'';
};
openFirewallOnTailscale = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Open the service port on the tailscale0 interface only.";
};
}
xdg-portal.nix¶
modules/services/xserver/xdg-portal.nix
XDG Desktop Portal Configuration Module Configures desktop integration for Wayland and X11 applications
- Enable option: XDG desktop portal services
Options: enable, backend, enableScreencast, suppressIconWarning, forcePortalOpen
Options declaration (Nix)
options.modules.services.xdg-portal = {
enable = mkEnableOption "XDG desktop portal services";
backend = mkOption {
type = types.enum [ "sway" "gnome" "cosmic" ];
default = "gnome";
description = ''Primary desktop environment backend for portals'';
example = "sway";
};
enableScreencast = mkOption {
type = types.bool;
default = true;
description = ''Enable screencasting support through portals'';
example = false;
};
suppressIconWarning = mkOption {
type = types.bool;
default = true;
description = ''Suppress XDG icon protocol warnings'';
example = false;
};
forcePortalOpen = mkOption {
type = types.bool;
default = true;
description = ''Force applications to use portal for file operations'';
example = false;
};
}
xdg.nix¶
modules/services/xserver/xdg.nix
No option declarations; see source for implementation.