204 lines
7.1 KiB
Markdown
204 lines
7.1 KiB
Markdown
# 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).
|