This file provides guidance for AI coding agents working on the Alby Lightning Browser Extension repository.
Alby Lightning Browser Extension — a WebExtension (Chrome MV3, Firefox/Opera MV2) that brings Bitcoin Lightning Network payments and Nostr signing to the browser. It implements the WebLN, Nostr (NIP-07), WebBTC, and Liquid provider APIs by injecting them into web pages and routing requests through a background script that talks to a configurable Lightning backend (LND, LNbits, LNC, LNDhub, NWC, Alby account, Eclair, Galoy, plus native-companion variants for Tor).
- Build: Webpack 5 + SWC, separate builds per target browser (
dist/<env>/<browser>) - Frontend (UI surfaces): React 19, React Router v6, TypeScript (strict), Tailwind CSS 3, i18next, Zustand, axios
- Storage:
browser.storage(settings/state) + Dexie/IndexedDB (allowances, payments, permissions, blocklist) viadb.ts - Crypto:
@noble/*,@scure/*,bitcoinjs-lib,liquidjs-lib,nostr-tools,crypto-js(legacy) - Testing: Jest (unit, via
@swc/jest). The Playwright e2e suite undertests/e2e/is currently broken and unmaintained — rely on Jest only. - Manifest: Single
src/manifest.jsonwith__chrome__/__firefox__/__opera__prefixes resolved bywext-manifest-loaderat build time
yarn install
yarn dev:chrome # watch build → dist/development/chrome
yarn dev:firefox # watch build → dist/development/firefox
yarn dev:operaLoad dist/development/<browser> as an unpacked / temporary extension. See doc/SETUP.md for browser-specific load instructions and doc/SETUP_ANDROID.md for Firefox Android.
yarn build:chrome # single-browser production build
yarn build # chrome + firefox + opera
yarn package # builds all + zips into dist/production via create-packages.shmanifest.json ships with version 0.0.0; the webpack loader rewrites it from package.json at build time.
yarn lint # eslint + tsc --noEmit + prettier --write (the canonical pre-commit check)
yarn lint:js # eslint src --max-warnings 0
yarn lint:js:fix
yarn tsc:compile # tsc --noEmit
yarn format # prettier --check
yarn format:fixESLint, Prettier, and tsc --noEmit are enforced on staged files via Husky + lint-staged. Commit messages are validated by commitlint (Conventional Commits).
yarn test:unit # all jest tests
yarn test:unit path/to/file.test.ts # single file
yarn test:unit -t "fragment of test name" # by test name
yarn test:coverageThe Playwright yarn test:e2e / yarn test scripts still exist in package.json but the e2e suite is broken and not maintained — do not rely on them. Add new tests as Jest unit tests (colocated *.test.ts next to source, or under tests/unit/).
The extension code is split across three runtime contexts that cannot share modules or memory directly — they communicate only via browser.runtime.sendMessage and window.postMessage.
- Background script (
src/extension/background-script/) — long-lived (MV2) / service-worker-like (MV3) singleton. Owns the unlocked wallet state, IndexedDB, the connector instance, and is the only place that touches the Lightning backend or private keys. - Content scripts (
src/extension/content-script/*.js) — injected into every page. They inject the corresponding inpage script (inpage-script/) into the page's main world and proxy messages between page ↔ background. There is one content script per provider (webln.js,alby.js,nostr.js,liquid.js,webbtc.js) plusonstart.tsandcontext-menu.ts. - UI surfaces (
src/app/router/) — separate React apps bundled as different webpack entries:popup— the toolbar popupprompt— modal window opened by the background script for user confirmation (payment, signMessage, permission, etc.)options— full-page settingswelcome— onboarding flow
Each surface lives under src/app/router/<Name>/index.tsx and is referenced from webpack.config.js entry: and from static/views/*.html.
Inpage providers live in src/extension/providers/<api>/index.ts. Each method calls this.execute(actionName, args) which postMessages to its content-script bridge; the bridge forwards to router.ts and the response comes back the same way (see postMessage.ts and providerBase.ts). All methods require enable() to have resolved first; providerBase enforces this. Public docs: https://guides.getalby.com/developer-guide/developer-guide/alby-browser-extension-apis.
window.webln — providers/webln/index.ts (WebLN spec)
| Method | Purpose |
|---|---|
enable() |
Request user permission for the site; must be called before any other method. |
getInfo() |
Returns connected node info (alias, pubkey, color, network). |
sendPayment(paymentRequest) |
Pay a BOLT11 invoice. Opens a prompt unless an allowance covers it. |
sendPaymentAsync(paymentRequest) |
Fire-and-forget payment (no waiting for preimage). |
keysend({ destination, amount, customRecords? }) |
Spontaneous (keysend) payment to a pubkey. |
makeInvoice(amountOrArgs) |
Create a BOLT11 invoice on the active account. |
signMessage(message) |
Sign an arbitrary message with the wallet's message-signing key. |
verifyMessage(signature, message) |
Verify a previously signed message. |
getBalance() |
Return the current spendable balance. |
request(method, params) |
Escape hatch — forward an arbitrary RPC to the active connector. |
on(event, handler) / off(...) |
Subscribe to provider events (e.g. accountChanged). |
window.nostr — providers/nostr/index.ts (NIP-07)
| Method | Purpose |
|---|---|
getPublicKey() |
Return the user's Nostr pubkey (hex). |
signEvent(event) |
Sign a Nostr event; fills pubkey, id, and sig. |
signSchnorr(sigHash) / hashAndSignSchnorr(message) |
Raw schnorr signature over a 32-byte hash, or over sha256(message). |
nip04.encrypt(peerHexPubkey, plaintext) / nip04.decrypt(peerHexPubkey, ciphertext) |
NIP-04 (legacy) DM encryption. |
nip44.encrypt(peerHexPubkey, plaintext) / nip44.decrypt(peerHexPubkey, ciphertext) |
NIP-44 v2 encryption. peer must be a 32-byte hex pubkey, not bech32 (npub...). |
on(event, handler) / off(...) |
Subscribe to provider events. |
window.webbtc — providers/webbtc/index.ts (on-chain BTC, taproot account only)
| Method | Purpose |
|---|---|
getInfo() |
Connected-wallet info for the BTC side. |
getAddress() |
Return the user's on-chain taproot address. |
signPsbt(psbtHex) |
Sign a PSBT with the taproot key. |
sendTransaction(address, amount) |
Construct, sign, and broadcast an on-chain payment. |
request(method, params) |
Generic RPC passthrough. |
window.liquid — providers/liquid/index.ts (Liquid Network, taproot account only)
| Method | Purpose |
|---|---|
getAddress() |
Return a confidential Liquid taproot address. |
signPset(psetBase64) |
Sign a Liquid PSET. |
window.alby — providers/alby/index.ts (vendor-specific)
| Method | Purpose |
|---|---|
addAccount({ name, connector, config }) |
Programmatically create a new account in the extension (e.g. onboarding flows). |
All providers also inherit enable() / isEnabled() / on() / off() from providerBase.ts. To add a new method: add it to the inpage provider, register a corresponding action in router.ts, implement the handler under actions/, define the message type in src/types.ts, and (for WebLN-like flows) extend connector.interface.ts on every connector.
Web page
→ injected provider (src/extension/providers/<api>/index.ts, run in MAIN world)
→ window.postMessage to content script
→ content script (src/extension/content-script/<api>.js) → browser.runtime.sendMessage
→ background-script router (src/extension/background-script/router.ts)
→ action handler (src/extension/background-script/actions/<area>/<verb>.ts)
→ either: returns directly, OR opens a Prompt window for user confirmation
→ connector (src/extension/background-script/connectors/<backend>.ts) implements connector.interface.ts
→ response propagates back the same path
router.ts is the central map from string action name → handler. Adding a new RPC means registering it there and exposing it via src/common/lib/api.ts (the typed client used by React UI and content scripts).
src/extension/background-script/connectors/connector.interface.ts is the abstraction every Lightning backend implements (getInfo, sendPayment, keysend, makeInvoice, signMessage, etc.). Adding/modifying a method here means updating every connector implementation (alby, lnd, lnc, lnbits, lndhub, nwc, eclair, galoy, and the native* companion-app variants). The Native.ts base class handles the native-messaging transport used for Tor proxying via the alby-companion-rs host app.
state.ts(Zustand-style store in background) — unlocked wallet state, current account, current connector instance, password. Rehydrated frombrowser.storageon startup; the password lives in memory only.db.ts(Dexie / IndexedDB) — allowances, payments, permissions, blocklist. The UI reads via the typedrequest()API and never importsdb.tsdirectly.browser.storage.local— settings, account metadata (with the connector config encrypted by the unlock password).- Migrations: src/extension/background-script/migrations/index.ts — a single object keyed by migration name. Each key runs once on startup; the executed names are tracked in
state.migrations. Add a new key for a new migration; never rename or remove an existing one.
These are the user-visible behaviors backed by db.ts. An agent touching auth/payment flows must understand which one applies — they are distinct mechanisms, not synonyms.
- Allowance (
db.allowances) — a per-host payment budget (totalBudget/remainingBudget, in sats). When a site has an allowance,webln.sendPayment/keysenddebit it silently up toremainingBudget; over-budget or expired allowances fall back to a Prompt. Allowances also flaglnurlAuth(auto-login allowed) andenabledFor(the WebLN method scopes). Created via the Prompt's "remember" option. - Permission (
db.permissions) — a per-host, per-method auto-approve flag independent of any budget. Keyed by(accountId, host, method)wheremethodis one ofPermissionMethodNostr(nostr/signMessage,nostr/signSchnorr,nostr/getPublicKey,nostr/encrypt,nostr/decrypt),PermissionMethodBitcoin(bitcoin/getAddress), orPermissionMethodLiquid(liquid/getAddress).blocked: truemeans "always deny without prompting." Managed viaaddPermissionFor/hasPermissionFor/isPermissionBlockedin permissions/. - Blocklist (
db.blocklist) — a site the user has fully suppressed; the inpage providers should not be active there. Distinct from blocked Permission rows (which gate specific methods). - Payment (
db.payments) — a local audit log of every sent payment (preimage,paymentHash,host,allowanceId, fees). Read-only history; the UI lists this in account detail. - Account encryption — every
account.config(connector creds: macaroons, NWC URIs, mnemonics, …) is encrypted with the user's unlock password before being written tobrowser.storage. The decrypted config is held only onstate.connectorwhile the wallet is unlocked. Never persist a connector config in plaintext, and never log it. - Onboarding (Welcome flow) — each backend has a dedicated connect screen under src/app/screens/connectors/Connect<Backend>/. Adding a new connector means: (a) implement the class under
background-script/connectors/, (b) add theConnect<Name>screen, (c) register the route in src/app/router/connectorRoutes.tsx, (d) translate the labels. - LNURL flows — handled in actions/lnurl/:
auth(LUD-04 login, optionally via mnemonic-derived linking key per LUD-05),pay(LUD-06 pay-to-LN-address),withdraw(LUD-03),channel(LUD-07). These run in the background script, open a Prompt, then call the active connector.
tsconfig.json defines ~/* → src/*, @components/* → src/app/components/*, @screens/* → src/app/screens/*. Prefer these over deep relative imports.
src/manifest.json uses __chrome__, __firefox__, __opera__, and __chrome|firefox__ key prefixes. wext-manifest-loader strips the prefixes for the active TARGET_BROWSER at build time. Chrome ships as MV3 (service worker, host_permissions); Firefox/Opera ship as MV2 (background page, combined permissions). When adding a permission or background entry, set both variants.
- TypeScript strict mode — no
any. New code is.ts/.tsx; pre-existing.jscontent scripts and inpage scripts may stay JS. - i18n: All user-facing strings go through
react-i18next. Translations live insrc/i18n/locales/<lang>/translation.json. Usesnake_casekeys, nest by screen/component, only indent when there's more than one attribute (title/label). Seedoc/CONTRIBUTION.mdfor full rules — translations are managed via Weblate, so structure matters. - HTTP: Use
axios(notfetch) — this is an explicit project preference. - Commits: Conventional Commits (
feat:,fix:,chore:,refactor:,docs:) — enforced by commitlint. Same prefixes for branch names (feat/...). - No sensitive data in logs. The background script's console is shared; never log seeds, mnemonics, macaroons, NWC URLs, or unlock passwords.
- Prompt windows are user-facing. Any new action that requires confirmation should open a Prompt route under
src/app/router/Promptand resolve via the message returned from it.
| File | Purpose |
|---|---|
webpack.config.js |
Build configuration; lists every entry (popup/prompt/options/welcome + content/inpage scripts) |
src/manifest.json |
Multi-browser manifest with __chrome__ / __firefox__ / __opera__ overrides |
src/extension/background-script/index.ts |
Background entry: migrations, state hydration, tab listeners, prompt orchestration |
src/extension/background-script/router.ts |
Single source of truth for background RPC routes |
src/extension/background-script/connectors/connector.interface.ts |
The interface every Lightning backend implements |
src/extension/background-script/state.ts |
In-memory wallet/session state |
src/extension/background-script/db.ts |
Dexie/IndexedDB schema |
src/common/lib/api.ts |
Typed RPC client used by UI and content scripts |
src/app/router/{Popup,Prompt,Options,Welcome}/index.tsx |
The four React app entry points |
src/i18n/locales/en/translation.json |
Source-of-truth English strings |