bitspire/docs/machine-installation.md
Padreug 723553522c docs: purge stale lamassu naming; fix the hal-check skill's provenance boundary
- @lamassu/clink import examples → @bitSpire/clink, the package's real name.
- machine-installation.md: the service user is `bitspire`, not `lamassu`
  (renamed in configuration.nix long ago; the doc never followed).
- README: clone aiolabs/bitspire, not lamassu-next; the fleet sentence
  claiming batm3/douro run `main` against Lightning.Pub was stale.
- nostr-check skill: table headers say bitSpire.
- hal-check skill: the boundary is c0b69d1, not v8.1.5 (CLAUDE.md corrected
  this 2026-07-04; the skill kept asserting the wrong tag), and the
  "forbidden operations" now reflect the recorded permission —
  reference over port, name the source commit — plus a rule born of the
  GTQ window: no value table without a test over it.

Deliberately kept: every `aiolabs/lamassu-next#NN` issue citation, the
provenance sections, "Ported from lamassu-machine" driver headers, and
the hardware names "Lamassu Sintra/Tejo/Douro" — those are the machines.
2026-10-09 21:58:55 +02:00

8.1 KiB

Deploying bitSpire to a Sintra (or other ATM)

This document gives the high-level shape of an ATM deployment. For the step-by-step walkthrough — every command from nix build to a kiosk on a real Sintra — see deploy/nixos/README.md. This file is the orientation doc that explains why the pipeline looks the way it does.

The pre-NixOS workflow described in earlier versions of this file (manual AppImage scp, hand-written systemd unit, ad-hoc env files) has been retired. NixOS is the supported deployment path on dev. The historical AppImage build still exists for one-off testing on non-NixOS dev boxes, but it is not how production ATMs are installed.

The pipeline at a glance

   ┌─────────────────┐  nix build .#         ┌─────────────────────────┐
   │ dev box         │ ─disk-image-sintra──▶ │ result/nixos.img        │
   │ (Nix + flake)   │                       │ (8.5 GB sparse, GPT)    │
   └─────────────────┘                       └────────────┬────────────┘
                                                          │ dd
                                                          ▼
                                                ┌──────────────────────┐
                                                │ USB stick            │
                                                └──────────┬───────────┘
                                                           │ carry to ATM
                                                           ▼
   ┌─────────────────┐                          ┌─────────────────────┐
   │ Alpine live USB │   boot Sintra            │ Sintra (UP Board)   │
   │ (toolchain)     │ ─────────────────────▶   │  eMMC: empty        │
   └────────┬────────┘                          └──────────┬──────────┘
            │ apk add + dd USB → eMMC + parted resize + reboot
            ▼
   ┌────────────────────────────────────────────────────────────────┐
   │ Sintra booted from eMMC, bitspire.service in needs-prov state  │
   └──────────────────────────────┬─────────────────────────────────┘
                                  │ provision-atm.sh from dev box
                                  ▼
   ┌────────────────────────────────────────────────────────────────┐
   │ /var/lib/bitspire/.env populated, service restarted, kiosk     │
   │ connects to LNbits over nostr-transport, ready for transactions │
   └────────────────────────────────────────────────────────────────┘

Why this pipeline

Three properties matter for ATM software running unattended in retail locations:

  1. Reproducible boot media. Two ATMs flashed from the same nixos.img boot identical environments — same kernel, same systemd units, same Electron build, same nix-store closure. No "works on my machine" drift between fleet units.
  2. Auditable provenance. Every file on the ATM traces back to a derivation in /nix/store/, and every derivation traces back to a commit in this repo. There's no pip install reaching out to PyPI, no npm install pulling un-pinned packages, no apt update mutating the system underfoot.
  3. Safe in-place updates. nixos-rebuild switch against the dev branch atomically installs a new generation; if the new generation fails to boot or activate, the previous one stays bootable. Auto-upgrade at 04:00 daily means an operator can patch the fleet by pushing to dev.

The disk-image approach (versus nixos-install from a live USB) is specifically to avoid the human-in-the-loop installation step. Every Sintra gets the same image; the only per-machine variation is the .env written at provisioning time.

What's in the image

Conceptually:

Layer Source Purpose
Kernel + initrd nixpkgs 24.11 + upboard.nix initrd modules Boot the Sintra hardware (eMMC via sdhci-acpi, validator/dispenser at ttyJ5/ttyJ7)
NixOS base nixpkgs 24.11 systemd, Xorg, openbox, the bitspire user, sshd for provisioning
bitspire.service deploy/nixos/bitspire-atm.nix systemd unit that launches the Electron kiosk
The Electron app apps/machine built into a nix derivation The actual ATM UI + state machine + Lightning client
Hardware-specific config deploy/nixos/hardware/upboard.nix (or douro.nix, batm3.nix) udev rules, kernel modules, panel calibration

What's NOT in the image

The image is identity-free by design. After flashing, the ATM has no concept of:

  • Which LNbits server to talk to
  • Which nostr relay to use
  • Its own nostr identity (signing key)
  • Which fiat currency to display (defaulted from build args, but overridable)

All of these come from /var/lib/bitspire/.env, which is written by provision-atm.sh after first boot. This separation means a single image flavor can serve dev, staging, and prod just by changing the provisioning data.

Pre-deployment checklist

Before flashing a Sintra you'll want:

  • An Alpine live USB for use as the installer-OS on the Sintra (Alpine is small, has a working busybox toolchain, and apk is fast). The bitSpire image itself is not a live system — it expects to live on the eMMC.
  • A second USB stick to flash the bitSpire image onto (this is what you'll dd between on the Sintra).
  • A running LNbits instance with the nostr-native-transport branch built in. The dev compose at ~/dev/local/docker/regtest provides one; production deployments point at lnbits.aiolabs.dev or your operator's instance.
  • Network reachability between the Sintra and the LNbits host. The ATM connects to the relay endpoint at ws://<lnbits-host>:5001/nostrrelay/test (no separate strfry container — LNbits ships its own nostrrelay extension).
  • The LNbits server pubkey (docker logs <lnbits-container> | grep 'Public key (share this)').
  • A freshly generated nostr private key for the ATM (openssl rand -hex 32). Each ATM should have its own; never share keys between machines.

After deployment

Once the kiosk is up, useful things to know:

  • Service status: ssh bitspire@<atm> 'sudo systemctl status bitspire'
  • Live log tail: ssh bitspire@<atm> 'sudo journalctl -u bitspire -f'
  • Re-provision (e.g., wrong relay URL): rerun provision-atm.sh from the dev box with the new env vars
  • Push a code change without reflashing: nixos-rebuild switch --flake .#sintra-installed --target-host bitspire@<atm> --use-remote-sudo
  • Inspect transaction history: ssh bitspire@<atm> 'sudo bash /etc/nixos/atm-transactions.sh' (queries /var/lib/bitspire/state.db)
  • deploy/nixos/README.md — the full step-by-step walkthrough and NixOS module reference
  • device-configuration.md — hardware-specific configuration (validator types, cassette layouts, fiat currency)
  • adr/001-hal-architecture.md — why HAL is TypeScript-in-Node rather than Rust-via-Tauri
  • CLAUDE.md — the dev-facing overview, including the hardware-specific gotchas we hard-learned on the first real Sintra flash