The root README pitched a unified 'dev up' CLI and called the dev-env stubs — both stale now that the real implementation has landed. Rewrite the status header, the day-to-day command set, the module file tree, and drop the (planned)/(stub) markers on bootstrap, prb, and nav helpers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14 KiB
lnbits-sensei
Status: working dev-env. Module schema, flake wiring, and the dev-env tooling (bootstrap, worktree/nav helpers, regtest wrappers, upstream-PR flow, deploy wrapper, declarative tmux) are all real and
nix flake check-green. Fill insettings.nix+ a project list and it's usable end-to-end.
Opinionated, bare-skeleton scaffold for running an LNbits stack — inspired by the omnixy NixOS module pattern but stripped of any personal identity so the template is consumable as-is.
The goal: clone, fill in your identity + remote topology + a project list, rebuild, and get a working multi-project LNbits dev environment — bare repos + worktrees materialized in one command, with navigation, sync, regtest, and upstream-PR helpers on PATH:
dev-env-bootstrap # materialize bare repos + worktrees on disk
lb dev # cd to the lnbits dev worktree
dev-status # dirty + ahead/behind across every worktree
regtest-start dev # build lnbits from a worktree, bring up regtest
prb lnbits fix-x # spin a throwaway worktree for an upstream PR
See modules/dev-env/README.md for the
full command set and bootstrap walkthrough.
FakeWallet (LNBITS_BACKEND_WALLET_CLASS=FakeWallet) is the default
iteration path for extension/frontend work — no docker needed. The
regtest-* helpers (gated on devEnv.regtest.enable) exist for the
moments you need real channels/payments.
Remote topology
settings.nix exposes three slots so the same scaffold works whether
you push to GitHub only, run a private mirror, or both:
git.remotes = {
upstream = "https://github.com/lnbits/lnbits"; # always
fork = null; # or "https://github.com/<you>/lnbits"
extras = []; # or [{ name = "private"; url = "ssh://…"; }]
};
See docs/remotes.md for the three canonical
patterns (upstream-only, github-fork-for-PRs, multi-remote-with-private
host on forgejo / gitea / codeberg / sourcehut).
Repository layout
flake.nix # inputs: nixpkgs, home-manager, lnbits-src
flake.lock
settings.nix # consumer fills in: user, host, identity, remotes
configuration.nix # NixOS entry — thin imports list
home.nix # home-manager entry — thin
hardware-configuration.nix # placeholder; overwrite with nixos-generate-config
modules/
├── core.nix # lnbits-sensei.* option schema
├── lib.nix # shared helpers — config.lnbits-sensei.lib
├── secrets.nix # sops-nix wiring (inert until you add a secret)
├── git/
│ └── remotes.nix # remote topology (upstream / fork / extras)
└── dev-env/
├── README.md # command set + bootstrap walkthrough
├── default.nix # module entry; imports options + lib + config
├── options.nix # devEnv schema (projects, deploy, regtest, tmux)
├── lib.nix # mkProject + path/remote helpers
├── config.nix # renders /etc/dev-env/*, installs scripts
├── presets/example.nix # worked, generic project list — copy and edit
├── tests/smoke.nix # nix flake check schema test
└── scripts/
├── bootstrap.sh # materialize bare repos + worktrees
├── nav.sh # lb / g / ext / dep / prs / shared / repos
├── worktree.sh # wt / wts / wtu / wtn + lnbits sync helpers
├── pr-helpers.sh # prb / prc / prl — upstream-PR worktrees
├── rebase.sh # safe fork-onto-upstream rebase
├── status.sh # dev-status — divergence across worktrees
├── deploy.sh # dev-deploy — nixos-rebuild wrapper
├── regtest.sh # regtest-start/-stop/-status (gated)
├── tmux-launch.sh # dev-tm — declarative tmux sessions
└── git-hooks/pre-commit # shared secret-scanner hook
docs/ # human-facing references (also seeded to ~/dev/)
└── remotes.md # the three remote-topology patterns
LICENSE # MIT (placeholders — fill in year + holder)
Development flow
First-time bootstrap (per machine)
-
Clone the repo into a working directory of your choice.
-
Edit
settings.nix— fill inuser,hostName,timeZone,gitName,gitEmail, andgit.remotes(see docs/remotes.md for topology patterns). -
Replace the placeholder hardware config with your real one:
sudo nixos-generate-config --root / --dir $(pwd)(The shipped
hardware-configuration.nixevaluates but won't boot — intentional, so a forgotten regeneration fails loudly rather than silently booting on someone else's disk layout.) -
Validate the schema before activating anything:
nix flake check --no-buildShould print
all checks passed!. -
Switch when ready:
sudo nixos-rebuild switch --flake .#<hostName>
Day-to-day
After a rebuild, the dev-env helpers are on PATH (standalone commands)
or sourced into interactive shells (navigation/worktree functions).
Run dev-env-bootstrap once to materialize the repos + worktrees you
declared, then:
dev-env-bootstrap # bring the ~/dev/ tree up to spec (idempotent)
lb dev / g <cat> <repo> # navigate worktrees
dev-status # divergence + dirty state across everything
wts / wtu <repo> # sync worktrees / fetch upstream + show divergence
rebase status # which forks need rebasing onto upstream
regtest-start dev # build lnbits from a worktree, bring up regtest
prb lnbits fix-x # throwaway worktree for an upstream PR
dev-tm <session> # launch a declared tmux session
dev-deploy <host> # nixos-rebuild against your deploy flake
The full command table + bootstrap walkthrough lives in
modules/dev-env/README.md. See
Claude orientation below if you want
AI-assisted work to pick up the project's gotchas for free.
Workspace layout (the ~/dev/ tree)
The dev-env module's job is to put every project, every worktree, and every upstream PR in a predictable place — so navigation, grepping across reference code, and switching between branches stays mechanical.
~/dev/
├── repos/ # bare repos, one per project
│ ├── lnbits.git
│ ├── lnbits-extensions.git
│ └── …
│
├── lnbits/ # one dir per project; worktrees inside
│ ├── main/ # worktree on `main`
│ ├── dev/ # worktree on `dev`
│ └── fix-issue-123/ # ad-hoc feature worktree
│
├── lnbits-extensions/
│ ├── main/
│ └── …
│
├── upstream-prs/ # PRs against upstreams (separate tree)
│ ├── lnbits-fix-payment-race/ # each worktree branched from upstream/main
│ └── …
│
├── shared/ # cross-project utilities / notes
├── scratch/ # ephemeral one-off scratchpads
└── refs/ # curated read-only reference repos
Declare which projects materialize via settings.nix:
devEnv.projects = {
lnbits = {
url = "https://github.com/lnbits/lnbits";
worktrees = {
main = { branch = "main"; };
dev = { branch = "dev"; };
};
};
};
dev-env-bootstrap materializes the bare repo at
~/dev/repos/lnbits.git, wires its remotes, and checks out one
worktree per declared entry. It's idempotent — re-running only fills in
what's missing, and never touches a worktree whose branch has drifted
from the spec. See docs/remotes.md and
modules/dev-env/presets/example.nix
for richer project declarations (explicit url, upstream, fork,
category, single-clone projects).
Why bare repos + worktrees (rather than fresh clones per branch):
- One copy of the git object database on disk — five branches checked out is five lightweight worktrees, not five clones.
git fetchonce and every worktree sees the new refs.- The worktree directory name is the branch — no "wait, what am I on?"
Why a separate upstream-prs/ tree:
PRs against upstream repos (e.g. lnbits/lnbits) want to branch from
upstream's main, not your day-to-day fork. Mixing PR branches into
the per-project tree pollutes the branch list and tempts "I'll just
commit this here" accidents. The upstream-prs/ tree makes the
contract explicit: this worktree is on upstream/main, branch off,
push to your fork, open the PR. The prb helper does this:
prb lnbits fix-payment-race # create ~/dev/upstream-prs/lnbits-fix-payment-race
# branched from upstream/main, fork as push target
prc lnbits fix-payment-race # remove the worktree after the PR merges
prl # list active PR worktrees
Navigation helpers — shell functions sourced into interactive
shells, driven by what's on disk under ~/dev/:
lb dev # cd ~/dev/lnbits/dev
lb fix-issue-123 # cd ~/dev/lnbits/fix-issue-123
g extensions myext # cd ~/dev/extensions/myext
ext <name> # cd ~/dev/shared/extensions/<name>
Claude orientation
For workflows that use Claude Code,
lnbits-sensei ships curated workspace orientation that gets symlinked
into ~/dev/ so AI-assisted work picks up lnbits-specific gotchas
without anyone copying files around or re-explaining them in every
session.
Concrete example. A Claude session iterating in
~/dev/lnbits/extensions/<my-ext>/templates/ automatically inherits
the per-project CLAUDE.md from ~/dev/lnbits/, which carries the
Vue/Quasar UMD "no self-closing tags" rule. Without that seed,
Claude would happily emit:
<q-input v-model="amount" label="Amount" />
Correct in any Vue SFC repo, silently broken under LNbits's UMD load model — the browser's HTML parser doesn't honor self-close on non-void elements, the close tag gets implied at the wrong place, and nesting corrupts. With the seed loaded, the model writes:
<q-input v-model="amount" label="Amount"></q-input>
— the form that actually works. Same story for every other gotcha the per-project CLAUDE.md surfaces:
- Settings precedence — editing
.envpost-first-boot is a no-op for editable fields. AI knows to checksystem_settingsrows or Admin UI, not chase env-var edits. - Auth decorators —
require_admin_key(per-wallet write) ≠check_admin(instance admin). Easy to misuse. - Upstream PR target —
lnbits/lnbitsaccepts PRs againstdev, notmain. Lowercase conventional-commit titles. - CLINK ↔ LNbits scope —
ndebit/noffersemantics live in Lightning.Pub, not LNbits. Don't propose CLINK shims for LNbits.
The full set lives in docs/; the seeded CLAUDE.md files are tight
summaries with pointers back.
How it works
Files in this repo's files/ directory are symlinked to your ~/dev/
paths via home-manager's mkOutOfStoreSymlink — edits in your
checkout take effect on the next Claude session, no rebuild.
files/lnbits-CLAUDE.md→~/dev/lnbits/CLAUDE.md(per-project)files/dev-CLAUDE.md→~/dev/CLAUDE.md(workspace, opt-in)
Wiring
In your settings.nix:
devEnv = {
enable = true;
scaffoldPath = "/home/<you>/dev/lnbits-sensei"; # absolute path
claude = {
enable = true; # ~/dev/lnbits/CLAUDE.md
workspaceOrientation = false; # also ~/dev/CLAUDE.md (opt-in;
# clobbers any existing one)
};
};
If you don't use Claude Code, leave both off — the same content is in
docs/ for human reading. The seeding is purely an optional layer on
top of the human-facing docs, not a replacement for them.
Further reading
docs/remotes.md— the three remote-topology patterns (upstream-only / github-fork / multi-remote).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— reference for howlnbits/lnbitsitself moves:dev/mainbranch model, squash-merge convention, release tagging.docs/lnbits-workspace-notes.md— practical gotchas and design constraints for day-to-day lnbits work: port choice,LNBITS_SRCbuild-context traps, the extension-folder-upgrade-wipes-fork issue, settings precedence (.envvs DB), Nostr key handling, CLINK protocol scope, fork versioning.docs/lnbits-extension-dev.md— building and maintaining LNbits extensions: auth-decorator distinctions, FakeWallet vs regtest testing strategy, themigrations_fork.pypattern for keeping fork-only schema deltas out of upstream-trackedmigrations.py.docs/lnbits-frontend-gotchas.md— Vue/Quasar UMD traps in lnbits page templates: no self-closing tags, CSS specificity vs Quasar's!importantutilities, cache busting via?v={server_startup_time}, dark-mode color discipline.docs/secrets-management.md— beginner-friendly walkthrough for getting secrets out of.envand into sops-encrypted YAML files: generating an age key, adding recipients, declaring secrets in NixOS, rotating, multi-host server setups, and common pitfalls. The scaffold ships the sops-nix wiring already (inert until you create your first encrypted file).
Contributing to this scaffold
Iteration happens under the sandboxed-claude policy from omnixy's
scripts/sandbox-claude.sh — a per-launch .claude/settings.json
that allows nix/git/inspection and hard-denies sudo, push/remote,
container engines, etc. Substantive scaffolding work happens in that
sandbox; promotion (git push) is done from the main shell after
review.
License
MIT.