---
title: How to Build a Wear OS App for Your Capacitor App
description: Capacitor Wear OS apps are possible. Add a native Kotlin watch module and exchange messages, state, and transfers with your Capacitor app.
date:
  created: 2026-08-19
  updated: 2026-08-19
authors:
  - robingenz
categories:
  - Capacitor
  - Guides
  - SDKs
links:
  - Capacitor Watch: sdks/capacitor/watch.md
faq: true
---

# How to Build a Wear OS App for Your Capacitor App

Ask around whether you can build a Capacitor Wear OS app and the answer has been the same for years: no. That answer settles a different question than the one most people are asking. Wear OS devices have no WebView, so Capacitor cannot render your web app on the watch. Your Capacitor app can still exchange data with a Wear OS watch, which is what a watch experience actually needs. This guide walks through the full setup: a native Kotlin watch module inside your existing Android project, paired with the [Capacitor Watch plugin](../../sdks/capacitor/watch.md) on the phone, talking over the Google Play services Data Layer.

<!-- more -->

<div class="capawesome-z29o10a">
  <a href="https://capawesome.io/" target="_blank">
    <img alt="Build and deploy your Capacitor app with Capawesome Cloud" src="https://capawesome.io/assets/banners/cloud-build-and-deploy-capacitor-apps.png?t=1" />
  </a>
</div>

## Key Takeaways

- A Capacitor app can communicate with a Wear OS watch, but it cannot run on one — Wear OS has no WebView, so the watch UI is native Kotlin (typically Jetpack Compose).
- Data crosses over the Google Play services Data Layer. The Capacitor Watch plugin ships a Kotlin watch-side SDK at `node_modules/@capawesome-team/capacitor-watch/sdks/wearos`.
- Three channels: `sendMessage(...)` for live delivery, `updateState(...)` for latest-wins state, `transferUserInfo(...)` for queued transfers.
- Payloads are capped at roughly **100 KB** by the operating system on every channel, and on Android queued transfers have **no ordering guarantee**.
- The Wear OS module must use the **same `applicationId`** and the **same signing certificate** as the phone app.
- A Wear OS watch paired to an iPhone is not supported. The Data Layer connects Android phones and Wear OS watches only.

## Can a Capacitor app communicate with a Wear OS watch?

Yes. Since the release of the Capacitor Watch plugin, your Capacitor app can send data to and receive data from a Wear OS watch app, though probably not in the way you first imagined.

