Files
Nearfield_Friends/docs/backend-api-spec.md

274 lines
8.6 KiB
Markdown
Raw Normal View History

# Proximity — Backend API Specification
This document defines the server contract that the **Swift iOS client** expects.
Every endpoint and payload here mirrors exactly what's implemented in the client
(`Services/Network/APIClient.swift` and `Services/Network/RealtimeClient.swift`).
The goal is that a backend team can stand up this API from scratch and the
existing client will work without changes.
**Base URL:** `https://api.proximity.app`
---
## 1. Design Principles
1. **Zero-knowledge by default.** The server correlates anonymous tokens and
relays ciphertext. It never stores identity, plaintext messages, or precise
location history.
2. **Two identities, never conflated.**
- *Account identity* — via auth (`/v1/auth/*`). Private, used for sessions.
- *Social identity* — only surfaces in a **mutual** reveal, and even then
the server only relays opaque keys; it never stores the profile.
3. **Foreground-first.** The server is a correlation + relay layer, not a
background continuous-tracking layer. Presence sessions are short-lived.
---
## 2. Authentication
### 2.1 `POST /v1/auth/exchange`
Exchange a provider ID token for a Proximity session.
**Request:**
```json
{
"provider": "apple", // "apple" | "google"
"idToken": "<provider-jwt>",
"nonce": "<nonce-or-empty>"
}
```
**Response `200``Session`:**
```json
{
"token": "<proximity-session-bearer-token>",
"account": {
"id": "<opaque-account-id>",
"providerID": "apple",
"displayName": "…",
"email": "…"
}
}
```
**Server responsibilities:**
- Verify the provider ID token with the provider (Apple/Google public keys).
- For Apple, verify the `nonce` matches the one in the ID token.
- Issue a short-lived Proximity session token (e.g. JWT, 7-day expiry).
- Return only the fields the client's `Account` model decodes.
### 2.2 `POST /v1/auth/revoke`
Revoke a session. Bearer token in `Authorization` header. No body expected.
---
## 3. Presence & Token Relay
> **Key model:** While present, a client broadcasts a **rotating anonymous
> token** (see `TokenManager`). The server maps token → connected socket, so it
> can push events to the right device without ever knowing the user's identity.
### 3.1 `POST /v1/token`
Register the current anonymous token so the server can route to this device.
**Request:**
```json
{
"token": "<rotating-anon-token>",
"tier": 1, // DistanceTier.rawValue: 1=UWB, 2=BLE, 3=area
"geohash": "…" // optional, for Tier 3 discovery
}
```
**Auth:** `Authorization: Bearer <session-token>`.
**Server:** associate `token` with the authenticated session and its open
WebSocket. This is the **session → own-token** mapping that lets the server
derive who a wave is *from*. Drop associations older than one rotation window
(5 min).
### 3.2 `GET /v1/nearby?geohash=<h>&tier=<n>`
Fetch anonymous tokens of present users near a coarse location.
**Response `200`:**
```json
["<token-a>", "<token-b>", "…"]
```
**Server:** return tokens registered within the same coarse region for the
requested tier. Used for Tier 3 (network) discovery. Tier 1/2 handshakes happen
directly over UWB/BLE and are reported via `/v1/encounter`.
### 3.3 `POST /v1/encounter`
Report a local physical encounter (fire-and-forget).
**Request body** is the client's `Encounter` model:
```json
{
"id": "<uuid>",
"remoteAnonToken": "<token>",
"timestamp": "…",
"tier": 1,
"engine": "uwb",
"geohash": "…",
"status": "silhouette"
}
```
**Server:** correlate the two tokens that were physically near each other and
record the mutual relationship for later matching. **Do not** store identity or
precise location.
---
## 4. The Reveal (Wave) Flow
This is the heart of the product. The server's job is to reliably relay a
mutual wave between two present devices in real time.
### 4.1 `POST /v1/wave`
Send a wave to a silhouette — signals interest in mutual reveal.
**Request:**
```json
{ "token": "<remote-anon-token>" }
```
**Auth:** `Authorization: Bearer <session-token>`.
**Server behavior:**
1. Derive the sender's **own** token from the session (via `/v1/token`).
2. Record a directional edge `ownToken → remoteToken`.
3. Push a **`mutual`** event to the *other* device only if **both** users have
waved at each other. (If the other user already waved, this device gets a
`mutual` push back immediately.)
4. On mutual, create/reuse a `connections` entry and include `connectionID`
in the pushed event so messaging can route.
5. A wave is **directional**: `A → B` does not reveal until `B → A`.
### 4.2 `POST /v1/peer-key` — upload own public key
Upload the caller's E2E public key so a mutual peer can fetch it.
**Request:**
```json
{ "publicKey": "<base64 raw X25519 public key>" }
```
**Auth:** `Authorization: Bearer <session-token>`. The server stores the key
against the caller's own token.
### 4.3 `GET /v1/peer-key?token=<remote-token>` — fetch peer key
Fetch the peer's public key to establish the E2E session key.
**Auth:** `Authorization: Bearer <session-token>`.
**Response `200`:**
```json
{ "publicKey": "<base64 raw X25519 public key>" }
```
**Server:** return the peer's key **only** if the caller and peer have a
**mutual** wave relationship (`403` otherwise). This is the only
identity-bearing data the server touches, and only within an established
mutual relationship.
### 4.4 `POST /v1/block`
Permanently block a token. Removes it from the graph in both directions.
**Request:**
```json
{ "token": "<remote-anon-token>" }
```
**Server:** delete the edge and refuse future waves/messages from this token.
---
## 5. Messaging (E2E)
### 5.1 `POST /v1/message`
Send an encrypted message. **Only ciphertext leaves the device.**
**Request body** is the client's `ChatMessage` model:
```json
{
"id": "<uuid>",
"connectionID": "<uuid>",
"senderID": "<opaque-user-id>",
"ciphertext": "<base64 ChaChaPoly sealed box>",
"sentAt": "…"
}
```
**Server:**
- Validate the sender is part of the `connectionID`.
- Store/relay the ciphertext only. **Never decrypt.**
- Push a **`message`** event to the recipient's socket in real time.
---
## 6. Real-time WebSocket
**Endpoint:** `wss://api.proximity.app/ws`
**Auth:** `Authorization: Bearer <proximity-session-token>`
The client connects when the user goes **present** and disconnects when they
leave. The server must route events to the correct socket via the token
registration from `/v1/token`.
### 6.1 Server → Client events
The client's `RealtimeClient` parses this envelope:
```json
{ "type": "mutual", "remoteToken": "<token>" }
{ "type": "message", "message": { …ChatMessage… } }
```
**`mutual`** — the other party waved back. The client matches `remoteToken`
against its waiting encounter and advances to the reveal.
**`message`** — a new encrypted message. The client decrypts and appends.
### 6.2 Client → Server events (optional)
The client may send `ping`/JSON keepalives; the server should respond with
`{ "type": "pong" }`. The server must tolerate silent clients and drop stale
socket→token mappings.
---
## 7. Data Model (server-side)
| Table | Fields | Notes |
|---|---|---|
| `accounts` | id, provider, display_name, email, created_at | account identity |
| `sessions` | token, account_id, expires_at | bearer sessions |
| `presence` | token, account_id, tier, geohash, socket_id, updated_at | rotating anon presence |
| `encounters` | id, token_a, token_b, tier, engine, created_at | mutual proximity record |
| `waves` | from_token, to_token, created_at | directional interest edges |
| `keys` | token, public_key | E2E public keys (mutual only) |
| `connections` | id, user_a, user_b, created_at | mutual reveals |
| `messages` | id, connection_id, sender_id, ciphertext, created_at | ciphertext only |
---
## 8. Security & Privacy Checklist
- [ ] Provider tokens verified server-side (never trust client).
- [ ] Session tokens short-lived + revocable.
- [ ] Presence tokens rotate client-side; server drops stale ones.
- [ ] `waves` are directional; no reveal without mutual consent.
- [ ] `peer-key` returned only within a mutual relationship.
- [ ] Messages stored as ciphertext only; no decryption server-side.
- [ ] `block` is permanent and bidirectional.
- [ ] Right-to-be-forgotten: deleting an account purges all rows.
- [ ] No precise location history retained by default.
---
## 9. Suggested Tech Stack
- **Language:** Go or Node.js (TypeScript) — good WebSocket support.
- **Transport:** HTTPS + WebSocket (single origin to share auth).
- **Persistence:** PostgreSQL (encounters/waves/messages) + Redis (presence
sockets, ephemeral token→socket mapping).
- **Push:** APNs for offline wake-ups (best-effort; presence is foreground).