Skip to content
Shiny.NET
Commercial SupportGuaranteed response times on Shiny bug fixes, straight from the maintainers. Plans from US$1,500 a year.See the plans

Wearables

NuGet package Shiny.AppDeviceBridge.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.9. 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
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
Terminal window
dotnet add package Shiny.AppDeviceBridge.Wearables
builder.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.

What the page sends reaches the watch as UTF-8 JSON text, so a string goes out quoted ("hi") and a companion app should read it with its JSON parser. What the watch sends reaches the page as JSON. Plain UTF-8 text, which is what Shiny.Wearables’ string overloads (WearableData.FromString) and a Swift or Kotlin String send, arrives as a string. Anything else arrives as a base64 string with binary: true, so nothing is dropped.

// 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, sendMessage fails with 409 (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.completed reports that id when the send is delivered, fails or is cancelled. GET /_bridge/wearables/transfers lists what’s still pending, and DELETE /_bridge/wearables/transfers/{id} cancels it.
  • Errors: 501 not_supported (no wearable API), 409 not_reachable, 502 wearable_failed (the platform refused), 400 for a bad path or body.

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 open
appdevicebridge.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.

Direction Details
Sending POST /_bridge/wearables/files with { 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 until wearables.completed reports its id. It answers 404 when the file isn’t there.
Receiving files from the watch are saved to data/wearables/{id}/{name} (set by Root and Folder). The handler and event get them as a BridgeFile to 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.
GET/_bridge/wearables#

Whether a wearable is paired, has the companion app, and is reachable for live messages.

C#IWearablesBridge.GetStatusAsyncTSWearablesBridge.getStatus
Response
200 WearableStatus
WearableStatus · 5 fields
supportedbooleanFalse where the platform has no wearable API — anything but iOS and Android, an iPad, an Android device without the Wear OS app.
pairedbooleanA wearable is paired (iOS) or connected (Android).
appInstalledbooleanThe companion app is installed on it.
reachablebooleanA live message can be sent now. Context, transfers and files queue and do not need this.
nodesWearableNode[]The wearables the platform reports.
POST/_bridge/wearables/messages#

Sends a live message and waits for the companion app's reply. Fails with 409 (not_reachable) when no wearable is reachable — a message never queues; use transfer for that.

C#IWearablesBridge.SendMessageAsyncTSWearablesBridge.sendMessage
Body
application/json · WearableMessageRequest
WearableMessageRequest · 3 fields
pathstringWhat it is about — sync, workout/start.
dataunknown | nullThe body, any JSON value. Keep it small: about 64 KB on iOS, 100 KB on Wear OS.
nodeIdstring | nullWear OS only: the node to send to. Omitted, the nearby node that runs the companion app.
Response
200 WearableReply
WearableReply · 2 fields
dataunknownThe reply; JSON null when the companion app replied with nothing.
binarybooleanThe reply was neither JSON nor UTF-8 text: data is its bytes as a base64 string.
Errors
409Conflict
GET/_bridge/wearables/context#

The context this app last shared and the context the wearable last shared.

C#IWearablesBridge.GetContextAsyncTSWearablesBridge.getContext
Response
200 WearableContextState
WearableContextState · 2 fields
sentWearableContext | nullWhat this app last shared, or null.
receivedWearableContext | nullWhat the wearable last shared, or null.
PUT/_bridge/wearables/context#

Replaces the context shared with the wearable. Only the latest value is kept; it arrives when the wearable can take it.

C#IWearablesBridge.UpdateContextAsyncTSWearablesBridge.updateContext
Body
application/json · WearableContextUpdate
WearableContextUpdate · 1 field
dataunknown
Response
204 No content
POST/_bridge/wearables/transfers#

Queues data for the wearable, delivered in order even if it is out of range or its app is not running.

C#IWearablesBridge.TransferAsyncTSWearablesBridge.transfer
Body
application/json · WearableTransferRequest
WearableTransferRequest · 2 fields
pathstringWhat it is.
dataunknownAny JSON value.
Response
200 WearableTransferTicket
WearableTransferTicket · 1 field
idstringWhat wearables.completed and the pending list report it by.
POST/_bridge/wearables/files#

Queues a file from a file root for the wearable. Fails with 404 when the file does not exist. The platform reads it while it transfers, so leave it in place until onTransferCompleted reports the id.

C#IWearablesBridge.SendFileAsyncTSWearablesBridge.sendFile
Body
application/json · WearableFileRequest
WearableFileRequest · 3 fields
pathstringWhat it is — maps/offline.
fileBridgeFileThe file, in a root on disk.
metadataRecord<string, string> | nullStrings the companion app receives with it.
Response
200 WearableTransferTicket
WearableTransferTicket · 1 field
idstringWhat wearables.completed and the pending list report it by.
Errors
404Not found
GET/_bridge/wearables/transfers#

Outgoing transfers and files the wearable has not received yet.

C#IWearablesBridge.GetPendingTransfersAsyncTSWearablesBridge.getPendingTransfers
Response
200 WearablePendingTransfer[]
WearablePendingTransfer · 4 fields
idstringThe id the send returned.
pathstringWhat it is.
kindWearableTransferKindData or a file.
progressnumber | null0–1 where the platform reports it (iOS files); null otherwise.
DELETE/_bridge/wearables/transfers/{id}#

Cancels an outgoing transfer or file. Fails with 404 when nothing pending has the id.

C#IWearablesBridge.CancelTransferAsyncTSWearablesBridge.cancelTransfer
ParameterInType
idpathstringrequired
Response
204 No content
Errors
404Not found
EVENTwearables.status#

The wearable was paired or unpaired, installed or removed the app, or became reachable or unreachable.

C#IWearablesBridge.OnStatusChangedAsyncTSWearablesBridge.onStatusChanged
Payload
WearableStatus
supportedbooleanFalse where the platform has no wearable API — anything but iOS and Android, an iPad, an Android device without the Wear OS app.
pairedbooleanA wearable is paired (iOS) or connected (Android).
appInstalledbooleanThe companion app is installed on it.
reachablebooleanA live message can be sent now. Context, transfers and files queue and do not need this.
nodesWearableNode[]The wearables the platform reports.
EVENTwearables.message#

A live message arrived. To answer it, register a wearables.message handler instead (page or background.js): what the handler returns is the reply.

C#IWearablesBridge.OnMessageAsyncTSWearablesBridge.onMessage
Payload
WearableMessage
pathstringWhat it is about.
dataunknownThe body.
binarybooleanThe body was neither JSON nor UTF-8 text: data is its bytes as a base64 string.
nodeIdstring | nullThe node that sent it.
expectsReplybooleanThe sender waits on a reply — what a wearables.message handler returns.
EVENTwearables.context#

The wearable shared new context. Also handed to a wearables.context handler.

C#IWearablesBridge.OnContextAsyncTSWearablesBridge.onContext
Payload
WearableContext
dataunknownThe context.
binarybooleanIt was neither JSON nor UTF-8 text: data is its bytes as a base64 string.
nodeIdstring | nullThe node that shared it; null for this app's own.
EVENTwearables.transfer#

A queued transfer arrived. Also handed to a wearables.transfer handler.

C#IWearablesBridge.OnTransferAsyncTSWearablesBridge.onTransfer
Payload
WearableTransfer
idstringThe transfer's id.
pathstringWhat it is about.
dataunknownThe body.
binarybooleanThe body was neither JSON nor UTF-8 text: data is its bytes as a base64 string.
nodeIdstring | nullThe node that sent it.
EVENTwearables.file#

A file arrived and was filed into a file root. Also handed to a wearables.file handler.

C#IWearablesBridge.OnFileAsyncTSWearablesBridge.onFile
Payload
WearableReceivedFile
idstringThe sender's transfer id.
pathstringWhat the sender said it is.
fileNamestringThe file's name on the sender.
fileBridgeFileWhere it is now — read, move or delete it through the files bridge.
sizenumberIts size in bytes.
metadataRecord<string, string>The strings sent with it.
nodeIdstring | nullThe node that sent it.
EVENTwearables.completed#

An outgoing transfer or file was delivered, failed, or was cancelled.

C#IWearablesBridge.OnTransferCompletedAsyncTSWearablesBridge.onTransferCompleted
Payload
WearableTransferCompleted
idstringThe id the send returned.
pathstringWhat it was; empty on Wear OS, where the delivery receipt carries only the id.
kindWearableTransferKindData or a file.
succeededbooleanThe wearable received it.
errorstring | nullWhy it failed or was cancelled; null when delivered.

JSON property names are camelCase and enums travel as their names. A ? marks a property the payload may omit;| null one that may be null.

WearableStatusobject#

Whether there is a wearable to talk to.

supportedbooleanFalse where the platform has no wearable API — anything but iOS and Android, an iPad, an Android device without the Wear OS app.
pairedbooleanA wearable is paired (iOS) or connected (Android).
appInstalledbooleanThe companion app is installed on it.
reachablebooleanA live message can be sent now. Context, transfers and files queue and do not need this.
nodesWearableNode[]The wearables the platform reports.
WearableNodeobject#

A connected wearable.

idstringThe node id — always watch on iOS, which has one active watch.
displayNamestringThe name the platform gives it.
isNearbybooleanDirectly connected rather than reached through the cloud.
hasAppbooleanRuns the companion app.
WearableMessageRequestobject#

A live message to send.

pathstringWhat it is about — sync, workout/start.
data?unknown | nullThe body, any JSON value. Keep it small: about 64 KB on iOS, 100 KB on Wear OS.
nodeId?string | nullWear OS only: the node to send to. Omitted, the nearby node that runs the companion app.
WearableReplyobject#

The companion app's reply to a message.

dataunknownThe reply; JSON null when the companion app replied with nothing.
binarybooleanThe reply was neither JSON nor UTF-8 text: data is its bytes as a base64 string.
WearableContextStateobject#

Both sides' context.

sentWearableContext | nullWhat this app last shared, or null.
receivedWearableContext | nullWhat the wearable last shared, or null.
WearableContextobject#

Shared context.

dataunknownThe context.
binarybooleanIt was neither JSON nor UTF-8 text: data is its bytes as a base64 string.
nodeIdstring | nullThe node that shared it; null for this app's own.
WearableContextUpdateobject#

New context to share.

dataunknown
WearableTransferRequestobject#

Data to queue for the wearable.

pathstringWhat it is.
dataunknownAny JSON value.
WearableTransferTicketobject#

A queued transfer or file.

idstringWhat wearables.completed and the pending list report it by.
WearableFileRequestobject#

A file to queue for the wearable.

pathstringWhat it is — maps/offline.
fileBridgeFileThe file, in a root on disk.
metadata?Record<string, string> | nullStrings the companion app receives with it.
BridgeFileobject#

A file the page can reach through the files bridge: a named root and a path inside it. Bridges that take or return a file use this, so nothing reaches the page that /_bridge/files would not also serve.

rootstring
pathstring
WearablePendingTransferobject#

An outgoing transfer or file the wearable has not received yet.

idstringThe id the send returned.
pathstringWhat it is.
kindWearableTransferKindData or a file.
progressnumber | null0–1 where the platform reports it (iOS files); null otherwise.
WearableTransferKindenum#
"Data""File"
WearableMessageobject#

A live message from the wearable.

pathstringWhat it is about.
dataunknownThe body.
binarybooleanThe body was neither JSON nor UTF-8 text: data is its bytes as a base64 string.
nodeIdstring | nullThe node that sent it.
expectsReplybooleanThe sender waits on a reply — what a wearables.message handler returns.
WearableTransferobject#

A queued transfer from the wearable.

idstringThe transfer's id.
pathstringWhat it is about.
dataunknownThe body.
binarybooleanThe body was neither JSON nor UTF-8 text: data is its bytes as a base64 string.
nodeIdstring | nullThe node that sent it.
WearableReceivedFileobject#

A file from the wearable, filed into a file root.

idstringThe sender's transfer id.
pathstringWhat the sender said it is.
fileNamestringThe file's name on the sender.
fileBridgeFileWhere it is now — read, move or delete it through the files bridge.
sizenumberIts size in bytes.
metadataRecord<string, string>The strings sent with it.
nodeIdstring | nullThe node that sent it.
WearableTransferCompletedobject#

An outgoing transfer or file finished.

idstringThe id the send returned.
pathstringWhat it was; empty on Wear OS, where the delivery receipt carries only the id.
kindWearableTransferKindData or a file.
succeededbooleanThe wearable received it.
errorstring | nullWhy it failed or was cancelled; null when delivered.

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.