Communication between the main process (Node.js) and the renderer (WebView).
Register a handler for an IPC channel.
import { ipcMain } from 'voltkit';
ipcMain.handle('get-user', async (args) => {
const { id } = args as { id: number };
return { name: 'Alice', id };
});Parameters:
channel: string— Channel namehandler: (args: unknown) => Promise<unknown> | unknown— Handler function
Throws: If a handler is already registered for the channel.
Remove a previously registered handler.
ipcMain.removeHandler('get-user');Check if a handler is registered for a channel.
Process an IPC request. Used internally by the native bridge.
Returns: Promise<{ id: string; result?: unknown; error?: string; errorCode?: IpcErrorCode; errorDetails?: unknown }>
Stable error codes for IPC failures:
IPC_HANDLER_NOT_FOUNDIPC_HANDLER_ERRORIPC_HANDLER_TIMEOUTIPC_PAYLOAD_TOO_LARGEIPC_IN_FLIGHT_LIMIT
ipcMain.processRequest uses a bounded handler timeout (default 5000ms) to avoid hanging the native reply path.
An optional fourth argument { timeoutMs } can override this when used internally.
These functions are available in the WebView context via window.__volt__.
Invoke a main-process handler from the renderer.
import { invoke } from 'voltkit/renderer';
const user = await invoke<{ name: string }>('get-user', { id: 1 });
console.log(user.name); // 'Alice'Throws: If called outside the renderer context (no window.__volt__).
Listen for events emitted from the main process.
import { on } from 'voltkit/renderer';
on('notification', (data) => {
console.log('Received:', data);
});Remove an event listener.
Volt also provides an opt-in typed contract layer for channel definitions, compile-time inference, and runtime request/response validation.
import { IpcSchema, defineCommands } from 'voltkit/ipc-contract';
export const commands = defineCommands({
'demo.compute': {
request: IpcSchema.object({
a: IpcSchema.number(),
b: IpcSchema.number(),
}, 'ComputeArgs'),
response: IpcSchema.object({
sum: IpcSchema.number(),
}, 'ComputeResult'),
aliases: ['compute'],
},
});import { ipcMain } from 'volt:ipc';
import { registerContractHandlers } from 'voltkit/ipc-contract';
import { commands } from './ipc-contract';
registerContractHandlers(ipcMain, commands, {
'demo.compute': ({ a, b }) => ({ sum: a + b }),
});import { createContractInvoker } from 'voltkit/renderer';
import { commands } from './ipc-contract';
const typedIpc = createContractInvoker(commands, (channel, args) =>
window.__volt__!.invoke(channel, args),
);
const result = await typedIpc.invoke('demo.compute', { a: 2, b: 3 });
// result is inferred as { sum: number }If you need to keep old channel names while migrating, use aliases plus the legacy adapter:
import { createLegacyInvokeAdapter } from 'voltkit/renderer';
import { commands } from './ipc-contract';
const invokeLegacy = createLegacyInvokeAdapter(
commands,
(channel, args) => window.__volt__!.invoke(channel, args),
);
await invokeLegacy('compute', { a: 2, b: 3 }); // resolves to demo.computeregisterContractHandlers registers both canonical and legacy alias channels, so migration can be incremental.
defineCommands(...) validates alias collisions up-front and throws immediately for duplicate alias mappings.
createSchema<T>(...) uses user-defined guards, and TypeScript cannot prove the guard and T are perfectly aligned.
Prefer IpcSchema helpers (null, string, number, boolean, object, array, literal, optional) when possible.
import { isIpcContractValidationError } from 'voltkit/renderer';
try {
await typedIpc.invoke('demo.compute', { a: 2, b: 3 });
} catch (error) {
if (isIpcContractValidationError(error)) {
console.error(error.channel, error.phase, error.message);
} else {
console.error(error);
}
}- All IPC messages are checked for prototype pollution (
__proto__,constructor,prototype) - A rate limiter (1000 req/s) prevents IPC flooding
- Oversized request payloads are rejected with
IPC_PAYLOAD_TOO_LARGE - Per-window in-flight IPC load is capped; overflow is rejected with
IPC_IN_FLIGHT_LIMIT - Response payloads are escaped before injection into the WebView
- Timeout failures return
errorCode: "IPC_HANDLER_TIMEOUT"witherrorDetails - Reserved
volt:native:*channels used by built-in fast-path APIs still pass through the same payload, prototype-pollution, and rate-limit guards
Volt reserves channel prefixes for framework-owned behavior:
volt:*volt:native:*
Application code should not register handlers for those channels. Use the exported framework APIs (data, workflow, backend runtime modules, etc.) instead of invoking reserved channel names directly.