18 KiB
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. For
fleet deploy mechanics, see 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:
- Self-hostable end to end. No SaaS dependencies in the critical
path. Every running service has a corresponding entry in
aiolabs/ server-deployand can be reproduced on commodity hardware. - 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.
- Internal-network defaults. The community-organizer
capabilities work during WAN outages —
docs.ariege.io, the Nostr relay, LNbits, and the maubot daemon all colocate onhost1so the foyer e-ink panel and the Matrix bots keep functioning without the internet. - 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 siblingREADME.md). Replaces user-relayed messages between the parallellnbits,bitspire,spirekeeper, andalfredsessions. 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 onmasterso 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_paymentdefense-in-depth dedup (low priority, symmetric toaiolabs/bitspire#50).aiolabs/lnbits#21— umbrella audit catching 5 extensions still readingaccount.prvkeydirectly. Each affected ext now has its own tracking issue (or pre-existing fornostrmarket#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.md2026-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 commit4ebcd959.
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
NostrSignerABC inlnbits/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.
!addis 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.