Skip to content

Repository files navigation

LNURLserver

A self-hosted LNURL server for a Lightning node: payment requests, withdraw requests and passwordless login, backed by your own LND or Core Lightning node for the actual invoice/payment plumbing. Run it just for yourself, or host it for friends and family - either way, no third-party custodian, just your own node and infrastructure.

Scope

LNURLserver lets each registered user manage their own set of LNURL links from a web UI, and serves the corresponding LNURL/LN Address endpoints publicly. It implements:

LUD What it gives you
01 Base bech32 LNURL encoding/decoding
02 channelRequest - convert a user's balance into one incoming channel
03 withdrawRequest - hand out QR codes any wallet can withdraw from
04 lnurl-auth - passwordless login/registration via a linking key
05 Seed-phrase-derived auth keys, no external wallet required
06 payRequest - accept payments against a link
07 hostedChannelRequest - advertise this instance's node for a hosted channel
08 Fast withdrawRequest - discovery fields embedded directly in the link
09 / 10 Success actions after payment (message, URL, AES payload)
11 disposable - per-link toggle for whether a payRequest is one-shot
12 Payer comments on a payRequest
13 signMessage-derived auth keys, for hardware/node-signed logins
14 balanceCheck - a reusable withdrawRequest for the default link
15 balanceNotify - accepts and stores a wallet's notify-me-later URL
16 Lightning Address (user@domain) for every payment link
17 Raw lnurlp:/lnurlw:/keyauth: URL schemes alongside bech32
18 Payer identity (name/email/pubkey/identifier/auth) in payRequest
19 payLink - a user's payRequest, discoverable from their withdrawRequest
20 Long payment description for a payRequest
21 verify - check whether a payRequest invoice was actually paid
23 addressRequest - let a wallet push a receive address to a user

A user can hold several payment (lnurlp) and withdraw (lnurlw) links at once - one of each is the account's default, tied to its username. Every user's links move funds through this instance's own node - see Configuration - configured once by the operator, not per-user.

Each link holds its own spendable balance: a lnurlp accrues balance as its payRequest invoices get paid, and a lnurlw spends down its own balance on withdrawal - unless it's linked to a payLink (LUD-19), in which case it charges that payLink's balance directly instead. Balance can also be moved explicitly from a lnurlp to a lnurlw (in full or in part). The /u/wallet page lists every invoice received and payment sent, filterable per link, updates live over a websocket whenever a balance changes anywhere on the account, and embeds a "paste an LNURL, Lightning Address, or raw BOLT11 invoice" widget to pay or withdraw right there and watch the resulting balance change land.

Architecture

  • backend/ - a FastAPI app (lnurl_server package) that serves the public LNURL endpoints, a cookie-session-authenticated /u API for managing a user's links and settings, and talks to this instance's own LND/CLN node over HTTP for invoice creation/payouts. Storage is SQLite via SQLAlchemy, schema-versioned with Alembic.
  • frontend/ - a SolidJS + TypeScript SPA (Vite build) that logs in via lnurl-auth and drives the /u API to manage payment/withdraw links, Lightning Address, account settings, and the wallet view. The API client is generated from the backend's OpenAPI schema; a GET /u/balance/ws websocket pushes a refresh signal to every logged-in tab whenever a balance moves.

Getting started

Requires uv and Node.js.

make install    # uv sync + apply Alembic migrations
make dev        # backend, http://localhost:8000
make frontend   # frontend dev server, http://localhost:3000

See the Makefile for the full set of targets - make lint runs ruff, pyright, mypy and tsc; make check runs the black/ruff/prettier formatting checks. See Testing for make test and everything else.

Configuration

Backend runtime settings are read from environment variables (via pydantic-settings), optionally loaded from a .env file. Copy backend/.env.template to backend/.env and fill in whatever you need - every setting is optional.

Variable Purpose
FUNDINGSOURCE_BACKEND lnd or cln - this instance's own node, used for every user's lnurlp/lnurlw invoices and for LUD-02 channelRequest (node identity, connect/balance-check/open-channel). Unset disables all of that.
FUNDINGSOURCE_URL REST base URL of that node (lnd's REST API, or cln's clnrest plugin).
FUNDINGSOURCE_MACAROON lnd only: hex-encoded macaroon. Needs offchain/onchain/address/peers/info permissions (an admin.macaroon-level scope) to fully automate channelRequest; getinfo alone is enough for just node identity + regular payments.
FUNDINGSOURCE_RUNE cln only: a rune (from lightning-cli createrune) with equivalent permissions.
FUNDINGSOURCE_CERT_PATH Optional: path to a self-signed TLS cert to verify the funding source against - both backends commonly self-sign. Leave unset if it's fronted by a reverse proxy with a real certificate.

Don't have a node handy to test against? See Testing below - e2e/docker/ spins up a local bitcoind + lnd + cln regtest environment with a channel already opened between them.

Testing

make test           # everything: backend + frontend + e2e
make test-backend    # pytest
make test-frontend   # vitest
  • backend/tests/ - unit/integration tests. The funding source is mocked (httpx.MockTransport) except in test_regtest.py, which runs the same lnd/cln calls for real and self-skips unless REGTEST_LND_URL is set (see "End-to-end and regtest" below) - it never accidentally runs outside CI or a real regtest environment.
  • frontend/src/**/*.test.tsx - component tests (Vitest + @solidjs/testing-library).
  • e2e/ - its own package, independent of frontend/. Drives a real browser (Playwright) against the actual built app with no mocked funding source at all - see below.

End-to-end and regtest

e2e/docker/ spins up a local Bitcoin regtest environment (bitcoind + lnd + cln, with a channel opened between them) plus two real, containerized instances of this app - one backed by lnd, one by cln - so e2e tests (and manual testing, below) exercise real invoices, real payments, and real LUD-02 channel opens instead of a mocked funding source. Full details (including first-time browser cert setup for https://s1.dev/https://s2.dev) are in e2e/docker/README.md.

sudo apt install jq   # if not already installed
make run-test-fresh    # spins up regtest + both app instances
make test-e2e            # runs the Playwright suite against them
make run-test-stop        # stop the app instances (leaves regtest running)
make regtest-stop          # tear down regtest too (chain/wallet state and all)

Manual testing

make run-test-fresh (first time) / make run-test (after, to pick up code changes) already gives you two fully configured, fully functional instances of the real app - no separate dev servers or hand-written backend/.env needed:

make run-test-fresh   # from repo root - see "End-to-end and regtest" above

(Direct, no TLS: http://localhost:8010 / http://localhost:8020 - but https://s1.dev/https://s2.dev match production more closely, including the cross-instance forward_lnaddress/"fetch on server" paths, which rely on those hostnames actually resolving. First time through, there's a one-time step to trust Caddy's local cert - see "First-time browser setup" in e2e/docker/README.md.)

Register an account on each (seed-phrase login, no external wallet needed), create a payment/withdraw link, and pay/withdraw against one from the other instance - each is backed by a different real node with a real channel open between them.

Running with Docker

make build   # docker build, bundles the frontend into the backend image
make run     # runs it on :8000, with the SQLite file persisted in ./data

make run automatically passes --env-file backend/.env if that file exists (see Configuration).

License

MIT, see backend/pyproject.toml.

About

LNURL server, utilizing all LUDS. Run for yourself or friends and family.

Resources

Stars

Watchers

Forks

Contributors

Languages