-
Notifications
You must be signed in to change notification settings - Fork 0
Installation
-
LNbits
>= 1.5.6(the background task manager differs between releases: the extension useslnbits.task_managerwhen available and falls back to the legacylnbits.tasksmodule on LNbits 1.5.x — both work, but nothing below 1.5.x has been tested). -
Python
>= 3.10(whatever ships with your LNbits image). - A working Lightning backend configured in LNbits for the extension's wallet backend. See Lightning backend below.
- Network access to the Nostr relays you configure (
wss://…).
The extension adds no new Python dependencies. It reuses LNbits' bundled
coincurve, websockets, and bech32 packages and LNbits' own Nostr utilities
(lnbits.utils.nostr).
v0.1.5 is the current release (paying ClinkWallet backend via a Lightning.pub
account — see below). Once the registry pull request
(lnbits/lnbits-extensions #835)
is merged it will be installable from the LNbits extension manager:
- Log in to LNbits.
- Open Extensions and enable CLINK for your user.
- The extension appears under CLINK in the sidebar.
Until the PR is merged (or if you do not want to wait), use Option B.
The release pipeline automatically publishes GitHub releases and opens a pull request to the official
lnbits/lnbits-extensionsregistry.
If you do not want to wait for the registry PR (or the extension manager is not available), install by placing the code in LNbits' extensions directory:
# Inside the LNbits app folder
cd extensions
git clone https://github.com/WoompaLoompa/lnbits-clink.git clinkImportant: LNbits scans the directory
<LNBITS_EXTENSIONS_PATH>/extensions/ (that is, it appends an extensions
sub-folder to the configured path — the default lnbits gives
lnbits/extensions). The extension folder must be named clink — LNbits
derives the extension id from the folder name and imports it as the clink
Python package. (The repository is named lnbits-clink only because GitHub
disallows single-word lowercase repo names.)
Then restart LNbits. On a stock install (non-admin-gated extension) the extension is available to every user without per-user enablement.
The repository ships a top-level manifest.json
({"repos":[{"id":"clink","organisation":"WoompaLoompa","repository":"lnbits-clink"}]}).
LNbits (>= 1.5.6) merges manifest URLs into the installable-extension listing,
so clink becomes installable from the extension manager before the
registry PR is merged — no manual copy needed:
# Add the manifest URL alongside the default registry manifest
LNBITS_EXTENSIONS_MANIFESTS="https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json,https://raw.githubusercontent.com/WoompaLoompa/lnbits-clink/main/manifest.json"For each repo entry LNbits resolves the repository's releases/latest
GitHub release and the config.json from its default branch to build an
installable extension. The extension id comes from config.json["id"]
(clink).
GitHub rate limits: manifest resolution calls the GitHub API (
releases/latest) and is rate-limited to 60 requests/hour per IP for unauthenticated clients. A busy Fly machine shares its IP, so this budget is easily exhausted. SetLNBITS_EXT_GITHUB_TOKENto a GitHub token (fine-grained, read-onlyContentson the extension repo) to avoid intermittent failures — LNbits documents this setting as "required due to GitHub rate-limit":fly secrets set -a <app> LNBITS_EXT_GITHUB_TOKEN=<token>
The LNbits Docker image ships without extensions, and anything written to the container filesystem is lost on redeploy. Extensions must live on a persistent volume.
-
Point
LNBITS_EXTENSIONS_PATHat a path on your volume infly.tomland redeploy:[env] LNBITS_EXTENSIONS_PATH = "/app/data/extensions"
LNbits creates
<path>/extensions/automatically on startup. -
Upload the code.
gitis usually not installed in the container, so package the repo locally and push it over SFTP:tar czf clink.tgz --exclude='.git' --exclude='__pycache__' --exclude='*.pyc' \ -C /path/to/lnbits-clink . printf 'put clink.tgz /tmp/clink.tgz\nbye\n' | fly ssh sftp shell -a clink-lnbits
-
Extract it into the volume (note the nested
extensionsfolder):fly ssh console -a clink-lnbits -C 'sh -s' <<'EOF' mkdir -p /app/data/extensions/extensions/clink tar xzf /tmp/clink.tgz -C /app/data/extensions/extensions/clink rm /tmp/clink.tgz ls /app/data/extensions/extensions/clink/config.json EOF
-
Restart the machine so LNbits discovers the extension and runs migrations:
fly machine restart <machine-id> -a clink-lnbits
-
Verify (see Verifying the install).
- Extensions stored under
/app/data/...survivefly deployand machine restarts because the volume is persistent. - The LNbits extension builder in the admin UI (
/extensions/builder) can only build from the official registry — it cannot import an existing repo, so it is not usable for this extension. - On the demo app
clink-lnbitsthe Lightning backend is nowClinkWallet(see CLINK as the Lightning backend): invoices are requested from a real Lightning.pub node over Nostr. Earlier the backend wasFakeWallet(in-memory, always "payable"), which is still useful for walking the UI/protocol flow without moving real funds. - The demo app runs both manifests:
LNBITS_EXTENSIONS_MANIFESTS="https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json,https://raw.githubusercontent.com/WoompaLoompa/lnbits-clink/main/manifest.json", soclinkshows up in the extension manager there.
The superadmin flow differs from the old per-user extension toggle:
-
With
LNBITS_ADMIN_UI=true, LNbits auto-generates a superuser on first run and stores it in<data>/data/.super_user. Read it over SSH:fly ssh console -a clink-lnbits -C 'cd /app && uv run lnbits-cli superuser' -
Open
https://<your-lnbits>/adminand log in as the superuser (or uselnbits-cli superuser-urlto get a?usr=<id>deep link). -
Create a wallet/user to click around with:
fly ssh console -a clink-lnbits -C 'sh -s' <<'EOF' cd /app uv run lnbits-cli users new -u alice -p change-me EOF
-
Log in as that user at the dashboard. On LNbits 1.5.x, a non-admin-gated extension like
clinkis enabled for every user automatically — the per-userextensionslist in the admin API only gates extensions listed inLNBITS_ADMIN_EXTENSIONS. The CLINK pages are then reachable at/clink/payand/clink/subscriptions.
The extension configures its node with environment variables:
-
LNBITS_CLINK_FUNDING_NOFFER— anoffer1...string that points at a Lightning.pub node's offer service. When set, theClinkWalletfunding source is ready to receive; when unset it reports a clear error and LNbits falls back toVoidWalletafter the startup retries. -
LNBITS_CLINK_FUNDING_ACCOUNT— (v0.1.5) a Lightning.pub account connection string:nprofile1...(NIP-19, from the node's Profile → Copy connection string), optionally with a:tokensuffix. When set, the wallet can pay out (PayInvoice), report a real balance (GetUserInfo), and track invoice status (GetPaymentState) via the node's Nostr user API (kind 21000). Without it,pay_invoicereturns a clear "receive-only without a Lightning.pub account" error.Both fields are also editable in the
/admin → Funding Sourcescard (see CLINK as the Lightning backend); a value saved there is stored insystem_settingsand overrides the env var at runtime, so re-save it in the UI if you rotate the secret. -
Backend wallet: the extension is not just a consumer of whatever backend is configured — it can be the backend (see below). The public checkout, debit settlement, and subscriptions call
create_invoice/pay_invoiceon the configured backend. -
First relay: you add relays from the UI or the API (
/clink/api/v1/relays). Nothing works until at least one relay is enabled.
For a real end-to-end experience (actually creating and paying Lightning invoices) you need a real Lightning backend such as:
-
LND (
LNDWallet) -
Core Lightning (
CoreLightningWallet) -
Eclair (
EclairWallet) - A remote service such as Alby (
AlbyWallet)
The default VoidWallet backend reports "cannot create invoices" — it is only
useful for exercising the protocol layer, not for payments. FakeWallet
(in-memory) lets you walk the full checkout/subscription UI flow.
Since v0.1.4 CLINK itself can be the funding source, and since v0.1.5
it is a fully paying backend: invoices are requested from a remote
Lightning.pub node's offer service (kind 21001) using only the node's
noffer1... string, and with a Lightning.pub account connection string
(nprofile1...[:token]) the wallet also pays out (PayInvoice), reports a
real balance (GetUserInfo), and tracks invoice status (GetPaymentState)
over the node's Nostr user API (kind 21000, NIP-44 v1 encryption).
clink/__init__.py registers the ClinkWallet class on the lnbits.wallets
module at extension load. LNbits resolves the backend with
getattr(lnbits.wallets, settings.lnbits_backend_wallet_class), and because
extensions are imported before that lookup at startup, selecting
ClinkWallet requires no core changes. The class implements the same
Wallet API as the bundled backends (verified against LNbits 1.5.6 and
1.6.x).
The /admin → Funding Sources card renders inputs from a hardcoded list of
bundled backends plus the settings model. To show CLINK with Noffer and
Account inputs, two core files are patched:
-
lnbits/settings.py— aClinkFundingSource(LNbitsSettings)mixin withlnbits_clink_funding_noffer/lnbits_clink_funding_account(envLNBITS_CLINK_FUNDING_NOFFER/_ACCOUNT), wired into theFundingSourcesSettingsbase-class list. This makes the fields valid onSettings,UpdateSettings(admin save) andAdminSettings(admin read). -
lnbits/static/bundle-components.min.js— a["ClinkWallet", "CLINK (Lightning.pub via Nostr)", {...}]entry in therawFundingSourcesarray so the card renders the two inputs.
Durability on Fly. The container rootfs is recreated from the image on
every restart/deploy, so a direct edit is lost. On clink-lnbits the patches
are re-applied at every boot by a sitecustomize.py on the persistent
volume (/app/data/lnbits_patch/, containing sitecustomize.py +
boot_patch.py), enabled with the PYTHONPATH=/app/data/lnbits_patch secret.
Python imports sitecustomize before the LNbits app, and boot_patch.py
idempotently patches the two core files (logging to
/app/data/lnbits_patch/patch.log); if a future core version changes the
anchors it logs and skips instead of crashing. To re-create the mechanism from
scratch, drop the two files on the volume and set the secret:
# once (files: /app/data/lnbits_patch/{sitecustomize,boot_patch}.py)
fly secrets set -a clink-lnbits "PYTHONPATH=/app/data/lnbits_patch"-
Make
ClinkWalletselectable — add it to the allowed funding sources.lnbits_allowed_funding_sourcesis not stored in the DB on this instance, so it comes from the env secret:fly secrets set -a clink-lnbits \ "LNBITS_ALLOWED_FUNDING_SOURCES=<the-22-core-defaults>,ClinkWallet"
(Keep the existing defaults in the value or the other backends vanish from the dropdown.) The dropdown in
/admin → Funding Sourceswill then listClinkWallet. -
Point the wallet at the node — set the noffer secret (this is what the
ClinkWalletconstructor reads):fly secrets set -a clink-lnbits "LNBITS_CLINK_FUNDING_NOFFER=noffer1..."
-
Add the account (optional, for sending + balance) — set the Lightning.pub account connection string:
fly secrets set -a clink-lnbits "LNBITS_CLINK_FUNDING_ACCOUNT=nprofile1...[:token]"
fly secrets setrestarts the machine. Settings precedence note: values in thesystem_settingstable override env vars, so do the DB step below too (or save the fields from the admin card). -
Switch the backend class in the DB — this is the exact row the admin UI writes when you pick a backend and save.
lnbits_backend_wallet_classis stored insystem_settings(JSON-encoded string), and it takes precedence overLNBITS_BACKEND_WALLET_CLASS:fly ssh console -a clink-lnbits -C 'sh -s' <<'PY' import sqlite3 db = sqlite3.connect("/app/data/database.sqlite3") db.execute("UPDATE system_settings SET value=? WHERE id=?", ('"ClinkWallet"', "lnbits_backend_wallet_class")) db.commit() PY
The stored value is the JSON-encoded string
"ClinkWallet"(with the quotes).Alternatively — and equivalent — select
ClinkWalletin/admin → Funding Sources → LNbits Wallet (Current backend wallet)and save. -
Restart and verify:
fly machine restart <machine-id> -a clink-lnbits fly logs -a clink-lnbits --no-tail | grep -i "funding source" # expect: clink: registered ClinkWallet as an LNbits funding source # Funding source: ClinkWallet # SUCCESS | ✔️ Backend ClinkWallet connected and with a balance of <n> msat.
With an account configured the balance is the real node balance; without one it is
0 msat. A healthy boot does not guarantee the relay is reachable — create a real invoice to check the receive path:fly ssh console -a clink-lnbits -C 'cd /app && PYTHONPATH=/app/data/extensions/extensions \ /app/.venv/bin/python - <<"PY" import asyncio, clink, lnbits.wallets async def main(): w = lnbits.wallets.ClinkWallet() print(await w.status()) r = await w.create_invoice(1000, "check", "check") print(r.ok, r.error_message, r.payment_request[:40]) asyncio.run(main()) PY'
ok: True+ a BOLT11 string means the node answered over the relay. With an account configured,await w.pay_invoice(invoice)then pays it out.
-
Watchdog: the automatic voiding check
(
lnbits_watchdog_switch_to_voidwallet) is off on this instance. With an account the wallet reports a real balance, so the watchdog's balance-delta check is meaningful — but keep it off until you confirm the account's balance tracking matches LNbits' expectations. -
Receive-only caveat: without an account,
ClinkWalletcan receive but cannot pay out (pay_invoicereturns a clear "receive-only without a Lightning.pub account" error). KeepFakeWalletas an alternative in the allowed list to switch back quickly if needed. -
Account secret vs. admin form: a value saved in the admin card goes to
system_settingsand overrides the env secret at runtime. If you later rotate a secret, re-save it in the card (or clear the DB row).
# Public info endpoint: relay list + listener state
curl https://<your-lnbits>/clink/api/v1/info
# Auth-protected pages (401 without a user context is expected)
curl -i https://<your-lnbits>/clink/pay
# In the container
uv run lnbits-cli extensions list
# Installed extensions:
# - clink (0.0)On startup the LNbits log confirms migrations and background tasks:
running migration clink.1
running migration clink.2
running migration clink.3
running migration clink.4
✔️ All migrations done.
clink: listener task started
clink: subscription poller started
-
Blank/white extension pages (
/clink/pay,/clink/subscriptions) with a console error likeCannot read properties of undefined (reading 'user')mean the frontend read the oldthis.gobject. On LNbits 1.5.x extension pages must usewindow.ginstead (see Development). Use a release that includes the fix (v0.1.1+). -
Tabs/dialog inputs stacking on top of each other (Offers/Debits/Relays tabs collapsing into one another, overlapping form fields) means the page still ships self-closing component tags (
<q-tab ... />). The in-DOM template is browser-parsed before Vue compiles it, so self-closed non-void tags swallow their siblings. Usev0.1.2+(see Development). -
Extension card shows version
0.0and a broken icon in the LNbits extension manager. That happens when the extension was installed by copying the repo folder into the extensions directory: LNbits records version0.0and no GitHub metadata. Reinstall from a release (via the extension manager or a manifest) so theinstalled_extensionsrow gets the real version (0.1.5) and an absolute icon URL (https://github.com/WoompaLoompa/lnbits-clink/raw/main/static/clink.png). -
A button/tab icon renders as two or three icons (e.g. the Plans tab showed
event+repeat). The icon name isn't a single ligature in the loaded Material Icons font. Use a single-glyph icon name (see Development); the affected names were fixed inv0.1.3.
lnbits-clink — CLINK (Nostr-native Lightning) for LNbits.