# 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": "", "nonce": "" } ``` **Response `200` — `Session`:** ```json { "token": "", "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": "", "tier": 1, // DistanceTier.rawValue: 1=UWB, 2=BLE, 3=area "geohash": "…" // optional, for Tier 3 discovery } ``` **Auth:** `Authorization: Bearer `. **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=&tier=` Fetch anonymous tokens of present users near a coarse location. **Response `200`:** ```json ["", "", "…"] ``` **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": "", "remoteAnonToken": "", "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": "" } ``` **Auth:** `Authorization: Bearer `. **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": "" } ``` **Auth:** `Authorization: Bearer `. The server stores the key against the caller's own token. ### 4.3 `GET /v1/peer-key?token=` — fetch peer key Fetch the peer's public key to establish the E2E session key. **Auth:** `Authorization: Bearer `. **Response `200`:** ```json { "publicKey": "" } ``` **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": "" } ``` **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": "", "connectionID": "", "senderID": "", "ciphertext": "", "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 ` 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": "" } { "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).