Compare commits
No commits in common. "1a75a04c2fae71466c3534040aed6076d0b2f988" and "3f88551f1bf98b56c33b61838be52f472a7a90d8" have entirely different histories.
1a75a04c2f
...
3f88551f1b
10 changed files with 1 additions and 1026 deletions
62
README.md
62
README.md
|
|
@ -114,40 +114,6 @@ 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
|
||||||
|
|
@ -226,33 +192,7 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
## Further reading
|
## Contributing
|
||||||
|
|
||||||
- [`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`
|
||||||
|
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
# `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.
|
|
||||||
|
|
@ -1,243 +0,0 @@
|
||||||
# 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).
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
# 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>`.
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
# `~/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.
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
# `~/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,25 +35,4 @@
|
||||||
# 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,21 +97,6 @@ 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";
|
||||||
|
|
@ -123,23 +108,6 @@ 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