$npx -y skills add lightninglabs/lightning-agent-tools --skill lnc-appGuide for building a Lightning Node Connect (LNC) web application using lnc-web
| 1 | The user wants guidance on building an LNC-powered web application. Use the knowledge below to produce clear, accurate advice or code. |
| 2 | |
| 3 | --- |
| 4 | |
| 5 | # Building a Lightning Node Connect (LNC) Web Application |
| 6 | |
| 7 | ## What is LNC? |
| 8 | |
| 9 | Lightning Node Connect lets a browser-based app communicate with an LND node without exposing any ports. The connection is end-to-end encrypted and routed through a mailbox proxy server. The user generates a **pairing phrase** (a BIP39-style mnemonic) in Lightning Terminal (`litd`) that encodes the cryptographic key material for the session. The client derives its keys from this phrase, connects to the mailbox, and performs a Noise protocol handshake with the node. |
| 10 | |
| 11 | ## Package |
| 12 | |
| 13 | ```bash |
| 14 | npm install @lightninglabs/lnc-web |
| 15 | ``` |
| 16 | |
| 17 | The package ships a prebuilt UMD bundle. Import it as a default import: |
| 18 | |
| 19 | ```js |
| 20 | import LNC from '@lightninglabs/lnc-web'; |
| 21 | ``` |
| 22 | |
| 23 | **Vite config** — tell Vite to pre-bundle it (converts UMD to ESM): |
| 24 | |
| 25 | ```js |
| 26 | // vite.config.js |
| 27 | export default { |
| 28 | optimizeDeps: { |
| 29 | include: ['@lightninglabs/lnc-web'], |
| 30 | }, |
| 31 | }; |
| 32 | ``` |
| 33 | |
| 34 | --- |
| 35 | |
| 36 | ## Key concepts |
| 37 | |
| 38 | ### The credential store |
| 39 | |
| 40 | lnc-web stores everything it needs to reconnect in `window.localStorage`, namespaced to avoid conflicts. The credential store holds: |
| 41 | |
| 42 | | Field | Description | |
| 43 | |---|---| |
| 44 | | `pairingPhrase` | The original mnemonic — **one-time use only** | |
| 45 | | `serverHost` | `host:port` of the mailbox proxy (no protocol prefix) | |
| 46 | | `localKey` | Client's private key, generated on first connect | |
| 47 | | `remoteKey` | Node's static public key, received on first connect | |
| 48 | | `password` | Not read/written by LNC itself — exposed for your convenience | |
| 49 | |
| 50 | `isPaired` is a read-only getter that returns `true` when `localKey` and `remoteKey` are both stored, meaning the session can reconnect without the pairing phrase. |
| 51 | |
| 52 | ### What the pairing phrase encodes |
| 53 | |
| 54 | The pairing phrase encodes **cryptographic key material only** — it does not encode the mailbox server address. The `serverHost` must be provided separately and is stored in the credential store after first connection. |
| 55 | |
| 56 | ### Password encryption |
| 57 | |
| 58 | The credential store encrypts `localKey` and `remoteKey` at rest using the password. The password is applied transparently inside the getter/setter — you never call encrypt/decrypt yourself. Set it before connecting: |
| 59 | |
| 60 | ```js |
| 61 | lnc.credentials.password = 'user-chosen-password'; |
| 62 | ``` |
| 63 | |
| 64 | --- |
| 65 | |
| 66 | ## Connection flows |
| 67 | |
| 68 | ### First-time pairing |
| 69 | |
| 70 | Pass `pairingPhrase` and `password` in the **constructor** (not as properties afterwards — the WASM module must be initialised with them): |
| 71 | |
| 72 | ```js |
| 73 | const lnc = new LNC({ |
| 74 | namespace: 'my-app', // isolates localStorage keys |
| 75 | pairingPhrase: phrase, // mnemonic from litcli |
| 76 | password: 'local-password', |
| 77 | }); |
| 78 | |
| 79 | // Override the mailbox if the user is not using the default |
| 80 | lnc.credentials.serverHost = 'mailbox.terminal.lightning.today:443'; |
| 81 | |
| 82 | await lnc.connect(); |
| 83 | |
| 84 | // IMPORTANT: clear the stored pairing phrase after success. |
| 85 | // On reconnect lnc-web would otherwise try to pair again with a |
| 86 | // one-time-use phrase and get "stream not found" from the mailbox. |
| 87 | lnc.credentials.pairingPhrase = ''; |
| 88 | ``` |
| 89 | |
| 90 | ### Reconnection (returning user) |
| 91 | |
| 92 | When `isPaired` is true the stored `localKey`/`remoteKey` and `serverHost` are all that is needed. Do **not** pass `pairingPhrase` — leave it out of the constructor entirely and clear it explicitly before connecting as a safety measure: |
| 93 | |
| 94 | ```js |
| 95 | const lnc = new LNC({ namespace: 'my-app' }); |
| 96 | lnc.credentials.password = 'local-password'; |
| 97 | lnc.credentials.pairingPhrase = ''; // ensure it is never reused |
| 98 | await lnc.connect(); |
| 99 | ``` |
| 100 | |
| 101 | ### Checking connection state |
| 102 | |
| 103 | After `connect()` resolves, check `lnc.isConnected`. If it is `false`, read `lnc.status` for a human-readable reason (e.g. `"Session Not Found"`, `"Wallet Locked"`). Reset the cached instance and throw so the UI can surface the message: |
| 104 | |
| 105 | ```js |
| 106 | if (!lnc.isConnected) { |
| 107 | const reason = lnc.status || 'Unknown error'; |
| 108 | lnc = null; // force a fresh instance on next attempt |
| 109 | throw new Error(reason); |
| 110 | } |
| 111 | ``` |
| 112 | |
| 113 | ### Checking whether credentials exist |
| 114 | |
| 115 | Use a **throwaway instance** to read `isPaired` — do not cache the result or the instance, as you do not yet have the proxy or password: |
| 116 | |
| 117 | ```js |
| 118 | function hasPairedCredentials() { |
| 119 | try { |
| 120 | return new LNC({ namespace: 'my-app' }).credentials.isPaired; |
| 121 | } catch { |
| 122 | return false; |
| 123 | } |
| 124 | } |
| 125 | ``` |
| 126 | |
| 127 | ### Disconnecting / logging out |
| 128 | |
| 129 | ```js |
| 130 | lnc.disconnect(); |
| 131 | lnc.credentials.clear(); // wipes localStorage |
| 132 | lnc = null; |
| 133 | ``` |
| 134 | |
| 135 | --- |
| 136 | |
| 137 | ## Settings to offer the user |
| 138 | |
| 139 | | Setting | Default | Notes | |
| 140 | |---|---|---| |
| 141 | | Proxy server | `mailbox.terminal.lightning.today:443` | Show on pairing screen only — stored in credentials, not needed again | |
| 142 | | Local password | — | Required; encrypts keys in localStorage | |
| 143 | | Pairing phrase | — | One-time use; cleared after first connect | |
| 144 | |
| 145 | The proxy field should be shown **only on the first-time pairing screen**, not o |