docs: finish the LNbits-era doc sweep across docs/ + .claude/skills/

Second + final batch of the doc refresh. README + CLAUDE went out in
8c9ae29; deploy/nixos/README + obsolete-flow flags in 924844f. This
commit covers everything left.

docs/machine-installation.md
  Was describing a manual AppImage scp deploy + a `lamassu-kiosk`
  systemd unit that hasn't been the deployment path for months.
  Replaced with a high-level "what the pipeline does and why"
  overview that points at deploy/nixos/README.md for the full
  command-by-command walkthrough. Includes the BATM3 chassis-mod
  note (custom Dell OptiPlex retrofit, not a stock Dell).

docs/architecture-comparison.md
  Rewrote the comparison to be lamassu-server (≤ v8.1.5) vs bitSpire
  (LNbits-backed) instead of the original lamassu-server vs LP-backed
  lamassu-next framing. Updated the cash-out + cash-in flow diagrams
  to show the actual nostr-transport path (LNbits-bundled nostrrelay
  extension at ws://<host>:5001/nostrrelay/test, no separate strfry
  container). Replaced the migration-path section with a softer
  "when to choose what" framing that includes Lamassu's current
  commercial offering as a legitimate third option. Added a header
  pointer to the Acknowledgements section.

docs/business-model.md
  Light touch-ups: Lightning.Pub → LNbits where it appeared, swapped
  the [[ndebit-cash-in-flow]] link for [[architecture-comparison]],
  noted the kind-30078 service beacon for availability broadcasts.

