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

8.6 KiB

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:

{
  "provider": "apple",         // "apple" | "google"
  "idToken": "<provider-jwt>",
  "nonce": "<nonce-or-empty>"
}

Response 200Session:

{
  "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:

{
  "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:

["<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:

{
  "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:

{ "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:

{ "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:

{ "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:

{ "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:

{
  "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:

{ "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).