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>
348 lines
15 KiB
Nix
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 ];
|
|
};
|
|
}
|
|
);
|
|
}
|