299 lines
18 KiB
Markdown
299 lines
18 KiB
Markdown
# aiolabs system map
|
||
|
||
The orientation doc for someone returning to this stack after a
|
||
break — or arriving fresh. Where everything lives, how the pieces
|
||
fit together, who holds which keys, where to start for common
|
||
tasks.
|
||
|
||
For omni-the-desktop specifically (Hyprland, themes, mksystem,
|
||
flake structure), see [`ARCHITECTURE.md`](ARCHITECTURE.md). For
|
||
fleet deploy mechanics, see [`deploy-strategy.md`](deploy-strategy.md).
|
||
This doc is the **cross-project** map.
|
||
|
||
---
|
||
|
||
## The intent
|
||
|
||
The aiolabs stack is a self-hosted, Bitcoin/Lightning- and Nostr-
|
||
native ecosystem built around the operational needs of **Château du
|
||
Faune** — a collective farm + artist residency in Ariège, France —
|
||
designed from the start to be adopted by other communities with
|
||
similar shape (co-ops, intentional communities, small-scale
|
||
agriculture, retreat centers).
|
||
|
||
The principles:
|
||
|
||
1. **Self-hostable end to end.** No SaaS dependencies in the critical
|
||
path. Every running service has a corresponding entry in `aiolabs/
|
||
server-deploy` and can be reproduced on commodity hardware.
|
||
2. **Open protocols over proprietary schemas.** Nostr (NIP-52,
|
||
NIP-72, NIP-46), iCalendar VTODO, ActivityStreams. Other
|
||
communities can adopt the same shape; their renderers and ours
|
||
interoperate by virtue of being on the same wire.
|
||
3. **Internal-network defaults.** The community-organizer
|
||
capabilities work during WAN outages — `docs.ariege.io`, the
|
||
Nostr relay, LNbits, and the maubot daemon all colocate on
|
||
`host1` so the foyer e-ink panel and the Matrix bots keep
|
||
functioning without the internet.
|
||
4. **Identity is the user's, not the operator's.** Per-user Nostr
|
||
identity, held by a sidecar nsec bunker, with per-device scoped
|
||
tokens for every client that signs on the user's behalf. See
|
||
*Trust boundaries* below.
|
||
|
||
---
|
||
|
||
## Component map
|
||
|
||
```
|
||
───── aiolabs stack ─────
|
||
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ DEV BOX (this machine — bohm) │
|
||
│ │
|
||
│ /etc/nixos (omni) ──── this machine's NixOS + home-manager │
|
||
│ ~/dev/ ────────────── worktree of every project repo │
|
||
│ │
|
||
└────────────────────────────────────┬─────────────────────────────┘
|
||
│ deploys via
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ FLEET HOSTS (via server-deploy) │
|
||
│ │
|
||
│ host1 (Château du Faune) host2 host4 │
|
||
│ • Continuwuity • dev • staging │
|
||
│ (Matrix homeserver) • lnbits • webapp demo │
|
||
│ • maubot daemon │
|
||
│ • LNbits + nsecbunkerd │
|
||
│ • Nostr relay │
|
||
│ • castle-docs (Quartz) │
|
||
│ • nginx + sops │
|
||
│ │
|
||
│ host5 host3 host6 (WG hub) │
|
||
│ production-tier hosts │
|
||
└────────────────┬──────────────────────────────┬─────────────────┘
|
||
│ │
|
||
▼ ▼
|
||
┌────────────────────────────┐ ┌──────────────────────────────┐
|
||
│ USER-FACING SURFACES │ │ PHYSICAL DEVICES │
|
||
│ │ │ │
|
||
│ webapp (PWA) │ │ inky-impression (Pi + eink) │
|
||
│ activities / wallet │ │ foyer display │
|
||
│ forum / market / chat │ │ │
|
||
│ │ │ bitSpire ATM │
|
||
│ Matrix client (Element) │ │ Nostr-native kiosk │
|
||
│ │ │ │
|
||
│ docs.ariege.io │ │ tufty-badge, InkyImpression │
|
||
│ (Quartz static site) │ │ LivestockGuardDogs │
|
||
│ │ │ standalone hardware │
|
||
└────────────────────────────┘ └──────────────────────────────┘
|
||
```
|
||
|
||
### What lives where in `~/dev/`
|
||
|
||
| Path | Role | Where to read more |
|
||
|---|---|---|
|
||
| `deploy/server-deploy/` | NixOS flake for the fleet — single source of truth for what runs on which host | `flake.nix` + per-host modules |
|
||
| `lnbits/main/`, `lnbits/dev/`, `lnbits/nostr-transport/` | Lightning + Nostr identity backend | `lnbits/main/CLAUDE.md`; per-extension READMEs |
|
||
| `webapp/main/`, `webapp/dev/` | Vue/Vite/Electron PWA — user-facing apps over Nostr | `webapp/main/CLAUDE.md` |
|
||
| `bitspire/bitspire/{main,dev}`, `bitspire/atm-tui/` | bitSpire — KYC-free, Nostr-native Bitcoin ATM frontend (+ atm-tui) | `bitspire/bitspire/CLAUDE.md` |
|
||
| `maubot-plugins/` | Matrix bot plugins: `journal`, `tracker`, `wiki` | `maubot-plugins/CLAUDE.md`; `maubot-plugins/docs/community-organizer-spec.md` |
|
||
| `inky-impression/` | E-ink renderer for the foyer panel | `inky-impression/CLAUDE.md` |
|
||
| `docs/castle-docs/` | Operational wiki (Quartz, Obsidian-flavored markdown) | `docs/castle-docs/CLAUDE.md` |
|
||
| `lnbits-extensions/` | The aiolabs LNbits extension catalog (`extensions.json`) | `~/dev/CLAUDE.md` — see "Forgejo conventions" |
|
||
| `shared/extensions/` | LNbits extension source tree (mounted into dev compose) | Per-extension READMEs |
|
||
| `local/docker/regtest/` | Local Lightning regtest stack for end-to-end testing | `local/README.md` |
|
||
| `refs/` | Curated mirrors of upstream reference codebases (Nostr NIPs, Khatru, nostr-tools, etc.) | `refs/README.md` |
|
||
| `upstream-prs/` | Branches staged for upstream contribution | per-branch context |
|
||
|
||
### Per-project CLAUDE.md (defer to these for repo-specific work)
|
||
|
||
The cross-cutting facts live in `~/dev/CLAUDE.md`. Anything more
|
||
specific belongs in the per-project `CLAUDE.md`. Start at the most
|
||
specific one for your task and walk outward only if the answer isn't
|
||
there.
|
||
|
||
---
|
||
|
||
## The end-to-end data flow
|
||
|
||
This is the picture once everything's wired up. Some pieces (per-user
|
||
signing) are in flight — see *Status snapshot* below.
|
||
|
||
```
|
||
USER (in a Matrix room)
|
||
│
|
||
│ !task fix the south fence #urgent
|
||
▼
|
||
Continuwuity homeserver ─────── Matrix federation
|
||
│
|
||
▼
|
||
maubot daemon (tracker plugin)
|
||
│
|
||
│ resolve binding: MXID → LNbits user
|
||
│ resolve signer: LNbits → bunker URL + scoped token
|
||
│
|
||
│ build NIP-52 event (kind 31922, community a-tag)
|
||
│ NIP-46 RPC to nsecbunkerd ──→ sign as user
|
||
│
|
||
▼
|
||
Internal Nostr relay (loopback / wss://lnbits.<domain>/nostrrelay)
|
||
│
|
||
│ fan out to public relays (relay.ariege.io, …)
|
||
│
|
||
▼
|
||
PUBLIC NOSTR
|
||
│
|
||
├─── inky-impression subscribes ──→ renders foyer scene
|
||
│
|
||
├─── webapp subscribes ──→ shows user's items in PWA
|
||
│
|
||
└─── any other subscriber ──→ mobile push, etc.
|
||
```
|
||
|
||
The community-organizer protocol that defines these event shapes is at
|
||
`~/dev/maubot-plugins/docs/community-organizer-spec.md`. It's
|
||
runtime-agnostic — other projects can produce the same NIP-52 events
|
||
from any source (CLI, web form, voice assistant) and the renderers
|
||
don't care.
|
||
|
||
---
|
||
|
||
## Trust boundaries (where keys live)
|
||
|
||
Knowing what's compromised when X is compromised is the first thing
|
||
to need when something breaks. The honest picture:
|
||
|
||
| Key | Where it lives | Compromise impact | Reference |
|
||
|---|---|---|---|
|
||
| **User target keys** (`X_alice`, `X_bob`, …) | `nsecbunkerd` sidecar on the LNbits host, encrypted at rest with a passphrase the operator supplies at boot | Catastrophic for user identity *if* the bunker itself is breached. Bunker has a small attack surface (no web server, no plugins). | `aiolabs/lnbits#9`, `#18` |
|
||
| **Per-device scoped tokens** (tracker, ATM, webapp, …) | The client device that holds them. Tracker holds them in its SQLite. Webapp holds them in IndexedDB/localStorage. ATM holds them in its config. | Bounded by token scope. A compromised tracker can publish NIP-52 events as the user; nothing else. Revoke the token at the bunker side; target key unaffected. | spec §7.3 |
|
||
| **Operator master `M_lnbits`** | `nsecbunkerd` admin key. Used by LNbits to authorize admin RPCs (create-account, issue-token, revoke). | Full identity-pool admin access. Operator is on the hook. | `aiolabs/lnbits#18` |
|
||
| **LNbits agent npub** | LNbits process memory, sops-managed at rest | Allows the holder to call `create_account` / `create_token` / `revoke_user` — but NOT to sign as users directly (no nsec). Bounded compared to holding the master. | `aiolabs/lnbits#18` admin client section |
|
||
| **maubot bot keypair** (`@trackerbot:ariege.io`) | maubot's sops-managed config on host1 | Can publish events under the bot's own pubkey (used as fallback when a user is unbound). No access to user signing. | `server-deploy/modules/services/maubot.nix` |
|
||
| **Server identity (`NOSTR_TRANSPORT_PRIVATE_KEY`)** | LNbits env var, sops-managed | Allows impersonation of the LNbits instance itself on the Nostr transport channel. Migrating to bunker-signed in #18. | `aiolabs/lnbits#4`, `#9` |
|
||
| **Wireguard / SSH host keys** | Per host, NixOS-generated | Lateral movement between fleet hosts. Operator concern. | `server-deploy/hosts/<host>/` |
|
||
|
||
### What's public vs community-private
|
||
|
||
| Channel | Audience |
|
||
|---|---|
|
||
| `wss://relay.ariege.io` | Public Nostr — anyone can subscribe. Community-organizer events for outward-facing rooms land here. |
|
||
| `wss://lnbits.ariege.io/nostrrelay/<channel>` | Auth-gated internal relays. `nsecbunker` channel is bunker RPC; other channels carry community-scoped events that shouldn't leave the host. |
|
||
| Matrix rooms | Federated by default. Set rooms to invite-only for community-private discussions; the bot reads room membership as trust boundary. |
|
||
| LNbits API | Per-user auth (NIP-98 or session). Bot agent has its own scoped credentials. |
|
||
| `docs.ariege.io` | Public read; published Quartz site. Castle-docs `private/` directory is git-ignored. |
|
||
|
||
---
|
||
|
||
## Cross-repo dependencies
|
||
|
||
When you change repo A, repo B may need a follow-up to actually
|
||
deploy. The chain:
|
||
|
||
```
|
||
~/dev/<project>/ changes go here first
|
||
│
|
||
▼
|
||
`aiolabs/<project>` (forgejo) push to the canonical remote
|
||
│
|
||
▼
|
||
`aiolabs/server-deploy` flake.lock bump the input pointing at <project>
|
||
│ (`nix flake lock --update-input <project>`)
|
||
▼
|
||
deploy.sh <host> rolls the change to the host
|
||
```
|
||
|
||
Catalogs (LNbits extension catalog, etc.) are the exception — they
|
||
read live from `aiolabs/lnbits-extensions/extensions.json` at install
|
||
time, so no flake.lock bump is needed there. See
|
||
`~/dev/CLAUDE.md` "Forgejo conventions" for the per-repo specifics
|
||
(direct-to-main vs PR, fork versioning scheme, catalog rules).
|
||
|
||
---
|
||
|
||
## Status snapshot (2026-05-26)
|
||
|
||
Not everything in the data-flow diagram is shipped. The current state:
|
||
|
||
| Capability | Status |
|
||
|---|---|
|
||
| Matrix + Continuwuity homeserver on host1 | ✅ live |
|
||
| `journal` maubot plugin (`@journalbot:ariege.io`) | ✅ live |
|
||
| `tracker` maubot plugin (Phase 1 — Matrix + SQLite, rules classifier) | ✅ in repo, ready to deploy |
|
||
| `wiki` maubot plugin (HTTP search against `docs.ariege.io`) | ✅ in repo, ready to deploy |
|
||
| Community-organizer protocol spec | ✅ at `~/dev/maubot-plugins/docs/community-organizer-spec.md` |
|
||
| LNbits Nostr-transport (HTTP-free API) | ✅ merged (#4) |
|
||
| LNbits signer abstraction + transitional LocalSigner | ✅ Phase 1 of #9 — PR #17 (open, awaiting cascading merge) |
|
||
| **`nsecbunkerd` aiolabs fork functional** | ✅ Fork at `aiolabs/nsecbunkerd@master`; 4 upstream-rot patches applied (`#1`–`#4`, `#8`); native nix flake at commit `711a017`; 4 known upstream issues still open (`#5`/`#6`/`#7`/`#8` echo + multi-arch + getKeys) |
|
||
| **LNbits bunker provisioning primitive (phase 2.1)** | ✅ PR #19 (open, stacked on #17) — `NsecBunkerAdminClient` + `RemoteBunkerSigner`; live `ping` + `create_new_key` round-trip ~30ms |
|
||
| LNbits NIP-46 `sign_event` over bunker (phase 2.2-2.3) | 🚧 blocked on upstream `aiolabs/nsecbunkerd#4`/`#7` stabilizing |
|
||
| `error_code` wire vocabulary (14 codes, ATM ↔ LNbits) | ✅ agreed; lands alongside phase 2.3 — see `~/dev/coordination/log.md` |
|
||
| Extension `account.prvkey` migration to `resolve_signer` | 🚧 pre-cascade prerequisite for PR #17 — umbrella at `aiolabs/lnbits#21`; per-extension issues filed (`nostrmarket#5`, `restaurant#11`, `tasks#3`, `events#23`, spirekeeper TBD) |
|
||
| Tracker Nostr publish bridge (Phase 2a) | ⏸ waiting on bunker phase 2.3 |
|
||
| Per-user signing via bunker (Phase 2c) | ⏸ waiting on bunker phase 2.3 |
|
||
| `inky-impression` Nostr-native scene plugin | ⏸ waiting on tracker publishing |
|
||
| Webapp Nostr identity via NIP-46 | ⏸ waiting on bunker phase 2.3 |
|
||
|
||
### 2026-05-26 day's shipping
|
||
|
||
- **Cross-session coordination log** established at `~/dev/coordination/log.md` (protocol in sibling `README.md`). Replaces user-relayed messages between the parallel `lnbits`, `bitspire`, `spirekeeper`, and `alfred` sessions. Append-only, chronological.
|
||
- **LNbits PR #19** opened — operator-IdP signup primitive via `aiolabs/nsecbunkerd`. Stacked under PR #17 awaiting cascading merge.
|
||
- **`aiolabs/nsecbunkerd`** — 4 upstream-rot patches landed on `master` so the fork builds + runs from a clean clone (`06272c8`, `960b939`, `42dbbd7`, `e39eaa6`); 8 issues filed total documenting the upstream gaps.
|
||
- **`aiolabs/lnbits#20`** — outbound `_fanout_payment` defense-in-depth dedup (low priority, symmetric to `aiolabs/bitspire#50`).
|
||
- **`aiolabs/lnbits#21`** — umbrella audit catching 5 extensions still reading `account.prvkey` directly. Each affected ext now has its own tracking issue (or pre-existing for `nostrmarket#5`).
|
||
- **Canonical sat-amount vocabulary** locked across spirekeeper / bitspire / atm-tui: `wire_sats` / `principal_sats` / `fee_sats` / `fee_fraction` (unit fraction, never percentage). See `~/dev/coordination/log.md` 2026-05-26T17:10Z + 18:50Z entries.
|
||
- **`bitspire` #49 + #50** closed (Schnorr-verify on RPC replies + two-tier hash dedup on subscribe_payments callbacks). Symmetric lift on the bunker-client side landed in PR #19 commit `4ebcd959`.
|
||
|
||
---
|
||
|
||
## Start-here runbook
|
||
|
||
| I want to… | Read first |
|
||
|---|---|
|
||
| Add a new aiolabs project | `~/dev/CLAUDE.md` (workspace conventions); pick the repo's own CLAUDE.md once it exists |
|
||
| Deploy a change to a fleet host | `deploy-strategy.md` in this dir; `aiolabs/server-deploy` README |
|
||
| Add a new maubot plugin | `~/dev/maubot-plugins/CLAUDE.md`; the multi-line `@command.passive` footgun in `~/dev/CLAUDE.md` |
|
||
| Modify a verb / event shape | `~/dev/maubot-plugins/docs/community-organizer-spec.md` **first**, then the plugin code |
|
||
| Add a page to the operational wiki | `~/dev/docs/castle-docs/CLAUDE.md`; commit + push triggers Quartz rebuild |
|
||
| Render something on the foyer e-ink panel | `~/dev/inky-impression/` (currently HTTP-screenshot; Nostr scenes are planned) |
|
||
| Fork or bump an LNbits extension | `~/dev/CLAUDE.md` "Extension version-bump procedure" |
|
||
| Add a NixOS option / tool on this dev box | `/etc/nixos/home.nix` or `/etc/nixos/configuration.nix`, then `omni-rebuild` |
|
||
| Where are user nsecs? | `nsecbunkerd` on the LNbits host. See `aiolabs/lnbits#9`, `#18`. **Not in lnbits's DB.** |
|
||
| Where do connection tokens live? | The client device (maubot SQLite, webapp localStorage, ATM config). Revoke at the bunker. |
|
||
|
||
---
|
||
|
||
## Glossary
|
||
|
||
A few terms recur across the stack:
|
||
|
||
- **IdP** — identity provider. In our stack, LNbits is the IdP for end
|
||
users; it brokers token issuance against the sidecar bunker.
|
||
- **Bunker** — `nsecbunkerd` (or any NIP-46-compliant sidecar) that
|
||
holds the actual nsec material. Speaks kind-24133 (signing) and
|
||
kind-24134 (admin) over a relay.
|
||
- **Target key** — a per-user nsec held inside the bunker. Addressable
|
||
by its npub; never leaves the bunker.
|
||
- **Operator master `M_lnbits`** — the bunker's admin key. Used to
|
||
authorize admin RPCs (create-account, issue-token, revoke).
|
||
- **Connection token** — a per-device NIP-46 credential, scoped to
|
||
specific event kinds, issued by the bunker to a specific client
|
||
(tracker, webapp, ATM). Compromise leaks only the scope.
|
||
- **Signer abstraction** — the `NostrSigner` ABC in
|
||
`lnbits/core/signers/` and mirrored in tracker. Four implementations:
|
||
`BotSigner`, `RemoteBunkerSigner` (steady state), `ClientSideOnly
|
||
Signer` (sovereignty exit), `LocalSigner` (transitional migration
|
||
helper).
|
||
- **Community a-tag** — NIP-72 community reference, `34550:<pubkey>:
|
||
<d-tag>`. The way every community-organizer event scopes itself to
|
||
a community.
|
||
- **Capture / clarify** — GTD framing. `!add` is capture (frictionless,
|
||
inbox); the rules classifier (or future LLM) does the clarify step
|
||
that sorts into a kind.
|
||
- **Universal verb vs community shortcut** — the spec mandates universal
|
||
verbs (`!add`, `!task`, `!journal`, `!remind`, `!done`, `!list`).
|
||
Communities configure their own shortcuts (`!buy`, `!steward`, …)
|
||
per room.
|
||
- **Spec-first** — the working convention for cross-system features:
|
||
write the protocol doc before the implementation. See community-
|
||
organizer-spec.md as the worked example.
|