Expose JSON array of hex pubkeys backed by ListActivePubkeys query. Includes OpenAPI documentation and integration tests. Made-with: Love
nip05api
A single-domain NIP-05 identity service. Lightning-paid registration through LNbits. Lifecycle DMs over Nostr. Optional outbound webhook on payment events. Swagger docs. One binary, one SQLite file, one systemd unit.
See SPEC.md for the full design specification.
Features
- NIP-05 lookup at
/.well-known/nostr.jsonfor active subscribers - Yearly or lifetime subscriptions, BOLT11 invoices via LNbits
- Renewal detection by pubkey: same user, same username, extended expiry
- Configurable expiry-reminder DMs and grace-period username reservation
- Profile sync: pulls
kind:0from configured relays and updates non-pinned usernames - HMAC-signed webhook outbox with exponential retry
- NIP-04 or NIP-17 (gift-wrapped) DM delivery
- Pure-Go SQLite, no CGO; static binary under 20 MB
- Embedded migrations applied on boot
- Embedded OpenAPI spec served at
/openapi.json, Swagger UI at/docs
Quick start
git clone https://github.com/noderunners/nip05api.git
cd nip05api
cp .env.example .env
cp messages.example.yaml messages.yaml
# Edit .env: DOMAIN, ADMIN_API_KEY, LNBITS_*, DM_NSEC, RELAYS
make build
./bin/nip05api
The binary creates .data/nip05.db, applies migrations, and starts listening
on PORT (default 8080). Hit http://localhost:8080/healthz to confirm.
Configuration
All config is environment-driven. See .env.example for the full list. Required keys:
| Key | Notes |
|---|---|
DOMAIN |
the apex domain users will be name@domain on |
ADMIN_API_KEY |
24+ chars; sent as X-API-Key on /v1/admin/* |
LNBITS_URL, LNBITS_INVOICE_KEY |
required when LIGHTNING_ENABLED=true |
RELAYS |
comma-separated wss://..., required when DM or sync enabled |
DM_NSEC |
required when DM_ENABLED=true |
Subsystems toggle independently: LIGHTNING_ENABLED, DM_ENABLED,
USERNAME_SYNC_ENABLED, plus webhook is enabled by setting WEBHOOK_URL.
API surface
Eleven endpoints, full schema at /openapi.json and rendered at /docs:
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /.well-known/nostr.json |
— | NIP-05 lookup |
| GET | /healthz |
— | DB ping + version |
| GET | /v1/pricing |
— | yearly/lifetime sat pricing |
| GET | /v1/users/{pubkey} |
— | lookup by hex or npub |
| GET | /v1/usernames/{name}/available |
— | availability check |
| POST | /v1/invoices |
— | create payment invoice |
| GET | /v1/invoices/{hash} |
— | invoice status |
| POST | /v1/admin/users |
api key | manual add |
| GET | /v1/admin/users |
api key | list / search |
| PUT | /v1/admin/users/{pubkey} |
api key | pin username |
| DELETE | /v1/admin/users/{pubkey} |
api key | hard delete |
| POST | /v1/admin/users/{pubkey}/extend |
api key | extend or upgrade |
Deployment
systemd (recommended for VMs)
make build
sudo make install-systemd
sudo cp .env /opt/nip05api/.env
sudo systemctl enable --now nip05api
journalctl -u nip05api -f
The unit is hardened (no-new-privileges, ProtectSystem=strict, capability-bound
to nothing) and runs as a dedicated nip05 user. See
deploy/nip05api.service.
Docker
docker build -t nip05api:latest .
docker compose up -d
The image is multi-stage and ships from gcr.io/distroless/static-debian12:nonroot.
Compose binds the data directory and messages.yaml from the host so updates
to copy don't require a rebuild.
Reverse proxy
A reverse proxy terminates TLS and forwards /.well-known/nostr.json, /v1/*,
/healthz, /openapi.json, and /docs to 127.0.0.1:8080.
Sample configs:
- nginx → deploy/nginx.conf
- Caddy → deploy/Caddyfile
The proxy must forward X-Forwarded-For or X-Real-IP; the API uses these
for rate limiting and access logs.
Operations
Database
SQLite WAL-mode at DATABASE_PATH (default .data/nip05.db). Backups while
the service is running:
sqlite3 .data/nip05.db ".backup .data/backup-$(date +%F).db"
WAL files (-wal, -shm) are checkpointed on graceful shutdown. To rotate:
stop the service, copy nip05.db, nip05.db-wal, nip05.db-shm together,
restart.
Migrations
Embedded as .sql files under internal/db/migrations/. They run inside a
transaction on every boot and record their version in schema_migrations.
Applying twice is a no-op. To add a migration: drop a new
NNNN_description.sql next to the others; rebuild.
Audit log
Admin actions, payment confirmations, expiries, and grace-period purges all
write to audit_log. Cleanup worker prunes entries older than 180 days
hourly.
Outbox cleanup
Delivered webhook and DM rows are pruned after 7 days; dead (gave up after
all retries) after 30 days. Tunable in cmd/nip05api/cleanup.go.
Crash safety
- Migrations: transactional, idempotent.
- Payment confirmation: idempotent. The first confirmation captures
target_expires_aton the invoice row; subsequent retries apply the same absolute value, so a crash mid-confirm cannot double-extend a renewal. - Webhook delivery: outbox + retry schedule (
30s, 2m, 10m, 1h, 6h), HMAC-signed ifWEBHOOK_SECRETis set. - DM delivery: outbox + retry schedule (
1m, 5m, 30m, 2h, 12h). - Expiry pass: each phase (reminders → expirations → grace cleanup) updates idempotent flags, so partial completion is safe to resume.
- Workers all watch a single root context; SIGINT/SIGTERM closes them in order with a 30s HTTP grace window.
Observability
- Logs: structured JSON to stdout via
slog; ship to journald or your logging stack of choice.request_idis propagated through middleware. - Secrets that never appear in logs:
ADMIN_API_KEY,LNBITS_INVOICE_KEY,WEBHOOK_SECRET,DM_NSEC, BOLT11 strings, DM plaintext. - Health:
/healthzreturns 200 with version + db status, 503 if DB unreachable.
Development
make tidy # go mod tidy
make build # static binary in bin/
make test # short tests
make test-race # all tests with -race
make lint # golangci-lint, see .golangci.yml
make docker # build container image
Module layout:
cmd/nip05api/ entry point + cleanup worker
internal/audit/ append-only audit log
internal/config/ env loader + validation
internal/db/ SQLite + embedded migrations
internal/dm/ outbox + NIP-04/NIP-17 builder + worker
internal/expiry/ daily reminders / expirations / grace cleanup
internal/http/ chi router, middleware, handlers, OpenAPI docs
internal/invoice/ LNbits client + service + repo
internal/log/ slog setup
internal/messages/ YAML template loader + renderer
internal/nostr/ keys, relay pool, profile fetch, publish
internal/payments/ poll LNbits → confirm → dispatch DM/webhook/audit
internal/sync/ periodic kind:0 username sync
internal/user/ repo + service
internal/webhook/ outbox + HMAC signer + worker
Each Go file is kept under ~200 lines. The dependency graph points downward:
handlers depend on services, services on repos, repos on db. Nothing in
internal/ depends on internal/http/.
License
See LICENSE (MIT).