The standing answer goes back to [ionic-team/capacitor Discussion #3455](https://github.com/ionic-team/capacitor/discussions/3455){:target="_blank"} from August 2020, where a maintainer wrote: "A far as I am aware, WearOS or any wearable OS do not have a webview that can support this." Android's own [WebView management guide](https://developer.android.com/develop/ui/views/layout/webapps/managing-webview){:target="_blank"} backs that up, noting that `getCurrentWebViewPackage()` returns `null` on a device that "doesn't support using `WebView`, such as a Wear OS device". And [ionic-team/CapacitorWatch](https://github.com/ionic-team/CapacitorWatch){:target="_blank"}, the official watch plugin, says it "currently only supports iOS" under the CapacitorLABS label: "This project is experimental. Support is not provided."

None of that is wrong. Running Capacitor itself on a Wear OS watch is off the table, and no plugin changes that. It was simply never an answer to the separate question of whether a Capacitor phone app and a Wear OS watch app can exchange data, which they can, over the same Data Layer any native Android app would use.

## How the Capacitor Wear OS bridge works

Your phone app stays a Capacitor app. Your watch app is an ordinary native Wear OS module in `android/wear` inside the same Gradle project. The two exchange JSON payloads over the Data Layer, and three pieces are involved.

- **The phone side**: the [Capacitor Watch plugin](../../sdks/capacitor/watch.md), a promise-based TypeScript API. It registers its own `WearableListenerService` in the plugin manifest, so no `MainActivity` changes are needed.
- **The transport**: the [Google Play services Data Layer](https://developer.android.com/training/wearables/data/overview){:target="_blank"}, using `MessageClient` for live messages and `DataItem` objects for state and transfers.
- **The watch side**: a Kotlin library shipped with the plugin, providing a listener service base class for receiving and a `CapawesomeWatch` class for sending.

What the plugin deliberately does not do is render your watch UI. You build the watch screens natively with Jetpack Compose and the full Wear OS toolkit, and use the bridge purely for communication.

## What you'll need

Before you start, get the following in place:

- An existing Capacitor app with the Android platform added.
- Android Studio, since the watch module builds and deploys like any other Android module.
- A Wear OS device or emulator paired with your phone or phone emulator.
- Google Play services on the phone. The Data Layer is part of Play services, and without it every plugin method rejects as unavailable.
- A Capawesome Insiders license, since the Capacitor Watch plugin is available to [Insiders](../../insiders/index.md) and installs from the Capawesome npm registry.
- Some Kotlin familiarity. You will write a listener service and a few Compose screens, nothing exotic.

## Installing and configuring the Capacitor Watch plugin

Installation is covered end to end in the [Installation](../../sdks/capacitor/watch.md#installation) section of the plugin documentation, including registry setup and an AI-assisted option, so start there and come back once `npx cap sync` has run.

With the plugin installed, there is exactly one thing to configure on Android: the capability string. Add it to your `capacitor.config.ts` file:

```typescript title="capacitor.config.ts"
import { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  plugins: {
    Watch: {
      capability: 'capawesome_watch',
    },
  },
};

export default config;
```

A capability is how the Data Layer advertises what a device can do. Your watch app declares one, and the phone looks for exactly that string to decide whether a compatible watch app is installed and reachable. The default is `capawesome_watch`, so you can skip the block if you keep it. What you cannot skip is making both sides agree: a mismatch between this string and the one your watch module declares is by far the most common reason `reachable` comes back as `false`. On iOS, no configuration is required.

## Adding the Wear OS module to your Android project

The watch app is a separate Gradle module inside your existing `android/` project, not a separate project. That keeps one repository, one `applicationId`, and one signing configuration, which the Data Layer insists on anyway.

### Create the `android/wear` module

Create an `android/wear` folder for the watch app. The plugin repository contains a minimal, working module in the [`example/wearos`](https://github.com/capawesome-team/capacitor-plugins/tree/main/packages/watch/example/wearos){:target="_blank"} folder that you can copy as a starting point. Change the `applicationId` to your app's id and you have a buildable watch module.

### Wire up Gradle

Next, tell Gradle about the new module and the Wear OS SDK that ships with the plugin. Both go into `android/settings.gradle`:

```groovy title="android/settings.gradle"
include ':wear'
include ':capawesome-watch-sdk'
project(':capawesome-watch-sdk').projectDir = new File('../node_modules/@capawesome-team/capacitor-watch/sdks/wearos')
```

The SDK is referenced straight out of `node_modules`, the same way Capacitor plugins are, so run `npm install` before Gradle syncs, especially on a fresh clone or in CI. Then add the SDK as a dependency of the watch module in `android/wear/build.gradle`:

```groovy title="android/wear/build.gradle"
dependencies {
    implementation project(':capawesome-watch-sdk')
}
```

### Declare the capability

This is the watch-side half of the configuration you added to `capacitor.config.ts` earlier, and the string has to match character for character. Declare it in `android/wear/src/main/res/values/wear.xml`:

```xml title="android/wear/src/main/res/values/wear.xml"
<resources>
    <string-array name="android_wear_capabilities">
        <item>capawesome_watch</item>
    </string-array>
</resources>
```

### Two rules the Data Layer will not bend on

Before you build, note two requirements that come from Google Play services rather than from the plugin.

!!! warning "Hard requirements"

    The Wear OS module must use the **same `applicationId`** as your phone app, and both apps must be signed with the **same signing certificate**. If either differs, the Data Layer treats them as unrelated applications and they will never see each other, no matter how correct your code is.

## Building the watch side in Kotlin

Everything the watch does comes down to two things: one listener service for receiving data, and one `CapawesomeWatch` instance for sending it.

### Receiving data with `WatchListenerService`

The SDK ships a `WatchListenerService` base class that handles all three channels and hands you three overrides, one per channel:

```kotlin
class MyWatchListenerService : WatchListenerService() {
    override fun onMessageReceived(data: JSONObject, reply: ((JSONObject) -> Unit)?) {
        reply?.invoke(JSONObject().put("text", "Hello back!"))
    }

    override fun onStateReceived(data: JSONObject) {}

    override fun onUserInfoReceived(data: JSONObject) {}
}
```

The `reply` parameter is nullable for a reason: it is non-null only when the phone sent the message with `expectsReply: true`, so invoking it conditionally as above is the safe pattern. Register the service in your watch app's manifest, paying attention to the `/capawesome/watch` path prefix that the plugin publishes on:

```xml
<service
    android:name=".MyWatchListenerService"
    android:exported="true">
    <intent-filter>
        <action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
        <action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
        <data
            android:scheme="wear"
            android:host="*"
            android:pathPrefix="/capawesome/watch" />
    </intent-filter>
</service>
```

### Sending data from the watch

Sending works through the `CapawesomeWatch` class. All of its methods are `suspend` functions, so call them from a coroutine scope:

```kotlin
val watch = CapawesomeWatch(context)
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

scope.launch {
    watch.sendMessage(JSONObject().put("text", "Hello from the watch!"))
    val reply = watch.sendMessageForReply(JSONObject().put("text", "Ping"))
    watch.updateState(JSONObject().put("counter", 42))
    watch.transferUserInfo(JSONObject().put("sentAt", System.currentTimeMillis()))
}
```

`sendMessageForReply(...)` is the watch-side counterpart to the phone's `expectsReply` option: it suspends until the phone answers and returns that reply. Everything around these calls, the screens and state holders and theming, is plain Jetpack Compose with nothing plugin-specific about it.

## Talking to the watch from your Capacitor app

On the phone side it is one import and a handful of promise-based calls, all in TypeScript.

### Check the connection first

Because `sendMessage(...)` rejects when the watch is not reachable, it pays to query the connection before you build UI around it:

```typescript
import { Watch } from '@capawesome-team/capacitor-watch';

const { reachable, paired, watchAppInstalled } = await Watch.getConnectionInfo();
```

Two Android specifics matter here. `paired` is always `null`, because the platform does not expose that information. And `watchAppInstalled` is derived from the configured capability, so it answers "is a watch app declaring `capawesome_watch` installed" rather than "is any app installed".

### Send data to the watch

Each of the three channels is a single call, and they differ in what they promise about delivery:

```typescript
await Watch.sendMessage({ data: { text: 'Hello from the phone!' } });

const { reply } = await Watch.sendMessage({
  data: { text: 'Ping' },
  expectsReply: true,
});

await Watch.updateState({ data: { counter: 42 } });

await Watch.transferUserInfo({ data: { sentAt: Date.now() } });
```

Reach for `sendMessage(...)` when the watch app is on screen and you want an immediate round trip, `updateState(...)` when only the newest value matters (a current step count, a workout status), and `transferUserInfo(...)` when every item counts, such as completed laps or logged events.

### Receive data from the watch

Incoming data arrives through listeners, one per channel plus one for reachability changes:

```typescript
await Watch.addListener('messageReceived', async event => {
  if (event.messageId) {
    await Watch.replyToMessage({
      data: { text: 'Hello back!' },
      messageId: event.messageId,
    });
  }
});
await Watch.addListener('reachabilityChange', event => console.log(event.reachable));
await Watch.addListener('stateReceived', event => console.log(event.data));
await Watch.addListener('userInfoReceived', event => console.log(event.data));
```

The `messageId` mirrors the watch's `sendMessageForReply(...)`: when it is present, the watch is waiting for an answer that `replyToMessage(...)` delivers. Data arriving while your app is closed is not lost either. The plugin's manifest-registered `WearableListenerService` receives and persists it, then replays the events once your listeners are registered, even across an app restart. If you only need the newest state rather than the event stream, `getReceivedState()` reads it at any time.

## Which channel should you use?

Pick the channel by the delivery guarantee you need, not by the payload. Each one maps directly onto an Android API:

| Method                  | Semantics             | Android API              | Guarantee                                      |
| ----------------------- | --------------------- | ------------------------ | ---------------------------------------------- |
| `sendMessage(...)`      | Live, optional reply  | `MessageClient`          | Requires reachability, not queued              |
| `updateState(...)`      | Latest state wins     | `DataItem` (fixed path)  | Replaces undelivered state, survives restarts  |
| `transferUserInfo(...)` | Queued, all delivered | `DataItem` (unique path) | Queued until delivered, even across relaunches |

The [Capacitor Watch plugin](../../sdks/capacitor/watch.md) documentation lists the full semantics for both platforms, including how each channel behaves on watchOS.

## What are the limits of Wear OS communication?

The Data Layer is a synchronization channel between two devices, not a file transfer API, and its constraints shape what you build:

- Payloads are capped at roughly **100 KB** per message on all three channels. Send references, then fetch the real content from your backend.
- Queued transfers have **no ordering guarantee** on Android, and only the 100 most recent undelivered ones are kept. On iOS, they arrive in the order they were queued.
- Identical state updates may not be redelivered if the data did not change, so `updateState(...)` is no heartbeat.
- A phone-to-watch `sendMessage(...)` does not launch the watch app. It starts the watch app's listener service, which is enough to receive data but not to show UI.
- Google Play services is required. Without it, every plugin method rejects as unavailable.
- A Wear OS watch paired to an **iPhone** is not supported. That is a Data Layer limitation, not a plugin limitation.
- If you use Proguard, keep the plugin classes with `-keep class io.capawesome.capacitorjs.plugins.** { *; }`.

## Running and testing your Wear OS app

The development loop is two deployments. Build and install the `wear` module to your watch target from Android Studio, then run your Capacitor app on the phone as usual. Both have to be present before anything connects, because the capability lookup only succeeds once the watch app has been installed.

Your first smoke test is `getConnectionInfo()`. If `watchAppInstalled` is `false`, the phone cannot find a device advertising your capability, which almost always means the watch app was never installed on that watch or the capability strings differ. If `reachable` is `false` while the app is installed, the two are simply not connected right now: `transferUserInfo(...)` keeps working, `sendMessage(...)` rejects with `WATCH_NOT_REACHABLE`. Compare the capability string in `capacitor.config.ts` and `wear.xml` before you go looking for a bug in your code.

## FAQ

### Does Capacitor support Wear OS?

Not as a build target. Wear OS devices have no WebView, so a Capacitor app cannot run on the watch itself. It can run on the phone and communicate with a native Wear OS app over the Google Play services Data Layer.

### Do I have to write Kotlin for the watch app?

Yes. The watch UI is a native Wear OS app, typically built with Jetpack Compose. The phone side stays entirely in TypeScript.

### Can my Wear OS watch app talk to an iPhone app?

No. The Data Layer only connects Android phones with Wear OS watches, and an Apple Watch only pairs with an iPhone. Both are platform limitations.

### How large can the data I send be?

About 100 KB per payload on every channel. For anything bigger, send a reference such as a URL or record id and fetch the content separately.

### Why does `sendMessage(...)` reject with `WATCH_NOT_REACHABLE`?

The message channel is live and requires the watch to be reachable. On Android, check that the watch app declares the capability configured in `capacitor.config.ts`. For guaranteed delivery, use `transferUserInfo(...)` instead.

### Is the Capacitor Watch plugin free?

No. It is available to Capawesome [Insiders](../../insiders/index.md), which also covers priority support and the other Insiders plugins.

### Does this work with Ionic, Angular, React, or Vue?

Yes. The plugin is framework-agnostic and works in any Capacitor app, including plain JavaScript projects.

Once the watch experience works, the phone app still has to ship. Capawesome Cloud handles native builds, live updates, and app store publishing, so your attention stays on the product.

[Try Capawesome Cloud Free](https://capawesome.io){ .md-button .md-button--primary }

## Conclusion

"Wear OS isn't supported" was always an answer about rendering. Separate rendering from communication and what remains is ordinary Android development: a Gradle module, a listener service, a few Compose screens, and a handful of TypeScript calls on the phone. The Data Layer's rules on identity and payload size are strict, but they are documented and predictable, and the [Capacitor Watch plugin](../../sdks/capacitor/watch.md) keeps the same API on both platforms, so adding an Apple Watch app later is mostly a matter of writing SwiftUI.

Building something health or fitness related? [How to Build a Heart Rate Monitor with Capacitor](./how-to-build-a-heart-rate-monitor-with-capacitor.md) pairs well with a watch companion. Questions about your setup, or want to show what you built? Join the [Capawesome Discord server](https://discord.gg/VCXxSVjefW){:target="_blank"}, and subscribe to the [Capawesome newsletter](https://capawesome.io/newsletter/){:target="_blank"} for new plugin releases and guides.
