Skip to content

Protocol ​

The code is the source of truth. This page explains intent. Exact shapes live in code:

Agent to relay ​

Target: OAuth 2.1, as in the MCP authorization spec. The MCP client signs in through the browser and sends a short-lived access token as a bearer token. Not live yet: see roadmap. The relay also accepts agent keys as a headless fallback (below).

Every agent call sends a bearer credential: Authorization: Bearer <token>. An unknown, expired or revoked credential gets 401.

HTTP: POST /v1/locate ​

Pings the user's phone and waits for the fix. Query: timeout_secs, default 60, capped at 120. Non-200 bodies are the agent text in the table.

StatusMeaningAgent text
200The fix. The body is a JSON location.
401Missing, unknown, expired or revoked credential.
403The user declined (denied, reason user).The user declined to share their location.
408The user did not tap in time (denied, reason no_answer).The user did not respond on their phone. Ask them before trying again.
424unavailable with reason permission.Location access is off for Whereabot on the user's phone. Ask them to allow it in Settings.
424unavailable with reason no_fix or error.The phone could not get a location fix. Try again in a moment.
503No device is connected for this user.The user's phone is not connected. Ask the user to open the Whereabot app, signed in as EMAIL and keep it open, then retry.
504The phone got the ping but did not answer before the timeout.The phone did not answer in time. Ask the user to open Whereabot and retry.

, signed in as EMAIL appears only when the token has an email claim. It helps when the phone and the agent use different accounts.

Check the fix time. An OS can return a cached position. The fix carries the phone's capture time.

MCP: /mcp ​

Streamable HTTP, with JSON responses and no session. The same bearer header gates it.

  • Tools: get_phone_location (optional timeout_secs, 1 to 120, default 60; optional person) and list_people_sharing_with_me. person locates someone who shares with the user: see location sharing.
  • A fix returns as text: the JSON location.
  • Every other outcome returns a tool error with the agent text above. The HTTP status stays 200.
  • A bad credential returns 401 before any tool runs.

Agent keys (headless fallback) ​

For headless or CI use only. Users do not onboard agents this way.

  • Create: in the app. The key starts with wb_. The app shows it once.
  • Rotate: create a new key, update the headless config, then revoke the old key in the app.
  • Leak: revoke the key in the app. A revoked key gets 401.

Phone to relay ​

The phone opens a WebSocket to /v1/device with its Supabase access token as a bearer token. A bad token gets 401 before the upgrade.

A user can have several devices connected at once, each named by its device_id (see Devices below). A new connection with the same device_id replaces that device only: the relay closes the old socket with Close 4000 "replaced". The app must not reconnect on its own after that. On SIGTERM the relay closes every device socket with Close 1012 "restart"; the app reconnects at once. The relay sends keep-alive pings, so idle sockets survive mobile networks.

  • Phone to relay, first: {"type":"hello","app_version","device_id","device_name","platform"}. device_id is the install's uuid, platform is ios, android or desktop. Until hello arrives the socket has no identity; an old app that sends no device_id still works, but is never replaced by its own reconnect.
  • Relay to phone: {"type":"locate","id","client_id","expires_ms","requester"}. requester is null for the user's own agent, else {user_id,name,email} of the person who asks (location sharing). client_id comes from the token (agent keys use agent-key). The relay sends no name: the app maps client_id to the client name with supabase.auth.oauth.listGrants(). expires_ms is the epoch time when the relay stops waiting; the app aborts the prompt then. The relay resends the ping to a new socket after a reconnect, so the app handles each id once.
  • Phone to relay, answers (all carry the ping id):
    • location: the fix.
    • denied, optional reason: user (default) or no_answer.
    • unavailable, reason: permission, no_fix or error.
  • Consent: the app asks the user (Reject, Allow, Always allow) before it answers. Always-allowed agents get an answer with no question, also with the app closed: see Always allow. Reject sends denied; no tap sends denied with no_answer.
  • Unknown messages: the relay ignores them. An answer with an unknown id is dropped. An unknown reason counts as user (denied) or error (unavailable).

Devices ​

Each install has a stable random device_id (uuid, kept across sign-out; on iOS in the Keychain, so it survives reinstall). It is the primary key of the devices table (migration); the app inserts its own row on first connect, then refreshes last_seen_at every 60 s while connected. A device counts as online when last_seen_at is under 90 s old. Rename and Remove (sets revoked_at) are plain updates under row-level security.

  • Fan-out: a locate request goes to every connected device of the user, and to each device that connects before it expires. The first answer (location, denied or unavailable) settles it. The relay then sends {"type":"cancel","id"} to every other device that got the ping; the app closes that prompt and records "answered on another device". The relay also sends cancel when the request times out. Offline (503) means no device is connected.
  • Revocation: on hello the relay reads the device row with the phone's own bearer (PostgREST, apikey = SUPABASE_ANON_KEY when set). If revoked_at is set, it closes the socket with Close 4000 "revoked". The app then forgets its device_id and signs out. An unreachable Supabase counts as not revoked. A device removed while connected is closed at its next connect (or when its own heartbeat sees the row).

Push wake-up ​

iOS suspends the app in the background, so a waiting request needs a nudge. When a request starts and no phone answers within about 2 s (or none is connected), the relay sends one APNs alert push per request to each token in push_tokens. Retries and reconnect resends send no more pushes. An always-allowed agent gets a silent push instead, and a "checking" alert only if that gets no answer: see Always allow.

  • Alert (other agents): title Whereabot, body Your agent NAME wants to know your location (or An agent wants to know your location when the name lookup fails), sound default, thread-id, and category LOCATION_REQUEST. Custom keys: request_id, client_id, agent_name, expires_ms, requester (same meaning as in locate; shared-request copy: location sharing). apns-push-type is alert, priority 10, and apns-expiration is the request deadline. Shape: server/src/push/apns_sender.rs.
  • Name: the relay reads the user's OAuth grants (GET /auth/v1/user/oauth/grants) with the agent's user JWT and takes the client name, as the app does.
  • Actions: the app registers the LOCATION_REQUEST category with Reject, Allow and Always allow. None opens the app: the phone answers in the background with POST /v1/device/answer (below). See architecture.
  • Tap: opens the app. The app reconnects on return to the foreground, and the relay resends the waiting locate to the new socket. The app needs no handling for request_id.
  • Tokens: the relay reads them with the agent's own user JWT (RLS limits it to that user's rows). It deletes a token when APNs answers 410 or BadDeviceToken.
  • Off: without the APNS_* settings the relay sends no APNs push; without VAPID_PRIVATE_KEY it sends no Web Push to browsers (request flow). Each logs once.

Phone to relay over HTTP: POST /v1/device/answer ​

Answers one request without a socket, from a notification action. Code: server/src/background_answer_http.rs.

  • Auth: Authorization: Bearer <phone Supabase access token>, as for /v1/device. A token with a client_id (an agent token) gets 403: an agent must never answer its own request.
  • Body: one answer message, the same JSON as on the socket: location, denied or unavailable, with the request id.
  • First answer wins, as on the socket. Every socket that got the ping then gets cancel.
StatusMeaning
204The answer settled the request.
400The body is hello or log, not an answer.
401Missing, unknown or expired token.
403Agent token, or the request belongs to another user.
410No request waits under this id: it was answered, timed out or never existed.
415/422The body is not JSON or not a known message.