Bitcoin Connect: window.webln Stays After Disconnect
Last week I defended a PR that exposed a latent Use-After-Disconnect bug in every app that sets window.webln from Bitcoin Connect. Defending my own code forced me to read the issue tracker, and that is where I found Issue #215, open since 2024, with zero fixes. Two days later, PR #385 merged.
Quick Take
- The
onConnectedpattern in the Bitcoin Connect README setswindow.weblnbut never clears it- Apps can still call
window.webln.sendPayment()after the user explicitly disconnects- The fix is three lines of README, not a code change
- PR #385 merged 2026-05-07, closing Issue #215 from 2024
- Maintainer asked to drop the inline issue-link from the README before merge
The README Pattern That Leaks a Provider
The Bitcoin Connect README recommends this pattern for apps that need a global window.webln:
import {onConnected} from '@getalby/bitcoin-connect';
onConnected((provider) => {
window.webln = provider;
});
This works on connect, window.webln gets set. But when the user clicks Disconnect, the provider stays on the global object. The original report on Issue #215 demonstrated that an app kept calling window.webln.sendPayment() after the wallet was disconnected, even though the user had explicitly removed it.
In practice, if your app uses this pattern and you run window.webln.getInfo() after disconnecting Alby ↗, you still get a valid response from the stale provider. That is the bug.
Why the Provider Sticks Around
Bitcoin Connect cannot own window.webln because:
- WebLN browser extensions like Alby and Joule also set
window.webln - The first assignment wins and stays until explicitly overwritten
- If the library assigned it internally, it would break apps that already set their own
window.webln
Therefore the responsibility sits with the consumer. The README shows how to set the provider on connect, but it omits the cleanup on disconnect:
// What the README showed:
onConnected((provider) => {
window.webln = provider;
});
// What was missing:
onDisconnected(() => {
delete window.webln;
});
The onDisconnected callback has existed in the library since 2023 (src/api.ts line 81), but pairing it with onConnected for the window cleanup pattern was never documented.
The Docs Fix That Closes a Two-Year-Old Issue
The fix is a three-line addition to the README. No code changes, no new APIs, just the missing pairing of callbacks:
### WebLN global object
-> WARNING: webln is no longer injected into the window object by default.
-> If you need this, execute the following code:
+> WARNING: webln is no longer injected into the window object by default.
+> If you need this, you must also clear it on disconnect. Bitcoin Connect
+> does not own window.webln once you've assigned it, so a disconnected
+> wallet remains reachable via the saved global until you remove it.
+> Pair onConnected with onDisconnected:
```ts
-import {onConnected} from '@getalby/bitcoin-connect';
+import {onConnected, onDisconnected} from '@getalby/bitcoin-connect';
onConnected((provider) => {
window.webln = provider;
});
+onDisconnected(() => {
+ delete window.webln;
+});
Branch: `docs/215-window-webln-disconnect-cleanup`. Two commits: one to add the cleanup pattern, one to drop the inline link to Issue #215 after maintainer feedback.
## Why a Docs Fix Is the Right Fix
Bitcoin Connect cannot clean up `window.webln` in its own disconnect handler because:
1. Other extensions may have set `window.webln` first; the library cannot safely delete what it did not assign
2. Apps store the provider in different ways: some in `window.webln`, some in local state, some in both
3. The WebLN spec expects consumers to manage their own global state, not the library
Therefore the correct fix is documentation that pairs `onConnected` with `onDisconnected`. Any library-side cleanup would break apps that rely on the global for other purposes.
The scope question was real. I considered opening a code PR that added automatic cleanup inside Bitcoin Connect's `disconnect()` path. I dropped that idea for two reasons. First, the fix would require the library to track whether it was the one who set `window.webln` or whether an extension did it first. That tracking logic is non-trivial and introduces a new surface for bugs. Second, rolznz's response to PR #384 made the design stance clear: the library intentionally does not own `window.webln` at all. A code PR trying to clean up a global the library explicitly avoids owning would contradict the maintainer's own design decision. Docs are the right fix here, because the design decision belongs in documentation, not in silent behavior.
## How Bitcoin Connect Signals Disconnection
The `onDisconnected` callback is not a workaround. It is a first-class part of the Bitcoin Connect event model, introduced in the same API surface as `onConnected`. When the user clicks the disconnect button inside the modal, Bitcoin Connect fires the disconnect event internally and calls all registered `onDisconnected` handlers. The library does not clean up `window.webln` because it doesn't know about it. But the event is reliable: tested on Bitcoin Connect as of the PR #385 review in May 2026, `onDisconnected` fires synchronously within the modal's click handler, before any UI state change propagates.
This means `delete window.webln` inside `onDisconnected` runs before your app could attempt another `window.webln.sendPayment()` call from any event triggered by the same user click. The cleanup is not a race. That is worth knowing, because a concern during my PR review was whether the delete could arrive too late if another component reacted to the disconnect event at the same time. It doesn't arrive too late: the callback chain is synchronous.
One caveat: if you store the provider reference in a local variable in addition to `window.webln`, the `onDisconnected` handler won't clear that local reference for you. You have to clear it yourself in the same callback. The README fix covers the `window.webln` case because that is what the original pattern documented. Any other references you created are your own responsibility.
## Lessons from a Two-Year-Old Issue
1. **Defending your own PR forces you to read the issue tracker.** I cited Issue #215 in my defense, only to realize it had been open for two years with no fix. The defensive read became a contribution.
2. **Two-year-old "good first issue" tickets are goldmines.** Low-attention, clearly scoped, occasionally eligible for bounties depending on the project's scope rules. For Bitcoin Connect specifically, Alby's reply from Roland on 2026-05-06 said no eligible BC bounties were available at that point, so this PR shipped on its own merits, not for sats.
3. **Docs PRs land faster than code PRs, but not "same-day" in this case.** No tests, no tooling reviews, no API-stability concerns. The dance was: open PR, [coderabbitai bot review with a nitpick about unsubscribe consistency, maintainer (rolznz) approval contingent on dropping the inline issue link, force-push the cleanup commit (da48a02), merge]. From PR open to merge: 48 hours, two commits.
4. **Read your dependencies' issue trackers.** I use Bitcoin Connect for the Lightning Zap modal on sovgrid.org. Issues that do not block me now might block me later, and I will not notice until I am defending my own code that interacts with the same surface.
## Status, 2026-05-07
[PR #385](https://github.com/getAlby/bitcoin-connect/pull/385) merged on 2026-05-07. Issue #215 closed by the merge. The next Bitcoin Connect release that ships this README will document the `onDisconnected` pairing inline. Apps already using the old pattern continue to work in production but leak the global; updating to the new pattern is one extra import and four extra lines of code.
A separate PR #384 in the same repo (also mine) proposed exposing a `webln:enabled` event for finer-grained app-side detection. That one was [closed by rolznz](https://github.com/getAlby/bitcoin-connect/pull/384) as a design decision: `window.webln` should not be globally set by the library at all; provider references should stay local to the component that initiated the connect. The doc fix in #385 is consistent with that stance, it documents the manual-assignment escape hatch and clarifies the cleanup required.
> **What I Actually Use**
> - Bitcoin Connect: lets me integrate WebLN without owning the global namespace
> - Alby Hub on ARM64 (DGX Spark): self-hosted Lightning node keeping keys local
> - The `onConnected` plus `onDisconnected` pairing in `sovgrid.org`'s Zap modal, since 2026-05 Fix WebLN Provider Leak
Problem to solution workflow for Bitcoin Connect window.webln cleanup