From 8fddb491469fff751cc260ed77527bd1d478a64a Mon Sep 17 00:00:00 2001 From: Taksh Date: Mon, 27 Jul 2026 10:05:01 +0300 Subject: [PATCH 1/3] docs: explain geohash relay selection for external publishers Document proximity-based relay picking, sparse-cell limits, and ephemeral delivery so tools publishing kind 20000/20001 can reach BitChat clients. --- README.md | 1 + docs/GEOHASH-PUBLISHER-RELAYS.md | 83 ++++++++++++++++++++++++++++++++ docs/GeohashPresenceSpec.md | 6 +++ 3 files changed, 90 insertions(+) create mode 100644 docs/GEOHASH-PUBLISHER-RELAYS.md diff --git a/README.md b/README.md index 36f50be2..85469068 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ construction, not NIP-44 encryption. - `region` (2 chars): Country/large region - **Internet**: Required (connects to Nostr relays) - **Use Case**: Location-based community chat, local events, regional discussions +- **External publishers:** relays are chosen by proximity to the geohash — see [Geohash publisher relays](docs/GEOHASH-PUBLISHER-RELAYS.md) ### Direct Message Routing diff --git a/docs/GEOHASH-PUBLISHER-RELAYS.md b/docs/GEOHASH-PUBLISHER-RELAYS.md new file mode 100644 index 00000000..1878232b --- /dev/null +++ b/docs/GEOHASH-PUBLISHER-RELAYS.md @@ -0,0 +1,83 @@ +# Geohash Publisher Relay Selection & Delivery + +Guide for external tools that publish into BitChat location channels (kind +`20000` chat, kind `20001` presence). BitChat clients do **not** use a fixed +global relay set for these channels — they pick relays by proximity to the +geohash cell. A publisher that targets the wrong relays will never reach those +clients. + +## Event shape + +Location chat messages are ephemeral Nostr events: + +| Field | Value | +|-------|--------| +| `kind` | `20000` (chat) or `20001` (presence heartbeat) | +| `tags` | Must include `["g", ""]`. Chat may also carry `["n", ""]`. | +| `content` | Message text for kind `20000`; empty for kind `20001` | +| `pubkey` | Ephemeral identity derived for that geohash (not the user's long-term key) | + +Clients subscribe with filters equivalent to: + +```json +{ "kinds": [20000, 20001], "#g": [""], "limit": 200 } +``` + +See [GeohashPresenceSpec.md](GeohashPresenceSpec.md) for presence broadcast +rules (precision limits, heartbeat cadence). + +## How clients choose relays + +On subscribe and publish, BitChat asks `GeoRelayDirectory` for the closest +relays to the geohash center: + +1. Decode the geohash to a lat/lon center. +2. Rank the bundled geo-relay directory by haversine distance. +3. Take the nearest **5** relays (`TransportConfig.nostrGeoRelayCount`). +4. Ties break by hostname so every device with the same directory picks the + same set — publishers and subscribers must agree. + +With an empty or unreachable directory for that cell, the client refuses to +publish rather than falling back to unrelated default relays. + +**Implication for external publishers:** replicate the same proximity selection +(or publish to a strict superset that includes those five hosts). Publishing +only to a personal fixed relay list is the most common reason a message never +appears in the app. + +## Sparse and ocean cells + +Some geohashes (oceans, deserts, polar regions) map to few or unreachable +nearby relays. The channel UI still opens normally — there is no separate +"undeliverable" banner — but live messages may not round-trip. Treat an empty +timeline in those cells as a relay-coverage problem, not a client bug. + +## Ephemerality and late joiners + +Kind `20000` / `20001` events are ephemeral. Relays typically do not store them +for historical replay. A client that subscribes minutes later cannot retrieve +messages that already left the relay's ephemeral buffer. Initial lookback on +subscribe is on the order of one hour for chat sampling +(`nostrGeohashInitialLookbackSeconds`), but that only recovers what the chosen +relays still hold. + +For durable geographic notes, BitChat uses persistent kind `1` events tagged +with `g` (location notes / drops) — a different surface from live channel chat. + +## Practical checklist for external publishers + +1. Compute the target geohash at the precision the channel uses (`block` ≈ 7, + `neighborhood` ≈ 6, `city` ≈ 5, etc.). +2. Resolve the closest ~5 `wss://` hosts from the same geo-relay directory the + app ships (or an equivalent proximity ranking over that list). +3. Publish the signed kind-`20000` event to those relays with a correct `g` tag. +4. Expect no backlog for late subscribers; design tools around live delivery. +5. Do not assume App Store / mesh Bluetooth peers will see Nostr-only publishes + — mesh and geohash are separate transports. + +## Related code + +- `bitchat/Nostr/GeoRelayDirectory.swift` — proximity ranking +- `bitchat/Services/GeohashPresenceService.swift` — subscribe/publish wiring +- `bitchat/Services/TransportConfig.swift` — `nostrGeoRelayCount`, lookback limits +- `bitchat/Nostr/NostrProtocol.swift` — kind `20000` / `20001` builders diff --git a/docs/GeohashPresenceSpec.md b/docs/GeohashPresenceSpec.md index 321a491f..c04c3b4b 100644 --- a/docs/GeohashPresenceSpec.md +++ b/docs/GeohashPresenceSpec.md @@ -94,3 +94,9 @@ The presentation of the participant count depends on the geohash precision level * **Privacy:** High-precision location presence is NOT broadcast. Temporal correlation between different levels is obfuscated via random delays. * **Consistency:** "Online" status is maintained globally while the app is open. * **Transparency:** The UI correctly reflects uncertainty (`?`) when privacy rules prevent accurate passive counting. + +## External publishers + +Tools that publish kind `20000` / `20001` into a geohash channel must target the +same proximity-selected relays BitChat clients use, or messages will not arrive. +See [GEOHASH-PUBLISHER-RELAYS.md](GEOHASH-PUBLISHER-RELAYS.md). From 0495c44be2f9f4c1d29c5fbe1af3dd69ac9dc0b9 Mon Sep 17 00:00:00 2001 From: Taksh Date: Wed, 29 Jul 2026 11:44:41 +0300 Subject: [PATCH 2/3] docs: correct geohash publisher relay guide Describe the empty-directory fallback to default relays, clarify that sparse cells are about stale directory rows (not geographic distance), and point publishers at the canonical main-branch CSV the app refreshes. --- docs/GEOHASH-PUBLISHER-RELAYS.md | 63 ++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/docs/GEOHASH-PUBLISHER-RELAYS.md b/docs/GEOHASH-PUBLISHER-RELAYS.md index 1878232b..2a002cd7 100644 --- a/docs/GEOHASH-PUBLISHER-RELAYS.md +++ b/docs/GEOHASH-PUBLISHER-RELAYS.md @@ -15,7 +15,7 @@ Location chat messages are ephemeral Nostr events: | `kind` | `20000` (chat) or `20001` (presence heartbeat) | | `tags` | Must include `["g", ""]`. Chat may also carry `["n", ""]`. | | `content` | Message text for kind `20000`; empty for kind `20001` | -| `pubkey` | Ephemeral identity derived for that geohash (not the user's long-term key) | +| `pubkey` | Ephemeral identity derived for that geohash (not the person's long-term key) | Clients subscribe with filters equivalent to: @@ -32,25 +32,49 @@ On subscribe and publish, BitChat asks `GeoRelayDirectory` for the closest relays to the geohash center: 1. Decode the geohash to a lat/lon center. -2. Rank the bundled geo-relay directory by haversine distance. +2. Rank the geo-relay directory by haversine distance. 3. Take the nearest **5** relays (`TransportConfig.nostrGeoRelayCount`). 4. Ties break by hostname so every device with the same directory picks the same set — publishers and subscribers must agree. -With an empty or unreachable directory for that cell, the client refuses to -publish rather than falling back to unrelated default relays. +### Directory source of truth -**Implication for external publishers:** replicate the same proximity selection -(or publish to a strict superset that includes those five hosts). Publishing -only to a personal fixed relay list is the most common reason a message never -appears in the app. +Do **not** rely only on the CSV bundled inside an older app build. At runtime +the client refreshes and caches the reviewed copy on `main`: -## Sparse and ocean cells +https://raw.githubusercontent.com/permissionlesstech/bitchat/refs/heads/main/relays/online_relays_gps.csv -Some geohashes (oceans, deserts, polar regions) map to few or unreachable -nearby relays. The channel UI still opens normally — there is no separate -"undeliverable" banner — but live messages may not round-trip. Treat an empty -timeline in those cells as a relay-coverage problem, not a client bug. +That URL is `GeoRelayDirectory`'s `remoteURL`. External publishers should rank +against this same file (or a freshly synced cache of it). A snapshot shipped +with an old binary can diverge after relay GPS rows change, so messages land +on hosts current clients no longer subscribe to. + +### Empty-directory fallback + +`closestRelays` returns `[]` only when the in-memory directory has no entries +(or `count <= 0`). In that case the publish path does **not** refuse — it +falls back to `NostrRelayManager`'s default relay list (built-in + any custom +relays). See `ChatPublicConversationCoordinator.sendPublicRaw`: empty +`targetRelays` → `sendEvent(event)` with no explicit `to:`. + +Publishers should still prefer proximity selection against the live CSV. +Default-relay fallback is a last resort when the geo directory failed to load; +relying on it intentionally will miss clients that *did* load the directory +and are subscribed only to the nearest five. + +## Sparse cells (stale directory rows) + +A "sparse" cell is about **directory freshness**, not geographic remoteness. +Haversine ranking always returns the nearest five hosts from whatever GPS rows +the CSV currently has — including for oceans or deserts. When those rows are +stale or thin for a region (relay moved, GPS never updated, host offline), the +selected five can be the wrong place for live traffic even though they look +"close" on paper. + +The channel UI still opens normally — there is no separate "undeliverable" +banner — but live messages may not round-trip. Treat an empty timeline in those +cells as a stale/coverage problem in the relay directory, not a client bug. +Refreshing from the canonical CSV above is the first fix to try. ## Ephemerality and late joiners @@ -68,8 +92,12 @@ with `g` (location notes / drops) — a different surface from live channel chat 1. Compute the target geohash at the precision the channel uses (`block` ≈ 7, `neighborhood` ≈ 6, `city` ≈ 5, etc.). -2. Resolve the closest ~5 `wss://` hosts from the same geo-relay directory the - app ships (or an equivalent proximity ranking over that list). +2. Fetch the **current** + [`online_relays_gps.csv`](https://raw.githubusercontent.com/permissionlesstech/bitchat/refs/heads/main/relays/online_relays_gps.csv) + (same URL the app refreshes from). Rank by haversine to the geohash center + and take the nearest ~5 `wss://` hosts (hostname tie-break). Optionally + publish to a strict **superset** that covers both that selection and any + older bundled snapshot you still support. 3. Publish the signed kind-`20000` event to those relays with a correct `g` tag. 4. Expect no backlog for late subscribers; design tools around live delivery. 5. Do not assume App Store / mesh Bluetooth peers will see Nostr-only publishes @@ -77,7 +105,10 @@ with `g` (location notes / drops) — a different surface from live channel chat ## Related code -- `bitchat/Nostr/GeoRelayDirectory.swift` — proximity ranking +- `bitchat/Nostr/GeoRelayDirectory.swift` — proximity ranking, `remoteURL`, cache +- `bitchat/ViewModels/ChatPublicConversationCoordinator.swift` — empty-selection + fallback to default relays - `bitchat/Services/GeohashPresenceService.swift` — subscribe/publish wiring - `bitchat/Services/TransportConfig.swift` — `nostrGeoRelayCount`, lookback limits - `bitchat/Nostr/NostrProtocol.swift` — kind `20000` / `20001` builders +- `relays/online_relays_gps.csv` — reviewed directory checked into this repo From 8cc56f8ddd718b79f42c2c5bc0374f215d83b0e7 Mon Sep 17 00:00:00 2001 From: Taksh Date: Sun, 2 Aug 2026 11:44:12 +0530 Subject: [PATCH 3/3] docs: clarify Haversine returns up to five relays --- docs/GEOHASH-PUBLISHER-RELAYS.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/GEOHASH-PUBLISHER-RELAYS.md b/docs/GEOHASH-PUBLISHER-RELAYS.md index 2a002cd7..e80242f0 100644 --- a/docs/GEOHASH-PUBLISHER-RELAYS.md +++ b/docs/GEOHASH-PUBLISHER-RELAYS.md @@ -60,16 +60,18 @@ relays). See `ChatPublicConversationCoordinator.sendPublicRaw`: empty Publishers should still prefer proximity selection against the live CSV. Default-relay fallback is a last resort when the geo directory failed to load; relying on it intentionally will miss clients that *did* load the directory -and are subscribed only to the nearest five. +and are subscribed only to up to the nearest five (fewer when the directory +is thin; none when it is empty — see the fallback above). ## Sparse cells (stale directory rows) A "sparse" cell is about **directory freshness**, not geographic remoteness. -Haversine ranking always returns the nearest five hosts from whatever GPS rows -the CSV currently has — including for oceans or deserts. When those rows are -stale or thin for a region (relay moved, GPS never updated, host offline), the -selected five can be the wrong place for live traffic even though they look -"close" on paper. +Haversine ranking returns *up to* the nearest five hosts from whatever GPS +rows the CSV currently has — including for oceans or deserts — and fewer +than five when the directory is thin. When those rows are stale or thin for +a region (relay moved, GPS never updated, host offline), the selected hosts +can be the wrong place for live traffic even though they look "close" on +paper. The channel UI still opens normally — there is no separate "undeliverable" banner — but live messages may not round-trip. Treat an empty timeline in those