docs/device-configuration.md
  Dropped "Lamassu" branding from the machine-model headings
  (Sintra / tejo / douro / batm3 are referenced by hardware identity
  here, not by Lamassu's product line). Added the Sintra-specific
  ttyS4-vs-placeholder-ttyS1..3 gotcha we hard-learned during the
  first flash. Corrected the BATM3 entry: stock GeneralBytes chassis
  with a Dell OptiPlex 9030 AIO motherboard physically grafted in,
  NOT a Dell out of the box. Updated the example /dev/ttyJ* symlink
  output to match what a healthy Sintra actually shows.

docs/adr/001-hal-architecture.md
  ADRs are historical artifacts — kept the original decision text
  intact. Added a postscript noting:
    - The package rename @lamassu/hal → @bitSpire/hal
    - The v8.1.5 boundary on any lamassu-machine source-tree
      references (Lamassu's 2024-01-26 license transition)
    - That the "Remaining Work" list is complete and the first
      successful Sintra hardware integration ran on 2026-05-13

.claude/skills/lightning-check.md
  Rewrote end-to-end. Was Lightning.Pub-flavoured with CLINK kinds
  21001/21002 as the primary flows; now validates the LNbits nostr-
  transport surface (kind-21000 envelope, NIP-44 v2 encryption,
  subscribe_payments filter discipline, lnurlw link composition).
  Preserved a --clink mode for the still-live kind-21003 operator-
  management surface. Includes a "what to check" rubric for cash-out
  vs cash-in flows that mirrors the actual code in
  apps/machine/src/services/lightning.ts.

.claude/skills/hal-check.md
  Two pivots: (1) acknowledge ADR-001's TypeScript-not-Rust choice
  and reframe all the safety checklists in TS-flavour (type safety,
  discriminated unions, single-writer serial, bounded emitters)
  instead of Rust-flavour (unsafe, borrow checker). (2) Add explicit
  v8.1.5 provenance boundary plus a "forbidden operations" section
  that prohibits diffing or porting from v8.1.6+ lamassu-machine
  source. Updated the port-validation source-reference table to
  list TS file paths under packages/hal/ instead of Rust paths.

.claude/skills/docs.md
  @lamassu/* → @bitSpire/*. Replaced the Lightning.Pub mermaid
  diagram with a current cash-out flow showing the nostr-transport
  RPC + subscribe_payments push path. Left the createOffer noffer
  example in the API-docs template section since it's illustrative
  ("here's what a good TSDoc block looks like") rather than current
  reference documentation.

.claude/skills/test.md
  One-line: @lamassu/nostr-client → @bitSpire/nostr-client in the
  pnpm-filter example.

deploy/nixos/README.md
  Single touch-up: clarified the douro/batm3 hardware-module comments
  to reflect that BATM3 is a custom-installed Dell board in a
  GeneralBytes BATM3 chassis (not a Dell OEM).

Files NOT touched in this sweep (intentionally):
  - packages/hal/src/**/*.ts attribution comments — those reference
    "lamassu-machine" in their port-source headers. Those are
    factually accurate (the drivers ARE ported from there, up to
    v8.1.5) and constitute necessary license/attribution metadata.
    Editing them would erase the provenance trail.
  - .claude/skills/{nostr-check,security}.md — already protocol-
    neutral, no LP/lamassu references to clean up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-14 08:25:18 +02:00
commit 53b0d382e8
10 changed files with 692 additions and 792 deletions

View file

@ -1,278 +1,96 @@
# Machine Installation
# Deploying bitSpire to a Sintra (or other ATM)
This document describes how to deploy the bitSpire ATM software to a Sintra machine. (Historical name: "Lamassu Next" — renamed on the `dev` branch as part of the LNbits-backend transition.)
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](../deploy/nixos/README.md).** This file is the orientation doc that explains *why* the pipeline looks the way it does.
## Prerequisites
> 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.
### On the Sintra
## The pipeline at a glance
- Linux OS with X11 display server
- Network connectivity (WiFi or Ethernet)
- User account with `dialout` group membership (for serial port access)
### Backend Services
The ATM requires network access to:
| Service | Purpose | Required |
| ------------- | ------------------- | -------- |
| Nostr relay | CLINK communication | Yes |
| Lightning.Pub | Payment processing | Yes |
These can be self-hosted or provided by a third party.
## Building the Application
### Development Machine Setup
1. Enter the development environment:
```bash
cd lamassu-next
devenv shell
```
2. Build the Electron application:
```bash
cd apps/machine
pnpm build:electron
```
3. Build artifacts are created in `apps/machine/release/`:
```
release/
├── bitSpire-0.1.0.AppImage # Portable executable (recommended)
└── linux-unpacked/ # Unpacked application directory
```
## Deployment
### Option A: AppImage (Recommended)
The AppImage is a self-contained executable that works on any Linux distribution.
1. Copy to Sintra:
```bash
scp "apps/machine/release/bitSpire-0.1.0.AppImage" user@sintra:/opt/lamassu/
```
2. Make executable:
```bash
ssh user@sintra
chmod +x "/opt/lamassu/bitSpire-0.1.0.AppImage"
```
3. Test manually:
```bash
export DISPLAY=:0
"/opt/lamassu/bitSpire-0.1.0.AppImage" --no-sandbox
```
### Option B: Unpacked Directory
For faster startup times, deploy the unpacked application:
1. Copy to Sintra:
```bash
scp -r apps/machine/release/linux-unpacked user@sintra:/opt/lamassu/
```
2. Test manually:
```bash
ssh user@sintra
export DISPLAY=:0
/opt/lamassu/linux-unpacked/lamassu-machine --no-sandbox
```
## Configuration
### Environment Variables
Create a configuration file at `/opt/lamassu/.env`:
```bash
# Machine model (sintra, gaia, or custom)
VITE_LAMASSU_MACHINE_MODEL=sintra
# Fiat currency code (ISO 4217)
VITE_LAMASSU_FIAT_CODE=USD
# Custom device paths (optional, uses preset defaults if not set)
# VITE_LAMASSU_VALIDATOR_DEVICE=/dev/ttyJ5
# VITE_LAMASSU_DISPENSER_DEVICE=/dev/ttyJ7
# Cassette configuration (optional)
# VITE_LAMASSU_CASSETTES='[{"denomination":20,"count":100}]'
```
┌─────────────────┐ 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 │
└────────────────────────────────────────────────────────────────┘
```
See [Device Configuration](./device-configuration.md) for detailed configuration options.
## Why this pipeline
### Lightning.Pub Connection
Three properties matter for ATM software running unattended in retail locations:
The ATM needs to connect to a Lightning.Pub instance. Configure via environment:
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`.
```bash
# Nostr relay WebSocket URL (required)
VITE_RELAY_URL=wss://your-relay.example.com
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.
# Lightning.Pub's Nostr public key (required)
# Get from: docker logs lamassu-lightning-pub | grep pubkey
VITE_LIGHTNING_PUB_PUBKEY=4be8e203a3341bb2b74a4dcbf8774e061437f63ec21af7ec3144c8d0a68e2f39
## What's in the image
# Lightning.Pub HTTP API URL (optional, for admin operations)
VITE_LIGHTNING_PUB_API_URL=https://lp.operator.com
Conceptually:
# ATM's Nostr private key (recommended for persistent identity)
# Generate with: npx @lamassu/nostr-client generate-keypair
# If not set, a new ephemeral identity is generated on each restart
VITE_ATM_PRIVATE_KEY=0123456789abcdef...
```
| Layer | Source | Purpose |
|---|---|---|
| **Kernel + initrd** | `nixpkgs` 24.05 + `upboard.nix` initrd modules | Boot the Sintra hardware (eMMC via `sdhci-acpi`, validator/dispenser at `ttyJ5`/`ttyJ7`) |
| **NixOS base** | `nixpkgs` 24.05 | systemd, Xorg, openbox, the `lamassu` 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 |
**Important:** The `VITE_LIGHTNING_PUB_PUBKEY` is required. Without it, the ATM cannot communicate with Lightning.Pub.
## What's NOT in the image
## Auto-Start on Boot
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)
### Systemd Service
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.
Create `/etc/systemd/system/lamassu-kiosk.service`:
## Pre-deployment checklist
```ini
[Unit]
Description=bitSpire ATM Kiosk
After=graphical.target network-online.target
Wants=network-online.target
Before flashing a Sintra you'll want:
[Service]
Type=simple
User=lamassu
Environment=DISPLAY=:0
EnvironmentFile=/opt/lamassu/.env
WorkingDirectory=/opt/lamassu
ExecStart=/opt/lamassu/linux-unpacked/lamassu-machine --no-sandbox
Restart=always
RestartSec=5
- [ ] **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.
[Install]
WantedBy=graphical.target
```
## After deployment
Enable and start:
Once the kiosk is up, useful things to know:
```bash
sudo systemctl daemon-reload
sudo systemctl enable lamassu-kiosk
sudo systemctl start lamassu-kiosk
```
- **Service status:** `ssh lamassu@<atm> 'sudo systemctl status bitspire'`
- **Live log tail:** `ssh lamassu@<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 lamassu@<atm> --use-remote-sudo`
- **Inspect transaction history:** `ssh lamassu@<atm> 'sudo bash /etc/nixos/atm-transactions.sh'` (queries `/var/lib/bitspire/state.db`)
### Monitoring
## Related documentation
```bash
# Check status
sudo systemctl status lamassu-kiosk
# View logs
sudo journalctl -u lamassu-kiosk -f
# Restart after configuration changes
sudo systemctl restart lamassu-kiosk
```
## Hardware Verification
### Serial Port Access
1. Verify devices exist:
```bash
ls -la /dev/ttyJ*
# Expected: /dev/ttyJ4 (printer), /dev/ttyJ5 (validator), /dev/ttyJ7 (dispenser)
```
2. Check user permissions:
```bash
groups
# Should include 'dialout'
```
3. If not in dialout group:
```bash
sudo usermod -a -G dialout $USER
# Logout and login for changes to take effect
```
### Display Configuration
The Sintra uses a 1080x1920 portrait display. Verify X11 is running:
```bash
echo $DISPLAY
# Should output :0 or similar
xdpyinfo | head -5
# Should show display information
```
## Troubleshooting
### Application Won't Start
1. **Missing display**: Ensure `DISPLAY=:0` is set
2. **Sandbox error**: Use `--no-sandbox` flag
3. **Permission denied**: Check file is executable (`chmod +x`)
### Hardware Not Responding
1. **Check serial ports exist**: `ls -la /dev/ttyJ*`
2. **Check permissions**: User must be in `dialout` group
3. **Check connections**: Ensure cables are properly seated
4. **Power cycle hardware**: Turn validator/dispenser off and on
### Network Issues
1. **Test relay connection**: `websocat wss://your-relay.example.com`
2. **Check DNS resolution**: `ping your-relay.example.com`
3. **Verify firewall**: Ensure outbound WebSocket connections allowed
### Logs
Application logs are written to:
- **Systemd**: `journalctl -u lamassu-kiosk`
- **Electron**: `~/.config/lamassu-machine/logs/`
## Updating
1. Build new version on development machine
2. Stop the service:
```bash
sudo systemctl stop lamassu-kiosk
```
3. Replace application files:
```bash
scp -r apps/machine/release/linux-unpacked/* user@sintra:/opt/lamassu/linux-unpacked/
```
4. Restart service:
```bash
sudo systemctl start lamassu-kiosk
```
## Security Notes
- The `--no-sandbox` flag is required for Electron on some Linux configurations. This is acceptable for a dedicated kiosk machine.
- Keep the ATM's nsec private key secure. It authorizes all transactions from this machine.
- Use a dedicated user account (`lamassu`) with minimal privileges.
- Consider firewall rules to restrict network access to only required services.
- [deploy/nixos/README.md](../deploy/nixos/README.md) — the full step-by-step walkthrough and NixOS module reference
- [device-configuration.md](./device-configuration.md) — hardware-specific configuration (validator types, cassette layouts, fiat currency)
- [adr/001-hal-architecture.md](./adr/001-hal-architecture.md) — why HAL is TypeScript-in-Node rather than Rust-via-Tauri
- [CLAUDE.md](../CLAUDE.md) — the dev-facing overview, including the hardware-specific gotchas we hard-learned on the first real Sintra flash