# How to Connect iOS Client to Desktop Core via TunnelTransport

> Learn to connect your iOS client to the desktop core using TunnelTransport. Securely route encrypted RPC calls via Socket.IO for seamless inter-device communication.

- Repository: [Tiny Humans/openhuman](https://github.com/tinyhumansai/openhuman)
- Tags: tutorial
- Published: 2026-06-16

---

**The iOS client connects to the desktop core by instantiating a `TunnelTransport` with a channel ID, authentication token, and the core's Base-64 public key, then registering it with the `TransportManager` to route end-to-end encrypted RPC calls through a Socket.IO-based relay.**

The OpenHuman project uses a transport abstraction called `TunnelTransport` to bridge the React-based iOS client with the Rust desktop core over the internet. This guide explains how to establish that connection using the actual implementation found in the `tinyhumansai/openhuman` repository.

## Understanding the TunnelTransport Architecture

The tunnel system relies on a **CoreTransport** abstraction that works identically on both desktop and mobile clients. On the iOS side, the `TunnelTransport` class (TypeScript) handles the Socket.IO connection and XChaCha20-Poly1305 encryption, while the desktop core runs a small Rust client in [`src/openhuman/devices/tunnel_client.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/devices/tunnel_client.rs) to decrypt and process packets.

The connection flow requires three components:
- A **tunnel relay** URL (public Socket.IO endpoint)
- A **channel ID** and short-lived **auth token** generated by the core
- The core's **public key** for session key negotiation

## Step 1: Request a Tunnel Channel from the Desktop Core

Before the iOS client can connect, it must ask the running desktop core to create a tunnel channel. This is done via the `core.tunnel_create` JSON-RPC method, which is defined in the Rust RPC schemas (typically located in `src/openhuman/*/schemas.rs`).

From your iOS client code, call the RPC endpoint:

```typescript
import { coreRpcClient } from '@/services/api/coreRpcClient';

async function requestTunnelCredentials() {
  const { channelId, token, corePublicKey } = await coreRpcClient.call(
    'core.tunnel_create',
    {} // no arguments required
  );
  return { channelId, token, corePublicKey };
}

```

The core returns a unique `channelId` for this session, a short-lived `token` for authentication, and the `corePublicKey` (Base-64 encoded) needed for encryption handshake.

## Step 2: Instantiate TunnelTransport on the iOS Client

With the credentials in hand, create a `TunnelTransport` instance and register it with the `TransportManager`. The manager (located in [`app/src/services/transport/TransportManager.ts`](https://github.com/tinyhumansai/openhuman/blob/main/app/src/services/transport/TransportManager.ts)) handles transport selection and lifecycle across the OpenHuman app.

```typescript
import { TunnelTransport } from '@/services/transport/TunnelTransport';
import { TransportManager } from '@/services/transport/TransportManager';

async function establishTunnelConnection() {
  // 1. Obtain credentials from the core
  const { channelId, token, corePublicKey } = await requestTunnelCredentials();

  // 2. Configure the tunnel backend URL (must match the desktop core's relay)
  const tunnelBackendUrl = 'https://backend.tunnel.openhuman.dev';

  // 3. Create the transport instance
  const tunnel = new TunnelTransport(
    tunnelBackendUrl,
    channelId,
    corePublicKey, // Base-64 encoded
    token
  );

  // 4. Register with the manager and start the connection
  const manager = new TransportManager();
  manager.setTransport(tunnel);
  await manager.start(); // Opens Socket.IO connection
  
  console.log('TunnelTransport connected and ready');
}

```

The `TunnelTransport` constructor (implemented in [`app/src/services/transport/TunnelTransport.ts`](https://github.com/tinyhumansai/openhuman/blob/main/app/src/services/transport/TunnelTransport.ts)) immediately prepares the XChaCha20-Poly1305 handshake but defers the actual Socket.IO connection until `manager.start()` is invoked.

## Step 3: How the Encrypted Tunnel Works Under the Hood

Once `start()` is called, the following sequence occurs:

1. **Socket.IO Negotiation**: The iOS client connects to the tunnel relay via Socket.IO, presenting the `channelId` to join the correct room.
2. **Key Exchange**: The client and core perform a handshake using the core's long-term public key and the short-lived auth token to derive a per-session XChaCha20-Poly1305 key (providing forward secrecy).
3. **Encrypted RPC**: All JSON-RPC payloads are encrypted by `TunnelTransport.emit()` before transmission. The desktop core's [`tunnel_client.rs`](https://github.com/tinyhumansai/openhuman/blob/main/tunnel_client.rs) decrypts packets and forwards them to the internal RPC router.
4. **Bidirectional Flow**: Responses from the core are encrypted with the same session key and decrypted by the iOS client before being returned to the calling service.

Debug logs for this flow are prefixed with `[transport:tunnel]` in both the TypeScript client and Rust core when verbose logging is enabled.

## Complete iOS Integration Example

Place this initialization logic in your root component startup sequence (e.g., inside a `useEffect` hook) to ensure the transport is ready before any RPC calls are made:

```typescript
import { useEffect } from 'react';
import { coreRpcClient } from '@/services/api/coreRpcClient';
import { TransportManager } from '@/services/transport/TransportManager';
import { TunnelTransport } from '@/services/transport/TunnelTransport';

export function useCoreConnection() {
  useEffect(() => {
    async function init() {
      try {
        // Request tunnel from desktop core
        const { channelId, token, corePublicKey } = await coreRpcClient.call(
          'core.tunnel_create',
          {}
        );

        // Initialize tunnel transport
        const tunnel = new TunnelTransport(
          process.env.VITE_TUNNEL_RELAY_URL || 'https://backend.tunnel.openhuman.dev',
          channelId,
          corePublicKey,
          token
        );

        // Set up manager and connect
        const manager = new TransportManager();
        manager.setTransport(tunnel);
        await manager.start();

        console.log('✅ iOS client connected to desktop core via TunnelTransport');
      } catch (error) {
        console.error('Failed to establish tunnel:', error);
      }
    }

    init();
  }, []);
}

```

## Configuration and Troubleshooting Tips

**Environment Variables**: Ensure the iOS client uses the same tunnel relay URL that the desktop core expects. The core reads `OPENHUMAN_TUNNEL_RELAY_URL` from its environment, while the iOS client typically uses `VITE_TUNNEL_RELAY_URL`.

**Token Expiry**: Tunnel tokens are valid for approximately 5 minutes. If you encounter "unauthorized" or "channel not found" errors, request a fresh channel by calling `core.tunnel_create` again.

**Firewall and NAT Traversal**: The tunnel functions even when the iOS device is behind restrictive firewalls or NAT because the connection originates as an outbound WebSocket to the public relay.

**Debugging**: Enable verbose logging by setting `OPENHUMAN_LOG=debug` in the desktop core environment and `VITE_LOG_LEVEL=debug` in the iOS build configuration. Search logs for the `[transport:tunnel]` prefix to trace the handshake and packet flow.

## Summary

- The desktop core exposes `core.tunnel_create` to generate temporary channel credentials and a public key for encryption.
- The iOS client instantiates `TunnelTransport` (from [`app/src/services/transport/TunnelTransport.ts`](https://github.com/tinyhumansai/openhuman/blob/main/app/src/services/transport/TunnelTransport.ts)) with the relay URL, channel ID, core public key, and auth token.
- Register the transport with `TransportManager` (from [`app/src/services/transport/TransportManager.ts`](https://github.com/tinyhumansai/openhuman/blob/main/app/src/services/transport/TransportManager.ts)) and call `start()` to initiate the Socket.IO connection.
- All traffic is encrypted with XChaCha20-Poly1305, decrypted by the Rust [`tunnel_client.rs`](https://github.com/tinyhumansai/openhuman/blob/main/tunnel_client.rs), and processed as standard RPC by the core.

## Frequently Asked Questions

### What is TunnelTransport in the OpenHuman architecture?

`TunnelTransport` is a TypeScript class that implements the `CoreTransport` interface to provide end-to-end encrypted communication between the iOS client and desktop core. It wraps Socket.IO and handles XChaCha20-Poly1305 encryption, making it suitable for internet-based connections where direct TCP might be blocked by firewalls.

### How long does a tunnel channel remain valid?

Tunnel channels and their associated auth tokens expire after approximately 5 minutes. According to the implementation in [`src/openhuman/devices/tunnel_client.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/devices/tunnel_client.rs), these tokens are single-use and short-lived to prevent replay attacks. If your connection attempt fails with an authentication error, simply request a new channel via `core.tunnel_create`.

### Can the tunnel work if the iOS device is on a cellular network?

Yes. Because the `TunnelTransport` initiates an outbound WebSocket connection to the public relay server, it works across cellular networks, corporate firewalls, and NAT environments. The desktop core maintains a corresponding connection to the same relay, allowing bidirectional data flow without requiring inbound ports to be open on either device.

### Where is the encryption handled in the source code?

Encryption is handled on both sides: the iOS client encrypts payloads in [`app/src/services/transport/TunnelTransport.ts`](https://github.com/tinyhumansai/openhuman/blob/main/app/src/services/transport/TunnelTransport.ts) before emitting them over Socket.IO, and the desktop core decrypts them in [`src/openhuman/devices/tunnel_client.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/devices/tunnel_client.rs) before passing the plaintext RPC to the core's internal router. The symmetric key is derived during the initial handshake using the core's long-term public key and the ephemeral session token.