# 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](SPEC.md) for the full design specification. ## Features - NIP-05 lookup at `/.well-known/nostr.json` for 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:0` from 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 ```bash 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](.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) ```bash 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](deploy/nip05api.service). ### Docker ```bash 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](deploy/nginx.conf) - Caddy → [deploy/Caddyfile](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: ```bash 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_at` on 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 if `WEBHOOK_SECRET` is 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_id` is propagated through middleware. - Secrets that never appear in logs: `ADMIN_API_KEY`, `LNBITS_INVOICE_KEY`, `WEBHOOK_SECRET`, `DM_NSEC`, BOLT11 strings, DM plaintext. - Health: `/healthz` returns 200 with version + db status, 503 if DB unreachable. ## Development ```bash 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](LICENSE) (MIT).