Add Lightning.Pub NixOS module
Some checks are pending
nix-bitcoin tests / build_test_drivers (push) Waiting to run
nix-bitcoin tests / test_scenario (default) (push) Blocked by required conditions
nix-bitcoin tests / test_scenario (joinmarket-bitcoind-29) (push) Blocked by required conditions
nix-bitcoin tests / test_scenario (netns) (push) Blocked by required conditions
nix-bitcoin tests / test_scenario (netnsRegtest) (push) Blocked by required conditions
nix-bitcoin tests / check_flake (push) Waiting to run

Lightning.Pub is a Lightning Network account system running on top of LND.
The module follows nix-bitcoin patterns with a build oneshot service and
a hardened runtime service wired to LND via gRPC.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-02-27 09:21:46 -05:00
parent 216368a688
commit dd399773da
2 changed files with 218 additions and 0 deletions

217
modules/lightning-pub.nix Normal file
View file

@ -0,0 +1,217 @@
{ config, lib, pkgs, ... }:
with lib;
let
options.services.lightning-pub = {
enable = mkEnableOption "Lightning.Pub, a Lightning Network account system running on top of LND";
port = mkOption {
type = types.port;
default = 1776;
description = "Port for the Lightning.Pub REST API.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/lightning-pub";
description = "Data directory for Lightning.Pub.";
};
user = mkOption {
type = types.str;
default = "lightning-pub";
description = "User to run Lightning.Pub as.";
};
group = mkOption {
type = types.str;
default = cfg.user;
description = "Group to run Lightning.Pub as.";
};
source = {
url = mkOption {
type = types.str;
default = "https://git.atitlan.io/aiolabs/lightning-pub";
description = "Git repository URL for Lightning.Pub source.";
};
ref = mkOption {
type = types.str;
default = "master";
description = "Git ref (branch, tag, or commit) to checkout.";
};
};
logLevel = mkOption {
type = types.enum [ "DEBUG" "INFO" "WARN" "ERROR" ];
default = "INFO";
description = "Logging level for Lightning.Pub.";
};
nostrRelays = mkOption {
type = types.str;
default = "wss://relay.lightning.pub";
description = "Comma-separated list of Nostr relay WebSocket URLs.";
};
serviceFee = {
bps = mkOption {
type = types.int;
default = 60;
description = "Service fee in basis points.";
};
floorSats = mkOption {
type = types.int;
default = 10;
description = "Minimum service fee floor in satoshis.";
};
};
watchdogMaxDiffSats = mkOption {
type = types.int;
default = 0;
description = "Maximum allowed balance difference in satoshis for the watchdog (0 = disabled).";
};
extraEnv = mkOption {
type = with types; attrsOf str;
default = {};
description = "Extra environment variables to pass to Lightning.Pub.";
};
};
cfg = config.services.lightning-pub;
nbLib = config.nix-bitcoin.lib;
lnd = config.services.lnd;
lightningPubEnv = pkgs.writeShellScript "lightning-pub-env" ''
set -euo pipefail
export PATH=${pkgs.nodejs_22}/bin:$PATH
cd ${cfg.dataDir}/source
exec "$@"
'';
in {
inherit options;
config = mkIf cfg.enable {
services.lnd.enable = true;
users.users.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
home = cfg.dataDir;
extraGroups = [ lnd.group ];
};
users.groups.${cfg.group} = {};
nix-bitcoin.operator = {
groups = [ cfg.group ];
allowRunAsUsers = [ cfg.user ];
};
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
];
# Build service (oneshot) — clones repo, installs deps, compiles TypeScript
systemd.services.lightning-pub-build = {
description = "Clone and Build Lightning.Pub";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
path = with pkgs; [
nodejs_22 python3 git coreutils bash
stdenv.cc gnumake pkg-config
];
environment = {
HOME = cfg.dataDir;
npm_config_cache = "${cfg.dataDir}/.npm-cache";
};
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = cfg.user;
Group = cfg.group;
TimeoutStartSec = "30min";
ProtectSystem = "strict";
ProtectHome = true;
NoNewPrivileges = true;
ReadWritePaths = [ cfg.dataDir ];
PrivateTmp = true;
};
script = ''
set -euo pipefail
SOURCE_DIR="${cfg.dataDir}/source"
# Clone or update
if [ ! -d "$SOURCE_DIR/.git" ]; then
git clone "${cfg.source.url}" "$SOURCE_DIR"
cd "$SOURCE_DIR"
git checkout "${cfg.source.ref}"
NEEDS_BUILD=1
else
cd "$SOURCE_DIR"
git fetch origin
LOCAL=$(git rev-parse HEAD)
REMOTE=$(git rev-parse "origin/${cfg.source.ref}" 2>/dev/null || git rev-parse "${cfg.source.ref}")
if [ "$LOCAL" != "$REMOTE" ]; then
git checkout "${cfg.source.ref}"
git pull origin "${cfg.source.ref}" 2>/dev/null || true
NEEDS_BUILD=1
else
NEEDS_BUILD=0
fi
fi
# Check build artifacts
[ ! -d "node_modules" ] || [ ! -d "build" ] && NEEDS_BUILD=1
[ "$NEEDS_BUILD" = "0" ] && exit 0
npm install
npx tsc
'';
};
# Main runtime service
systemd.services.lightning-pub = rec {
description = "Lightning.Pub";
wantedBy = [ "multi-user.target" ];
requires = [ "lnd.service" "lightning-pub-build.service" ];
after = requires ++ [ "nix-bitcoin-secrets.target" ];
environment = {
PORT = toString cfg.port;
DATA_DIR = cfg.dataDir;
LOG_LEVEL = cfg.logLevel;
NOSTR_RELAYS = cfg.nostrRelays;
SERVICE_FEE_BPS = toString cfg.serviceFee.bps;
SERVICE_FEE_FLOOR_SATS = toString cfg.serviceFee.floorSats;
WATCHDOG_MAX_DIFF_SATS = toString cfg.watchdogMaxDiffSats;
LND_ADDRESS = "${lnd.rpcAddress}:${toString lnd.rpcPort}";
LND_CERT_PATH = lnd.certPath;
LND_MACAROON_PATH = "${lnd.networkDir}/admin.macaroon";
} // cfg.extraEnv;
serviceConfig = nbLib.defaultHardening // {
ExecStart = "${lightningPubEnv} ${pkgs.nodejs_22}/bin/node build/src/index.js";
SyslogIdentifier = "lightning-pub";
User = cfg.user;
Restart = "on-failure";
RestartSec = "10s";
ReadWritePaths = [ cfg.dataDir ];
} // nbLib.allowAllIPAddresses
// nbLib.nodejs;
};
networking.firewall.allowedTCPPorts = [ cfg.port ];
};
}

View file

@ -29,6 +29,7 @@
./joinmarket-ob-watcher.nix
./hardware-wallets.nix
./lamassu-lnbits.nix
./lightning-pub.nix
# Support features
./versioning.nix