@mavolostudio/electron-rpc

Introduction

A type-safe RPC (Remote Procedure Call) library for Electron applications. It simplifies communication between the main process and renderer process by ensuring type safety and code completion using a natural, proxy-based API.

Features

  • 🔒 Type-Safe: End-to-end type safety from main to renderer.
  • 🚀 Proxy-Based: Call server methods as if they were local functions.
  • 🧩 Flexible: Supports nested API structures (e.g. api.users.get(...)).
  • 🛡️ Secure: Built-in channel whitelisting and context isolation support.

Installation

pnpm add @mavolostudio/electron-rpc

Usage

1. Define Context (Shared/Main)

Define the context that will be available to all procedures.

// src/main/context.ts
import { IpcMainInvokeEvent } from 'electron';

export type AppContext = {
  user: { id: string; role: 'admin' | 'user' };
};

export async function createContext(event: IpcMainInvokeEvent): Promise<AppContext> {
  return {
    user: { id: 'user-1', role: 'admin' }, // Load from session/token
  };
}

2. Define Procedures (Shared/Main)

Use the ProcedureBuilder to define your API with validation and middleware.

// src/main/router.ts
import { createProcedure, z } from '@mavolostudio/electron-rpc';
import type { AppContext } from './context';

const t = createProcedure<AppContext>();

// Middleware example
const logger = t.use(async ({ input }, next) => {
  console.log('Request:', input);
  return next();
});

export const router = {
  greeting: t
    .input(z.object({ name: z.string() }))
    .output(z.string())
    .use(async ({ ctx }, next) => {
      // Access context before handler
      console.log('User:', ctx.user.id);
      return next();
    })
    .query(async (ctx, input) => {
      return `Hello, ${input.name}!`;
    }),
    
  deleteData: t
    .input(z.object({ id: z.string() }))
    .output(z.boolean())
    .use(async ({ ctx }, next) => {
      if (ctx.user.role !== 'admin') throw new Error('Unauthorized');
      return next();
    })
    .mutation(async (ctx, input) => {
      // Perform dangerous action
      return true;
    })
};

export type AppRouter = typeof router;

3. Register Router (Main Process)

Register the router with the IPC channel.

// src/main/ipc.ts
import { registerIpcRouter } from '@mavolostudio/electron-rpc';
import { router } from './router';
import { createContext } from './context';

// Listen on "rpc" channel
registerIpcRouter('rpc', router, createContext);

4. Create Client (Renderer Process)

Wrap the proxy with our TanStack Query adapter for auto-generated hooks.

// src/renderer/client.ts
import { createProxy } from '@mavolostudio/electron-rpc/client';
import { tanstackProcedures } from '@mavolostudio/electron-rpc/tanstack-procedures';
import { QueryClient } from '@tanstack/react-query';
import type { AppRouter } from '../../main/router';

const queryClient = new QueryClient();

// Create base proxy
const rpc = createProxy<AppRouter>((path, args) => 
  window.rpc.invoke('rpc', { key: path[0], input: args[0] })
);

// Create TanStack wrapper
export const client = tanstackProcedures(rpc, queryClient);

Usage in React:

function App() {
  const { data, isLoading } = client.greeting.useQuery({ name: 'Alice' });
  const mutation = client.deleteData.useMutation();

  if (isLoading) return <div>Loading...</div>;

  return (
    <button onClick={() => mutation.mutate({ id: '123' })}>
      {data}
    </button>
  );
}

Option B: Direct RPC

You can also use the RPC client directly.

// src/renderer/client.ts
import { createProxy } from '@mavolostudio/electron-rpc/client';
import type { AppRouter } from '../../main/router';

export const rpc = createProxy<AppRouter>((path, args) => 
  window.rpc.invoke('rpc', { key: path[0], input: args[0] })
);

// Usage
const result = await rpc.greeting({ name: 'Bob' });

API Reference

createProcedure<Context>()

Creates a builder for defining procedures with input/output validation and middleware.

registerIpcRouter(channel, router, createContext, plugins?)

Registers a router object to handle IPC requests.

  • channel: IPC channel name.
  • router: Object containing procedures.
  • createContext: Function generating context for each request.
  • plugins: Optional array of lifecycle plugins.

tanstackProcedures(client, queryClient)

Wraps the RPC client to provide useQuery, useMutation, and getQueryKey.

exposeRpc(config)

Exposes the secure bridge in the preload script.

  • config.name: Global variable name (e.g., "rpc").
  • config.whitelist: Allowed channels.