Partner services (easy-connect)
Some data reaches HDS through connectors: bridges that import a partner service’s data (Mira hormones and cycles, Tempdrop basal body temperature) into the user’s account. Your app can offer “Connect your Mira account” without running the connector itself, and show whether it is working.
Two rules:
- Your app never talks to a bridge. It links to one HDS page,
/connect, which does the rest. - Connector status lives in the user’s account. Your app reads it with an ordinary
readpermission.
Which connectors
Section titled “Which connectors”The operator catalogue lists them: apps/list.json, reachable from the platform’s service info
(assets.definitions). A connector your app can use through /connect is an entry with a hook.initiate;
today bridge-mira (demo and prod) and bridge-tempdrop (prod only: Tempdrop has no sandbox). Each
entry carries its name, icon, a paused flag, and statusStreamId: the stream where its status lives
(bridge-mira → sync-status-mira). Read the stream id from there; do not derive it from the app id.
A connector that reads a device also names it: deviceName (Mira, Tempdrop) is the name to show for the
values it writes, for instance as the name of a chart line, and deviceItems lists the item keys the device
measures (Mira: body-urine-hormones-lh, -e3g, -pdg, -fsh, -hcg). Without deviceItems, deviceName
applies to everything the connector writes; without deviceName, the connector is a service, not a device.
The same file has a second list, partnerApps: apps the person uses in their own window with their own
sign-in (Cycle Intelligence, the DysNet registry). They are not connectors: no hook, no /connect. An entry
gives the app’s name, description, icon, its address per platform (urls.web, urls.ios, urls.android; no
address, no card on that platform) and how its grant is recognised: grant: "access" (an app access named
appCode) or grant: "cmc" (a CMC relationship whose app code is appCode). How an entry vouches for an app is in
data-model
APP-DESCRIPTION.md.
Connect, reconnect, disconnect
Section titled “Connect, reconnect, disconnect”Send the user’s browser to:
https://account.hds.ngo/connect?app=<catalogue id>&returnUrl=<your https URL>&state=<nonce>[&action=connect|reconnect|disconnect]Demo: https://demo-account.datasafe.dev/connect?…
| parameter | |
|---|---|
| app | The catalogue id, e.g. bridge-mira. |
| returnUrl | Where the answer goes: any absolute https URL without credentials. No registration. |
| state | Your nonce (≤ 1024 characters), echoed back unchanged. Check it on return. |
| action | connect (default), reconnect or disconnect. |
The user signs in to HDS (or creates an account) if needed, sees a confirmation card naming the connector and your host, and nothing happens before they click. Then:
- connect: the user signs in to the partner and consents; HDS grants the connector the access it needs. If the service is already connected, the user sees “already connected” and can finish or reconnect: no second connection is made.
- reconnect: the user signs in to the partner again on the existing connection (use it when the status
says
needs-reauth). - disconnect: after the user confirms, HDS stops the connector, revokes its access and sets the status to
disconnected. Data already imported stays in the user’s account.
The answer
Section titled “The answer”The browser comes back to returnUrl with these query parameters (any of the same name you put in
returnUrl are replaced):
| parameter | values |
|---|---|
| state | your nonce |
| app | the catalogue id |
| action | the action actually run: a connect on a connected service can answer reconnect |
| result | connected, disconnected, cancelled or error |
| reason | on cancelled and error only: a stable id |
Reasons: user-cancelled, partner-denied (the user refused at the partner), partner-error,
bridge-unavailable, hds-error, offer-invalid, session-lost, and the accept’s own cmc-* ids. Treat an
id you do not know as a generic error.
Nothing sensitive comes back: no token, no apiEndpoint, no link. Your app gets an outcome only; the connector’s access goes to the connector.
A request HDS cannot run (unknown or paused connector, malformed request, catalogue unavailable) shows an HDS
error page with a “Return to <your host>” link carrying result=error&reason=… (app-unknown,
app-paused, invalid-request, catalogue-unavailable, hds-error). A returnUrl that is not https gets
an error page with no link.
Show the status
Section titled “Show the status”Request read on the connector’s status stream only when your app signs in. With hds-lib:
const permissions = model.authorizations.forItemKeys(myItemKeys, { preRequest: [{ streamId: 'sync-status-mira', level: 'read' }] // the catalogue entry's statusStreamId});Never request the sync-status root: that would show you every service the user has connected. The stream
is created in its place in the user’s account on sign-in, so you can request it before the user connects.
Then read the one status event:
const [res] = await connection.api([{ method: 'events.get', params: { streams: ['sync-status-mira'], types: ['sync-status/connector-v1'], limit: 1 } }]);const status = res.events?.[0]?.content; // undefined = not connected| field | meaning |
|---|---|
| status | active, needs-reauth, error or disconnected. Show any other value as unknown. |
| connectedAt | when the current connection was made |
| lastRunAt / lastSuccessAt | last sync attempt / last successful sync |
| syncedUntil | how far the partner data goes, when the connector knows it |
| lastError | { class: auth / upstream / hds / other, code, at }: code is a short operator code, never partner text |
Times are Unix seconds. No event, or unknown-referenced-resource on the stream, means not connected.
A simple UI: Connect when not connected or disconnected; Reconnect when needs-reauth; last sync from
lastSuccessAt; Disconnect when connected.
Mira and Tempdrop publish their status. A Tempdrop user connected before 2026-10-05 has no status event until
they reconnect once through /connect.
Full specification: data-model
SYNC-STATUS.md.