286 lines
13 KiB
Markdown
286 lines
13 KiB
Markdown
# 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.
|