NWC Listener Setup
Optionally run the NWC relay bridge next to your LaWallet instance or host it separately for Vercel/Netlify.
What the listener does
The NWC Listener (apps/listener) is an optional companion service that keeps
persistent Nostr relay connections open for every active NWC wallet on your
instance:
- Incoming payments become webhooks — NIP-47
payment_received/payment_sentnotifications are forwarded to the web app (HMAC-signed), flipping invoices to paid in real time. - Card payments get fast without duplicate risk — an idempotent, wallet-ID-routed endpoint reuses the already-open relay socket, journals the dispatch before NWC, and reconciles late results by request ID.
- Other outgoing NWC calls get fast — the general compatibility proxy can reuse the pool for balance checks and invoice minting.
- Live dashboard —
Admin → NWC Listenershows active relays, connections and recent events.
LaWallet works without it: NWC calls fall back to per-request relay connections, and no incoming payment notifications are received.
Do I need to deploy anything?
| Your deployment | Listener setup |
|---|---|
| Docker Compose | Optional — enable the listener profile and provide a generated secret. Plain docker compose up remains web-only. |
| Umbrel / Start9 | Nothing — bundled with the package since v1.1.0. |
| Vercel / Netlify | Yes — serverless platforms can't run a long-lived websocket process. Host the listener elsewhere (below), then connect it in Settings → NWC Services. |
| Local dev | pnpm dev:listener (env is provisioned by pnpm dev:setup). |
Environment variables
The listener is one small container — use the prebuilt multi-arch image
masize/lawallet-nwc-listener
(published alongside each release) or build from
apps/listener/Dockerfile. Default port 4100.
For a Vercel or Netlify deployment, generate matched web/listener files with:
pnpm deploy:env:cloudReplace the placeholders, then use lawallet-cloud-env/listener.env on this
host. See Environment Variables for the role and
placement of each value.
Its required and optional variables are:
| Variable | Required | Description |
|---|---|---|
DATABASE_URL | ✅ | The same Postgres your web app uses (it reads NWC wallets and stores processed events plus the idempotent payment journal). Must be a direct connection — see the pooling note below. |
LISTENER_AUTH_SECRET | ✅ | Webhook HMAC secret, min 32 chars. Also guards the listener API when no dedicated request secret is set. Must match the web-side webhook secret. |
LISTENER_REQUEST_AUTH_SECRET | — | Optional dedicated web→listener bearer secret, min 32 chars. Set the same value on web; otherwise both sides fall back to LISTENER_AUTH_SECRET. |
WEB_ORIGIN | ✅ | Public URL of your web app (e.g. https://wallet.example.com) — payment webhooks are POSTed back there. |
NWC_VAULT_SECRET | NWC data | Required on both web and listener whenever NWC RemoteWallets or deferred forwarding are used. Encrypts/decrypts RemoteWallet and proxy NWC URIs; min 32 chars. |
PROXY_RECONCILE_INTERVAL_MS | — | Deferred proxy recovery scheduler (default 600000, ten minutes). |
LISTENER_PORT | — | HTTP port (default 4100). |
LOG_LEVEL / LOG_PRETTY | — | Logging (default info / JSON). |
EVENT_RETENTION_DAYS | — | Days of processed events kept for dedup + dashboard feed (default 30). |
NWC_REQUEST_TIMEOUT_MS | — | Default timeout for proxied /nwc/request calls (default 30000). |
WEBHOOK_MAX_ATTEMPTS | — | Inline webhook delivery attempts before the sweep takes over (default 5). |
RECONCILE_INTERVAL_MS | — | Periodic full pool reconcile, the LISTEN/NOTIFY safety net (default 300000). |
CATCHUP_ENABLED | — | Missed-event recovery after downtime (default true). |
CATCHUP_MAX_WINDOW_HOURS | — | Furthest back a catch-up looks (default 24). |
CATCHUP_OVERLAP_SECONDS | — | Overlap subtracted from the per-wallet cursor (default 300). |
CATCHUP_INTERVAL_MS | — | Periodic safety catch-up; 0 disables (default 900000). |
DEAD_WALLET_DETECTION_ENABLED | — | Auto-archive a disposable LNCurl wallet as DEAD after it stops answering for DEAD_THRESHOLD_HOURS while its relays stay up (default true; set false to disable). Non-LNCurl wallets are never auto-archived. |
DEAD_THRESHOLD_HOURS | — | Silence (relays up) before a wallet is declared dead (default 4). |
Connectivity requirements: the listener must reach your Postgres and the Nostr relays; your web app must reach the listener's URL; the listener must reach your web app's URL. With a hosted Postgres (Neon, Supabase, Railway Postgres — typical for Vercel deployments) all of this works over the public internet.
Use the direct (unpooled) database URL
The listener keeps a dedicated LISTEN client open so wallet changes apply
instantly. Transaction-mode poolers drop LISTEN/NOTIFY silently, so a pooled
URL leaves the listener reconciling only every five minutes instead of on
every change. Neon publishes the direct host as DATABASE_URL_UNPOOLED (the
pooled one has a -pooler suffix); on Supabase use port 5432, not the
6543 pooler.
Hosting options for Vercel/Netlify deployments
Railway (walkthrough)
- In Railway, create a New Project → Deploy from
GitHub repo and pick your fork of
lawallet-nwc(or the upstream repo). - In the service settings, set Dockerfile Path to
apps/listener/Dockerfile(Root Directory stays the repo root — the build needs the whole workspace). - Add the environment variables from the table above. Generate the
LISTENER_AUTH_SECRETin Settings → NWC Services (the Generate button) and copy the same value here. For separately managed HTTP credentials, set the sameLISTENER_REQUEST_AUTH_SECRETon both hosts; this is optional. - Under Networking, generate a public domain for the service.
- Deploy. The healthcheck path is
/health.
Render
Create a Web Service from the repo, environment Docker, set the
Dockerfile path to apps/listener/Dockerfile, add the env vars, and use the
assigned onrender.com URL.
Fly.io
The recommended host, and the only one here with a committed fly.toml. From
the repo root:
fly auth login
fly apps create lawallet-listener
pnpm deploy:fly --web-origin https://wallet.example.com \
--from-vercel your-scope/your-projectFull walkthrough, verification steps, and the traps worth knowing about — pooled database URLs, single-machine enforcement, and Vercel's write-only secrets — are on the dedicated Fly.io page.
Any VPS with Docker
docker run -d --restart unless-stopped -p 4100:4100 \
-e DATABASE_URL="postgresql://..." \
-e LISTENER_AUTH_SECRET="<same secret as the web app>" \
-e NWC_VAULT_SECRET="<same NWC vault secret as the web app>" \
-e WEB_ORIGIN="https://wallet.example.com" \
masize/lawallet-nwc-listener:latestTo separate HTTP and webhook credentials, also pass
-e LISTENER_REQUEST_AUTH_SECRET="…" and configure the same value on web.
Put it behind your reverse proxy with TLS if the web app connects over the public internet.
Connecting it to LaWallet
- Open Admin → Settings → NWC Services.
- Paste the listener's public URL (e.g.
https://lawallet-listener.up.railway.app). - Set the shared secret — use Generate, then copy the same value to
the listener host's
LISTENER_AUTH_SECRET(redeploy it if needed). The Settings field is the compatibility shared credential; dedicatedLISTENER_REQUEST_AUTH_SECRETseparation is available for env-managed deployments. - Click Test connection — you should see uptime and connection counts.
- Flip Enable listener integration. The NWC Listener dashboard appears in the sidebar.
For the bundled private listener, generate a secret and opt in explicitly:
LISTENER_URL=http://listener:4100 \
LISTENER_AUTH_SECRET="$(openssl rand -hex 32)" \
docker compose --profile listener up -dPersist those values in .env for subsequent Compose commands. Plain
docker compose up -d leaves the listener unconfigured and all payments use
the regular direct NWC path.
Deferred proxy settlement additionally requires a persistent
NWC_VAULT_SECRET in that .env file. Compose passes it to both services. The
NIP-57 receipt signer remains a write-only nsec field in Admin Settings and
must not be copied into the listener environment.
Health & failure modes
GET /health— liveness (used by container healthchecks).GET /ready— unauthenticated capability/readiness probe. Confirm it lists"nwc_payments_v1"and shows ready/not-ready wallet counts.GET /status— authenticated status (relays, connections, counters) — surfaced in the admin dashboard.- Wallet state normally progresses
connecting → negotiating → ready.disconnectedmeans the relay link dropped and is reconnecting;errormeans warm-up failed and is backing off. A wallet is not used for the card fast path until it reachesready. - Listener down → the web app keeps working, degraded to per-request relay connections for payments that have not been sent to listener; the dashboard shows unreachable. If a payment POST times out or resets, web reconciles that request ID and does not issue another NWC payment.
- Web app down → events accumulate in the listener's store and are re-delivered by a sweep when it returns.
Full API reference (endpoints, /nwc/request error codes, webhook signature
and payload): NWC Payment Listener architecture.
Ops-level detail (storage, catch-up rules, failure modes):
docs/services/NWC-LISTENER.md.
Umbrel Deployment
Install LaWallet NWC on umbrelOS from the LaWallet community app store — web app, NWC listener, and PostgreSQL in one app.
Fly.io (NWC Listener)
Run the NWC listener as an always-on Fly.io machine next to a Vercel, Netlify, or any other web deployment that cannot hold websockets open.