Backend service for 21.gifts — a peer-to-peer Bitcoin Lightning donation platform with NOSTR as the invisible communication substrate.
This repository is the canonical home for project-level documentation
(see CONCEPT.md). The implemented HTTP surface is documented
in SPEC.md. The frontend lives at
21gifts/app.
Pre-built images are published to Docker Hub on every push to develop (:beta)
and main (:latest).
| Tag | Source | Deploy target | Public URL |
|---|---|---|---|
21gifts/api:beta |
develop |
DEV | https://dev-api.21.gifts |
21gifts/api:latest |
main |
PRD | https://api.21.gifts |
Pull and run locally:
docker run --rm -p 3000:3000 21gifts/api:beta
# → http://localhost:3000/healthzFull image list and history: hub.docker.com/r/21gifts/api.
The api is the "thick server" in a thin-client / thick-server architecture. It handles everything that doesn't have to run client-side:
- NOSTR fan-out — accepts signed events from clients, verifies signatures, publishes to the shared
nostr.spacerelay - NOSTR aggregation — subscribes to the relay, indexes events, exposes paginated reads
- LN-Address resolution + caching — LUD-16 endpoints health-checked and normalized
- Discovery — recent campaigns, ordering, future categories / search
- Anti-abuse — rate-limiting, spam scoring, malformed-event rejection
It explicitly does not hold keys or sign events for the non-custodial
target (v1 signs custodial identities server-side). Guest Donate LNURL-pay
stays in the browser; the spend-worker path is the exception
(POST /invoices fetches a recipient BOLT11, this api still does not pay).
| Layer | Choice |
|---|---|
| Runtime | Bun ≥ 1.3 |
| Language | TypeScript (strict) |
| Framework | Hono |
| Test runner | Vitest + @vitest/coverage-v8 |
| Lint | ESLint (flat config) + Prettier |
| Validation | Zod |
All non-trivial functions ship with TSDoc. Coverage is hard-gated at 100% (lines, branches, functions, statements) on the activated surface — CI red otherwise.
git clone https://github.com/21gifts/api.git
cd api
bun install
bun run dev # → http://localhost:3000/healthzbun run typecheck # tsc --noEmit
bun run lint # eslint + prettier --check
bun run handbook:check # every function and HTTP endpoint must be documented
bun run e2e:check # every HTTP endpoint request and Function: <Name> title
bun run test:coverage # vitest with 100% threshold
bun run build # bun build to dist/
bun run e2e # Playwright against bun src/index.ts| Variable | Required | Purpose |
|---|---|---|
WEBAUTHN_RP_ID |
For passkey login | WebAuthn RP ID (21.gifts / dev.21.gifts / localhost). Missing → passkey routes return 500; process still boots. Not a secret. |
WEBAUTHN_RP_NAME |
no | Human-readable RP name (default 21.gifts). |
CORS_ALLOWED_ORIGINS |
no | Comma-separated browser origins. Passkey finish allows those whose hostname is the RP ID or app.<rpId> only. |
| Doc | Purpose |
|---|---|
CONCEPT.md |
Project vision, architecture, principles, decisions |
SPEC.md |
Implemented HTTP surface (request/response contracts) |
FLOWS.md |
Core UI journeys (sign-in → profile → donate → recurring → message) |
docs/handbook/ |
Mandatory: every function and HTTP endpoint |
CONTRIBUTING.md |
Dev setup, conventions, workflow |
SECURITY.md |
Reporting vulnerabilities |
MIT — see LICENSE.