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
- Zero-knowledge by default. The server correlates anonymous tokens and relays ciphertext. It never stores identity, plaintext messages, or precise location history.
- 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.
- Account identity — via auth (
- 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 200 — Session:
{
"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
noncematches 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
Accountmodel 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:
- Derive the sender's own token from the session (via
/v1/token). - Record a directional edge
ownToken → remoteToken. - Push a
mutualevent to the other device only if both users have waved at each other. (If the other user already waved, this device gets amutualpush back immediately.) - On mutual, create/reuse a
connectionsentry and includeconnectionIDin the pushed event so messaging can route. - A wave is directional:
A → Bdoes not reveal untilB → 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
messageevent 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.
wavesare directional; no reveal without mutual consent.peer-keyreturned only within a mutual relationship.- Messages stored as ciphertext only; no decryption server-side.
blockis 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).