Docs

ADR-0015: Classic Electron IPC vs Guest↔Host RPC — when to use which

Status: Active · Date: 2026-07-23

References

  • ADR-0003: Cap’n Web RPC — the Guest↔Host RPC layer this decision draws a boundary around
  • Guest↔Host RPC — the living document for that layer (transport, trust model, capabilities)
  • First applied by the New Default Window command (tranquil-configatom-application.js)

Context

Tranquil now has two ways to cross a process/document boundary, and new features were starting to guess which to use:

  1. Classic Electron IPC — named string channels. Host renderer ↔ main process via ipcRenderer/ipcMain (wrapped by ipc-helpers); host renderer ↔ browser <webview> guest via the preload bridge (window.electron.sendToHost / receive). Structured-clone payloads; functions cannot cross.
  2. Guest↔Host RPC — the tranquil-rpc object-capability layer over Cap’n Web (ADR-0003). Functions/objects pass by reference as stubs; the host hands a trusted guest page a host capability object (window.tranquilHost). Runs only in trusted file:// webview pages under a registered root; untrusted origins get nothing (default-deny).

The trigger was “File → New Default Window”: a tranquil-config renderer command that asks the main process to seed and open a window. Which mechanism? The answer needs to be a rule, not a per-feature judgement call.

Decision

  • Use classic Electron IPC for first-party host↔main work — application and window lifecycle, commands, config. New Default Window sends tranquil:new-default-window over ipcRenderer.sendipcHelpers.on(ipcMain, …), exactly like the existing application:new-window-open-url command.
  • Use Guest↔Host RPC only for browser <webview> guest pages (trusted file:// content, e.g. the business-theme mockups) that need host capabilities across the webview boundary — notify, paneControls.register, etc. Untrusted web content is never given the runtime.

Window and application lifecycle are main-process concerns and are never guest capabilities, so they stay on classic IPC. The RPC layer operates on the renderer↔webview boundary; it does not (and should not) reach the main process.

Consequences

  • A clear, mechanical rule: who are the two parties? Host renderer ↔ main → classic IPC. Host renderer ↔ trusted webview guest → Guest↔Host RPC. Untrusted web content → no host surface at all.
  • We do not route everything through the RPC layer. It is renderer↔webview only, and it carries trust-classification and per-session machinery that is irrelevant (and unavailable) on the host↔main path.
  • This refines ADR-0003 rather than changing it — ADR-0003 introduced the capability layer; this ADR fixes the boundary between it and classic IPC so the two don’t get conflated.