Add native NixOS deployment and UI-managed provider credentials

This commit is contained in:
Lars Nolden
2026-09-10 14:25:37 +02:00
parent 9843fe0c50
commit 964b9dfc15
21 changed files with 2084 additions and 104 deletions
+122 -10
View File
@@ -36,11 +36,99 @@ The container is unprivileged with a read-only root and a persistent named
volume at /data. A host bind mount must be writable by UID/GID 10001.
Never change the port mapping to public 0.0.0.0 without VPN/firewall isolation.
Native NixOS: the flake exports nixosModules.default. Import it into your host,
then set services.finance-duck.enable=true and publicURL to the exact browser
origin. Deploy with your normal nixos-rebuild switch. The module runs ONE
systemd service; React is embedded in the Go binary, not a separate service.
Defaults: listen 127.0.0.1:8080, service user finance-duck, persistent data at
/var/lib/finance-duck (0700). It does not configure networking or Tailscale.
Optional services.finance-duck.environmentFile names an existing runtime file
read by systemd for optional provider startup fallbacks. Both Enable Banking
and OpenRouter can be configured directly in Settings without that file.
Keep private keys outside the Nix store and readable only by the service.
See README's native NixOS section for optional environment-file permissions.
Deployed host: bender@100.87.224.3
--------------------------------
URL: https://nixos.taile9e6d9.ts.net:8444 (Tailscale only, no application login).
The backend binds 127.0.0.1:8080. Existing Funnel on 443 and OpenClaw on 8443
are separate. Never enable Funnel for Finance Duck.
Update from your workstation:
ssh -t bender@100.87.224.3 'sudo finance-duck-update'
Or on the host:
sudo finance-duck-update
sudo finance-duck-update --no-pull # deploy existing checkout edits
The updater serializes updates, pulls ~/projects/finance-duck as bender with
git pull --ff-only, creates a Git-filtered source snapshot, updates ONLY the
finance-duck input in /etc/nixos/flake.lock, runs nixos-rebuild switch, then
checks /api/health with the correct Host. The app has its own pinned Nixpkgs;
the host's Nixpkgs and NixVirt pins stay unchanged. A build failure leaves the
running service untouched; an activation/health failure exits nonzero.
Application and deployment source are tracked in the project repository.
Commit and push changes before running the normal updater. New local source
files must be tracked with git add before a --no-pull deployment. Pull refuses
to overwrite conflicting local edits; resolve them before retrying. No
automatic stashing, resetting, committing or pushing occurs. Keep credentials
and live finance data out of Git.
Host files:
/etc/nixos/flake.nix application input and module import
/etc/nixos/modules/finance-duck.nix service, tailnet proxy, update command
/var/lib/finance-duck-source replaceable Git-filtered source snapshot
/var/lib/finance-duck persistent data, finance-duck:finance-duck 0700
The source snapshot excludes .git, ignored files and untracked files. It is
not a data backup. The initial deployment starts empty without provider
credentials; it does not copy the workstation's finance/ directory.
Inspect on the host:
systemctl status finance-duck finance-duck-tailscale-serve
sudo journalctl -u finance-duck -f
tailscale serve status
curl -f https://nixos.taile9e6d9.ts.net:8444/api/health
Before significant upgrades, stop the service and back up the entire data
directory to a protected location (including hidden recovery files). Restart
after the backup, and protect any separate credential files as well:
sudo systemctl stop finance-duck
sudo install -d -m 0700 /var/backups/finance-duck
sudo sh -c 'umask 077; tar -C /var/lib -czf /var/backups/finance-duck/data-$(date +%Y%m%dT%H%M%S).tar.gz finance-duck'
sudo systemctl start finance-duck
Copy backups to secure off-host storage; a backup on this disk cannot protect
against disk loss.
Binary/system rollback:
sudo nixos-rebuild switch --rollback
This switches the previous NixOS generation, NOT financial data or config
source. The generation before the first deployment has no Finance Duck service.
For a lasting rollback, restore the intended source/config/lock before the
next update. Do not remove or roll back live data blindly.
OpenRouter
----------
Set OPENROUTER_API_KEY in the process environment, then choose a model in
Settings. The model and chosen endpoint must support strict structured outputs
and the configured privacy routing. Every classification request sets:
Open Settings -> OpenRouter credentials, paste the API key, and Save key.
Choose the exact provider/model identifier in Classification preferences and
save preferences. Replace key rotates it; Remove key disables AI. No SSH,
Nix rebuild or restart is needed. Changes affect future classification
requests; in-flight requests retain their original key.
The key is stored as private 0600 plaintext in state/openrouter.json under
the data directory, never in config.toml or browser storage. API responses
never return the saved key. Backups of the data directory contain this secret.
Only allow trusted users to reach the application; there is no separate
administrator login for changing credentials.
OPENROUTER_API_KEY remains a startup fallback only when the saved credential
file is absent. A saved key overrides it. Removing the key in Settings saves
an explicit disable, which also overrides the environment after restart.
Malformed saved credentials fail startup rather than reverting to another key.
Configured means present, not verified with OpenRouter. A successful
AI classification -> Analyse request verifies access with the selected model.
The model and endpoint must support strict structured outputs and the
configured privacy routing. Every classification request sets:
provider.data_collection = deny
provider.zdr = true
provider.require_parameters = true
@@ -71,17 +159,39 @@ Configure the allowed redirect URL to the exact externally reachable URL:
https://finance.example.internal/api/banking/callback
The browser must be able to reach this callback through your VPN.
Environment variables (all three required when enabling banking):
Open Settings -> Enable Banking credentials. Enter the application ID, upload
its matching private-key PEM file, and Save configuration. Settings supplies
the exact browser-origin callback URL to register. Keys must be a single
unencrypted RSA PKCS#1 or PKCS#8 PEM, >=2048 bits and <=32 KiB.
No SSH, environment file, Nix rebuild or restart is needed.
Save validates local credentials without contacting Enable Banking. It does
not register/activate the app or authorize an account. Configured means local
setup is present; complete registration above and authorize under Accounts.
Leave the key upload empty when editing the same application to keep its key.
Same-app key/callback rotation preserves sessions and cursors. Changing the
application ID requires a new key upload and reconnecting your banks. Remove
configuration confirms before disabling banking and clearing local connection
state. Canonical accounts/history remain; upstream bank consent is not revoked.
Each successful change invalidates pending authorization flows.
Credentials persist privately (0600) in state/enablebanking.json beneath the
data directory. The API never returns the private key; browser storage never
contains it. Session state is bound to the application configuration generation,
so stale sessions cannot be revived by a restart after app changes/removal.
Back up the entire data directory securely, not isolated credential/state files.
Optional environment startup fallback, used ONLY when no saved config exists:
ENABLEBANKING_APP_ID=<registered application ID>
ENABLEBANKING_KEY_FILE=/run/secrets/enablebanking.key
ENABLEBANKING_REDIRECT_URL=https://finance.example.internal/api/banking/callback
Use a PEM RSA private key (PKCS#1 or PKCS#8, at least 2048 bits). Mount it
read-only with permissions allowing the service user to read it. Never commit
it to Git. The Compose file contains a commented example key mount; set the
container-side KEY_FILE path when enabling that mount.
The referenced key must be readable by the service. Environment changes need
a restart. UI configuration or explicit removal overrides all three variables;
invalid saved configuration fails startup rather than using another key.
Accounts shows a copyable callback URL. Register it exactly with Enable Banking
and set ENABLEBANKING_REDIRECT_URL to the same value. The redirect carries a
Accounts shows the configured callback URL. Register it exactly with Enable
Banking. Settings saves the current browser callback URL. The redirect carries a
one-time code and state, not a reusable API key. Go verifies state, exchanges the
code for a session_id, and persists session details locally with mode 0600.
@@ -143,6 +253,8 @@ finance/
merchants.finance
journal/YYYY/YYYY-MM.finance
state/sync-state.json sensitive local consent/session metadata
state/openrouter.json sensitive UI-managed OpenRouter key or explicit disable
state/enablebanking.json sensitive UI-managed banking key/configuration or disable
cache/finance.duckdb disposable analytical projection
The custom grammar is deliberately small: