diff --git a/README.md b/README.md index 8030ac6..8dd7b38 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,40 @@ in follow-up passes. The `dev` CLI is the single entry point. See the top of this README for the verb set. +### Claude orientation seeding (optional) + +If you use [Claude Code](https://docs.anthropic.com/en/docs/claude-code), +opt in to having curated CLAUDE.md files seeded into your workspace. +The files live under `files/` in this repo and are symlinked into +`~/dev/` via home-manager's `mkOutOfStoreSymlink` — edits in your +checkout take effect on the next Claude session, no rebuild. + +Wire it in `settings.nix`: + +```nix +devEnv = { + enable = true; + scaffoldPath = "/home//dev/lnbits-sensei"; # absolute path to your checkout + claude = { + enable = true; # ~/dev/lnbits/CLAUDE.md + workspaceOrientation = false; # also ~/dev/CLAUDE.md (opt-in; + # clobbers any existing one) + }; +}; +``` + +- `claude.enable` seeds `~/dev/lnbits/CLAUDE.md` from + `files/lnbits-CLAUDE.md` — project-level orientation that loads for + any Claude session in the lnbits worktree subtree (settings + precedence, Quasar UMD self-closing-tag rule, auth-decorator + distinctions, …). +- `claude.workspaceOrientation` *additionally* seeds `~/dev/CLAUDE.md` + from `files/dev-CLAUDE.md` — generic workspace orientation. Off by + default because most people already maintain their own. + +If you don't use Claude Code, leave both off — the same content is in +`docs/` for human reading. + ### Workspace layout (the `~/dev/` tree) The dev-env module's job is to put every project, every worktree, and @@ -192,7 +226,33 @@ lb dev # cd ~/dev/lnbits/dev lb fix-issue-123 # cd ~/dev/lnbits/fix-issue-123 ``` -## Contributing +## Further reading + +- [`docs/remotes.md`](docs/remotes.md) — the three remote-topology + patterns (upstream-only / github-fork / multi-remote). +- [`docs/upstream-prs.md`](docs/upstream-prs.md) — how to send a PR + upstream using the `~/dev/upstream-prs/` worktree flow. Includes + a primer for anyone new to forks / pull requests. +- [`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, settings precedence + (`.env` vs DB), Nostr key handling, CLINK protocol scope, fork + versioning. +- [`docs/lnbits-extension-dev.md`](docs/lnbits-extension-dev.md) — + building and maintaining LNbits extensions: auth-decorator + distinctions, FakeWallet vs regtest testing strategy, the + `migrations_fork.py` pattern for keeping fork-only schema deltas + out of upstream-tracked `migrations.py`. +- [`docs/lnbits-frontend-gotchas.md`](docs/lnbits-frontend-gotchas.md) — + Vue/Quasar UMD traps in lnbits page templates: no self-closing + tags, CSS specificity vs Quasar's `!important` utilities, cache + busting via `?v={server_startup_time}`, dark-mode color discipline. + +## Contributing to this scaffold Iteration happens under the sandboxed-claude policy from omnixy's `scripts/sandbox-claude.sh` — a per-launch `.claude/settings.json` diff --git a/docs/lnbits-extension-dev.md b/docs/lnbits-extension-dev.md new file mode 100644 index 0000000..3f7e27c --- /dev/null +++ b/docs/lnbits-extension-dev.md @@ -0,0 +1,143 @@ +# lnbits extension development + +Reference for building and maintaining LNbits extensions — the parts +that catch first-time extension authors and the patterns worth +adopting once you maintain a fork-modified extension. + +## Auth decorators + +Easy to confuse. Different scopes: + +| Decorator | Auth scope | Returns | Use for | +|---|---|---|---| +| `require_invoice_key` | Wallet invoice key — read access | `Wallet` | Read endpoints (balance, payment history) callable with a user's invoice key | +| `require_admin_key` | Wallet admin key — write to **own wallet only** | `Wallet` | Endpoints that create payments / modify wallet state | +| `check_admin` | **LNbits instance admin** (super_user + `lnbits_admin_users`), Bearer token | `Account` | Cross-user / admin-only operations | +| `check_super_user` | LNbits super user only, Bearer token | `Account` | Operations restricted to the single super_user | + +**`require_admin_key` ≠ `check_admin`.** This is the most common +misuse. `require_admin_key` is a *wallet*-level write key — every +user has one for each of their own wallets. `check_admin` is +*instance* admin access. Endpoints that operate on other users' data +or change global settings need `check_admin`, not `require_admin_key`. + +## Testing + +Use **FakeWallet** +(`LNBITS_BACKEND_WALLET_CLASS=FakeWallet`) for testing extension CRUD, +API endpoints, and UI changes. Spin up the full regtest stack only +when end-to-end Lightning payment behavior is actually under test. + +**Why:** regtest takes time to start (docker build + multiple LND/CLN +containers), uses real resources, and adds no fidelity for non-payment +flows. FakeWallet makes payments succeed instantly with no network — +ideal for testing the surrounding logic. + +If you ship a `dev` CLI (see lnbits-sensei's pattern), keep +`--fakewallet` as the default mode for this reason. + +## Fork-migrations pattern (`migrations_fork.py`) + +Long-lived forks need to add schema columns and tables on top of +upstream's. The naive approach — append fork-only migration functions +to `migrations.py` — guarantees merge conflicts on every upstream +rebase. The pattern below sidesteps that by keeping `migrations.py` +byte-identical to upstream and putting fork deltas in a sibling file. + +> **Note:** this pattern requires a patch to LNbits core's +> `migrate_extension_database()` that loads `migrations_fork.py` under +> a `_fork` key in the `dbversions` table. As of 2026 that +> patch is fork-internal (an upstream PR is the natural follow-up). +> If you maintain a fork that ships this patch, the rest of this +> section is the user-facing pattern. + +### Architecture facts + +- `dbversions` lives in the **core LNbits DB** (`database.sqlite3`), + not per-extension. Schema: `(db TEXT PRIMARY KEY, version INT)`. + `update_migration_version` is an INSERT-OR-UPDATE, so a new + `_fork` row appears on first run with no schema migration + needed core-side. +- Extension data tables live in `ext_.sqlite3` (SQLite) or a + Postgres schema named after ``. Created lazily on first + `Database.connect()` via `ATTACH` (SQLite) or `CREATE SCHEMA` + (Postgres). +- The version-bump connection is routed based on `db.schema`: `None` + means a core migration (same connection), set means an extension + (opens a fresh `core_db.connect()` to write `dbversions`). See + `lnbits/core/helpers.py:run_migration`. +- **No cross-DB atomicity.** Extension migration writes commit to + `ext_.sqlite3`; the `dbversions` upsert commits to + `database.sqlite3`. If the extension write succeeds and the + dbversions write fails, the migration is orphaned — re-runs on + next startup. **Every fork migration MUST be idempotent** (use + an `_alter_add_column_safe` wrapper that swallows + duplicate-column errors, `CREATE TABLE IF NOT EXISTS`, etc.). + Self-healing covers the orphan case. + +### Squash recipe (adopting the pattern on an existing fork) + +If your fork has accumulated fork-only migrations interleaved into +`migrations.py`: + +1. **Restore `migrations.py` to upstream-byte-identical content.** + Drop all fork-only `m{NNN}_*` functions and any helper added only + for them. +2. **Create `migrations_fork.py`** with a SINGLE + `m001___schema` function that idempotently applies + every fork-only schema delta the old migrations used to do. One + readable file forever. +3. The squash uses `_alter_add_column_safe` per ALTER and + `CREATE TABLE IF NOT EXISTS` per table — no-ops cleanly on + installs that already ran the old fork migrations. + +### One-time fix on existing installs adopting the pattern + +Installs that previously ran the old fork migrations have their +`dbversions['']` row ahead of upstream (e.g. `events|11` after +fork-only `m007`–`m011`). After moving to `migrations_fork`, the +next upstream rebase that adds e.g. `m007_add_allow_fiat` would +compare `7 > 11 → false` and **silently skip** the new upstream +migration. + +**Reset the row to match upstream's actual migration count** before +the rebase lands: + +```sql +-- Run against the CORE DB (database.sqlite3), not the extension DB. +UPDATE dbversions SET version = WHERE db = ''; +``` + +For containerized dev where the file is root-owned inside the +container: + +```bash +docker compose exec lnbits python3 -c "import sqlite3; \ + c = sqlite3.connect('database.sqlite3'); \ + c.execute(\"UPDATE dbversions SET version = WHERE db = ''\"); \ + c.commit()" +``` + +### Upstream-overlap scenario at rebase time + +If upstream eventually adds a schema change you already carry in +`migrations_fork.py` (e.g. they add the same `ALTER TABLE … ADD COLUMN +bar` you shipped), the next rebase creates a problem: fresh installs +work (your `_alter_add_column_safe` guards swallow the dup), but +**existing installs crash** when upstream's now-redundant migration +runs without an idempotency guard (upstream rarely uses them). + +Mitigation at rebase time: + +1. **Prune the redundant block from `migrations_fork.py`** so future + fresh installs get the column from upstream's migration. +2. **Pre-deploy `dbversions` surgery on every affected install**: + `UPDATE dbversions SET version = WHERE db = ''` + so the loader skips the now-overlapping upstream migration. + +Do both — (1) keeps future fresh installs clean, (2) keeps existing +installs alive through the deploy. + +**Don't** patch upstream's migration with idempotency guards in your +fork. That breaks the "migrations.py == upstream byte-identical" +property and reintroduces every-rebase conflicts. diff --git a/docs/lnbits-frontend-gotchas.md b/docs/lnbits-frontend-gotchas.md new file mode 100644 index 0000000..cbd1b7b --- /dev/null +++ b/docs/lnbits-frontend-gotchas.md @@ -0,0 +1,105 @@ +# lnbits frontend gotchas + +LNbits ships its UI with **Vue 3 + Quasar 2 as UMD globals** — no +build step, plain Jinja templates with per-page JS. This applies +across lnbits core (`lnbits/templates/*.html`), every extension +(`/templates/`), and every fork that doesn't restructure the +frontend stack. The UMD load model has several traps that don't +manifest under the build-step model most Vue tutorials assume. + +## No self-closing tags + +Per [Quasar's UMD usage rules](https://quasar.dev/start/umd/#usage), +components must use the explicit-close form: + +```html + + + + + + + +``` + +**Why:** UMD-loaded templates are parsed by the browser's HTML parser, +not Vue's compiler. The HTML parser doesn't honor self-close on +non-void elements (per the HTML spec). The close tag gets implied at +the wrong place, nesting breaks silently, and subsequent siblings end +up nested inside the prior component. + +Self-closing is fine in `.vue` SFCs (the build step rewrites them +before the browser sees anything), so if you copy a snippet from a +Vue SFC repo into an LNbits template, **expand all self-closing tags +before saving**. + +## CSS specificity trap + +LNbits applies its own theme overrides on Quasar's typography +utilities (`.text-caption`, `.text-grey-*`, etc.) with `!important`. +Class-based CSS rules in an extension page — *even with `!important`* — +lose this fight unless your selector is strictly more specific than +the upstream rule. + +**Rule:** for per-element typography/color overrides on LNbits pages, +reach for Vue `:style` bindings (or static `style="..."` attrs), not +` + + + + +``` + +Background/border tweaks at card-level via class are fine — the trap +is specifically the typography utilities (`text-*`) and Quasar's +color utilities. + +## Cache busting + +Static assets are served with `?v={server_startup_time}` appended +(see `static_url_for` in `lnbits/helpers.py`). Consequences: + +- **Bumping JS requires a server restart.** Reloading the browser + doesn't help if `?v=` hasn't changed — the browser keeps serving + the cached file. +- **Jinja templates re-render on every request** (the `?v=` is only + on static assets). No restart needed for template edits — just + refresh. + +If a browser keeps serving stale JS after a restart, hard-refresh +(`Ctrl+Shift+R`) to bypass the HTTP cache. + +## Dark-mode color discipline + +LNbits's dark theme inverts text colors on most surfaces but **not** +on `bg-{color}-1` pale-background utilities. Result: a `bg-red-1` +without an explicit text color renders white-on-cream under dark +theme — basically invisible. + +**Rule:** pair every pale-background utility with an explicit dark +text class: + +```html + +
Warning
+ + +
Warning
+``` + +Same for `bg-green-1`, `bg-blue-1`, `bg-amber-1`, etc. The `text-grey-9` +choice is the safe default; pick a darker shade if you want stronger +contrast. + +## When in doubt + +Test under both light and dark themes (Quasar's theme toggle is at +the top of every LNbits page once you're logged in). Most of the +above gotchas are silent under one theme and obvious under the other +— don't ship UI without flipping the toggle at least once. diff --git a/docs/lnbits-upstream-flow.md b/docs/lnbits-upstream-flow.md new file mode 100644 index 0000000..3284f69 --- /dev/null +++ b/docs/lnbits-upstream-flow.md @@ -0,0 +1,128 @@ +# `lnbits/lnbits` — Upstream Development Flow + +Reference for [`github.com/lnbits/lnbits`](https://github.com/lnbits/lnbits). +Verified against git history on 2026-05-18; bump the date when you +re-verify, and patch this doc if the model has drifted. + +## Branch model + +Two long-lived branches: + +| Branch | Role | How it moves | +|---|---|---| +| `dev` | Integration / staging | One squash-merge commit per PR. Linear. | +| `main` | Release | A non-fast-forward merge of `dev` at each release. | + +`main` never receives feature PRs directly. **Every change reaches +`main` through `dev`.** + +## PR flow → `dev` + +1. Contributor opens a PR targeting `dev` (not `main`). +2. Maintainer squash-merges via the GitHub UI. +3. The resulting commit on `dev` has: + - exactly one parent (linear history), + - subject ending in `(#NNNN)` — the squash-merge signature, + - lowercase conventional-commit prefix: `feat:`, `fix:`, `chore:`, + `chore(deps):`, `docs:`, `ci:`, `test:`, `refactor:`. +4. CI runs on the PR; nothing else is required between merge and the + commit appearing on `dev`. + +Example chain on `dev`: + +``` +c9c68bd8 Fix: Use default reaction on bootstrap (#3965) +810a1372 fix: tighten agents file (#3966) +36d696b2 Fix: wrong use of `in` operator (#3960) +8b426efa test: add pyinstrument profiler (#3955) +``` + +Each is a single squashed commit. No merge commits inside `dev`. + +## Release flow → `main` + +When `dev` is ready to ship: + +1. A release-candidate version bump lands on `dev` as a normal PR: + `chore: update to version vX.Y.Z-rcN (#NNNN)`. +2. Validation happens against the RC. +3. A final version-bump PR lands on `dev`: + `chore: update to version vX.Y.Z (#NNNN)`. +4. A maintainer runs the release merge locally: + + ```bash + git checkout main + git pull --ff-only origin main + git merge dev # true (non-FF) merge, default message + git push origin main + git tag vX.Y.Z # tag the merge commit or the bump commit + git push origin vX.Y.Z + ``` + +5. The resulting merge commit on `main` has: + - **two parents** (prior `main` tip + `dev` tip), + - subject exactly `Merge branch 'dev'` (git default when on `main`), + - **not** authored via the GitHub PR-merge UI (that would produce + `Merge pull request #N from …`). + +Because `main` typically carries a stray release-bump commit that isn't +on `dev`, the histories have diverged and git is forced into a true +merge. `--no-ff` is not needed for that reason. + +## Reading the history + +```bash +# Release log (one entry per release): +git log main --first-parent --oneline + +# Full changelog leading into the next release: +git log dev --oneline + +# Verify a merge is a true non-FF merge: +git log -1 --format='%P' # two parent hashes = true merge +``` + +## Diagram + +``` + (PRs squash-merged one at a time) + │ + ▼ +dev: ── A ── B ── C ── D ── E ── F (rc1) ── G ── H (v1.5.4) + ╲ + ╲ (true merge) + ╲ +main: ────────── prev release ─────────────── X ─────────── M + ▲ + │ + Merge branch 'dev' + tag: v1.5.4 +``` + +## Implications for contributors + +- **Base PRs on `dev`.** PRs against `main` will not be accepted. +- **Use lowercase conventional-commit titles.** Verified stable across + the last 25+ merged PRs. +- **Don't expect `main` to move between releases.** It only advances + when a maintainer cuts a release merge. +- **Tagging is on `main`.** Consumers pinning to a tag get the + released state, never a `dev` snapshot. + +## Adapting this in your own fork + +If you maintain a long-lived fork of `lnbits/lnbits` for production use, +you'll likely want to: + +1. Mirror upstream's `dev` / `main` split — easier to track upstream + merges back into your fork. +2. Adopt a version-suffix convention that surfaces fork identity in + tags and the packaged `pyproject.toml` version, e.g. + `v-.`. This makes it unambiguous in logs and + deployed-package metadata that you're running fork-modified code. +3. Decide whether you also need pre-release channels (`-rcN`, `-devN`) + on top of upstream's; useful if your fork ships to staging hosts + before promotion. + +Specifics depend entirely on your team's needs — this scaffold +deliberately doesn't prescribe a fork-versioning scheme. diff --git a/docs/lnbits-workspace-notes.md b/docs/lnbits-workspace-notes.md new file mode 100644 index 0000000..104af1b --- /dev/null +++ b/docs/lnbits-workspace-notes.md @@ -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 ` 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-.` + 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). diff --git a/docs/upstream-prs.md b/docs/upstream-prs.md new file mode 100644 index 0000000..f1816fa --- /dev/null +++ b/docs/upstream-prs.md @@ -0,0 +1,138 @@ +# Contributing back to upstream + +How to send fixes or features upstream (to projects like `lnbits/lnbits`) +using the worktree-based PR flow built into this scaffold's dev-env +layer. + +> **New to forks and pull requests?** Skim the [Primer](#primer) first. +> Otherwise jump to [Workflow](#workflow). + +## Primer + +GitHub, Forgejo, Gitea, and similar services host *git repositories*. +To contribute to a project you don't own: + +1. **Fork** the upstream repo via the web UI ("Fork" button). This + creates `//` — your personal copy. +2. **Branch** off your fork's `main` (or whichever branch upstream + accepts PRs against). Make your change. +3. **Push** the branch to your fork. +4. **Open a Pull Request** from your fork's branch into the upstream. +5. **Iterate** — maintainers review, you push follow-up commits to the + same branch and the PR updates automatically. +6. **Maintainer merges** — usually as a single squashed commit on + upstream's target branch. +7. **Delete the branch** on both sides once merged. + +The three roles a remote can play in your local clone: + +| Remote name | What it points at | Read or write? | +|---|---|---| +| `upstream` | The canonical project you're contributing to | Read (you fetch from it) | +| `github-fork` (or `fork`) | Your personal copy you push to | Write (you push to it) | +| `origin` | Optional: a private mirror you control (forgejo / gitea / codeberg / sourcehut) | Read + write | + +This scaffold's `settings.nix` declares all three via `git.remotes`; +the bootstrap script wires them into the local bare repo so every +worktree sees the same set automatically. See +[remotes.md](remotes.md) for topology patterns. + +## Prerequisites (per project) + +1. **Fork** the upstream repo on the relevant host. +2. **Set your fork URL** in `settings.nix`: + + ```nix + git.remotes.fork = "https://github.com//lnbits"; + ``` + + Use the SSH form (`git@github.com:/lnbits.git`) if + you've set up SSH push. + +3. **Bootstrap** (or re-bootstrap). The (planned) bootstrap script + adds the `github-fork` remote to the bare repo at + `~/dev/repos/lnbits.git`. + +Verify the remotes are wired: + +```sh +git -C ~/dev/repos/lnbits.git remote -v +``` + +Expected at minimum: + +``` +upstream https://github.com/lnbits/lnbits (fetch / push) +github-fork https://github.com//lnbits (fetch / push) +``` + +If `origin` is set, it points at your private remote (forgejo etc.). + +## Workflow + +### 1. Create the PR branch + +```sh +prb lnbits fix-invoice-validation +``` + +The (planned) `prb` helper: + +- `git fetch upstream` +- creates branch `fix-invoice-validation` off `upstream/main` +- adds a worktree at `~/dev/upstream-prs/lnbits-fix-invoice-validation` +- sets the worktree's push target to your fork (`github-fork`) + +### 2. Make changes + +```sh +cd ~/dev/upstream-prs/lnbits-fix-invoice-validation +# … edit, test, commit … +git commit -am "Fix invoice validation for zero-amount invoices" +``` + +### 3. Push to your fork + open the PR + +```sh +git push -u github-fork fix-invoice-validation +``` + +git prints a URL to open the PR. Alternatively, with the GitHub CLI: + +```sh +gh pr create --base main --head :fix-invoice-validation +``` + +(Use the project's preferred target branch — for `lnbits/lnbits` it's +`dev`, not `main`. See [lnbits-upstream-flow.md](lnbits-upstream-flow.md).) + +### 4. Iterate on review + +Push more commits to the same branch — the PR updates automatically. +Don't force-push unless you've squashed/rebased deliberately. + +### 5. After merge — clean up + +```sh +prc lnbits fix-invoice-validation +``` + +The (planned) `prc` helper removes the worktree and prunes the local +branch. On the web UI, click "Delete branch" on the merged PR to +remove your fork's copy too. + +## Common pitfalls + +- **PR target branch.** Many projects (including `lnbits/lnbits`) + accept PRs against `dev`, not `main`. Read the project's + CONTRIBUTING file or its branch-model reference before opening. +- **Stale branches on your fork.** They accumulate forever if you + don't clean up. `prc` handles the local side; the web UI handles + the fork side. +- **Commits drifting onto your fork's `main`.** Keep your fork's + `main` strictly in lockstep with `upstream/main`. All feature + work lives in branches. +- **Wrong remote when pushing.** If `git push` defaults to `upstream` + (because that's `origin`), you'll get an access-denied error. The + worktree's tracking remote should be `github-fork`; if not, set it: + `git branch --set-upstream-to=github-fork/`. diff --git a/files/dev-CLAUDE.md b/files/dev-CLAUDE.md new file mode 100644 index 0000000..6b2f150 --- /dev/null +++ b/files/dev-CLAUDE.md @@ -0,0 +1,61 @@ +# `~/dev` — lnbits dev workspace + +You're working in a dev workspace bootstrapped by **lnbits-sensei**. +This file is workspace-level orientation; per-project CLAUDE.md files +(e.g. `~/dev/lnbits/CLAUDE.md`) layer project-specific gotchas on top. + +> This file is a symlink into your lnbits-sensei checkout's +> `files/dev-CLAUDE.md` (via home-manager's `mkOutOfStoreSymlink`). +> Edits should happen in the checkout and be committed there. + +## Layout + +``` +~/dev/ +├── repos/ # bare git repos, one per project +├── // # worktrees per branch +├── upstream-prs// # PR branches against upstreams +├── shared/, scratch/ # workspace utilities +└── refs/ # curated reference repos (optional) +``` + +Rationale: bare-repos + per-project-worktree directories means one git +object database per project (no clone-per-branch churn) and an explicit +PR contract in `upstream-prs/`. See the lnbits-sensei README's +"Workspace layout" section for the full reasoning. + +## Quick commands + +- `dev up [--fakewallet|--regtest]` — start the lnbits dev server. + Default `--fakewallet` is instant, no docker, good for + extension/UI/API work. +- `dev down` / `dev logs` / `dev shell` — control / inspect. +- *(planned)* `prb ` — create a PR worktree branched + from `upstream/main` under `~/dev/upstream-prs/`. +- *(planned)* `lb ` — cd shortcut into `~/dev/lnbits/`. + +## Reference docs + +Full reference lives in the lnbits-sensei checkout's `docs/`: + +- `docs/remotes.md` — three remote-topology patterns (upstream-only / + github-fork / multi-remote-with-private). +- `docs/upstream-prs.md` — PR workflow with a primer for anyone new + to fork-based contribution. +- `docs/lnbits-upstream-flow.md` — how `lnbits/lnbits` itself moves + (the `dev` / `main` branch split, squash-merge convention). +- `docs/lnbits-workspace-notes.md` — practical gotchas: port choice, + `LNBITS_SRC` build-context traps, extension-folder-upgrade wiping + forks, Nostr key handling, CLINK scope, fork versioning, **settings + precedence (`.env` vs DB)**. + +## Per-project orientations + +Per-project CLAUDE.md files are placed at the worktree root: + +- `~/dev/lnbits/CLAUDE.md` — LNbits dev gotchas (settings precedence, + frontend rules, auth decorators, …). + +These are also symlinked from your lnbits-sensei checkout (under +`files/`); the seeding is driven by `lnbits-sensei.devEnv.claude.*` +options. diff --git a/files/lnbits-CLAUDE.md b/files/lnbits-CLAUDE.md new file mode 100644 index 0000000..a091104 --- /dev/null +++ b/files/lnbits-CLAUDE.md @@ -0,0 +1,94 @@ +# `~/dev/lnbits` — LNbits dev orientation + +Per-project CLAUDE.md for the LNbits worktree subtree. Workspace-level +orientation is at `~/dev/CLAUDE.md` (when wired). Full reference +material lives in your lnbits-sensei checkout under `docs/`. + +> This file is a symlink to `files/lnbits-CLAUDE.md` in your +> lnbits-sensei checkout. Edits should happen there. + +## Critical gotchas (read first) + +### Settings: `.env` seeds the DB on first boot, then DB wins + +After first boot, **editing `.env` and restarting changes nothing** +for editable fields. Only `ReadOnlySettings` (`host`, `port`, +`lnbits_path`, `lnbits_data_folder`, `lnbits_extensions_path`, +`lnbits_database_url`, `auth_secret_key`, `first_install_token`, +`lnbits_title`, `lnbits_admin_ui`, `lnbits_allowed_funding_sources`) +re-read env every boot. + +Editable settings (site title, fees, watchdog, funding-source +credentials, the whole Admin UI form) change only via: + +- `PUT /api/v1/settings` (Admin UI), or +- Clearing the relevant rows in the `system_settings` table. + +Deploy-side: declarative env vars are a *seed* for editable fields, +not authoritative. Plan accordingly. + +Full reference: `docs/lnbits-workspace-notes.md` → "Settings precedence". + +### Frontend templates: NO self-closing tags + +LNbits pages load Vue 3 + Quasar 2 as UMD globals. The browser's +HTML parser doesn't honor self-close on non-void elements — the +close tag gets implied at the wrong place and subsequent siblings +end up inside the prior component. + +```html + + +``` + +Applies to `lnbits/templates/*.html`, every extension's `templates/`, +every fork. Fine in `.vue` SFCs (the build step rewrites them) — if +you copy a snippet from a Vue SFC repo into an LNbits template, +**expand all self-closing tags before saving**. + +### Auth decorators + +Easy to confuse. Different scopes: + +| Decorator | Auth scope | +|---|---| +| `require_invoice_key` | Wallet invoice key — read access | +| `require_admin_key` | Wallet admin key — write to **own wallet only** | +| `check_admin` | **LNbits instance admin** (super_user / admin users) | +| `check_super_user` | LNbits super user only | + +`require_admin_key` ≠ `check_admin`. Use `check_admin` for +cross-user / admin-only endpoints. + +### Upstream PRs target `dev`, not `main` + +PRs to `github.com/lnbits/lnbits` go to `dev`. `main` only moves on +release merges. Use lowercase conventional-commit titles +(`feat:`, `fix:`, `chore:`, `chore(deps):`, `docs:`, `ci:`). + +Full reference: `docs/lnbits-upstream-flow.md`. + +## Default dev workflow + +``` +dev up # FakeWallet, instant +dev up --regtest # multi-node regtest (slower boot, real channels) +``` + +Use **FakeWallet** for extension CRUD / UI / API work. Spin up +**regtest** only when actual channel/payment behavior is under test. + +## Key paths + +- `~/dev/lnbits/main/` — worktree on `main` (your fork's day-to-day) +- `~/dev/lnbits/dev/` — worktree on `dev` (integration / staging) +- `~/dev/upstream-prs/lnbits-/` — PR worktrees branched from + `upstream/main` +- `~/dev/repos/lnbits.git` — the bare repo all the above point at + +## When in doubt + +The canonical reference for any of the above lives in your +lnbits-sensei checkout's `docs/`. Trust the docs over this file when +they diverge — this CLAUDE.md is a quick-reference; the docs are the +source of truth. diff --git a/home.nix b/home.nix index 3f02f44..6ae7214 100644 --- a/home.nix +++ b/home.nix @@ -35,4 +35,25 @@ # Future passes: shell prompt, editor, lnbits dev shell aliases, # direnv integration, tmux launcher, etc. Keep this file thin and # delegate behaviour to modules/. + + # CLAUDE.md seeding — symlinks under ~/dev/ that point at files + # tracked in your lnbits-sensei checkout, so Claude sessions in the + # workspace pick up the curated orientation automatically. Gated on + # `lnbits-sensei.devEnv.claude.*` options in the NixOS config. + # + # `mkOutOfStoreSymlink` keeps the link pointed at your live checkout + # (not the Nix store), so editing the source file takes effect on + # the next Claude session without a rebuild. + home.file = lib.mkMerge [ + (lib.optionalAttrs osConfig.lnbits-sensei.devEnv.claude.enable { + "dev/lnbits/CLAUDE.md".source = + config.lib.file.mkOutOfStoreSymlink + "${osConfig.lnbits-sensei.devEnv.scaffoldPath}/files/lnbits-CLAUDE.md"; + }) + (lib.optionalAttrs osConfig.lnbits-sensei.devEnv.claude.workspaceOrientation { + "dev/CLAUDE.md".source = + config.lib.file.mkOutOfStoreSymlink + "${osConfig.lnbits-sensei.devEnv.scaffoldPath}/files/dev-CLAUDE.md"; + }) + ]; } diff --git a/modules/dev-env/options.nix b/modules/dev-env/options.nix index 6f623bb..03425cb 100644 --- a/modules/dev-env/options.nix +++ b/modules/dev-env/options.nix @@ -97,6 +97,21 @@ in options.lnbits-sensei.devEnv = { enable = mkEnableOption "lnbits-sensei dev-env tooling"; + scaffoldPath = mkOption { + type = types.str; + description = '' + Absolute path to your lnbits-sensei checkout on this machine. + Used to source the seedable CLAUDE.md files (and, later, the + dev-env scripts) via `mkOutOfStoreSymlink` so edits in your + checkout take effect without a rebuild. + + Required when any `claude.*` integration is enabled. Type is + `str` (not `path`) intentionally — `path` would copy the file + into the Nix store and break the out-of-store-symlink semantics. + ''; + example = "/home/alice/dev/lnbits-sensei"; + }; + root = mkOption { type = types.str; default = "/home/${config.lnbits-sensei.user or "user"}/dev"; @@ -108,6 +123,23 @@ in ''; }; + claude = { + enable = mkEnableOption '' + Seed `~/dev/lnbits/CLAUDE.md` from + `''${scaffoldPath}/files/lnbits-CLAUDE.md` so Claude sessions + anywhere in the lnbits worktree subtree pick up project-specific + orientation (settings precedence, frontend rules, auth + decorators, …) + ''; + + workspaceOrientation = mkEnableOption '' + Also seed `~/dev/CLAUDE.md` from + `''${scaffoldPath}/files/dev-CLAUDE.md`. Skip if you already + maintain a workspace-level CLAUDE.md — this option clobbers + whatever is there + ''; + }; + projects = mkOption { type = types.attrsOf projectType; default = { };