> ## Documentation Index
> Fetch the complete documentation index at: https://www.domo.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Cross-App Broadcasting

> Build two coordinated apps that communicate via App Broadcasting — a data table that publishes selections and a detail panel that subscribes.

### Intro

***

This tutorial builds two Custom Apps that communicate in real time using [App Broadcasting](/docs/portal/Apps/App-Framework/Guides/broadcasting). You'll create:

1. **Selector App** — A data table that publishes a `selection:changed` event whenever a user clicks a row
2. **Detail App** — A panel that subscribes to `selection:changed` and displays details for the selected record

Along the way you'll learn:

* How to declare broadcast channels in `manifest.json`
* How to use `domo.broadcastState()` for sticky messages that survive late mounts
* How to filter messages by source using `domo.onBroadcastFrom()`
* Best practices for topic naming and payload design

<Note>
  **Prerequisites:** Complete the [Setup and Installation](/docs/portal/Apps/App-Framework/Quickstart/Setup-and-Installation) guide. You'll need two app designs published to the same Domo page.
</Note>

### Step 1: Scaffold both apps

***

Create two separate app projects:

```bash theme={"dark"}
da new selector-app
da new detail-app
```

Each produces a Vite + React + TypeScript project ready for Domo development.

### Step 2: Configure the Selector App manifest

***

Open `selector-app/manifest.json` and add the `channels` and `datasetsMapping` properties:

```json theme={"dark"}
{
  "name": "Selector App",
  "version": "1.0.0",
  "size": { "width": 3, "height": 3 },
  "datasetsMapping": [
    {
      "alias": "customers",
      "dataSetId": "YOUR_DATASET_ID",
      "fields": [
        { "alias": "id", "columnName": "Customer ID" },
        { "alias": "name", "columnName": "Customer Name" },
        { "alias": "region", "columnName": "Region" },
        { "alias": "revenue", "columnName": "Annual Revenue" }
      ]
    }
  ],
  "channels": {
    "publishes": [
      {
        "name": "selection:changed",
        "description": "Fires when the user clicks a customer row",
        "sticky": true
      }
    ]
  }
}
```

Key decisions:

* **`sticky: true`** — The Detail App might mount after the user has already clicked a row. Sticky ensures it receives the last selection immediately.
* **Topic naming** — `selection:changed` uses the `namespace:event` convention. The namespace groups related topics; the event describes what happened.

### Step 3: Build the Selector App component

***

Replace `selector-app/src/App.tsx`:

```tsx theme={"dark"}
import { useEffect, useState } from 'react';
import domo from 'ryuu.js';

interface Customer {
  id: string;
  name: string;
  region: string;
  revenue: number;
}

export default function App() {
  const [customers, setCustomers] = useState<Customer[]>([]);
  const [selectedId, setSelectedId] = useState<string | null>(null);

  useEffect(() => {
    domo.data.query('customers').then(setCustomers);
  }, []);

  function handleRowClick(customer: Customer) {
    setSelectedId(customer.id);

    // Publish sticky state — late-mounting subscribers get this immediately
    domo.broadcastState('selection:changed', {
      id: customer.id,
      name: customer.name,
      region: customer.region,
      revenue: customer.revenue,
    });
  }

  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Region</th>
          <th>Revenue</th>
        </tr>
      </thead>
      <tbody>
        {customers.map((c) => (
          <tr
            key={c.id}
            onClick={() => handleRowClick(c)}
            style={{
              background: c.id === selectedId ? '#e3f2fd' : 'transparent',
              cursor: 'pointer',
            }}
          >
            <td>{c.name}</td>
            <td>{c.region}</td>
            <td>${c.revenue.toLocaleString()}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
```

### Step 4: Configure the Detail App manifest

***

Open `detail-app/manifest.json`:

```json theme={"dark"}
{
  "name": "Detail App",
  "version": "1.0.0",
  "size": { "width": 3, "height": 3 },
  "channels": {
    "subscribes": [
      {
        "name": "selection:changed",
        "description": "Shows detailed info for the selected customer"
      }
    ]
  }
}
```

The Detail App has no datasets of its own — it gets all its data from the Selector App's broadcast messages.

### Step 5: Build the Detail App component

***

Replace `detail-app/src/App.tsx`:

