- @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.
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:
- Reproducible boot media. Two ATMs flashed from the same
nixos.imgboot identical environments — same kernel, same systemd units, same Electron build, same nix-store closure. No "works on my machine" drift between fleet units. - 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 nopip installreaching out to PyPI, nonpm installpulling un-pinned packages, noapt updatemutating the system underfoot. - Safe in-place updates.
nixos-rebuild switchagainst thedevbranch 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 todev.
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/regtestprovides one; production deployments point atlnbits.aiolabs.devor 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 ownnostrrelayextension). - 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.shfrom 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)
Related documentation
- 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