LaWalletdocs
Deploy

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_sent notifications 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 dashboardAdmin → NWC Listener shows 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 deploymentListener setup
Docker ComposeOptional — enable the listener profile and provide a generated secret. Plain docker compose up remains web-only.
Umbrel / Start9Nothing — bundled with the package since v1.1.0.
Vercel / NetlifyYes — serverless platforms can't run a long-lived websocket process. Host the listener elsewhere (below), then connect it in Settings → NWC Services.
Local devpnpm 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:cloud

Replace 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:

VariableRequiredDescription
DATABASE_URLThe 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_SECRETWebhook 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_SECRETOptional dedicated web→listener bearer secret, min 32 chars. Set the same value on web; otherwise both sides fall back to LISTENER_AUTH_SECRET.
WEB_ORIGINPublic URL of your web app (e.g. https://wallet.example.com) — payment webhooks are POSTed back there.
NWC_VAULT_SECRETNWC dataRequired 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_MSDeferred proxy recovery scheduler (default 600000, ten minutes).
LISTENER_PORTHTTP port (default 4100).
LOG_LEVEL / LOG_PRETTYLogging (default info / JSON).
EVENT_RETENTION_DAYSDays of processed events kept for dedup + dashboard feed (default 30).
NWC_REQUEST_TIMEOUT_MSDefault timeout for proxied /nwc/request calls (default 30000).
WEBHOOK_MAX_ATTEMPTSInline webhook delivery attempts before the sweep takes over (default 5).
RECONCILE_INTERVAL_MSPeriodic full pool reconcile, the LISTEN/NOTIFY safety net (default 300000).
CATCHUP_ENABLEDMissed-event recovery after downtime (default true).
CATCHUP_MAX_WINDOW_HOURSFurthest back a catch-up looks (default 24).
CATCHUP_OVERLAP_SECONDSOverlap subtracted from the per-wallet cursor (default 300).
CATCHUP_INTERVAL_MSPeriodic safety catch-up; 0 disables (default 900000).
DEAD_WALLET_DETECTION_ENABLEDAuto-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_HOURSSilence (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)

  1. In Railway, create a New Project → Deploy from GitHub repo and pick your fork of lawallet-nwc (or the upstream repo).
  2. In the service settings, set Dockerfile Path to apps/listener/Dockerfile (Root Directory stays the repo root — the build needs the whole workspace).
  3. Add the environment variables from the table above. Generate the LISTENER_AUTH_SECRET in Settings → NWC Services (the Generate button) and copy the same value here. For separately managed HTTP credentials, set the same LISTENER_REQUEST_AUTH_SECRET on both hosts; this is optional.
  4. Under Networking, generate a public domain for the service.
  5. 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-project

Full 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:latest

To 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

  1. Open Admin → Settings → NWC Services.
  2. Paste the listener's public URL (e.g. https://lawallet-listener.up.railway.app).
  3. 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; dedicated LISTENER_REQUEST_AUTH_SECRET separation is available for env-managed deployments.
  4. Click Test connection — you should see uptime and connection counts.
  5. 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 -d

Persist 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. disconnected means the relay link dropped and is reconnecting; error means warm-up failed and is backing off. A wallet is not used for the card fast path until it reaches ready.
  • 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.

On this page