IPC Bridge
The Electron adapter replaces HTTP sockets with Electron's IPC mechanism. Standard Request/Response objects are serialized into IPC-safe payloads, sent between processes, and deserialized on the other side — your controllers see the same Web Fetch API as any other adapter.
How It Works
Renderer Preload Main
──────── ─────── ────
ipcFetch(req)
→ toIpcRequest(req)
→ globalThis[channel](payload)
ipcRenderer.invoke(channel, payload)
ipcMain.handle(channel, payload)
→ toRequest(payload)
→ app.fetch(request)
→ toIpcResponse(response)
← IpcFetchResponse
← toResponse(payload)
← Response
Text content (JSON, HTML, XML) is serialized as strings; binary content as ArrayBuffer.
Configuration
All three layers must agree on the same channel string:
// main
const electronZelt = await onElectron(app, { ipcChannel: 'http://zelt-app' });
// preload
exposeIpc({ channel: 'http://zelt-app' });
// renderer
ipcFetch(input, init, { channel: 'http://zelt-app' });
The channel must start with http:// or https://. The default is 'http://zelt-ipc' if omitted.
Main Process: onElectron()
const electronZelt = await onElectron(app, {
ipcChannel: 'http://zelt-app',
warmup: true, // default: resolve all controllers at startup
});
Options
| Option | Type | Default | Description |
|---|---|---|---|
ipcChannel | `http://${string}` | `https://${string}` | 'http://zelt-ipc' | IPC channel identifier |
warmup | boolean | true | Resolve all controllers at startup |
ipcFeature | string | 'http' | Key of the HTTP feature to bind to the IPC bridge, for apps with multiple http() features |
Return Value (OnElectronApp)
Each configured feature is namespaced under its key (http by default). HTTP features additionally expose listen() to serve over TCP instead of IPC:
| Property | Type | Description |
|---|---|---|
http.fetch | (request: Request) => Promise<Response> | Handle a request directly |
http.request | (input: string | Request, init?: RequestInit) => Promise<Response> | Handle a request built from a URL/path and RequestInit |
http.listen | (portOrOptions?: number | ListenOptions) => Promise<ServerHandle> | Serve this feature over TCP instead of IPC |
shutdown | () => Promise<void> | Graceful shutdown |
get | <T>(Class) => Promise<T> | Resolve a service from DI |
Breaking change: the old electronZelt.fetch shorthand is gone — use electronZelt.http.fetch instead (or the key you configured via ipcFeature).
Warmup
By default, onElectron() eagerly resolves all controllers at startup (warmup: true). This ensures services are initialized before the first request. Set warmup: false for lazy initialization on first request.
Preload Script: exposeIpc()
exposeIpc({ channel: 'http://zelt-app' });
exposeIpc() registers an IPC sender function and exposes it to the renderer via contextBridge.exposeInMainWorld() (or globalThis if context isolation is disabled).
The exposed key is the channel string itself, so ipcFetch on the renderer side can look it up from globalThis[channel].
Renderer: ipcFetch()
const response = await ipcFetch('http://zelt-app/hello/world', undefined, {
channel: 'http://zelt-app',
});
const data = await response.json();
ipcFetch() has the same signature as fetch(), plus an optional third argument for the channel. It converts the request into an IPC payload, sends it through the preload bridge, and returns a standard Response.
Creating a Wrapper
In practice, wrap ipcFetch so the channel is configured once:
const CHANNEL = 'http://zelt-app';
export const apiFetch = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> =>
ipcFetch(input, init, { channel: CHANNEL });
Using with Hono Client
For type-safe API calls, combine with @zeltjs/hono-client:
const zeltIpcFetch = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> =>
ipcFetch(input, init, { channel: 'http://zelt-app' });
export const client = hc<AppType>('http://zelt-app', {
fetch: zeltIpcFetch,
});
AppType is generated from your app definition — either by the CLI plugin (zelt build) or, when your build is driven by electron-vite instead of the Zelt CLI, programmatically via GeneratorService.generateFromApp(). For renderer code that cannot import from the main process build output, generate with portable: true.
Accessing the IPC Event
In controllers, use ipcEvent() to access the underlying IpcMainInvokeEvent:
@Controller('/system')
export class SystemController {
@Get('/sender')
getSender() {
const event = ipcEvent();
return { processId: event?.processId };
}
}
Shutdown
Connect Zelt shutdown to Electron's quit lifecycle:
const electronZelt = await onElectron(app, { ipcChannel: 'http://zelt-app' });
const electronApp = await electronZelt.get(ElectronAdaptor);
electronApp.ready.app.on('will-quit', () => {
void electronZelt.shutdown();
});
Using will-quit keeps the HTTP bridge alive until Electron is actually quitting, so the renderer can send final IPC calls during teardown.