```tsx theme={"dark"}
import { useEffect, useState } from 'react';
import domo from 'ryuu.js';
import type { BroadcastMessage } from 'ryuu.js';

interface CustomerPayload {
  id: string;
  name: string;
  region: string;
  revenue: number;
}

export default function App() {
  const [customer, setCustomer] = useState<CustomerPayload | null>(null);
  const [source, setSource] = useState<string>('');

  useEffect(() => {
    const unsubscribe = domo.onBroadcast('selection:changed', (msg: BroadcastMessage) => {
      setCustomer(msg.payload as CustomerPayload);
      setSource(msg.sourceAppId);
    });

    return () => unsubscribe();
  }, []);

  if (!customer) {
    return <p style={{ color: '#666', padding: '2rem' }}>Click a row in the Selector App</p>;
  }

  return (
    <div style={{ padding: '1.5rem' }}>
      <h2>{customer.name}</h2>
      <dl>
        <dt>Region</dt>
        <dd>{customer.region}</dd>
        <dt>Annual Revenue</dt>
        <dd>${customer.revenue.toLocaleString()}</dd>
        <dt>Source App</dt>
        <dd><code>{source}</code></dd>
      </dl>
    </div>
  );
}
```

Because the Selector App publishes with `sticky: true`, if the Detail App mounts after a selection has been made, it immediately receives the last value — no waiting for the user to click again.

### Step 6: Publish and test

***

Publish both apps and place them on the same Domo page:

```bash theme={"dark"}
cd selector-app && domo publish
cd ../detail-app && domo publish
```

Create a new Dashboard in Domo and add both apps as cards. Click a row in the Selector App — the Detail App updates instantly.

### Pattern: Filtering by source

***

If your page has multiple Selector Apps and you only want to listen to one, use `domo.onBroadcastFrom()`:

```javascript theme={"dark"}
// Only react to messages from a specific app instance
domo.onBroadcastFrom('selection:changed', 'target-app-instance-id', (msg) => {
  renderDetail(msg.payload);
});
```

You can find an app's instance ID in the `sourceAppId` field of any message it sends, or in the App Broadcasting DevTools inspector.

### Pattern: Multi-topic coordination

***

Real apps often use multiple topics. Here's a Selector App that publishes both selection state and hover events:

```json theme={"dark"}
{
  "channels": {
    "publishes": [
      { "name": "selection:changed", "description": "Row click", "sticky": true },
      { "name": "chart:hover", "description": "Mouse hover on data point", "sticky": false }
    ]
  }
}
```

```javascript theme={"dark"}
// Selection is sticky — persists for late subscribers
domo.broadcastState('selection:changed', { id: row.id, name: row.name });

// Hover is ephemeral — only relevant right now
domo.broadcast('chart:hover', { seriesId: 'revenue', index: 3 });
```

The subscriber decides which topics it cares about:

```javascript theme={"dark"}
// Subscribe to selection (sticky — gets last value immediately)
domo.onBroadcast('selection:changed', (msg) => updateDetail(msg.payload));

// Subscribe to hover (ephemeral — only fires in real time)
domo.onBroadcast('chart:hover', (msg) => highlightPoint(msg.payload));
```

### Pattern: One-time initialization

***

Use `domo.onBroadcastOnce()` when an app only needs to receive a message once (e.g., initial configuration from a coordinator app):

```javascript theme={"dark"}
domo.onBroadcastOnce('config:init', (msg) => {
  applyConfig(msg.payload);
  // Automatically unsubscribed — no further messages received
});
```

### Error handling

***

The SDK throws synchronous errors for invalid usage:

```javascript theme={"dark"}
try {
  // Reserved namespace — throws immediately
  domo.broadcast('domo:system', { data: 'test' });
} catch (e) {
  console.error(e.message); // "Reserved namespace"
}

try {
  // Payload too large — throws immediately
  domo.broadcast('data:export', hugeObject);
} catch (e) {
  console.error(e.message); // "Payload too large"
}
```

Rate-limit errors from the host arrive asynchronously. The SDK logs a warning when messages are being throttled.

### Summary

***

| Concept                  | What you learned                                               |
| ------------------------ | -------------------------------------------------------------- |
| `channels` in manifest   | Declare what your app publishes and subscribes to              |
| `domo.broadcastState()`  | Publish sticky state that late subscribers receive immediately |
| `domo.onBroadcast()`     | Subscribe to a topic and react to messages                     |
| `domo.onBroadcastFrom()` | Filter messages by source app                                  |
| `domo.onBroadcastOnce()` | Auto-unsubscribe after first delivery                          |
| Sticky vs. ephemeral     | Use sticky for state, ephemeral for events                     |

### Next steps

* [App Broadcasting Guide](/docs/portal/Apps/App-Framework/Guides/broadcasting) — Architecture, security model, and lifecycle details
* [The Manifest File](/docs/portal/Apps/App-Framework/Guides/manifest#channels) — Full channel declaration reference
* [domo.js SDK Reference](/docs/portal/Apps/App-Framework/Tools/domo-js#broadcasting) — Complete API documentation
