# App-to-App Communication in io.Connect: Practical Guidance

**URL:** <https://community.interop.io/t/app-to-app-communication-in-io-connect-practical-guidance/553>\
**Category:** io.Connect Desktop\
**Tags:** Knowledge-base\
**Created:** [April 15, 2026, 2:21pm UTC](https://community.interop.io/t/app-to-app-communication-in-io-connect-practical-guidance/553 "2026-04-15T14:21:31Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![Georgi\_G](https://yyz2.discourse-cdn.com/flex028/user_avatar/community.interop.io/georgi_g/32/184_2.png) [@Georgi\_G](https://community.interop.io/u/Georgi_G)\
**Post date:** [April 15, 2026, 2:21pm UTC](https://community.interop.io/t/app-to-app-communication-in-io-connect-practical-guidance/553/1 "2026-04-15T14:21:32Z")

</div>

io.Connect offers various APIs for apps to communicate that are covered in [Key io.Connect Integration Concepts Explained](https://community.interop.io/t/key-io-connect-integration-concepts-explained/255). This article goes deeper into each one with code samples, flow diagrams, and notes on when to use it and when not to.

* * *

### 1. Pub/Sub

Apps publish messages on a **named topic**. All currently-subscribed apps receive it. **No persistence** - a late joiner misses everything.

**Use when:**

- Migrating apps already built on pub/sub

- Fire-and-forget events, no state needed

**Avoid when:**

- Building new integrations - use Shared Contexts

- Late joiners need the last-known value

```auto
// Provider
io.bus.publish("mkt.update", { ticker: "AAPL" });

// Consumer
await io.bus.subscribe("mkt.update", data => updateUI(data));

```

```auto
App A (publisher)
  │ publish("mkt.update", {ticker: "AAPL"})
  ▼
Gateway (fan-out broker — no state)
  ├──▶ App B (subscriber) → {ticker: "AAPL"} delivered ✓
  └──╳ App C (late joiner) → no history — missed ✗

```

* * *

### 2. Shared Contexts

A **named global object** stored in the Gateway. Any app can read, write, or subscribe. Late joiners receive the current state immediately.

**API surface:** `set()` · `get()` · `update()` · `subscribe()` · `all()` · `setPath()` · `setPaths()` · `destroy()`

**Use when:**

- Syncing a selected entity across apps (developer-driven)

- Late-joining apps must get current state immediately

- Multiple apps read and write the same shared data

**Avoid when:**

- You need user-driven grouping → use **Color Channels**

- You need Workspace-level isolation → use **Workspace Contexts**

```auto
// Provider
await io.contexts.update("selectedClient", { id: "C-001" });

// Consumer
await io.contexts.subscribe("selectedClient", ctx => load(ctx));

// set() replaces fully; update() merges; setPath() targets one key

```

```auto
App A (writer)
  │ update("selectedClient", {id: "C-001"})
  ▼
Gateway (stores state, persists)
  ├──▶ App B (subscribed) → notify → {id: "C-001"} ✓
  └──▶ App C (late joiner) → subscribe → {id: "C-001"} immediately ✓

```

* * *

### 3. Color Channels

Shared Contexts plus a **Channel Selector UI**. Users assign apps to a named colour; all apps on the same colour sync. Supports **directional mode** : users can restrict a window to publish-only or subscribe-only.

**API surface:** `all()` · `join()` · `leave()` · `publish()` · `subscribe()` · `get()`

**Use when:**

- The user decides which apps are linked at runtime

- Multiple independent sync groups coexist on screen

- FDC3 User Channels compatibility is needed

**Avoid when:**

- Linking is fixed in code → use **Shared Contexts** instead

- Full global visibility across all apps is required

> **Note:** Color Channels are syntactic sugar around Shared Contexts - each channel maps to a dedicated context object.

```auto
// Provider
await io.channels.join("Red");
await io.channels.publish({ ticker: "AAPL" });

// Consumer
io.channels.subscribe(data => updateUI(data));

```

```auto
App A (joins Red)
  │ join("Red") → publish({ticker: "AAPL"})
  ▼
Gateway (Red channel context)
  ├──▶ App B (joins Red) → {ticker: "AAPL"} to Red members ✓
  └──╳ App C (on Blue) → wrong channel = no delivery ✗

```

* * *

### 4. Workspace Contexts

Lets apps inside a Workspace **share state with each other**. The context is scoped to that Workspace, travels with it, and can be persisted with the layout.

**API surface:** `workspace.setContext()` · `workspace.getContext()` · `workspace.updateContext()` · `workspace.onContextUpdated()`

**Use when:**

- Running parallel workflows (e.g. two client portfolios in separate Workspaces)

- Context should save and restore with the Workspace Layout

- Hard isolation between Workspaces is required

**Avoid when:**

- Apps outside the Workspace need the data → use **Shared Contexts**

- Global visibility across all apps is required

> **Note:** Workspace Contexts are syntactic sugar around Shared Contexts - each Workspace gets its own scoped context.

```auto
// Provider
await workspace.setContext({ clientId: "C-001" });

// Consumer
workspace.onContextUpdated(ctx => load(ctx.clientId));

```

```auto
App A (WS1)
  │ setContext({clientId: "C-001"}) - WS1
  ▼
Gateway (scoped routing)
  ├──▶ App B (WS1) → notify WS1 members ✓
  └──╳ App C (WS2) → WS2 isolated = no delivery ✗

(WS2 has its own independent context, e.g. {clientId: "C-042"})

```

* * *

### 5. Interop Methods

Explicit, **service-like capability**. The caller knows exactly what it needs. An app registers a named function and others can invoke it and get a result.

**API surface:** `register()` · `unregister()` · `invoke()` · `methods()` · `servers()`

**Use when:**

- You need a well-defined service contract with a return value

- The target app is already running

- Precise functions: pricing, calculations, trade execution

**Avoid when:**

- Target may not be running → use **Intents** instead

- You need to broadcast state - not a service call pattern

```auto
// Provider (Risk App)
await io.interop.register("calcRisk", args => ({ risk: calc(args.id) }));

// Consumer (Blotter)
const res = await io.interop.invoke("calcRisk", { id: "C-001" });

```

```auto
Risk App (server)
  │ register("calcRisk")
  │ Gateway (method registry)
  │ ▲
Blotter (client) │
  │ invoke("calcRisk", {id: "C-001"}) ──▶ routed to server
  │ ◀── return {risk: 0.73} response delivered

```

* * *

### 6. Intents

Raise an **action, not a target**. The caller says “handle this” —\> the platform finds matching handlers and **can launch the app if not running**.

**API surface:** `register()` · `raise()` · `all()`

**Use when:**

- App-to-app handoffs, launch/activate workflows, user choice

- The caller doesn’t need to know the exact destination app

- Handler may need to be launched first

**Best mental model:** Phone “Share / Open with…” UI for desktop workflows.

> **Note:** When an intent is **ambiguous** (multiple handlers match), the **Intents Resolver UI** is displayed, letting the user pick.

```auto
// Provider
await io.intents.register("ViewChart", ctx => showChart(ctx.data));

// Consumer
await io.intents.raise({ intent: "ViewChart", context: { type: "Instrument", data: { ticker: "AAPL" } } });

```

```auto
Chart App (not running)
  ▲ launch + deliver {RIC: "AAPL"}
  │
Gateway (intent registry + launcher)
  ▲
  │
Blotter (raiser)
  │ raise("ViewChart", {RIC: "AAPL"})

```

* * *

### 7. Streams

Continuous, real-time data push. Best for **live alerts, telemetry, and any producer-\>many-subscribers flow**. The server controls who can subscribe via **branches**.

**API surface:** `createStream()` · `stream.push()` · `stream.branches()` · `subscribe()` · `subscription.close()` · `stream.close()`

**Use when:**

- Data changes continuously and polling would be wasteful

- Many consumers need the same live data

- Per-subscriber filtering is needed

**Avoid when:**

- Data updates only occasionally → use **Shared Contexts**

- You need a one-off response → use **Interop Methods**

**Branches** : Named sub-channels within a stream (e.g. per region). The provider pushes to a branch and only subscribers on that branch receive the data.

```auto
// Provider
const stream = await io.interop.createStream("prices");
stream.push({ ticker: "AAPL", bid: 182.50, ask: 182.55 });

// Consumer
const sub = await io.interop.subscribe("prices", { onData: d => update(d) });

```

```auto
Price Svc (provider)
  │ createStream("prices")
  │ push({ticker, bid}) ∞
  ▼
Gateway (stream registry)
  ├──▶ Blotter → fan-out continuously
  └──▶ Chart App → fan-out continuously

```

* * *

### 8. Window Context

Per-window state and launch context. Scoped to one window. Best for **restoring or initialising a specific window**. Can be saved as part of a **Global Layout**.

**API surface:** `window.setContext()` · `window.getContext()` · `window.updateContext()` · `window.onContextUpdated()`

**Use when:**

- Passing initial launch payload to a window

- Context should persist with a saved Global Layout

- FDC3 Private Channels scenario

**Avoid when:**

- Dynamic inter-app messaging is needed after launch

- Multiple apps need access to the same data

```auto
// Provider (Blotter - opening a chart)
await io.windows.open("chartApp", url, { context: { ticker: "AAPL" } });
await win.updateContext({ ticker: "MSFT" });

// Consumer (Chart App - reading its own context)
const ctx = await io.windows.my().getContext();
io.windows.my().onContextUpdated(ctx => reload(ctx));

```

```auto
Blotter (opener)
  │ open("chartApp", {ticker: "AAPL"})
  │ updateContext({ticker: "MSFT"})
  ▼
Gateway (window launcher)
  └──▶ Chart App (launched)
         getContext() → {ticker: "AAPL"}
         onContextUpdated → {ticker: "MSFT"}

```

Check also

- [Use cases and Patterns](https://community.interop.io/t/io-connect-patterns-and-apis-making-the-right-choice/570)
- [Key io.Connect Concepts](https://community.interop.io/t/key-io-connect-integration-concepts-explained/255)
- [io.Connect Complete Guide to Integration Mechanisms](https://community.interop.io/t/complete-guide-to-io-connect-integration-mechanisms/538)
