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
243
modules/dev-env/docs/lnbits-workspace-notes.md
Normal file
243
modules/dev-env/docs/lnbits-workspace-notes.md
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
# 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 <your-fork-url> <dir>` 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.
|
||||
|
||||
## Settings precedence: `.env` seeds the DB on first boot, then the DB wins
|
||||
|
||||
This one trips up most people who deploy lnbits declaratively (via NixOS,
|
||||
docker-compose, ansible, …). Verified 2026-05-24 against upstream
|
||||
`lnbits/main`.
|
||||
|
||||
LNbits has two sources of truth for settings depending on lifecycle.
|
||||
**On boot, when `lnbits_admin_ui=True`** (the default):
|
||||
|
||||
1. Read DB row via `get_super_settings()`
|
||||
(`lnbits/core/services/users.py:236`).
|
||||
2. If the DB row is empty → seed it from `.env` via
|
||||
`init_admin_settings()`. First-boot only.
|
||||
3. `update_cached_settings(settings_db.dict())` overwrites the
|
||||
in-memory `Settings` with the DB row. **`.env` values loaded by
|
||||
Pydantic at startup are clobbered.**
|
||||
|
||||
**Practical consequence:** once an instance has booted once, editing
|
||||
`.env` and restarting **changes nothing** for editable fields. They can
|
||||
only be changed via:
|
||||
|
||||
- The Admin UI (`PUT /api/v1/settings`, gated by `check_admin`), or
|
||||
- Clearing the relevant rows in the `system_settings` table in the
|
||||
core DB.
|
||||
|
||||
**Exceptions where `.env` still wins on every boot:**
|
||||
|
||||
- `super_user` — env overrides DB explicitly in `users.py`.
|
||||
- `lnbits_admin_ui=False` — the whole DB-load block is skipped; env
|
||||
stays authoritative (there's no Admin UI to populate the DB anyway).
|
||||
- All `ReadOnlySettings` fields (defined in `lnbits/settings.py` —
|
||||
`EnvSettings` / `PersistenceSettings` / `SuperUserSettings` /
|
||||
`ExtensionsInstallSettings`). Concretely:
|
||||
|
||||
- `host`, `port`
|
||||
- `lnbits_path`, `lnbits_data_folder`, `lnbits_extensions_path`
|
||||
- `lnbits_database_url`
|
||||
- `auth_secret_key`, `first_install_token`
|
||||
- `lnbits_title` (the API title — NOT `lnbits_site_title`, which is
|
||||
editable)
|
||||
- `lnbits_admin_ui` itself
|
||||
- `lnbits_allowed_funding_sources` (the *list of which sources can be
|
||||
enabled* — the per-source credentials live in `FundingSourcesSettings`
|
||||
→ `EditableSettings` and ARE DB-frozen)
|
||||
|
||||
`update_cached_settings` skips any key in `readonly_variables`, so
|
||||
env-loaded values for these fields survive the DB-load overwrite.
|
||||
|
||||
**Editable** settings — site title/tagline, theme, watchdog thresholds,
|
||||
fee defaults, rate limits, all per-funding-source credentials, the
|
||||
whole Admin UI form — get DB-frozen after first boot.
|
||||
|
||||
### `LNBITS_FIRST_INSTALL_TOKEN` rotation ≠ "reset settings to env"
|
||||
|
||||
Rotating the first-install token creates a new super_user account with
|
||||
a fresh random UUID and re-enables the `/first_install` endpoint. It is
|
||||
an **escape hatch for a locked-out admin** to re-claim the instance
|
||||
with a new username/password. It does NOT refresh any settings values
|
||||
from `.env` — by the time it runs, the DB row has already overwritten
|
||||
cached `settings`, and `init_admin_settings()` then upserts those same
|
||||
DB values back, so no env values flow through.
|
||||
|
||||
### Deploy-side implication
|
||||
|
||||
If you manage lnbits config declaratively (NixOS module, ansible role,
|
||||
docker-compose env file), **editable env vars only take effect on a
|
||||
fresh install** (empty `settings` table). For an existing deployment,
|
||||
changing them in your declarative source and redeploying won't change
|
||||
runtime behavior — you have to edit through the Admin UI OR clear the
|
||||
relevant rows in `system_settings`.
|
||||
|
||||
The "set it in nix, redeploy, done" mental model only works for
|
||||
`ReadOnlySettings` fields. Anything an admin can edit in the UI is
|
||||
DB-frozen post-first-boot. Plan your deploy story accordingly:
|
||||
|
||||
- For `ReadOnlySettings` (host, port, paths, secret_key, …):
|
||||
declarative source is authoritative on every boot. Reproducible.
|
||||
- For `EditableSettings` (site title, fees, funding-source creds, …):
|
||||
declarative source is a *seed*, not authoritative. To re-seed an
|
||||
existing instance, you must explicitly clear the DB row.
|
||||
|
||||
## 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<upstream-version>-<your-tag>.<N>`
|
||||
e.g. `v1.5.4-aio.1`, `v1.5.4-aio.2`, …
|
||||
- **`pyproject.toml` `version`:** `<upstream-version>+<your-tag>.<N>`
|
||||
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 (`+<tag>.<N>`) 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).
|
||||
Loading…
Add table
Add a link
Reference in a new issue