bitspire/docs/architecture-comparison.md
Padreug 4f68ddc40b refactor(machine): drop VITE_LNBITS_HTTP_URL — lnurl now arrives populated from LNbits (#57 gap 2)
Closes gap 2 from coord log 2026-06-01T18:30Z. The LNbits withdraw
extension's nostr-transport RPC now populates `link.lnurl` from
`settings.lnbits_baseurl` (aiolabs/withdraw#1 / commit e9d911e), so the
ATM no longer needs a separate HTTP URL on the wire to compose the
LNURL-withdraw callback itself.

What goes:

- `VITE_LNBITS_HTTP_URL` env var (renderer + Electron main)
- `lnbitsHttpUrl` field on `LightningConfig`, `RuntimeConfig`, and the
  Window mirror in `src/types/electron.d.ts`
- The manual `${lnbitsHttpUrl}/withdraw/api/v1/lnurl/${unique_hash}`
  composition in `generateLnurlWithdraw`
- The `encodeLnurl` bech32 helper in `lightning.ts` (LNbits returns
  bech32-encoded; we just `.toUpperCase()` to match BOLT/LNURL convention)
- `@scure/base` dep from `apps/machine/package.json` (only used by the
  removed helper; clink still uses it directly)
- The `lnbitsHttpUrl` option + `LNBITS_HTTP_URL=…` env var + boot echo
  in `deploy/nixos/bitspire-atm.nix`
- Doc references in CLAUDE.md, README.md, deploy/nixos/README.md,
  docs/architecture-comparison.md, and the lightning-check skill

What stays:

- `link.lnurl` consumption, with an explicit error if LNbits returns
  null (which signals `LNBITS_BASEURL` is unset on the server side —
  better to fail clearly than silently)
- The receiver-side bech32 uppercasing (LNbits returns lowercase per
  the standard library)

Why this is a net win:

- Removes a config-drift surface — if LNbits's external URL moved
  (DNS, port, reverse-proxy rewrite), every ATM in the field would
  stop issuing redeemable LNURL-withdraw QRs until reconfigured.
  Now LNbits derives its own URL from `settings.lnbits_baseurl`,
  one source of truth.
- Removes an extra provisioning step. No more `LNBITS_HTTP_URL=…`
  before running `provision-atm.sh`; the relay + server pubkey suffice.
- Removes the misleading boot echo that triggered the §`18:30Z`
  smoke triage confusion ("LNbits HTTP: <url>" read like ATM-→-LNbits
  connectivity, when it was only ever a URL embedded in customer QRs).

Also adds a `# pragma: allowlist secret` marker above the
`VITE_ATM_PRIVATE_KEY` doc block in `.env.example` so the global
secret scanner stops false-positiving on the documentation prose.

Workspace typecheck + 24/24 apps/machine tests still green.

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

305 lines
19 KiB
Markdown

# Architecture Comparison: bitSpire vs Traditional Lamassu
This document compares the bitSpire architecture — a Nostr-native Lightning ATM talking to LNbits over the nostr-native-transport — with the traditional `lamassu-server` + `lamassu-machine` architecture. The goal is to help an operator evaluating an open-source Lightning ATM stack understand what they're choosing between.
> A note on prior art: bitSpire's hardware drivers and cash-flow state machine derive from Lamassu Industries AG's open-source `lamassu-machine` and `lamassu-server` projects up to and including the v8.1.5 release line (the last published under a fully-open license). Lamassu [transitioned to a proprietary source-available license](https://blog.lamassu.is/updates-to-our-lamassu-software-license/) on 2024-01-26; bitSpire does **not** incorporate code from v8.1.6 or later and is not affiliated with Lamassu Industries AG. See the [Acknowledgements](../README.md#acknowledgements) section of the top-level README.
## Executive summary
| Aspect | Traditional (`lamassu-server` ≤ v8.1.5) | bitSpire |
|---|---|---|
| **Software license** | AGPL-3.0 (v8.1.5); v8.1.6+ is proprietary SLA | AGPL-3.0 |
| **Communication** | Custom WebSocket + GraphQL over HTTPS | Nostr kind-21000 events (NIP-01, NIP-44 v2) over a relay |
| **Identity** | Server-issued client certs | Nostr keypairs (`nsec`/`npub`) |
| **Account system** | PostgreSQL + custom auth | LNbits wallet auto-created from ATM's nostr pubkey |
| **Payment protocol** | Direct LND RPC | BOLT11 (cash-out) + LNURL-withdraw (cash-in) |
| **Server infrastructure** | App server + DB + Admin UI | A relay + an LNbits instance (both can be one container) |
| **Wallet compatibility** | Any Lightning wallet (cash-out); custom flow for cash-in | Any Lightning wallet (BOLT11 + LNURL-withdraw — both widely supported) |
| **State location** | Centralized in PostgreSQL | Distributed: LNbits holds Lightning state, ATM holds session state |
---
## Traditional architecture (lamassu-server / lamassu-machine, v8.1.5)
```
┌──────────────────┐ WebSocket ┌─────────────────────┐
│ ATM (kiosk) │◄──────────────────────────►│ lamassu-server │
│ lamassu-machine │ client-cert auth │ │
└──────────────────┘ │ ┌───────────────┐ │
│ │ PostgreSQL │ │
┌──────────────────┐ HTTPS │ └───────────────┘ │
│ Admin UI │◄──────────────────────────►│ │
│ (React SPA) │ GraphQL │ ┌───────────────┐ │
└──────────────────┘ │ │ LND │ │
│ └───────────────┘ │
┌──────────────────┐ │ │
│ Customer Wallet │◄── Lightning Invoice ──────│ ┌───────────────┐ │
│ (any LN wallet) │ │ │ Compliance │ │
└──────────────────┘ │ │ Services │ │
│ └───────────────┘ │
└─────────────────────┘
```
### Components
| Component | Description |
|---|---|
| `lamassu-server` | Node.js (Express + Apollo GraphQL) backend |
| `lamassu-machine` | Node.js kiosk app with hardware drivers |
| PostgreSQL | Central DB for transactions, users, config |
| Admin UI | React dashboard for fleet management |
| LND | Lightning Network Daemon for payments |
### Cash-out flow
1. Machine boots, authenticates via client cert
2. Server pushes config + state over WebSocket
3. Customer initiates cash-out at machine
4. Machine asks server for an invoice
5. Server creates invoice via LND, stores in PostgreSQL
6. Customer pays the invoice
7. Server polls LND, detects payment, updates DB
8. Server notifies machine to dispense
9. Compliance services log the transaction
### Characteristics
- **Centralized**: all state in `lamassu-server`'s PostgreSQL
- **Operator-managed**: requires real infrastructure (server, DB, certs, monitoring)
- **Tightly coupled**: machine doesn't transact if server is unreachable
- **Compliance-ready**: KYC/AML hooks built in (relevant in regulated jurisdictions)
---
## bitSpire architecture
```
┌──────────────────┐ ┌──────────────────┐
│ ATM (kiosk) │ │ Customer wallet │
│ npub_atm │ │ (any LN wallet) │
└────────┬─────────┘ └────────┬─────────┘
│ kind-21000 │
│ NIP-44 v2 encrypted │ BOLT11
▼ │ LNURL-withdraw
┌───────────────────────────────┐ │
│ Nostr Relay │ │
│ (LNbits' bundled │ │
│ nostrrelay extension, or │ │
│ any standalone relay) │ │
└────────────────┬──────────────┘ │
│ │
│ kind-21000 RPC + push │
▼ │
┌───────────────────────┐ ◄──────────────────────┘
│ LNbits │ BOLT11 settle / LNURLw redeem
│ ┌──────────────────┐ │
│ │ FakeWallet (dev) │ │
│ │ or LND/CLN/etc. │ │
│ └──────────────────┘ │
└───────────────────────┘
```
### Components
| Component | Description |
|---|---|
| Nostr relay | Message broker. In the dev compose, this is LNbits's bundled `nostrrelay` extension at `ws://<host>:5001/nostrrelay/test`. Production can use any standalone relay both peers subscribe to |
| LNbits | Lightning wallet platform; runs the `nostr-native-transport` branch. Auto-creates a wallet on first contact from any nostr pubkey |
| ATM machine | Electron + Vue 3 kiosk identified by its `npub`. Signing key IS the credential to LNbits |
| Customer wallet | Any LN wallet that handles BOLT11 (cash-out) and LNURL-withdraw (cash-in) — most do |
### Cash-out flow (customer pays ATM, gets cash)
1. ATM publishes a kind-21000 event to LNbits: `{rpc_name: "create_invoice", body: {amount: <sats>, memo: "..."}}`
2. LNbits replies on the same relay with the BOLT11 invoice
3. ATM displays the BOLT11 as a QR
4. Customer scans, pays from any LN wallet
5. LNbits's Lightning backend (FakeWallet / LND / CLN / etc.) settles the payment
6. LNbits pushes a kind-21000 settlement event to the ATM, filtered by `payment_hash`
7. ATM dispenses cash
### Cash-in flow (customer hands ATM cash, gets sats)
1. ATM publishes `lnurlw_create_link` (kind-21000) to LNbits with `{uses: 1, max_withdrawable: <sats>}`
2. LNbits replies with a `WithdrawLink` carrying a `lnurl` field already populated from `settings.lnbits_baseurl` (per aiolabs/withdraw#1 / `e9d911e`)
3. ATM displays the LNURL as a QR (uppercased per BOLT/LNURL convention)
4. Customer scans with any LNURL-withdraw-capable wallet, redeems it
5. LNbits settles the withdrawal via its Lightning backend
6. LNbits pushes a kind-21000 settlement event to the ATM, filtered by `tag="withdraw"` + `link_id`
7. ATM marks the session complete (cash already physically accepted earlier in the flow)
### Characteristics
- **Decentralized message layer**: any nostr relay works, no proprietary protocol
- **No admin tokens on the kiosk**: the ATM's signing key IS the credential — there are no HTTP API tokens or client certs to leak. LNbits derives the calling identity from the event signature.
- **Loose coupling**: ATM and LNbits don't need to share a network segment; only a relay both reach
- **Privacy-first**: no central transaction logs unless the operator builds them. LNbits stores its own ledger, but bitSpire writes no user-identifying data anywhere on its side.
---
## Detailed comparison
### 1. Infrastructure requirements
| Requirement | Traditional | bitSpire |
|---|---|---|
| Server hardware | Dedicated server (4+ GB RAM, SSD) | An LNbits instance (1 GB RAM is plenty in production) |
| Database | PostgreSQL (operator managed) | LNbits's SQLite (or its configured DB; LNbits manages it) |
| Client TLS | Required (client certs per machine) | Not required (signature-based auth over a nostr relay) |
| Domain | Required | Optional (only if exposing LNbits HTTP for LNURL redemption from the public internet) |
| Static IP | Recommended | Not required |
| Firewall | Multiple inbound ports | Outbound WebSocket to a relay; inbound only on the LNbits HTTP port for LNURL callbacks |
**Edge: bitSpire.** A single-container LNbits with FakeWallet is enough for an operator to evaluate the whole stack end-to-end.
### 2. Security model
| Aspect | Traditional | bitSpire |
|---|---|---|
| Machine auth | Client certificates | Nostr signing key |
| Transport security | TLS | NIP-44 v2 (XChaCha20 + HMAC-SHA256) |
| Compromise scope | Server breach exposes all machines | One machine's `nsec` compromises only that machine |
| Key storage | Server manages | Machine manages its own keys (`/var/lib/bitspire/.env`, mode 0600) |
| Revocation | CA-style cert revocation | Operator can stop honoring a pubkey at the LNbits layer; no fleet-wide blast radius |
**Edge: bitSpire.** No central authority that, if compromised, exposes the whole fleet. Per-machine compromise stays contained.
### 3. Payment flow
| Aspect | Traditional | bitSpire |
|---|---|---|
| Invoice creation | Server creates via LND | LNbits creates via its Lightning backend, returned over kind-21000 |
| Payment detection | Server polls LND | LNbits pushes a settlement event over kind-21000 (`subscribe_payments` filtered by `payment_hash`) |
| Cash-in detection | Custom flow | LNbits pushes a `tag="withdraw"` + `link_id` event when the LNURL is redeemed |
| Wallet compatibility | Any BOLT11 wallet | Any BOLT11 wallet (cash-out) + any LNURL-withdraw wallet (cash-in) |
| Offline capability | None | Cashu ecash (placeholder package, not yet wired) |
**Tied.** Both architectures end up using BOLT11 + LNURL-withdraw at the customer-wallet boundary. The difference is on the ATM side: traditional polls a server it owns; bitSpire subscribes to a push channel on a wallet it shares with anyone who can reach the relay.
### 4. Operator experience
| Aspect | Traditional | bitSpire |
|---|---|---|
| Setup time | Hours (provision server, DB, TLS, cert each machine) | Minutes (run an LNbits instance + a relay; flash + provision each ATM) |
| Maintenance | DB backups, server updates, monitoring | Update LNbits + the bitSpire ATMs (the latter auto-upgrades from the `dev` branch nightly) |
| Fleet management | Mature Admin UI | Per-ATM `journalctl`; centralized dashboard is planned, not built |
| Transaction history | PostgreSQL queries | Per-ATM `/var/lib/bitspire/state.db` (SQLite); operator-side aggregation is on them to build |
| Configuration | Server-pushed | Per-ATM `.env` written via `provision-atm.sh` |
**Mixed.** Traditional has mature operator tooling. bitSpire is simpler to stand up but the fleet-management story is currently "ssh + scripts" rather than a UI.
### 5. Customer experience
| Aspect | Traditional | bitSpire |
|---|---|---|
| Cash-out | Scan BOLT11, pay | Scan BOLT11, pay |
| Cash-in | Varies by deployment (LNURL-withdraw common, sometimes custom) | Scan LNURL-withdraw QR, redeem |
| Wallet requirements | Any LN wallet for cash-out | Any LN wallet for cash-out + any LNURL-w wallet for cash-in |
| Account required | Sometimes (compliance) | Never |
| Transaction speed | ~10 seconds | ~3-5 seconds (push-based; no server polling delay) |
**Tied on protocol; slight edge to bitSpire on speed** because LNbits push events skip the polling cycle that traditional setups have between LND and the application server.
### 6. Compliance & regulation
| Aspect | Traditional | bitSpire |
|---|---|---|
| KYC integration | Built-in | Not included |
| Transaction logs | PostgreSQL (server) | Per-ATM SQLite + whatever LNbits records on its side |
| Audit trail | Comprehensive | Operator-defined; LNbits's ledger is the canonical Lightning-side record |
| Reporting | Admin UI exports | Custom tooling needed |
| Regulatory fit | Designed for KYC/AML jurisdictions | Privacy-first design; suits jurisdictions without identity-collection requirements |
**Edge: Traditional** if you need built-in compliance. bitSpire targets KYC-free operation; operators in regulated jurisdictions would need to add compliance hooks themselves.
### 7. Resilience & availability
| Aspect | Traditional | bitSpire |
|---|---|---|
| Server down | All machines stop transacting | Machines stop transacting if LNbits is down; relay outage means RPCs hang |
| Network partition | Transactions fail | Can connect to multiple relays for failover |
| Database corruption | Catastrophic (cascading impact across fleet) | LNbits-side issue affects only the affected operator's wallet; per-ATM SQLite is local and independent |
| Recovery | Restore from backup | LNbits-side restore is its concern; ATMs come back up clean by re-reading `.env` |
| Geographic distribution | Single server | LNbits can be regional; relays can be anywhere |
**Edge: bitSpire** for blast-radius reasons. LNbits-side failures are no worse than `lamassu-server`-side failures, but bitSpire's per-ATM state is genuinely isolated.
### 8. Development & extensibility
| Aspect | Traditional | bitSpire |
|---|---|---|
| Codebase | Large monolith (`lamassu-server` is hundreds of files) | Modular workspace; ATM, transport client, HAL, state machine are separate packages |
| Protocol | Custom WebSocket schema | NIP-01 + NIP-44 v2 + a thin documented kind-21000 envelope |
| Third-party integration | Custom API needed | Any nostr/Lightning client can talk to LNbits over the transport |
| Community | Lamassu operators (and ex-operators after the license change) | Nostr + Bitcoin ecosystem; LNbits community |
| Forkability | Complex (server + machine are coupled) | Straightforward (replace any one package without touching the others) |
**Edge: bitSpire.** Open protocols mean a wider ecosystem of compatible clients, wallets, and tools.
---
## When to choose what
### Choose bitSpire if
- You operate in a jurisdiction without mandatory KYC/AML
- You want minimal infrastructure
- You value customer privacy (no PII collection, no central server-side ledger of who-paid-what)
- You're comfortable with emerging tooling (no admin UI yet)
- You want a license that stays open (AGPL-3.0)
### Stick with `lamassu-server` v8.1.5 if
- You need built-in compliance features
- You need a mature admin dashboard *today*
- You have existing infrastructure investment in PostgreSQL + LND tooling
- You're comfortable being on a code line that no longer receives upstream updates (v8.1.5 is the last open release; bug fixes in v8.1.6+ are paid-license-only)
### Choose Lamassu's current commercial offering if
- You want vendor support, official updates, and a commercial SLA
- You're willing to pay the per-machine subscription
- You operate in a regulated jurisdiction where KYC tooling matters
This last option is outside the scope of this document — see [lamassu.is](https://lamassu.is) for their current commercial terms.
---
## Trade-offs & honest assessment
### Where bitSpire excels
1. **Simplicity** — LNbits + a relay is a much smaller surface than a stateful application server + DB + admin tooling
2. **Privacy** — no central transaction logs by design
3. **Resilience** — no single point of failure on the ATM side; LNbits is replaceable
4. **Interoperability** — works with any BOLT11 / LNURL-withdraw wallet
5. **Speed** — push-based settlement detection skips the LND-poll cycle
6. **Cost** — no Lamassu license fees; no commercial admin-server hosting
### Where traditional (`lamassu-server` ≤ v8.1.5) is still better
1. **Compliance** — built-in KYC/AML if your jurisdiction requires it
2. **Tooling** — mature admin dashboard, fleet management, transaction reporting
3. **Maturity** — battle-tested in production over many years
4. **Documentation** — established support channels for operators (community-maintained for v8.1.5; commercial for v8.1.6+)
### Current limitations of bitSpire
| Limitation | Status | Mitigation |
|---|---|---|
| No fleet dashboard | Planned, not built | Per-ATM `journalctl` + custom scripts |
| Limited transaction reporting | SQLite per ATM | Aggregate via operator-side ETL if needed |
| No offline mode | `@bitSpire/cashu` is a placeholder | Requires LNbits connectivity currently |
| Smaller operator community | Growing | This doc + the README walkthrough |
---
## Further reading
- [LNbits nostr-native-transport docs](https://git.atitlan.io/aiolabs/lnbits) — the wire spec for what's actually happening on kind-21000
- [NIP-01 Nostr basics](https://github.com/nostr-protocol/nips/blob/master/01.md)
- [NIP-44 v2 encryption](https://github.com/nostr-protocol/nips/blob/master/44.md)
- [LUD-03 LNURL-withdraw](https://github.com/lnurl/luds/blob/luds/03.md)
- [CLINK Protocol](./clink-protocol.md) — historical reference, dormant on `dev`
- [bitSpire deployment walkthrough](../deploy/nixos/README.md)
- [machine-installation.md](./machine-installation.md) — high-level deployment overview