@murphai/device-syncd
v0.1.15
Published
Published local device sync runtime for Murph.
Downloads
106
Readme
@murphai/device-syncd
Published local device sync runtime for Murph.
Contributing a new wearable provider? Start with docs/device-provider-contribution-kit.md in the repo root, then use the scaffolds listed in docs/templates/README.md.
Murph's CLI can install, start, reuse, and stop this daemon for the selected vault through murph device daemon ..., so most operators should treat it as a built-in local service rather than a separately managed sidecar.
The daemon binds the control plane to localhost by default. CLI and web clients must authenticate that control plane with a bearer token. If provider callbacks or webhooks need public reachability, expose only the public callback/webhook routes through a separate listener or reverse proxy instead of widening /accounts/* and /providers/*/connect.
The package now also exports a reusable DeviceSyncPublicIngress layer that encapsulates provider-agnostic OAuth state, callback handling, and webhook verification/dispatch. Hosted or alternate HTTP surfaces should import that seam from @murphai/device-syncd/public-ingress; the package root stays daemon-oriented. That shared ingress is the seam for a future hosted Vercel control plane while keeping the current local/tunneled callback flow alive.
For non-daemon callers, @murphai/device-syncd/client is the canonical shared control-plane client surface for base-url/token resolution, loopback safety checks, and JSON request helpers.
What it does:
- serves a provider-agnostic local control plane for CLI and web auth flows
- owns OAuth connection state
- stores encrypted provider tokens in SQLite under
.runtime/device-syncd.sqlite - keeps
.runtime/device-syncd.sqliteplus.runtime/device-syncd/**local-only; those control, cursor, and log artifacts are not part of hostedagent-statebundles - accepts provider webhooks when a provider supports them
- runs background backfill and reconcile jobs
- serializes active jobs per account so rotating refresh-token flows do not race
- imports provider snapshots through
@murphai/importers
Current providers:
- Garmin
- WHOOP
- Oura
Shared public ingress
Use DeviceSyncPublicIngress when you need the same callback/webhook logic in a different HTTP surface:
- local
device-syncdwith an exposed public listener or tunnel - a future hosted Next.js/Vercel control plane that stores durable integration state in Postgres
The shared ingress owns:
- provider connect URL creation
- OAuth state validation
- OAuth callback completion
- provider webhook verification/parsing
- webhook dedupe and account lookup hooks
It does not own canonical health-data import. The local data plane should still be the only component that normalizes provider payloads and writes them into the Murph vault.
Provider model
device-syncd treats wearable providers as long-lived connectors with a shared lifecycle:
- one-time OAuth connect
- encrypted token storage with refresh support
- initial backfill
- scheduled reconcile polling
- optional webhook fan-in
- normalized snapshot import through
@murphai/importers
Garmin uses OAuth plus scheduled polling. Once the operator configures the Garmin client ID and secret, the end-user flow is connect once and let scheduled sync keep the account fresh.
WHOOP uses OAuth plus webhooks.
Oura uses OAuth plus refresh tokens and works well in a polling-first mode, so the basic Murph setup does not require Oura webhooks. Once the operator configures the Oura client ID and secret, the end-user flow is just connect once and let scheduled sync keep the account fresh.
The provider lifecycle metadata used here now comes from the shared @murphai/importers/device-providers/provider-descriptors surface, so callback paths, default scopes, webhook capabilities, sync windows, metric families, and source-priority hints stay aligned between connector code and snapshot normalization.
Environment
Required:
DEVICE_SYNC_VAULT_ROOTDEVICE_SYNC_PUBLIC_BASE_URLDEVICE_SYNC_SECRETfor the daemon's local bootstrap/service secretDEVICE_SYNC_CONTROL_TOKENfor the control-plane bearer token
At least one provider must be configured.
Common optional settings:
DEVICE_SYNC_PORTDEVICE_SYNC_HOST(defaults to127.0.0.1)DEVICE_SYNC_ALLOWED_RETURN_ORIGINSDEVICE_SYNC_STATE_DB_PATHDEVICE_SYNC_WORKER_POLL_MSDEVICE_SYNC_WORKER_BATCH_SIZEDEVICE_SYNC_SCHEDULER_POLL_MSDEVICE_SYNC_SESSION_TTL_MSDEVICE_SYNC_WORKER_LEASE_MSDEVICE_SYNC_PUBLIC_HOSTplusDEVICE_SYNC_PUBLIC_PORTto expose only/oauth/*/callbackand/webhooks/*OURA_WEBHOOK_VERIFICATION_TOKENwhen you want the daemon to answer Oura's webhook verification challenge overGET /webhooks/oura
Garmin settings:
GARMIN_CLIENT_IDGARMIN_CLIENT_SECRETGARMIN_AUTH_BASE_URLGARMIN_TOKEN_BASE_URLGARMIN_API_BASE_URLGARMIN_BACKFILL_DAYSGARMIN_RECONCILE_DAYSGARMIN_RECONCILE_INTERVAL_MSGARMIN_REQUEST_TIMEOUT_MS
WHOOP settings:
WHOOP_CLIENT_IDWHOOP_CLIENT_SECRETWHOOP_BASE_URLWHOOP_SCOPESWHOOP_BACKFILL_DAYSWHOOP_RECONCILE_DAYSWHOOP_RECONCILE_INTERVAL_MSWHOOP_WEBHOOK_TIMESTAMP_TOLERANCE_MSWHOOP_REQUEST_TIMEOUT_MS
Oura settings:
OURA_CLIENT_IDOURA_CLIENT_SECRETOURA_AUTH_BASE_URLOURA_API_BASE_URLOURA_SCOPESOURA_BACKFILL_DAYSOURA_RECONCILE_DAYSOURA_RECONCILE_INTERVAL_MSOURA_REQUEST_TIMEOUT_MS
Run
murph device daemon start --vault ./vaultManual direct execution remains available:
node packages/device-syncd/dist/bin.jsThe published bin name is also murph-device-syncd.
Control-plane clients
@murphai/device-syncd/clientis the canonical shared client/helper surface for device-sync control-plane callers@murphai/device-syncd/public-ingressis the canonical shared callback/webhook ingress surface for hosted or alternate HTTP adapters@murphai/device-syncdstays focused on the daemon/runtime surface rather than duplicating the shared ingress exportsvault-cli device ...can auto-start and reuse this daemon for the selected vault, or it can target an explicit control plane throughDEVICE_SYNC_BASE_URLvault-cliauthenticates local control routes withDEVICE_SYNC_CONTROL_TOKEN- cross-origin
returnToURLs are accepted only when their origin appears inDEVICE_SYNC_ALLOWED_RETURN_ORIGINS; relative paths remain allowed by default
HTTP routes
Control routes: loopback-only plus Authorization: Bearer <token>
GET /healthzGET /providersGET /connect/:provider?returnTo=/settings/devicesPOST /providers/:provider/connectGET /accountsGET /accounts/:idPOST /accounts/:id/reconcilePOST /accounts/:id/disconnect
Public routes: keep them on localhost unless you explicitly expose a separate callback/webhook listener
GET /oauth/:provider/callbackGET /webhooks/:providerfor provider webhook verification/health checks (GET /webhooks/ouraanswers Oura's verification challenge when configured)POST /webhooks/:providerfor providers that support webhooks
Notes for Garmin
Murph's Garmin connector is designed for the same connect-once experience as the other first-class wearable providers:
- the operator registers one Garmin API application once
- users connect their Garmin account through the normal OAuth PKCE consent flow
device-syncdkeeps the account alive with refresh tokens- scheduled backfill and reconcile jobs pull recent Garmin windows directly so sync keeps working without requiring a public Garmin webhook path
Notes for Oura
Murph's Oura connector is designed for the least-friction user path:
- the operator registers one Oura API application once
- users connect their Oura account through the normal OAuth consent flow
device-syncdkeeps the account alive with refresh tokens- reconcile jobs poll recent windows so ongoing sync works even without webhook setup
That keeps the user-facing experience at connect once, then auto-sync while still fitting the same provider lifecycle used by WHOOP.
If you do enable Oura webhooks locally, set OURA_WEBHOOK_VERIFICATION_TOKEN and expose GET /webhooks/oura plus POST /webhooks/oura through your public listener or tunnel so Oura can complete its verification handshake.
