Wearables
The wearables bridge connects the web app to its companion app on a paired Apple Watch (WatchConnectivity) or Wear OS device (the Data Layer). It’s built on Shiny.Wearables 5.8. The watch doesn’t run the web app: the page talks to the phone, and the phone talks to your native watch app.
| Mode | What it’s for | Bridge |
|---|---|---|
| Live message | request and reply while the watch is reachable | POST /_bridge/wearables/messages |
| Context | the latest shared state; only the newest value is kept | GET/PUT /_bridge/wearables/context |
| Transfer | queued data, delivered in order even when the watch is away | POST /_bridge/wearables/transfers |
| File | a queued file from a file root | POST /_bridge/wearables/files |
| Status | paired, companion app installed, reachable | GET /_bridge/wearables, wearables.status |
Platforms
Section titled “Platforms”| Platform | Backed by |
|---|---|
| iOS | WatchConnectivity (WCSession). Not on iPad. |
| Android | the Wear OS Data Layer (Google Play services). The device needs the Wear OS app. |
| Mac Catalyst, macOS, Windows, Linux | nothing: every route answers 501 |
Install and register
Section titled “Install and register”dotnet add package Shiny.AppDeviceBridge.Wearablesbuilder.UseAppDeviceBridge( bridge => bridge.AddWearablesBridge(), webApp => { /* … */ });AddWearablesBridge() also registers Shiny’s wearable service with the bridge’s delegate, so there’s nothing else to call.
WearablesBridgeOptions |
Default | |
|---|---|---|
Root |
data |
the file root that files from the watch are saved into |
Folder |
wearables |
the folder inside Root; each file lands in {Folder}/{id}/{name} |
RegisterWearableService |
true |
turn off to call services.AddWearables(…) yourself; the bridge’s delegate is added either way |
The companion app speaks Shiny.Wearables’ protocol: path/data dictionaries over WatchConnectivity, and paths
under /shiny plus the shiny_wearable capability on Wear OS. On Wear OS the phone and watch apps must share the
application id and signing key. The Shiny Wearables docs cover the protocol in full, with Swift
and Kotlin examples.
JSON on the wire
Section titled “JSON on the wire”What the page sends reaches the watch as UTF-8 JSON text. What the watch sends reaches the page as JSON. When it isn’t
JSON, it arrives as a base64 string with binary: true, so nothing is dropped.
From the page
Section titled “From the page”// Program.cs (Blazor WebAssembly)builder.Services.AddWebAppHostClient().AddWearablesBridgeClient();@inject IWearablesBridge Watch
var status = await Watch.GetStatusAsync();if (status.Reachable){ var reply = await Watch.SendMessageAsync(new WearableMessageRequest("workout/start", JsonSerializer.SerializeToElement(new { kind = "run" })));}
await Watch.UpdateContextAsync(new WearableContextUpdate(JsonSerializer.SerializeToElement(new { plan = "5k" })));var ticket = await Watch.TransferAsync(new WearableTransferRequest("log", JsonSerializer.SerializeToElement(entries)));import { WearablesBridge } from "@shinyorg/appdevicebridge";
const watch = new WearablesBridge();const { reachable } = await watch.getStatus();if (reachable) { const { data } = await watch.sendMessage({ path: "workout/start", data: { kind: "run" } });}await watch.updateContext({ data: { plan: "5k" } });const stop = await watch.onTransferCompleted(done => console.log(done.id, done.succeeded));- Messages never queue. When no watch is reachable,
sendMessagefails with409(not_reachable). Keep messages small: about 64 KB on iOS and 100 KB on Wear OS. Use a transfer for anything that must arrive. - Queued sends return immediately. Context, transfers and files hand back a
{ id }.wearables.completedreports that id when the send is delivered, fails or is cancelled.GET /_bridge/wearables/transferslists what’s still pending, andDELETE /_bridge/wearables/transfers/{id}cancels it. - Errors:
501 not_supported(no wearable API),409 not_reachable,502 wearable_failed(the platform refused),400for a bad path or body.
The watch calling the web app
Section titled “The watch calling the web app”What the watch sends goes to the web app’s handlers: the page if it’s listening, background.js otherwise. The
platform wakes the phone app for it. A wearables.message handler’s return value is the reply the watch gets.
// background.js — answers the watch even when no page is openappdevicebridge.on("wearables.message", async ({ path, data }) => { if (path === "steps") return { today: await (await fetch("/_bridge/settings/local/steps")).json() }; return null;});
appdevicebridge.on("wearables.transfer", async ({ path, data }) => { await fetch("/_bridge/files/data/append?path=watch-log.jsonl", { method: "POST", body: JSON.stringify(data) + "\n" });});| Handler | Payload |
|---|---|
wearables.message |
{ path, data, binary, nodeId, expectsReply }; the return value is the reply |
wearables.context |
{ data, binary, nodeId } |
wearables.transfer |
{ id, path, data, binary, nodeId } |
wearables.file |
{ id, path, fileName, file: { root, path }, size, metadata, nodeId } |
The same payloads are also events (wearables.message, wearables.context, wearables.transfer, wearables.file),
along with wearables.status and wearables.completed. The events are for a page that only displays traffic.
Answer a message from a handler, not from an event.
Files move through the file roots, so the page can’t send anything it couldn’t already read.
- Sending:
POST /_bridge/wearables/fileswith{ path, file: { root, path }, metadata? }. The file must be in a root on disk. The platform reads it while it transfers, so leave it in place untilwearables.completedreports its id. It answers404when the file isn’t there. - Receiving: files from the watch are saved to
data/wearables/{id}/{name}(set byRootandFolder). The handler and event get them as aBridgeFileto read, move or delete through the files bridge. Names are reduced to one safe path segment, so a sender can’t write outside that folder.
Security
Section titled “Security”The wearables routes sit under AppDeviceBridgePolicies.Bridges like every other bridge: callers on this device by
default. This bridge doesn’t change the default policy, the host-name check or the launch session. See
Security.


