Comments had drifted into multi-paragraph narrative (git commit lineage, debugging stories, restated code) in several hot spots (scripts/deploy, hermes-agent.nix, flake.nix, gitea.nix, headscale.nix). Trim every comment to its load-bearing "why" — gotchas, safety warnings, and non-obvious rationale survive verbatim in substance, just tightened to 1-2 sentences; historical narrative and anything already covered in CLAUDE.md is cut. No code/logic changed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UJqEmY1y3AYX3JoX4Y6b21
113 lines
5.0 KiB
Nix
113 lines
5.0 KiB
Nix
{ config, ... }:
|
|
|
|
# Plain CouchDB 3 node, tuned as the backend for Obsidian Self-hosted LiveSync
|
|
# (vrtmrz/obsidian-livesync), which replicates the vault into it via PouchDB.
|
|
#
|
|
# Published PUBLICLY as https://notes.mgaction.town via neptun's caddy, since
|
|
# Obsidian's mobile apps refuse cleartext HTTP and jupiter's *.jupiter.sol
|
|
# names can't get a real cert — so the settings below are security-relevant:
|
|
# - `require_valid_user` in both [chttpd] and [chttpd_auth], else CouchDB
|
|
# answers unauthenticated GETs on the open internet.
|
|
# - neptun's vhost allowlists only the plugin's endpoints; Fauxton and
|
|
# cluster/config are reachable only over the tailnet.
|
|
# - Turn on the plugin's end-to-end encryption (+ "Obfuscate Properties"),
|
|
# so this server only ever holds ciphertext — what makes a
|
|
# publicly-reachable credentialed database an acceptable trade.
|
|
#
|
|
# Its passphrase must stay a SEPARATE secret from couchdb_admin_password:
|
|
# the CouchDB password is stored here and in secrets/jupiter.yaml, while
|
|
# the E2EE passphrase never leaves the clients (kept in the HomeLab Proton
|
|
# Pass vault, not sops) — reusing one string for both would hand the
|
|
# decryption key to whoever gets the CouchDB credential. Losing the
|
|
# passphrase costs the remote database, not the notes: wipe and
|
|
# re-initialize from a device that still holds the plaintext vault.
|
|
{
|
|
services.couchdb = {
|
|
enable = true;
|
|
|
|
# Listens on all interfaces, but :5984 is not opened in the firewall, so
|
|
# it's reachable only over tailscale0 (trusted) and localhost — the path
|
|
# neptun's caddy takes.
|
|
bindAddress = "0.0.0.0";
|
|
port = 5984;
|
|
|
|
# The vault database is the ONLY copy of the notes once LiveSync is the
|
|
# source of truth, so it belongs on the array, not the 29G eMMC — all
|
|
# three default under /var/lib/couchdb and must move together.
|
|
databaseDir = "/mnt/data/AppData/couchdb";
|
|
viewIndexDir = "/mnt/data/AppData/couchdb";
|
|
configFile = "/mnt/data/AppData/couchdb/local.ini";
|
|
|
|
# [admins] ini fragment from sops; services.couchdb.adminPass would render
|
|
# into the world-readable store instead.
|
|
#
|
|
# ⚠️ CouchDB hashes the password at startup and persists it to local.ini
|
|
# (above), which then takes precedence — so changing the sops value alone
|
|
# does NOT rotate it. Also delete the `[admins]` line from
|
|
# /mnt/data/AppData/couchdb/local.ini and restart.
|
|
extraConfigFiles = [ config.sops.templates."couchdb-admins.ini".path ];
|
|
|
|
# Values taken from LiveSync's own CouchDB setup documentation; the plugin
|
|
# refuses to replicate (or silently truncates) without them.
|
|
extraConfig = {
|
|
couchdb = {
|
|
# Creates _users/_replicator on first boot instead of leaving the node
|
|
# in the un-set-up state where every request 500s.
|
|
single_node = "true";
|
|
# LiveSync splits notes into chunks, but a big pasted image still
|
|
# arrives as one document. 8MB (the default) is too small.
|
|
max_document_size = "50000000";
|
|
};
|
|
|
|
chttpd = {
|
|
require_valid_user = "true";
|
|
max_http_request_size = "4294967296";
|
|
enable_cors = "true";
|
|
};
|
|
|
|
chttpd_auth = {
|
|
require_valid_user = "true";
|
|
authentication_redirect = "/_utils/session.html";
|
|
};
|
|
|
|
httpd = {
|
|
# Makes CouchDB answer 401 with a WWW-Authenticate challenge rather
|
|
# than a bare 401 body — the plugin's basic-auth flow depends on it.
|
|
"WWW-Authenticate" = ''Basic realm="couchdb"'';
|
|
enable_cors = "true";
|
|
};
|
|
|
|
# Obsidian is an Electron/Capacitor app, so its requests carry these
|
|
# non-http origins. Without them desktop and mobile both fail CORS
|
|
# preflight and the plugin reports a bare "cannot connect".
|
|
cors = {
|
|
credentials = "true";
|
|
origins = "app://obsidian.md,capacitor://localhost,http://localhost";
|
|
headers = "accept, authorization, content-type, origin, referer";
|
|
methods = "GET, PUT, POST, HEAD, DELETE";
|
|
max_age = "3600";
|
|
};
|
|
|
|
# The module points [log] file at /var/log/couchdb.log, which nothing
|
|
# rotates — on a 29G eMMC an info-level log of every replication request
|
|
# is a slow disk-fill. stderr hands it to journald's capped storage
|
|
# instead (the file setting is then ignored).
|
|
log = {
|
|
writer = "stderr";
|
|
level = "warning";
|
|
};
|
|
};
|
|
};
|
|
|
|
# /mnt/data/AppData is drwx--x--- darman:users, so the couchdb user needs
|
|
# group "users" just to traverse into its own database dir. The dir itself
|
|
# is created couchdb:couchdb by the module's tmpfiles rule.
|
|
users.users.couchdb.extraGroups = [ "users" ];
|
|
|
|
# databaseDir is outside /var/lib, so systemd derives no mount dependency
|
|
# from it. Without this CouchDB starts with the array missing, creates an
|
|
# empty database on the eMMC, and LiveSync sees a remote vault that lost
|
|
# every note — which it would then happily replicate back to the clients.
|
|
systemd.services.couchdb.unitConfig.RequiresMountsFor = [ "/mnt/data" ];
|
|
}
|