Skip to content

Electron IPC ​

StreamooS uses two distinct communication channels between its three processes (main, preload, renderer) and the overlays. This document describes how each layer communicates.

Overview ​

Core Rules ​

  • The renderer can never import Node.js / Electron modules
  • The renderer can never call ipcRenderer directly
  • All communication goes through window.streamoos (preload bridge)
  • Overlays communicate only via WebSocket (never IPC)

Preload Bridge (window.streamoos) ​

The preload script (app/src/preload/index.cts) exposes a typed API via contextBridge that the renderer can call safely:

typescript
// app/src/preload/index.cts
contextBridge.exposeInMainWorld("streamoos", {
  sendCommand: (payload: FeatureCommandPayload) => ipcRenderer.invoke("feature/command", payload),

  onFeatureUpdate: (callback: (event: FeatureUpdateEvent) => void) =>
    ipcRenderer.on("feature/update", (_, event) => callback(event)),

  // ... other methods
});

In the renderer:

typescript
// app/src/renderer/
const state = await window.streamoos.sendCommand("timer", "get_state");
window.streamoos.onUpdate((update) => {
  if (update.feature === "timer") {
    /* ... */
  }
});

IPC Channels (app/src/shared/ipc.ts) ​

Channels are defined in IPC_CHANNELS in app/src/shared/ipc.ts and shared between main and preload:

Application ​

ChannelDirectionDescription
app/pingrenderer → mainHealth check
app/statusrenderer → mainGet app status (WS server, overlays, windows)

Feature Commands ​

ChannelDirectionDescription
feature/commandrenderer → mainSend a command to a feature handler
feature/updatemain → rendererState broadcast (mirrors WS updates)
feature/errormain → rendererFeature error notification
ws/eventmain → rendererWebSocket runtime events (client connect/disconnect, commands)

Twitch ​

ChannelDirectionDescription
twitch/auth-startrenderer → mainStart Twitch OAuth PKCE flow
twitch/auth-statusmain → rendererAuth result callback
twitch/disconnectrenderer → mainDisconnect Twitch

User Data ​

ChannelDirectionDescription
favorites/get / favorites/setrenderer ↔ mainUser favorite radio preference
settings/get / settings/setrenderer ↔ mainApp settings (theme, language, presets)
profile/get / profile/setrenderer ↔ mainUser profile (display name, avatar)
profile/upload-avatarrenderer → mainUpload avatar image (file dialog)

Remote Control ​

ChannelDirectionDescription
remote/generate-pinrenderer → mainGenerate a new PIN + remote URLs

Overlays ​

ChannelDirectionDescription
overlay/openrenderer → mainOpen a built-in overlay in external window
custom-overlay/listrenderer → mainList user-created overlays
custom-overlay/addrenderer → mainImport a custom overlay
custom-overlay/removerenderer → mainDelete a custom overlay
custom-overlay/openrenderer → mainOpen a custom overlay

Handler Registry (Main) ​

Each feature implements the FeatureHandler interface (app/src/main/handlers/types.ts):

typescript
interface FeatureHandler {
  readonly feature: string;
  handle(action: string, data: unknown): Promise<unknown>;
  getState(): FeatureState;
}

HandlerRegistry (app/src/main/handlers/registry.ts) routes commands to the correct handler:

ipcMain.handle('command', async (event, { feature, action, data }) => {
  // 1. Sender origin check (event.senderFrame)
  // 2. Zod payload validation
  // 3. Dispatch to FeatureHandler
  // 4. emitUpdate() → IPC + WS simultaneously
})

emitUpdate - Simultaneous Sync ​

Overlays and the dashboard always receive the same state at the same time.

IPC Security ​

Sender Origin Check ​

Every ipcMain handler verifies the message comes from a known frame:

typescript
// ❌ Never do this
ipcMain.handle('command', async (event, data) => { ... })

// ✓ Always check
ipcMain.handle('command', async (event, data) => {
  if (!isAllowedSender(event.senderFrame)) return;
  // ...
})

Defensive Payload Mapping ​

Never spread an unknown IPC payload:

typescript
// ❌
store.update({ ...payload });

// ✓ Explicitly map known fields
store.update({
  volume: payload.volume,
  track: payload.track,
});

Zod Validation ​

All incoming payloads (IPC and WS) go through Zod safeParse before processing. An invalid payload returns an error without crashing the process.

Shared Types (app/src/shared/) ​

IPC types are defined in app/src/shared/ipc.ts and importable from main, preload, and renderer:

typescript
import type { FeatureUpdateEvent, FeatureCommandPayload } from "@shared/ipc";

app/src/shared/ has no runtime dependencies - TypeScript only.

Released under the MIT License.