Skip to content

A completely type-safe RPC (Remote Procedure Call) library for Electron applications. It eliminates the need for manual IPC event handling and ensures that your main and renderer processes stay in sync with strict TypeScript validation.

Notifications You must be signed in to change notification settings

mavolostudio/electron-rpc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

58 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

@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.

Not gRPC, Just IPC

Unlike gRPC, RMI, or remote object proxies, this library does not give the renderer direct access to Node.js functions or objects. It strictly uses Electron's standard IPC to pass serializable messages (JSON).

This design ensures that no backend code is executed directly by the renderer. The renderer simply requests a predefined procedure by name (string), and the main process decides how to handle it.

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)

Option A: TanStack Query (Recommended)

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.

About

A completely type-safe RPC (Remote Procedure Call) library for Electron applications. It eliminates the need for manual IPC event handling and ensures that your main and renderer processes stay in sync with strict TypeScript validation.

Topics

Resources

Stars

Watchers

Forks

Contributors