Native companion API contract (phone + Wear)

August 4, 2026 · View on GitHub

Audience: Implementers of native companion apps (Android phone + Wear OS, iOS, etc.) that pair with a hapi hub via FCM.

Auth: Exchange the pairing code / CLI access token with POST /api/auth: { "accessToken": "<code>" }. Use the returned JWT as Authorization: Bearer <token> for device registration and session actions. POST /api/bind is only for Telegram Mini App binding (requires Telegram initData).

Scope

A companion implementing this contract is a native client to the same hub the PWA talks to, surfacing notifications and reply / approve actions on a phone or wearable. Hub topology is unchanged - the hub still runs on the operator's dev machine.


Device registration (FCM)

Register

POST /api/devices/register

{
  "token": "<fcm-registration-token>",
  "platform": "phone",
  "deviceId": "<stable-install-id>"
}

platform: "phone" | "wear"

deviceId: any string of 1-128 characters chosen by the client (does not have to be a UUID). Must be stable across re-registrations of the same install.

Response: { "ok": true }

Upsert on (namespace, deviceId, platform) - same device re-registering replaces the FCM token.

Unregister

DELETE /api/devices/register

{
  "token": "<fcm-registration-token>"
}

Outbound push (hub → device)

Hub sends FCM HTTP v1 whenever a notification event is emitted for a namespace with registered native devices and FCM is configured. The native companion is treated as the canonical wrist-first surface, so FCM fires unconditionally (independent of whether a PWA tab happens to be foreground / visible via SSE) - that's deliberate, see FcmNotificationChannel.deliver(). Web Push is suppressed for the same namespace to avoid duplicate OS notifications.

Data payload (all platforms)

KeyExamplePurpose
typereadyready, permission-request, task-notification
sessionIduuidTarget session
sessionNamestringDisplay name (agent - project)
url/sessions/{id}Deep link path
requestIduuidPermission only - approve/deny
titlestringNotification title
bodystringNotification body
severityinfoinfo (ready), warning (permission), success / error (task)
contractVersion1Present on every message; see Versioning
notifySummaryJSON stringOnly on ready: parsed AGENT_NOTIFY_SUMMARY line from agent text, when present

Native apps must handle data for Wear; notification block is for display.

Client actions (native - not hub)

User actionHub API
Send textPOST /api/sessions/:id/messages { "text": "...", "localId": "..." }
AllowPOST /api/sessions/:id/permissions/:requestId/approve
DenyPOST /api/sessions/:id/permissions/:requestId/deny

localId is optional in the send-message body - an opaque client-generated id for reconciling the locally shown message with the server-echoed one.

sentFrom extension (optional future): android-phone, android-wear.


Environment (hub operator)

FCM_SERVICE_ACCOUNT_PATH=/path/to/service-account.json
FCM_PROJECT_ID=your-firebase-project-id

When unset, hub skips FCM channel (Web Push / Telegram unchanged).

The native push channel is opt-in: operators who don't run a companion app see no behavior change. When at least one device is registered for a namespace, the existing Web Push channel suppresses its fallback for that namespace to avoid double-notifying (one in the native app, one from the PWA service worker). PWA-only operators are unaffected.


Versioning

Contract version 1. Breaking changes require data.contractVersion in FCM payload and doc update.