Compare commits
5 commits
3f88551f1b
...
1a75a04c2f
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a75a04c2f | |||
| 0a5713c704 | |||
| 35ef01fd70 | |||
| 6c9f76bd70 | |||
| 30fbd0cef9 |
10 changed files with 1026 additions and 1 deletions
62
README.md
62
README.md
|
|
@ -114,6 +114,40 @@ in follow-up passes.
|
||||||
The `dev` CLI is the single entry point. See the top of this README
|
The `dev` CLI is the single entry point. See the top of this README
|
||||||
for the verb set.
|
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/<you>/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)
|
### Workspace layout (the `~/dev/` tree)
|
||||||
|
|
||||||
The dev-env module's job is to put every project, every worktree, and
|
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
|
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
|
Iteration happens under the sandboxed-claude policy from omnixy's
|
||||||
`scripts/sandbox-claude.sh` — a per-launch `.claude/settings.json`
|
`scripts/sandbox-claude.sh` — a per-launch `.claude/settings.json`
|
||||||
|
|
|
||||||
143
docs/lnbits-extension-dev.md
Normal file
143
docs/lnbits-extension-dev.md
Normal file
|
|
@ -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 `<ext.id>_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
|
||||||
|
`<ext>_fork` row appears on first run with no schema migration
|
||||||
|
needed core-side.
|
||||||
|
- Extension data tables live in `ext_<id>.sqlite3` (SQLite) or a
|
||||||
|
Postgres schema named after `<id>`. 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_<id>.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_<your-tag>_<ext>_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['<ext>']` 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 = <upstream-max> WHERE db = '<ext>';
|
||||||
|
```
|
||||||
|
|
||||||
|
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 = <N> WHERE db = '<ext>'\"); \
|
||||||
|
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 = <upstream-max> WHERE db = '<ext>'`
|
||||||
|
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.
|
||||||
105
docs/lnbits-frontend-gotchas.md
Normal file
105
docs/lnbits-frontend-gotchas.md
Normal file
|
|
@ -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
|
||||||
|
(`<ext>/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
|
||||||
|
<!-- correct -->
|
||||||
|
<q-input v-model="foo" label="Foo"></q-input>
|
||||||
|
<q-btn @click="bar" label="Bar"></q-btn>
|
||||||
|
|
||||||
|
<!-- wrong — silently broken in UMD/no-build mode -->
|
||||||
|
<q-input v-model="foo" label="Foo" />
|
||||||
|
<q-btn @click="bar" label="Bar" />
|
||||||
|
```
|
||||||
|
|
||||||
|
**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
|
||||||
|
`<style>` blocks targeting Quasar utility classes:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- ✗ likely loses to upstream's !important rule -->
|
||||||
|
<style>
|
||||||
|
.text-caption.my-fix { color: #ff0000 !important; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- ✓ inline style wins without an arms race -->
|
||||||
|
<span :style="{ color: '#ff0000' }">…</span>
|
||||||
|
<span style="color: #ff0000">…</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
<!-- ✗ unreadable on dark theme -->
|
||||||
|
<div class="bg-red-1 q-pa-md">Warning</div>
|
||||||
|
|
||||||
|
<!-- ✓ explicit text class survives theme switch -->
|
||||||
|
<div class="bg-red-1 text-grey-9 q-pa-md">Warning</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
128
docs/lnbits-upstream-flow.md
Normal file
128
docs/lnbits-upstream-flow.md
Normal file
|
|
@ -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 <sha> -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<upstream>-<your-tag>.<N>`. 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.
|
||||||
243
docs/lnbits-workspace-notes.md
Normal file
243
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).
|
||||||
138
docs/upstream-prs.md
Normal file
138
docs/upstream-prs.md
Normal file
|
|
@ -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 `<host>/<your-user>/<repo>` — 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/<your-username>/lnbits";
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the SSH form (`git@github.com:<your-username>/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/<your-user>/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 <your-username>: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/<branch>`.
|
||||||
61
files/dev-CLAUDE.md
Normal file
61
files/dev-CLAUDE.md
Normal file
|
|
@ -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
|
||||||
|
├── <project>/<worktree>/ # worktrees per branch
|
||||||
|
├── upstream-prs/<topic>/ # 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 <repo> <branch>` — create a PR worktree branched
|
||||||
|
from `upstream/main` under `~/dev/upstream-prs/`.
|
||||||
|
- *(planned)* `lb <worktree>` — cd shortcut into `~/dev/lnbits/<worktree>`.
|
||||||
|
|
||||||
|
## 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.
|
||||||
94
files/lnbits-CLAUDE.md
Normal file
94
files/lnbits-CLAUDE.md
Normal file
|
|
@ -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
|
||||||
|
<q-input v-model="foo" label="Foo"></q-input> <!-- ✓ correct -->
|
||||||
|
<q-input v-model="foo" label="Foo" /> <!-- ✗ silently broken -->
|
||||||
|
```
|
||||||
|
|
||||||
|
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-<topic>/` — 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.
|
||||||
21
home.nix
21
home.nix
|
|
@ -35,4 +35,25 @@
|
||||||
# Future passes: shell prompt, editor, lnbits dev shell aliases,
|
# Future passes: shell prompt, editor, lnbits dev shell aliases,
|
||||||
# direnv integration, tmux launcher, etc. Keep this file thin and
|
# direnv integration, tmux launcher, etc. Keep this file thin and
|
||||||
# delegate behaviour to modules/.
|
# 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";
|
||||||
|
})
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,21 @@ in
|
||||||
options.lnbits-sensei.devEnv = {
|
options.lnbits-sensei.devEnv = {
|
||||||
enable = mkEnableOption "lnbits-sensei dev-env tooling";
|
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 {
|
root = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = "/home/${config.lnbits-sensei.user or "user"}/dev";
|
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 {
|
projects = mkOption {
|
||||||
type = types.attrsOf projectType;
|
type = types.attrsOf projectType;
|
||||||
default = { };
|
default = { };
|
||||||
|
|
|
||||||
Reference in a new issue