OpenClaw Swarm

Architecture

How the Swarm app is structured

Overview

The main process owns all WebSocket connections to remote gateways via GatewayManager. The renderer never talks to the network directly. Instead it communicates with the main process through oRPC procedures exposed over a MessagePort.

Renderer (React)
  └── oRPC client (MessagePort)
        └── Main process (oRPC server)
              ├── GatewayManager
              │     └── WebSocket connections → OpenClaw Gateways
              ├── Logger (console + file + memory transports)
              └── electron-store (safeStorage encryption)

Key directories

All paths relative to apps/swarm/:

DirectoryPurpose
src/routes/File-based routes (TanStack Router)
src/components/ui/shadcn/ui components
src/lib/Utilities, oRPC client, shared helpers
electron/Main process, preload, API
electron/api/routers/oRPC router files (gateway, swarm, events, logs, window)
electron/api/types.tsAll shared TypeScript types (persistence, protocol, runtime, domain)
electron/gateway/WebSocket connection layer (manager, connection, protocol, publisher, schemas)
electron/logger/Transport-agnostic logging system
electron/store.tselectron-store persistence with safeStorage encryption
electron/device-identity.tsEd25519 keypair generation + challenge signing

oRPC IPC

The renderer creates a MessageChannel and sends one port to the main process via ipcMain. The oRPC client (src/lib/orpc.ts) uses RPCLink over this channel. The server side uses RPCHandler from @orpc/server/message-port.

// Queries — use directly in components, never in custom hooks
useQuery(orpc.gateway.list.queryOptions())
useQuery(orpc.gateway.get.queryOptions({ input: { id: gatewayId } }))

// Mutations
useMutation(orpc.gateway.add.mutationOptions())

// Event streaming (real-time updates from main process)
useQuery(
  orpc.events.subscribe.experimental_streamedOptions({
    queryFnOptions: { refetchMode: 'replace' },
  }),
)

oRPC context

Every procedure receives a context with three fields:

type Context = {
  win: BrowserWindow
  gatewayManager: GatewayManager
  logger: Logger
}

Procedure definitions

Server-side procedures use zod/v4 for input validation:

import { z } from 'zod/v4'
import { p } from '../orpc'

export const exampleRouter = {
  get: p.input(z.object({ id: z.string() })).handler(({ input, context }) => {
    return context.gatewayManager.getGateway(input.id)
  }),
}

Routers

The app exposes five oRPC routers:

RouterPurpose
gatewayCRUD for gateway connections, pairing, sessions, session details
swarmAggregated overview, cost, presence, search across all gateways
eventsAsync generator streaming gateway events to the renderer
logsTail the memory ring buffer, export log files, get log path
windowWindow management (maximize, close)

Event streaming

Real-time events from the main process use MemoryPublisher from @orpc/experimental-publisher/memory. The gateway layer publishes events and the events router yields them to the renderer via an async generator:

// Server-side (electron/api/routers/events.ts)
export const eventsRouter = {
  subscribe: p.handler(async function* ({ context, signal }) {
    for await (const event of context.gatewayManager.events.subscribe(
      'gatewayEvent',
      { signal },
    )) {
      yield event
    }
  }),
}

Event types include sessions, presence, health, chat, and execApproval.

Device identity

Each Swarm installation generates a persistent Ed25519 keypair on first launch. The deviceId is the SHA-256 hex digest of the raw 32-byte public key. This identity is used to authenticate with gateways via challenge-response signing. Keys are stored in electron-store.

Route structure

Routes use TanStack Router's file-based convention with memory history (for Electron). The app has a dashboard layout with nested routes:

src/routes/
├── __root.tsx                              # Root layout
├── index.tsx                               # Landing / redirect
└── dashboard/
    ├── route.tsx                            # Dashboard layout (sidebar + Outlet)
    ├── index.tsx                            # Dashboard overview
    ├── logs/index.tsx                       # Log viewer
    └── gateways/
        ├── index.tsx                        # Gateway list
        ├── $gatewayId/
        │   ├── route.tsx                    # Gateway detail layout
        │   ├── index.tsx                    # Gateway overview
        │   ├── settings.tsx                 # Gateway settings
        │   └── sessions/index.tsx           # Session list
        └── $gatewayId_.sessions.$sessionKey.tsx  # Session detail (flat route)

src/routeTree.gen.ts is auto-generated — never edit it manually.

On this page