A full-stack application for sending and receiving Bitcoin payments over the Lightning Network using two LND nodes.
┌─────────────────────────────────────────────────────────────────────┐
│ Client │
│ (Next.js + React) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────────┐ │
│ │ Receive │ │ Send │ │ Transaction History │ │
│ │ Invoice │ │ Payment │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
│ HTTP/REST
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Server │
│ (Express + Node.js) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────────┐ │
│ │ /invoice │ │ /payment │ │ /transactions /balance │ │
│ │ routes │ │ routes │ │ routes │ │
│ └──────┬──────┘ └──────┬──────┘ └──────────────┬──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Lightning Service │ │
│ │ (ln-service) │ │
│ └──────────────────────────┬──────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ PostgreSQL (Prisma ORM) │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
│ gRPC (TLS + Macaroon Auth)
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Polar (Local Dev) │
│ │
│ ┌───────────────┐ Channel ┌───────────────┐ │
│ │ Node A │◄────────────────────────►│ Node B │ │
│ │ (Alice) │ 1,000,000 sats │ (Bob) │ │
│ │ Receiver │ │ Sender │ │
│ └───────────────┘ └───────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Bitcoin Core │ │
│ │ (regtest) │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
-
Alice creates an invoice (Node A)
- Generates a random
preimage(32 bytes) - Computes
payment_hash = SHA256(preimage) - Creates BOLT11 invoice containing: amount, payment_hash, expiry, destination
- Generates a random
-
Bob pays the invoice (Node B)
- Decodes the invoice to verify amount and destination
- Routes payment through channel to Alice
- Payment is locked with HTLC (Hash Time-Locked Contract)
-
Alice reveals preimage
- Alice reveals
preimageto claim the payment - Bob receives
preimageas proof of payment - Channel balances are updated
- Alice reveals
| Term | Description |
|---|---|
| Payment Hash | SHA256 hash that uniquely identifies a payment |
| Preimage | Secret that proves payment was completed |
| BOLT11 | Standard invoice format (starts with lnbc, lntb, or lnbcrt) |
| Channel | Payment pathway between two nodes with locked funds |
| Satoshi | Smallest Bitcoin unit (1 BTC = 100,000,000 sats) |
- Runtime: Node.js + TypeScript
- Framework: Express.js
- Database: PostgreSQL + Prisma ORM
- Lightning: ln-service (LND wrapper)
- Validation: express-validator
- Logging: pino (structured JSON logs with request tracing)
- Framework: Next.js 15 (App Router)
- Language: TypeScript
- Styling: Tailwind CSS
- Data Fetching: TanStack Query (React Query)
- Lightning Network: Polar (local regtest environment)
- Database: PostgreSQL (via Postgres.app or Docker)
- Testing: Vitest
- CI: GitHub Actions
bitcoin-lightning-network-payment/
├── .github/
│ └── workflows/
│ └── test.yml # CI pipeline
│
├── client/ # Next.js frontend
│ ├── src/
│ │ ├── __tests__/ # Client tests
│ │ │ └── api-validation.test.ts
│ │ ├── app/ # App router pages
│ │ ├── components/ # React components
│ │ │ ├── ReceiveInvoice.tsx
│ │ │ ├── SendPayment.tsx
│ │ │ └── TransactionHistory.tsx
│ │ └── lib/ # Utilities
│ │ ├── api.ts # API client
│ │ ├── socket.ts # WebSocket client
│ │ └── query-provider.tsx
│ └── package.json
│
├── server/ # Express backend
│ ├── prisma/
│ │ ├── migrations/ # Prisma migration files
│ │ └── schema.prisma # Database schema
│ ├── src/
│ │ ├── __tests__/ # Test files
│ │ │ └── api.test.ts
│ │ ├── db/
│ │ │ └── database.ts # Prisma connection
│ │ ├── lib/
│ │ │ └── logger.ts # Pino logger
│ │ ├── middleware/
│ │ │ └── requestLogger.ts # Request ID + logging
│ │ ├── routes/
│ │ │ ├── invoice.ts
│ │ │ ├── payment.ts
│ │ │ └── transactions.ts
│ │ ├── services/
│ │ │ └── lightning.ts # LND service wrapper
│ │ └── index.ts # Entry point
│ ├── .env.example
│ └── package.json
│
├── docker-compose.yml # Docker setup
├── docker-setup.md # Docker instructions
└── README.md
- Node.js 18+
- PostgreSQL
- Polar for local Lightning Network
- Download and install Polar
- Create a new network with:
- 2 LND nodes (Alice and Bob)
- 1 Bitcoin Core node
- Start the network
- Open a channel from Bob → Alice with 1,000,000 sats
- Mine some blocks to confirm the channel
# Create the database
createdb lightning_paymentscd server
# Install dependencies
npm install
# Copy environment file
cp .env.example .env
# Edit .env with your Polar node credentials:
# - LND_A_* = Alice's credentials (receiver)
# - LND_B_* = Bob's credentials (sender)
# Find credentials in ~/.polar/networks/1/volumes/lnd/
# Run database migrations
npx prisma migrate dev
# Start server
npm run devcd client
# Install dependencies
npm install
# Copy environment file
cp .env.example .env
# Start client
npm run dev- Client: http://localhost:3000
- Server: http://localhost:3001
POST /api/invoice
Content-Type: application/json
{ "amount": 1000, "description": "Coffee" }Returns BOLT11 invoice string and payment hash.
POST /api/payment
Content-Type: application/json
X-Idempotency-Key: <uuid>
{ "payment_request": "lnbcrt1000n..." }Idempotency key prevents duplicate payments on retry.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/invoice/:payment_hash |
Get invoice status |
| POST | /api/invoice/decode |
Decode BOLT11 without paying |
| GET | /api/payment/:payment_hash |
Get payment status |
| GET | /api/transactions |
List all transactions (paginated) |
| GET | /api/balance |
Get balance summary |
| GET | /api/nodes |
Get node info |
This app uses WebSocket + LND subscriptions for real-time invoice status updates.
┌────────────┐ gRPC stream ┌────────────┐ WebSocket ┌────────────┐
│ LND │ ────────────────────►│ Server │ ───────────────►│ Client │
│ (Alice) │ subscribeToInvoices │ Socket.IO │ push update │ React │
│ │ │ │ │ │
│ invoice │ "invoice X paid!" │ │ invoice:updated│ UI updates│
│ gets paid │ │ │ │ instantly │
└────────────┘ └────────────┘ └────────────┘
Server (server/src/index.ts):
- Creates Socket.IO server alongside Express
- Subscribes to LND invoice updates via
subscribeToInvoices() - Emits
invoice:updatedevent to all connected clients when invoice status changes
Client (client/src/lib/socket.ts):
- Connects to Socket.IO server
- Listens for
invoice:updatedevents - Updates invoice status in real-time without manual refresh
- Instant feedback: Alice sees payment confirmation immediately when Bob pays
- No polling: Efficient - only sends data when something changes
- Production-ready: Same pattern used by real Lightning apps
invoices payments
├── payment_hash (PK) ├── payment_hash (PK)
├── payment_request ├── payment_request
├── amount ├── amount
├── status ├── fee
├── description ├── status
├── preimage ├── preimage
├── expires_at ├── destination
├── settled_at ├── error_message
└── created_at ├── idempotency_key (unique)
├── settled_at
└── created_at
Payments include idempotency_key to prevent duplicates - client sends X-Idempotency-Key header.
This project uses Prisma Migrate to manage schema changes. Migration files live in server/prisma/migrations/ and are tracked in git.
cd server
# Apply migrations in development (creates new migration if schema changed)
npx prisma migrate dev
# Create a migration without applying it
npx prisma migrate dev --name <description> --create-only
# Apply pending migrations in production/CI
npx prisma migrate deployWhen you change schema.prisma, run npx prisma migrate dev to generate a new migration file. Commit the migration file to git so other developers can apply it.
cd server
npm testIntegration tests verify API input validation:
- Invoice creation - Rejects invalid amounts (negative, zero, decimals)
- Invoice decode - Rejects malformed BOLT11 strings
- Payments - Rejects invalid payment requests
cd client
npm testValidation tests (no mocking - real logic):
- createInvoice - Rejects zero, negative, decimal amounts
- payInvoice - Requires idempotency key
- decodeInvoice - Rejects empty input
Structured logging with pino for production observability.
Features:
- Request IDs for tracing requests through logs
- Log levels (info, warn, error, fatal)
- Pretty output in development, JSON in production
- Automatic request/response logging with duration
GitHub Actions runs on every push to main:
| Job | Description |
|---|---|
test-server |
Server Vitest tests |
test-client |
Client Vitest tests |
lint-client |
ESLint |
build |
Builds server and client |
This is a demo application. For production:
-
Idempotency race condition - Current check-then-insert has a small window for duplicates. Fix: insert pending record first (uses DB constraint as lock) or use Redis
SETNXfor distributed systems. -
No HTTPS - API communicates over plain HTTP. Use TLS termination (nginx/Caddy) or cloud load balancer.
-
Macaroon Security: Use a secrets manager instead of file paths
-
Watchtowers: Set up watchtower for channel monitoring when offline
-
Open API - No authentication; anyone can create invoices or trigger payments. Add API keys or restrict to internal network.
-
Backups: Regularly backup LND channel state (
channel.backup)
-
Wallet integration - Let users connect their own wallets via WebLN or LNURL instead of shared nodes.
-
Rate limiting - Add express-rate-limit to prevent abuse.
-
Channel liquidity - Monitor and rebalance channels; payments fail when liquidity is exhausted.
-
Monitoring - Add Prometheus metrics and alerting beyond logs.
-
E2E tests - Add Playwright tests for automated payment flow testing.