bitspire/flake.nix
Padreug 055afba894 chore(nix): thread cfg.relayUrl into fresh-boot .env stub (#57 follow-up)
The fresh-boot `/var/lib/bitspire/.env` template at flake.nix's
`bitspire-env` activation script seeded `VITE_RELAY_URL=` empty, which
forced every operator to run `provision-atm.sh` (or hand-edit .env)
before the renderer could resolve a relay. Meanwhile the NixOS option
`services.bitspire.relayUrl` was wired only to the dead-code
`/etc/bitspire/config.env` (mkForce-shadowed by `/var/lib/bitspire/.env`).

Thread the NixOS option through: seed `VITE_RELAY_URL=${cfg.relayUrl}`
on first boot. The .env override path remains intact — provision-atm.sh
or a hand edit still take precedence at runtime (the file is the
EnvironmentFile, not the activation-time template). Existing ATMs
already have a populated `.env` and aren't affected (the activation
script's `if [ ! -f ... ]` guard skips the rewrite).

Renderer resolution order:
  /var/lib/bitspire/.env → NixOS module default → renderer fallback
  (`ws://localhost:7777` in lightning.ts:63 / main.ts:284)

Also expand the `services.bitspire.relayUrl` option description so
future readers see the wire-through + the override path documented
where the option lives.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 20:42:05 +02:00

348 lines
15 KiB
Nix

{
description = "Lamassu Next - Nostr-Native Lightning ATM";
inputs = {
# Stable NixOS for the ATM OS base
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
# Unstable for Electron, Node.js, pnpm (latest versions)
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs-unstable";
};
devenv = {
url = "github:cachix/devenv";
inputs.nixpkgs.follows = "nixpkgs-unstable";
};
# Determinate Nix — ensures douro uses same nix version as dev machines
# so cachix binary cache hits match (nix 2.33 hashes == nix 2.33 hashes).
# The bare `?3` semver pin (≥3.0.0) was resolving to 3.16.3, which
# ships a regressed nix-functional-tests:local-overlay-store /
# stale-file-handle that FAILs when the determinate-nix derivation
# has to be built from source (no binary cache hit) — this blocked
# disk-image-sintra builds locally. `?3.15` (semver ≥3.15.0) skips
# past the regression; flake.lock currently resolves it to 3.20.0,
# which builds and ships nix 2.34.6.
determinate.url = "https://flakehub.com/f/DeterminateSystems/determinate/3.15";
# ATM TUI — operator management tool
atm-tui = {
url = "git+ssh://forgejo@git.atitlan.io/aiolabs/atm-tui.git";
inputs.nixpkgs.follows = "nixpkgs-unstable";
};
};
outputs = { self, nixpkgs, nixpkgs-unstable, flake-utils, rust-overlay, devenv, determinate, atm-tui }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
};
pkgs-unstable = import nixpkgs-unstable {
inherit system;
config.allowUnfree = true;
overlays = [ (import rust-overlay) ];
};
# Pure ATM app builder (no --impure needed)
mkAtmApp = import ./nix/mkAtmApp.nix {
inherit pkgs pkgs-unstable;
src = self;
};
# Fiat code per machine model
fiatCodeForModel = {
douro = "GTQ";
tejo = "GTQ";
sintra = "EUR";
batm3 = "USD";
};
lib = nixpkgs.lib;
# Helper to create a live USB NixOS config for a specific machine model
mkLiveConfig = machineModel:
let
atm-app = mkAtmApp {
model = machineModel;
fiatCode = fiatCodeForModel.${machineModel} or "USD";
};
in
nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = {
inherit pkgs-unstable nixpkgs machineModel atm-app;
};
modules = [
./deploy/nixos/live.nix
determinate.nixosModules.default
{
environment.systemPackages = [
atm-tui.packages.${system}.default
(pkgs.writeShellScriptBin "fund-atm" ''
exec ${pkgs-unstable.nodejs}/bin/node ${atm-app}/dist-electron/fund-atm.bundle.cjs "$@"
'')
];
environment.variables.ATM_DB_PATH = "/var/lib/bitspire/state.db";
}
];
};
# Helper to create a disk-installed NixOS config for a specific machine model.
# Unlike live configs (squashfs + tmpfs), installed configs use ext4 root
# and support `nixos-rebuild switch` for in-place updates.
mkInstalledConfig = machineModel: hardwareModule:
let
atm-app = mkAtmApp {
model = machineModel;
fiatCode = fiatCodeForModel.${machineModel} or "USD";
};
fiatCode = fiatCodeForModel.${machineModel} or "USD";
in
nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = {
inherit pkgs-unstable atm-app;
};
modules = [
hardwareModule
./deploy/nixos/configuration.nix
./deploy/nixos/bitspire-atm.nix
determinate.nixosModules.default
({ config, lib, pkgs, ... }: {
services.bitspire = {
enable = true;
appDir = "${atm-app}";
};
# Operator TUI and CLI tools
environment.systemPackages = [
atm-tui.packages.${system}.default
(pkgs.writeShellScriptBin "fund-atm" ''
exec ${pkgs-unstable.nodejs}/bin/node ${atm-app}/dist-electron/fund-atm.bundle.cjs "$@"
'')
];
environment.variables.ATM_DB_PATH = "/var/lib/bitspire/state.db";
# Electron sandbox needs unprivileged user namespaces
boot.kernel.sysctl."kernel.unprivileged_userns_clone" = 1;
# Passwordless sudo for remote nixos-rebuild switch
security.sudo.wheelNeedsPassword = false;
# Allow bitspire user to use nix commands + pull from aiolabs binary cache.
# max-jobs = 1: prefer substitution from the cache, but allow ONE
# local build slot for tiny activation-time stitch derivations
# (boot.json, system-units, X-Restart-Triggers, etc.) that are
# inherently per-machine and can never be pre-cached. Heavy
# nixpkgs compiles (rustc, kernel, electron) are still
# effectively cache-only — they're upstream-cached, so a cache
# miss on them stays vanishingly rare in practice.
# max-jobs = 0 was tried first and silently bricked nightly
# auto-upgrades for 6+ days on an uncacheable trivial derivation
# (systemd-boot's boot.json).
nix.settings = {
max-jobs = 1;
# Hard ceiling on any local build's wall-clock time. Activation-
# time stitch derivations (boot.json, system-units, etc.) finish
# in well under a second; anything that doesn't return in 60s
# is by definition a heavy compile that has no business running
# on ATM hardware (kernel, electron, rustc). Kill it fast so
# the upgrade fails loudly instead of silently wedging the box
# for an hour. Time-bounds the max-jobs=1 escape hatch.
timeout = 60;
trusted-users = [ "root" "bitspire" ];
substituters = [ "https://cache.nixos.org" "https://aiolabs.cachix.org" ];
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"aiolabs.cachix.org-1:PrAjsGU9PE77tFKP2+iO+mgR88c4xv3utM9JmpTblUQ="
];
};
# Auto-upgrade: pulls latest flake and runs nixos-rebuild switch.
# NOTE: this branch (dev) pins the upgrade source to ?ref=dev so
# any ATM flashed from `dev` stays on `dev`. Without the explicit
# ref, nix would resolve to the repo's default branch (main),
# which would silently regress a dev-deployed Sintra back to the
# lamassu-next production code at 04:00. The `main` branch's
# flake.nix continues to omit ?ref= so production ATMs (batm3,
# douro) keep pulling main HEAD as before.
# To update manually: sudo nixos-rebuild switch --flake git+ssh://forgejo@git.atitlan.io/aiolabs/lamassu-next.git?ref=dev#<model>-installed
system.autoUpgrade = {
enable = true;
flake = "git+ssh://forgejo@git.atitlan.io/aiolabs/lamassu-next.git?ref=dev#${machineModel}-installed";
flags = [ "--refresh" ];
dates = "04:00"; # daily at 4am
allowReboot = false;
};
# Env template — runtime secrets provisioned via provision-atm.sh.
# Identity fields are intentionally empty so a fresh disk image
# boots cleanly into the "needs provisioning" state; provision-
# atm.sh SSHes in and overwrites with real values.
#
# VITE_RELAY_URL seeds from `config.services.bitspire.relayUrl`
# so the NixOS module's `relayUrl` option becomes the default
# without losing the operator's ability to override via .env
# (edit the file or re-run provision-atm.sh).
system.activationScripts.bitspire-env = ''
mkdir -p /var/lib/bitspire
if [ ! -f /var/lib/bitspire/.env ]; then
cp ${pkgs.writeText "bitspire-env-default" ''
VITE_RELAY_URL=${config.services.bitspire.relayUrl}
VITE_LNBITS_SERVER_PUBKEY=
VITE_ATM_PRIVATE_KEY=
VITE_APP_ID=
VITE_OPERATOR_PUBKEYS=
VITE_LAMASSU_MACHINE_MODEL=${machineModel}
VITE_LAMASSU_FIAT_CODE=${fiatCode}
ELECTRON_FORCE_PROD=1
DISPLAY=:0
''} /var/lib/bitspire/.env
chmod 600 /var/lib/bitspire/.env
chown bitspire:bitspire /var/lib/bitspire/.env
fi
'';
# Override systemd service for Electron runtime
systemd.services.bitspire = {
serviceConfig = {
EnvironmentFile = lib.mkForce "/var/lib/bitspire/.env";
Environment = "LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib";
ExecStart = lib.mkForce "${pkgs-unstable.electron}/bin/electron --no-sandbox --disable-gpu-sandbox --disable-gpu --disable-software-rasterizer --enable-logging ${atm-app}";
MemoryMax = lib.mkForce "1G";
NoNewPrivileges = lib.mkForce false;
ProtectSystem = lib.mkForce false;
ProtectHome = lib.mkForce false;
PrivateTmp = lib.mkForce false;
DevicePolicy = lib.mkForce "auto";
DeviceAllow = lib.mkForce [ "char-* rw" ];
};
};
# Reset eDP display output after X starts
systemd.services.display-reset = {
description = "Reset eDP display output";
after = [ "display-manager.service" ];
requires = [ "display-manager.service" ];
wantedBy = [ "graphical.target" ];
before = [ "bitspire.service" ];
serviceConfig = {
Type = "oneshot";
User = "bitspire";
Environment = "DISPLAY=:0";
ExecStart = "${pkgs.bash}/bin/bash -c '${pkgs.xorg.xrandr}/bin/xrandr --output eDP-1 --off; sleep 1; ${pkgs.xorg.xrandr}/bin/xrandr --output eDP-1 --auto'";
};
};
# Swap file — ATMs have ~2GB RAM; prevents hard-freeze under memory pressure
swapDevices = [{ device = "/var/swapfile"; size = 1024; }];
# Clean /tmp on boot to prevent stale build artifacts filling disk
boot.tmp.cleanOnBoot = true;
# SSH with password for initial provisioning
services.openssh.settings.PasswordAuthentication = lib.mkForce true;
})
];
};
in
{
# ── NixOS Configurations (top-level, not per-system) ──────────
nixosConfigurations = {
# Ergonomic names: `nixos-rebuild switch --flake .#douro`
douro = mkLiveConfig "douro";
tejo = mkLiveConfig "tejo";
sintra = mkLiveConfig "sintra";
batm3 = mkLiveConfig "batm3";
# Backwards-compat aliases. Renamed lamassu-live-* → bitSpire-live-*
# for the brand transition; both styles available until callers
# (CI / scripts / docs) catch up. Drop the lamassu-* names once
# nothing references them.
bitSpire-live-douro = mkLiveConfig "douro";
bitSpire-live-tejo = mkLiveConfig "tejo";
bitSpire-live-sintra = mkLiveConfig "sintra";
bitSpire-live = mkLiveConfig "douro";
lamassu-live-douro = mkLiveConfig "douro";
lamassu-live-tejo = mkLiveConfig "tejo";
lamassu-live-sintra = mkLiveConfig "sintra";
lamassu-live = mkLiveConfig "douro";
# Installed-to-disk configs (proper GPT + systemd-boot, supports nixos-rebuild)
douro-installed = mkInstalledConfig "douro" ./deploy/nixos/hardware/douro.nix;
tejo-installed = mkInstalledConfig "tejo" ./deploy/nixos/hardware/upboard.nix;
# Sintra shares Aaeon UP Board hardware with tejo (same validator
# at ttyJ5, dispenser at ttyJ7 layout) — reuse the same hw module.
sintra-installed = mkInstalledConfig "sintra" ./deploy/nixos/hardware/upboard.nix;
batm3-installed = mkInstalledConfig "batm3" ./deploy/nixos/hardware/batm3.nix;
};
# ── Standalone NixOS module ───────────────────────────────────
nixosModules.default = import ./deploy/nixos/bitspire-atm.nix;
nixosModules.bitspire = import ./deploy/nixos/bitspire-atm.nix;
# ── Packages (x86_64-linux only for ATM hardware) ─────────────
packages.${system} = {
# Pure ATM app derivations
atm-app-douro = mkAtmApp { model = "douro"; fiatCode = "GTQ"; };
atm-app-tejo = mkAtmApp { model = "tejo"; fiatCode = "GTQ"; };
atm-app-sintra = mkAtmApp { model = "sintra"; fiatCode = "EUR"; };
atm-app-batm3 = mkAtmApp { model = "batm3"; fiatCode = "USD"; };
# ISO images
iso-douro = self.nixosConfigurations.douro.config.system.build.isoImage;
iso-tejo = self.nixosConfigurations.tejo.config.system.build.isoImage;
iso-sintra = self.nixosConfigurations.sintra.config.system.build.isoImage;
iso-batm3 = self.nixosConfigurations.batm3.config.system.build.isoImage;
# Raw disk images (dd-able to mSATA/eMMC, proper GPT + ESP)
disk-image-douro = import (nixpkgs + "/nixos/lib/make-disk-image.nix") {
inherit pkgs lib;
config = self.nixosConfigurations.douro-installed.config;
format = "raw";
partitionTableType = "efi";
diskSize = "auto";
};
disk-image-sintra = import (nixpkgs + "/nixos/lib/make-disk-image.nix") {
inherit pkgs lib;
config = self.nixosConfigurations.sintra-installed.config;
format = "raw";
partitionTableType = "efi";
diskSize = "auto";
};
# Backwards compat
iso = self.nixosConfigurations.douro.config.system.build.isoImage;
};
}
//
# ── Dev shells (per-system via flake-utils) ───────────────────
flake-utils.lib.eachDefaultSystem (sys:
let
dev-pkgs = import nixpkgs-unstable {
system = sys;
overlays = [ (import rust-overlay) ];
};
in
{
devShells.default = devenv.lib.mkShell {
pkgs = dev-pkgs;
modules = [ ./devenv.nix ];
};
}
);
}