diff --git a/README.md b/README.md index cb0b93d..25add9e 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,11 @@ lb fix-issue-123 # cd ~/dev/lnbits/fix-issue-123 - [`docs/lnbits-upstream-flow.md`](docs/lnbits-upstream-flow.md) — reference for how `lnbits/lnbits` itself moves: `dev` / `main` branch model, squash-merge convention, release tagging. +- [`docs/lnbits-workspace-notes.md`](docs/lnbits-workspace-notes.md) — + practical gotchas and design constraints for day-to-day lnbits work: + port choice, `LNBITS_SRC` build-context traps, the + extension-folder-upgrade-wipes-fork issue, Nostr key handling, + CLINK protocol scope, fork versioning. ## Contributing to this scaffold diff --git a/docs/lnbits-workspace-notes.md b/docs/lnbits-workspace-notes.md new file mode 100644 index 0000000..728af93 --- /dev/null +++ b/docs/lnbits-workspace-notes.md @@ -0,0 +1,162 @@ +# lnbits workspace notes + +Practical reference for day-to-day work in an lnbits dev environment. +Collected gotchas, conventions, and design constraints that have +repeatedly surprised people. Not a tutorial — assumes you're already +running lnbits and contributing or extending it. + +## Pick a non-default port for your local dev server + +LNbits defaults to `:5000`. That collides with macOS AirPlay Receiver, +common docker-compose stacks, and other dev tools. Pick something +unambiguous (many forks settle on `:5001`) and stick to it across: + +- `settings.nix` → `lnbits.port` +- any MCP server config you wire against your dev instance +- bookmarks, env files, CI configs + +Once you wire it in three places, switching the port retroactively is +mostly find-and-replace pain. Decide early. + +## LNBITS_SRC and docker-compose build context + +If you run a docker-compose dev stack (regtest or otherwise) that +builds lnbits from a local checkout, the `Dockerfile` typically reads +from `${LNBITS_SRC:-/some/default}`. + +The trap: **commits to your day-to-day worktree don't reach the dev +image if `LNBITS_SRC` is currently pointed elsewhere** (e.g. at a +feature-branch worktree you were testing). Even +`docker compose build --no-cache` happily rebuilds from the wrong +checkout. + +Sanity-check before assuming a rebuild picked up your change: + +```sh +docker compose config | grep -A2 lnbits +# look for the resolved `context:` path +``` + +If it's pointing somewhere stale, either cherry-pick your commit onto +the active build branch or flip `LNBITS_SRC` and rebuild. + +## Extension folder is the install target — "Upgrade" wipes forks + +If your dev setup mounts an extension checkout directly into the lnbits +container (e.g. `~/dev/shared/extensions/` → `/shared`, with +`LNBITS_EXTENSIONS_PATH=/shared`), **the extension git checkout IS the +installed extension**. There's no separate copy. + +Consequence: clicking **"Upgrade"** in the LNbits UI on an extension +that's mounted from a fork checkout will: + +1. Download the catalog tarball. +2. Extract it directly over the mounted directory. +3. **Wipe `.git`**, replace every file with the catalog version, and + silently discard any local changes / fork patches. + +If this happens, recover with `git clone ` over +the wiped directory. Mitigations: + +- Set an extension version in your fork that always sorts above the + catalog's so "Upgrade" never thinks the catalog is newer. +- Or don't expose the catalog upgrade UI to a workflow that has + mounted-fork extensions. + +## Nostr key handling — don't persist plaintext nsecs + +As of 2026, LNbits's upstream account model server-generates a Nostr +private key (`accounts.prvkey`) for every new account and stores it +**plaintext in the DB**. The key is returned to clients over HTTPS via +auth endpoints. A DB snapshot, backup leak, or curious operator is a +wholesale identity compromise for every user. + +If your fork or extensions touch user Nostr keys, design around this. +A defensible shape: + +- A `NostrSigner` abstraction with at least three implementations: + - `LocalSigner` — server-side key, but envelope-encrypted at rest. + User unlocks per session. + - `RemoteBunkerSigner` — NIP-46 connection to a bunker the user + runs themselves. Server never sees the private key. + - `ClientSideOnlySigner` — the server only knows the user's + public key. Signing happens entirely in the browser / NIP-07 + extension / hardware signer. +- Optionally a `DelegatedSigner` via NIP-26 for accounts that want + a server-side signer for low-value operations while keeping + high-value ops client-side. + +Also audit every server-side Nostr key your extensions persist +(merchant signing keys, notification keys, transport keys, …) and +either encrypt at rest or document why ephemeral is fine. + +The general principle: **prefer "don't store the key" when possible.** +Let the user's signer (browser extension, hardware device, NIP-46 +bunker) do the signing. + +## CLINK is Lightning.Pub-specific, not LNbits + +Shocknet's [CLINK protocol](https://github.com/shocknet/CLINK) — the +Nostr-native payment flow with event kinds 21001 (offer/noffer), +21002 (debit/ndebit request), 21003 (debit response) — lives inside +`Lightning.Pub`, not LNbits. LNbits as of 2026 has zero CLINK +knowledge. + +If you're thinking about `ndebit` / `noffer` semantics in an lnbits +context: + +- They're Lightning.Pub primitives. LNbits doesn't implement them. +- Building "equivalent outcomes" in LNbits typically goes through + LNURL + payment-hash subscriptions + opaque `extra` payloads + rather than CLINK-style events. Different primitives, similar + effective semantics. +- If you genuinely need CLINK in lnbits, it's a new transport + module, not a tweak to existing code paths. NIP-44 plumbing in + the nostr-transport surface is the natural building block. + +Don't conflate the two — keep CLINK semantics on the Lightning.Pub +side of any cross-system design. + +## Fork versioning — surface your fork in the version string + +If you maintain a long-lived fork (especially one running in +production), the released version string should make it obvious that +fork-modified code is what's running. Consumers of the package, log +readers, and bug reporters all benefit. + +A common pattern: + +- **Git tag:** `v-.` + e.g. `v1.5.4-aio.1`, `v1.5.4-aio.2`, … +- **`pyproject.toml` `version`:** `+.` + e.g. `1.5.4+aio.1`. + +The two spellings differ because PEP 440 doesn't accept +hyphen-with-arbitrary-suffix as a valid package version, but the local- +version form (`+.`) is valid. Extensions don't have the PEP 440 +constraint (their `config.json` is freeform), so the tag form works +everywhere except `pyproject.toml`. + +Decide your own tag string and stick to it. The key property is that +`importlib.metadata.version("lnbits")` and the LNbits UI footer make +it visible to anyone looking. + +For the upstream lnbits branch / release model these tags sit on top +of, see [lnbits-upstream-flow.md](lnbits-upstream-flow.md). + +## Reading the codebase fast + +For mass-grep and reference reading, point a tools-friendly mirror of +the lnbits source at a known location (see your `~/dev/refs/` setup if +you use one). Key paths once you have a checkout: + +- `lnbits/core/services/` — core money flows. Start here for invoice + / payment / wallet logic. +- `lnbits/wallets/` — backend implementations. One file per + `LNBITS_BACKEND_WALLET_CLASS` value. +- `lnbits/extensions/` — first-party extensions. Third-party + extensions follow the same shape (`config.json`, `views.py`, + `migrations.py`, etc.). +- `git log --first-parent dev --oneline` — see the squash-merge + sequence into `dev` (see lnbits-upstream-flow.md for why + `--first-parent` matters here).