No description
  • Go 99%
  • Dockerfile 1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
agent 16a283c1a3 Take the public domain from X-Forwarded-Host when set (#3)
Follow-up to #2. Apps that proxy with their own HTTP client send the upstream address as `Host`, so the callback came out as `https://192.168.4.12:7233/...`. Now the server uses `X-Forwarded-Host` if it's set and falls back to `Host`.

Each app must send `X-Forwarded-Host: <its domain>` when it calls lnurl. The server trusts the header, so it must only be reachable over the tailnet.

Tested with `go vet ./...` and `go test ./...`. There's a new test for a callback built from `X-Forwarded-Host`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://git.jouzina.com/pycan/lnurl-server/pulls/3
2026-09-28 10:49:26 +00:00
.forgejo/workflows ci: move Docker workflow to Forgejo Actions (#1) 2026-09-28 10:14:12 +00:00
cmd/lnurl-server feat: print bech32-encoded LNURL for unclaimed vouchers at startup 2026-03-29 13:36:56 +02:00
internal Take the public domain from X-Forwarded-Host when set (#3) 2026-09-28 10:49:26 +00:00
.gitignore feat: Dockerfile, docker-compose, and .gitignore 2026-03-28 07:48:27 +01:00
config.example.yaml Serve Lightning Addresses on the request's domain (#2) 2026-09-28 10:15:15 +00:00
docker-compose.yaml fix: add CMD prefix to docker-compose healthcheck test 2026-03-29 10:38:26 +02:00
Dockerfile chore: remove redundant HEALTHCHECK from Dockerfile 2026-03-29 10:27:20 +02:00
go.mod refactor: hardcode LND cert/macaroon paths, add reconnect backoff, include memo in payment notifications 2026-03-28 21:29:06 +01:00
go.sum feat: add per-IP rate limiting middleware (10 req/s, burst 20) 2026-03-28 09:39:49 +01:00
README.md Initialize README with project overview and setup 2026-08-06 15:16:07 +02:00

lnurl-server

A small, self-hosted Lightning Address server written in Go. It lets you receive Lightning payments at you@yourdomain.com using your own node, instead of relying on a custodial provider.

Most Lightning Addresses are issued by a third party. That means someone else's server creates the invoices you get paid with, and sees who is paying you. This serves them from your own LND node instead.

How it works

A Lightning Address is a human-readable wrapper around LNURL-pay:

  1. A wallet resolves you@yourdomain.com to GET /.well-known/lnurlp/you
  2. The server replies with a payRequest document: callback URL, min/max sendable amounts, and metadata
  3. The wallet calls the callback with the amount it wants to send
  4. The server asks LND to create an invoice (AddInvoice over gRPC) and returns the BOLT11 string
  5. The wallet pays the invoice and the funds settle in your node

Features

  • LNURL-pay / Lightning Address support (LUD-06, LUD-16)
  • LND backend over gRPC with macaroon authentication
  • YAML configuration
  • Request rate limiting
  • Docker image and Compose file for deployment
  • CI via GitHub Actions

Requirements

  • A running LND node reachable over gRPC, plus its TLS certificate and a macaroon
  • A domain with HTTPS terminated in front of the server (Caddy, nginx, or similar). The .well-known endpoint must be served over TLS for wallets to accept it.
  • Go 1.25+ if building from source

Quick start

git clone https://github.com/pycanis/lnurl-server.git
cd lnurl-server

cp config.example.yaml config.yaml
# edit config.yaml: your domain, address names, and LND connection details

docker compose up -d

Then verify it resolves:

curl https://yourdomain.com/.well-known/lnurlp/you

Configuration

See config.example.yaml for all available options, including the LND host, TLS certificate and macaroon paths, the addresses to serve, and the sendable amount limits.

Endpoints

Method Path Description
GET /.well-known/lnurlp/{name} Returns the LNURL-pay payRequest document
GET (callback from above) Returns a BOLT11 invoice for the requested amount

Security notes

Use an invoice-only macaroon rather than admin.macaroon. The server only ever needs to create invoices, so it should not be able to move funds or read your channel state. You can bake a restricted macaroon with lncli bakemacaroon.

The server never holds keys and never has spending authority — it asks your node for an invoice and hands it to the payer.

Development

go run ./cmd/lnurl-server
go test ./...

Layout follows the standard Go convention: entrypoint in cmd/lnurl-server, everything else under internal/.

References

License

MIT