Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions CONCEPT.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,9 @@ Every "message" the user writes in the UI is a NOSTR event. (v1 note: NOSTR
is fully custodial in v1 — the api holds one keypair per account and signs
events server-side with the account's own key, see "NOSTR in v1". The table
below applies to v1 for the surfaces v1 ships — profile metadata, campaign
post, public comment; the DM and Zap-receipt rows stay deferred, see MVP
scope. The client-side-signing flow beneath it is target state.)
post, public comment, and the custodial PN channel on `/conversations`.
Zap-receipt / leaderboard rows stay deferred, see MVP scope. The
client-side-signing flow beneath it is target state.)

| UI surface | NOSTR primitive |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
Expand Down Expand Up @@ -268,8 +269,9 @@ app ←──indexed feed── api ←──subscribe── relays
resource, not raw relay traffic
- Default relay set is configured server-side; users can opt into a "raw mode"
later (deferred) where the app talks to relays directly with the same key
- Private DMs (NIP-17) pass through the api as opaque encrypted payloads — the
api never sees plaintext
- Target-state private DMs pass through the api as opaque encrypted payloads
(client-side nsec). v1 is custodial: the api unwraps NIP-17 / decrypts
kind:4 with the account nsec for the `/conversations` PN channel.

### Public member forum (v1)

Expand Down Expand Up @@ -337,7 +339,8 @@ recurring paying stays in the external spend worker):
- Event signing (the api never sees the nsec)
- Guest Donate LNURL-pay flow (browser → wallet provider directly). Recurring
spend-worker invoices are the exception (`POST /invoices`).
- Decryption of NIP-17 sealed DMs (payloads pass through the api opaque)
- Client-side decryption of NIP-17 sealed DMs (v1 custodial unwrap is on
the api for `/conversations`)

The api lives in its own repository (`21gifts/api`) and is the **canonical
home for project-level documentation**, including this concept document. The
Expand Down Expand Up @@ -404,6 +407,12 @@ Encryption: AES-GCM 256, with two key-derivation paths:
- USD → sats conversion for recurring-gift amounts via an exchange-rate
source (fail-closed on a missing or implausible rate; paying stays in
the spend worker)
- Custodial PN channel on `GET/POST /conversations` (NIP-17 + kind:4;
official platform account; `Account.isPlatform`)
- Forum replies (`replyCount`, `GET /messages/:id/replies`) and public
`GET /messages/:id`
- NIP-57 mint probe before linking a Lightning Address (`POST /me/lightning-address`
and operator `POST /debug/accounts` unless `NIP57_PROBE=0`)

**Out, deferred:**

Expand All @@ -413,7 +422,8 @@ Encryption: AES-GCM 256, with two key-derivation paths:
accepted risk, see "v1 Transitional Model")
- Linking multiple LNURL-auth wallets to one account
- Non-custodial donor spending (replaces the v1 spend worker)
- Private DMs (NIP-17 sealed messages)
- Non-custodial client-side DMs (v1 ships a custodial PN channel on
`/conversations`: NIP-17 + kind:4, official platform account)
- NIP-57 Zap receipts / leaderboards
- NIP-05 verification badge
- Native mobile app
Expand Down
74 changes: 43 additions & 31 deletions CONTRIBUTING.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ COPY --from=builder /app/public ./public
USER app

ENV BIND_ADDR=0.0.0.0:3000
ENV MEDIA_DIR=/data/media
EXPOSE 3000

CMD ["bun", "run", "dist/index.js"]
18 changes: 13 additions & 5 deletions FLOWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,11 @@ notes (Damus-visible `#bitcoin` / `#21gifts` in content on first sign; forum `te
the worker fans out when `NOSTR_PUBLISH=1`. Pay-on-note is
`POST /messages/:id/invoice`. Do not invent `/events` or `/comments` paths.

Private donor↔receiver DMs (NIP-17) are **out of v1** (CONCEPT deferred). Do
not sketch a DM inbox as if it ships in v1.
Private messaging ships as one PN channel: `GET/POST /conversations` plus
member→platform via `POST /contact`. NIP-17 gift wraps and legacy kind:4
inbound; outbound wraps with the sender nsec (platform nsec for staff on
official threads). Forum replies stay on `/messages` and are not mixed
with PNs.

---

Expand All @@ -169,9 +172,14 @@ not sketch a DM inbox as if it ships in v1.
Private mailbox so members can write to 21.gifts without a published email.
Signed-in members POST `{ text }` to `POST /contact` (name snapshot as
forum messages; `normalizeForumText` plus a required 1–500 character body —
forum photo-only empty text does not apply). Operators read the mailbox via
`GET /debug/contacts` (`DEBUG_TOKEN`). No public list, no email delivery, no
DMs, no Nostr fan-out. Do not invent `/events`.
forum photo-only empty text does not apply). After the platform account
exists, the contact row is persisted first, then the same text is appended
to the member→platform conversation thread (`GET /conversations`).
Conversation append failure logs `conversations.contact_sync.failed` and
still 200. No platform account → 503 `Platform account is not configured`
(no writes). Operators still read
the legacy mailbox via `GET /debug/contacts` (`DEBUG_TOKEN` must not read
member PNs). No public list, no email delivery. Do not invent `/events`.

---

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ otherwise.
git clone https://github.com/21gifts/api.git
cd api
bun install
bun run dev # → http://localhost:3000/healthz
MEDIA_DIR="$(mktemp -d)" bun run dev # → http://localhost:3000/healthz
```

## Local checks (the same gates CI runs)
Expand All @@ -89,6 +89,7 @@ bun run e2e # Playwright against bun src/index.ts

| Variable | Required | Purpose |
| ---------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `MEDIA_DIR` | yes | Directory for forum video files. Missing or blank → process does not boot. Image/Compose pin `/data/media`. Not a secret. |
| `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. |
Expand Down
Loading
Loading