docs(dev-env): aiolabs stack overview and lnbits workflow notes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
dbd9e76027
commit
346042f1c5
8 changed files with 1276 additions and 0 deletions
286
modules/dev-env/docs/stack-overview.md
Normal file
286
modules/dev-env/docs/stack-overview.md
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
# Aiolabs stack — high-level overview
|
||||
|
||||
Orientation map for new contributors. The substrate is **Omnixient** (this
|
||||
repo): a NixOS desktop that declaratively materialises every project
|
||||
worktree below via `modules/dev-env/` + the
|
||||
[`aiolabs` preset](../presets/aiolabs.nix).
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph DEV["Omnixient — NixOS dev environment (this repo)"]
|
||||
DEVENV["modules/dev-env + aiolabs preset<br/>worktrees · tmux · git hooks<br/>Forgejo origin + GitHub upstream forks"]
|
||||
end
|
||||
|
||||
subgraph DEPLOY["Deploy"]
|
||||
DEPLOYFLAKE["server-deploy flake<br/>host1 · host5 · host7 · host8<br/>host4 · host3 · host6 · 484"]
|
||||
end
|
||||
|
||||
subgraph FRONT["User-facing frontends"]
|
||||
direction LR
|
||||
BITSPIRE["bitSpire<br/>Bitcoin ATM frontend<br/>KYC-free · Nostr-native"]
|
||||
WEBAPP["webapp / 'AIO'<br/>Vue 3 + TS · Electron + PWA<br/>wallet · events · Nostr"]
|
||||
end
|
||||
|
||||
subgraph CORE["LNbits — Lightning backend (Python / FastAPI)"]
|
||||
LNBITS["lnbits core<br/>wallets · per-wallet API keys · extension runtime<br/>nostr-native transport (kind-21000, NIP-44 v2)"]
|
||||
end
|
||||
|
||||
subgraph EXT["LNbits extensions — shared/extensions/"]
|
||||
direction LR
|
||||
SPIRE["spirekeeper<br/>bitSpire operator dashboard"]
|
||||
NREL["nostrrelay"]
|
||||
NCLI["nostrclient"]
|
||||
EVENTS["events"]
|
||||
NMKT["nostrmarket"]
|
||||
LIBRA["libra"]
|
||||
LNURLP["lnurlp"]
|
||||
REST["restaurant"]
|
||||
end
|
||||
|
||||
RELAYS(("Nostr relays<br/>event bus"))
|
||||
BTC["Bitcoin / Lightning<br/>LND · CLN · Phoenixd · …"]
|
||||
FAVA["Fava + Beancount<br/>double-entry ledger"]
|
||||
REF["Lightning.Pub (shocknet)<br/>reference implementation for the<br/>nostr-native Lightning interface"]
|
||||
|
||||
DEV -. develops .-> CORE
|
||||
DEV -. develops .-> FRONT
|
||||
DEV -. drives .-> DEPLOY
|
||||
DEPLOY -- ships --> CORE
|
||||
DEPLOY -- ships --> FRONT
|
||||
LNBITS -- hosts --> EXT
|
||||
|
||||
%% On Nostr there are no point-to-point links: the nostr-native
|
||||
%% parts all meet on the relay bus, addressed by pubkey. bitSpire↔
|
||||
%% LNbits (payments) and bitSpire↔spirekeeper (fleet) both resolve
|
||||
%% through the relay, not over a direct edge.
|
||||
BITSPIRE <-- "Lightning RPC + fleet (kind-21000)" --> RELAYS
|
||||
SPIRE <-- "bitSpire fleet control" --> RELAYS
|
||||
NCLI -- "LNbits ↔ relay" --> RELAYS
|
||||
NREL -- "in-stack relay" --> RELAYS
|
||||
EVENTS -- publishes --> RELAYS
|
||||
NMKT -- marketplace --> RELAYS
|
||||
WEBAPP -- subscribes --> RELAYS
|
||||
|
||||
%% non-Nostr edges
|
||||
WEBAPP -- REST + WebSocket --> LNBITS
|
||||
LNBITS -- funding source --> BTC
|
||||
LIBRA -- HTTP / JSON API --> FAVA
|
||||
REF -. reference impl .-> LNBITS
|
||||
|
||||
classDef reference stroke-dasharray: 4 4,opacity:0.65;
|
||||
class REF reference;
|
||||
```
|
||||
|
||||
## How to read this
|
||||
|
||||
- **Omnixient** is where you *work*. Cloning it and running the dev-env
|
||||
bootstrap materialises every box below as a worktree under
|
||||
`~/dev/`, set up with the right Forgejo origin and GitHub upstream
|
||||
fork.
|
||||
- **LNbits** is the backend platform. Everything user-visible either
|
||||
*is* an LNbits extension or *talks to* LNbits. It holds wallets,
|
||||
funds them from a Bitcoin/Lightning source, runs the extension
|
||||
runtime, and exposes both a classic HTTP/WebSocket API and a
|
||||
**nostr-native transport** — signed-event RPC over a relay
|
||||
(kind-21000, NIP-44 v2) with no bearer tokens on the wire.
|
||||
- **Extensions** are Python modules dropped into LNbits' extension
|
||||
runtime. They're cloned independently into `shared/extensions/` so
|
||||
multiple LNbits worktrees (dev/main) can share them.
|
||||
- **libra** is the unusual one: it doesn't reimplement double-entry
|
||||
accounting, it speaks to **Fava**'s JSON API. Fava is the web UI on
|
||||
top of **Beancount** plain-text ledgers. So libra = LNbits plugin
|
||||
that delegates accounting to a separate Fava service.
|
||||
- **webapp** ("AIO") is the polished, abstracted user frontend. It
|
||||
treats LNbits as a backend; configured via `VITE_LNBITS_BASE_URL`.
|
||||
- **bitSpire** is the Bitcoin ATM frontend — a KYC-free, Nostr-native
|
||||
ATM. It talks to its Lightning backend over the **nostr-native
|
||||
transport** on a relay rather than HTTP, so the kiosk holds no admin
|
||||
tokens: the ATM's own Nostr key *is* its credential, and LNbits
|
||||
auto-provisions its wallet on first contact from that signature.
|
||||
- **spirekeeper** is the operator control dashboard for bitSpire
|
||||
fleets, built as an LNbits extension. It owns the operator side of a
|
||||
deployment: fleet and cassette-inventory configuration, commission /
|
||||
DCA distribution (parsed from each cash-out's `Payment.extra`),
|
||||
operator branding, and telemetry — all carried over Nostr.
|
||||
spirekeeper and bitSpire never connect directly: like every
|
||||
nostr-native pair in the stack, they meet on the relay, each
|
||||
addressed by its own Nostr pubkey.
|
||||
- **server-deploy** is a separate flake that pins each project's
|
||||
source revision and builds NixOS configurations for the production
|
||||
hosts (`host8`, `host7`, `host5`, …).
|
||||
|
||||
> **Credit — Lightning.Pub.** LNbits' ATM-facing side — its
|
||||
> nostr-native Lightning node-management interface — is modeled on
|
||||
> [**Lightning.Pub**](https://github.com/shocknet/Lightning.Pub)
|
||||
> (shocknet), the reference implementation for this pattern. bitSpire
|
||||
> talks to LNbits the way it would talk to a Lightning.Pub node; the
|
||||
> functionality lives in LNbits, but the shape of the interface is
|
||||
> Lightning.Pub's.
|
||||
|
||||
## Future direction: Nostr-native transport
|
||||
|
||||
The `bitSpire ↔ LNbits` edge already runs on the nostr-native
|
||||
transport — signed-event RPC over a relay, with Nostr's
|
||||
key/signature model as the security primitive (auth, capabilities,
|
||||
audit trail) instead of bearer tokens and TLS-only trust. The
|
||||
longer-term direction is to move the *rest* of the stack's
|
||||
inter-service edges the same way, so `webapp ↔ LNbits` and
|
||||
`libra ↔ Fava` migrate from REST/WebSocket onto signed Nostr events
|
||||
too. The proposed security structure is described in
|
||||
[aiolabs/lnbits#9](https://git.atitlan.io/aiolabs/lnbits/issues/9).
|
||||
|
||||
The nostrclient/nostrrelay extensions already make LNbits a
|
||||
first-class Nostr participant; the issue tracks the broader
|
||||
auth/permissions design that has to land before the rest of the stack
|
||||
can follow.
|
||||
|
||||
The same model is the candidate for **unified authentication**
|
||||
across the whole platform: instead of each service running its own
|
||||
account system (LNbits accounts, Authentik SSO, Forgejo logins, …),
|
||||
a user's Nostr keypair becomes the identity and per-service
|
||||
capabilities are granted as signed permission events. One identity,
|
||||
one signing key, role-based access fanned out across LNbits,
|
||||
Authentik, Forgejo and every other internal service.
|
||||
|
||||
## Deployment model: location-portable instances
|
||||
|
||||
The stack is designed to be **deployed anywhere** — every venue (a
|
||||
co-living house, a makerspace, a community space, a bar running an
|
||||
ATM) runs its own full instance: LNbits + the relevant extensions +
|
||||
a local Nostr relay + supporting services. Each location is its own
|
||||
sovereign tenant; nothing has to phone home to a central server.
|
||||
|
||||
Because identity lives in the user's Nostr keypair (see above)
|
||||
rather than in any per-location account, the user experience is
|
||||
*roaming*: when someone walks into a new location they point their
|
||||
client at that location's relay and the same app surface comes up,
|
||||
populated with that location's data — their local membership,
|
||||
balances, ledger position, events. Identity travels with the user;
|
||||
data stays scoped to the place.
|
||||
|
||||
The `server-deploy` flake already supports this shape — `host8`,
|
||||
`host7`, `host4`, `host5` etc. are independent instances built
|
||||
from the same source tree.
|
||||
|
||||
## Target architecture (Nostr-native, location-portable)
|
||||
|
||||
If everything above lands, the picture collapses dramatically. There
|
||||
are no per-edge transports to draw and no per-service auth boxes —
|
||||
just **one identity**, **one client surface**, and **N self-similar
|
||||
location instances** that the client roams between by switching
|
||||
relays.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph USER["The user"]
|
||||
KEY["🔑 Nostr keypair<br/>identity · auth · permissions"]
|
||||
CLIENT["Client surface<br/>webapp · bitSpire · future apps"]
|
||||
KEY -. signs every event .-> CLIENT
|
||||
end
|
||||
|
||||
subgraph LOC_A["Location A — e.g. Castle"]
|
||||
RELAY_A(("Nostr relay"))
|
||||
SVCS_A["LNbits + extensions<br/>Fava / Beancount<br/>Authentik · Forgejo<br/>Bitcoin / Lightning"]
|
||||
SVCS_A <-- Nostr events --> RELAY_A
|
||||
end
|
||||
|
||||
subgraph LOC_B["Location B"]
|
||||
RELAY_B(("Nostr relay"))
|
||||
SVCS_B["same stack<br/>per-location data"]
|
||||
SVCS_B <-- Nostr events --> RELAY_B
|
||||
end
|
||||
|
||||
subgraph LOC_N["Location N …"]
|
||||
RELAY_N(("Nostr relay"))
|
||||
SVCS_N["same stack<br/>per-location data"]
|
||||
SVCS_N <-- Nostr events --> RELAY_N
|
||||
end
|
||||
|
||||
CLIENT == currently connected ==> RELAY_A
|
||||
CLIENT -. roam .-> RELAY_B
|
||||
CLIENT -. roam .-> RELAY_N
|
||||
```
|
||||
|
||||
Read this against the first diagram and the simplification is the
|
||||
point: the tangle of REST/WebSocket arrows becomes a single
|
||||
event-bus edge per service, the auth boxes disappear into the
|
||||
keypair, and every location is the same shape. New locations are
|
||||
just another copy of the bubble; new client apps are just another
|
||||
signer using the same key.
|
||||
|
||||
## The ATM as cash ↔ sats bridge
|
||||
|
||||
The picture above is the *sovereign* half of the story. The other
|
||||
half is that people still live in a **cash** economy — and at each
|
||||
location the ATM (`bitSpire`) is the protected portal between the two
|
||||
worlds. It sits inside the location instance and acts as a one-stop,
|
||||
identity-preserving bridge:
|
||||
|
||||
- **Cash stays anonymous.** Walking up to the ATM with a banknote
|
||||
needs no account, no KYC, no Nostr key. Cash keeps its
|
||||
permissionless, locally-spendable nature — useful at every local
|
||||
merchant whether or not they speak Lightning.
|
||||
- **Onramp: cash → sats.** The ATM credits the user's LNbits wallet
|
||||
at this location. The wallet is **custodial for now** — the
|
||||
operator holds the sats, the way a bartender holds a cash tab. The
|
||||
user trusts the operator to "carry the cash" between visits; the
|
||||
trust is bounded by the escape hatch below.
|
||||
- **Offramp: sats → cash.** The user can spend that location-wallet
|
||||
balance back out as cash through the same ATM, or use it directly
|
||||
with local merchants and LNbits extensions at the venue. No KYC
|
||||
is required in either direction.
|
||||
- **Escape to self-custody.** The custodial wallet is a checkpoint,
|
||||
not a destination. One tap pulls sats out to the user's **cold
|
||||
on-chain Bitcoin wallet** or to their **own self-custodied
|
||||
Lightning wallet** — so anyone uncomfortable trusting the operator
|
||||
can leave at any time with their full balance.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph FIAT["Local fiat economy"]
|
||||
CASH["💵 Cash<br/>anonymous · permissionless<br/>spendable at local merchants"]
|
||||
end
|
||||
|
||||
subgraph LOC["Location instance"]
|
||||
direction TB
|
||||
ATM["🏧 bitSpire ATM<br/>protected portal<br/>no KYC"]
|
||||
WALLET["⚡ LNbits wallet<br/>custodial · operator-held<br/>Nostr key = login"]
|
||||
STACK["LNbits + extensions<br/>local Nostr relay"]
|
||||
ATM <-- "nostr-native transport" --> WALLET
|
||||
WALLET --- STACK
|
||||
end
|
||||
|
||||
subgraph SELF["User self-custody (off-platform)"]
|
||||
COLD["❄️ cold on-chain<br/>Bitcoin wallet"]
|
||||
OWNLN["⚡ own Lightning<br/>wallet / node"]
|
||||
end
|
||||
|
||||
CASH <-- "cash ↔ sats" --> ATM
|
||||
WALLET == "withdraw on-chain" ==> COLD
|
||||
WALLET == "withdraw via LN" ==> OWNLN
|
||||
```
|
||||
|
||||
The ATM is therefore the **gateway** that lets a user move value
|
||||
freely between each location's legacy cash economy and the
|
||||
Nostr-native ecosystem — without surrendering the anonymity that
|
||||
cash and a Nostr keypair each provide on their own side of the
|
||||
boundary. Cash works as cash; sats work as sats; the ATM is the
|
||||
seam, and crossing it doesn't cost you your identity.
|
||||
|
||||
## Where the source lives
|
||||
|
||||
| Box | Path under `~/dev/` | Origin (Forgejo) |
|
||||
|----------------------|--------------------------------|------------------------------------|
|
||||
| Omnixient | (this repo, `/etc/nixos`) | `aiolabs/omnixient` |
|
||||
| LNbits | `lnbits/{dev,main}` | `aiolabs/lnbits` (no fork) |
|
||||
| Extensions | `shared/extensions/<name>` | `aiolabs/<name>` |
|
||||
| spirekeeper | `shared/extensions/spirekeeper`| `aiolabs/spirekeeper` |
|
||||
| webapp | `webapp/` | `aiolabs/webapp` |
|
||||
| bitSpire | `bitspire/bitspire/{main,dev}` | `aiolabs/bitspire` |
|
||||
| atm-tui | `bitspire/atm-tui` | `aiolabs/atm-tui` |
|
||||
| server-deploy | `deploy/server-deploy` | `aiolabs/server-deploy` |
|
||||
| nips (reference) | `nostr-protocol/nips` | nostr-protocol/nips (read-only) |
|
||||
| regtest | `local/docker/regtest` | `aiolabs/regtest` |
|
||||
|
||||
> Lightning.Pub is kept as a read-only reference mirror under
|
||||
> `~/dev/refs/` (see `refs.toml`), not as a runtime component.
|
||||
Loading…
Add table
Add a link
Reference in a new issue