{"id":20797180,"url":"https://github.com/daniguardiola/rpc-anywhere","last_synced_at":"2026-03-16T22:02:58.160Z","repository":{"id":212756941,"uuid":"731855562","full_name":"DaniGuardiola/rpc-anywhere","owner":"DaniGuardiola","description":"Create a type-safe RPC anywhere.","archived":false,"fork":false,"pushed_at":"2024-03-07T18:06:15.000Z","size":363,"stargazers_count":180,"open_issues_count":5,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-06T18:18:13.164Z","etag":null,"topics":["browser-extension","chrome-extension","electron","iframe","messaging","postmessage","rpc","service-worker","webworker"],"latest_commit_sha":null,"homepage":"https://rpc-anywhere.dio.la","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DaniGuardiola.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-12-15T03:21:00.000Z","updated_at":"2025-04-23T19:13:06.000Z","dependencies_parsed_at":"2023-12-19T21:30:56.498Z","dependency_job_id":"e8eca077-e295-4459-a7ee-d813d9647e45","html_url":"https://github.com/DaniGuardiola/rpc-anywhere","commit_stats":null,"previous_names":["daniguardiola/rpc-anywhere"],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DaniGuardiola%2Frpc-anywhere","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DaniGuardiola%2Frpc-anywhere/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DaniGuardiola%2Frpc-anywhere/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DaniGuardiola%2Frpc-anywhere/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DaniGuardiola","download_url":"https://codeload.github.com/DaniGuardiola/rpc-anywhere/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252741483,"owners_count":21797030,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["browser-extension","chrome-extension","electron","iframe","messaging","postmessage","rpc","service-worker","webworker"],"created_at":"2024-11-17T16:32:53.028Z","updated_at":"2026-03-16T22:02:58.111Z","avatar_url":"https://github.com/DaniGuardiola.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cimg alt=\"The RPC Anywhere logo\" src=\"https://github.com/DaniGuardiola/rpc-anywhere/raw/main/logo.png\"\u003e\n\u003c/div\u003e\n\u003cbr/\u003e\n\u003cdiv align=\"center\"\u003e\n\n[![API reference](https://img.shields.io/badge/tsdocs-%23007EC6?style=flat\u0026logo=typescript\u0026logoColor=%23fff\u0026label=API%20reference\u0026labelColor=%23555555)](https://tsdocs.dev/docs/rpc-anywhere/) [![Bundle size](https://deno.bundlejs.com/?q=rpc-anywhere%40latest\u0026treeshake=%5B%7B+createRPC+%7D%5D\u0026badge=\u0026badge-style=flat\u0026badge-raster=false)](https://bundlejs.com/?q=rpc-anywhere%40latest\u0026treeshake=%5B%7B+createRPC+%7D%5D)\n\n\u003c/div\u003e\n\nCreate a type-safe RPC anywhere.\n\n\u003e RPC Anywhere powers [Electrobun](https://www.electrobun.dev/), [Teampilot AI](https://teampilot.ai/), and more.\n\n```bash\nnpm i rpc-anywhere\n```\n\n[✨ Interactive iframe demo ✨](https://rpc-anywhere.dio.la/)\n\n---\n\nRPC Anywhere lets you create RPCs in **any** context, as long as a transport layer (a way for messages to move between point A and point B) is provided.\n\nDesigned to be the last RPC library you'll ever need, it ships with a few transports out of the box: iframes, Electron IPC, browser extensions, workers...\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eWhat is an RPC?\u003c/b\u003e\u003c/summary\u003e\n\n\u003e In the context of this library, an RPC is a connection between two endpoints, which send messages to each other.\n\u003e\n\u003e If the sender expects a response, it's called a \"request\". A request can be thought of as a function call where the function is executed on the other side of the connection, and the result is sent back to the sender.\n\u003e\n\u003e [Learn more about the general concept of RPCs on Wikipedia.](https://www.wikiwand.com/en/Remote_procedure_call)\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eWhat is a transport layer?\u003c/b\u003e\u003c/summary\u003e\n\n\u003e A transport layer is the \"channel\" through which messages are sent and received between point A and point B. Some very common examples of endpoints:\n\u003e\n\u003e - Websites: iframes, workers, `BroadcastChannel`.\n\u003e - Browser extensions: content script ↔ service worker.\n\u003e - Electron: renderer process ↔ main process.\n\u003e - WebSocket.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eWhy should I use RPC Anywhere?\u003c/b\u003e\u003c/summary\u003e\n\n\u003e While there are some really great RPC libraries out there, many of them are focused in a specific use-case, and come with trade-offs like being tied to a specific transport layer, very opinionated, very simple, or not type-safe.\n\u003e\n\u003e Because of this, many people end up creating their own RPC implementations, \"reinventing the wheel\" over and over again. [In a Twitter poll, over 75% of respondents said they had done it at some point.](https://x.com/daniguardio_la/status/1735854964574937483?s=20) You've probably done it too!\n\u003e\n\u003e By contrast, RPC Anywhere is designed to be the last RPC library you'll ever need. The features of a specific RPC (schema, requests, messages, etc.) are completely decoupled from the transport layer, so you can set it up and forget about it.\n\u003e\n\u003e In fact, you can replace the transport layer at any time, and the RPC will keep working exactly the same way (except that messages will travel through different means).\n\u003e\n\u003e RPC Anywhere manages to be flexible and simple without sacrificing robust type safety or ergonomics. It's also well-tested and packs a lot of features in a very small footprint (~1kb gzipped).\n\u003e\n\u003e If you're missing a feature, feel free to [file a feature request](https://github.com/DaniGuardiola/rpc-anywhere/issues/new?assignees=\u0026labels=enhancement\u0026projects=\u0026template=feature-request.yaml)! The goal is to make RPC Anywhere the best RPC library out there.\n\n\u003c/details\u003e\n\n---\n\n\u003c!-- vscode-markdown-toc --\u003e\n\n- [Features](#features)\n- [Usage example (parent window to iframe)](#usage-example-parent-window-to-iframe)\n  - [Iframe script (`iframe.ts`)](#iframe-script-iframets)\n  - [Parent window script (`parent.ts`)](#parent-window-script-parentts)\n- [Getting started](#getting-started)\n  - [Schemas](#schemas)\n  - [RPC instances](#rpc-instances)\n  - [Messages](#messages)\n  - [Requests](#requests)\n- [Documentation](#documentation)\n- [Type safety and features](#type-safety-and-features)\n- [Features under consideration](#features-under-consideration)\n- [Prior art](#prior-art)\n- [Contributing](#contributing)\n\n\u003c!-- vscode-markdown-toc-config\n\tnumbering=false\n\tautoSave=true\n\t/vscode-markdown-toc-config --\u003e\n\u003c!-- /vscode-markdown-toc --\u003e\n\n---\n\n## \u003ca name='Features'\u003e\u003c/a\u003eFeatures\n\n- Type-safe and extensively tested.\n- Transport agnostic, with ready-to-use transports:\n  - Iframes.\n  - Web workers.\n  - Browser extensions.\n  - Electron IPC (coming soon).\n  - Broadcast channels.\n  - Message ports: advanced use cases like service workers, worklets, etc.\n- Tiny (~1.4kb gzipped, transport included).\n- Flexible (no enforced client-server architecture).\n- Promise-based with optional proxy APIs (e.g. `rpc.requestName(params)`).\n- Schema type can be inferred from the request handlers.\n- Optional lazy initialization (e.g. `rpc.setTransport(transport)`).\n\n## \u003ca name='Usageexampleparentwindowtoiframe'\u003e\u003c/a\u003eUsage example (parent window to iframe)\n\nThis is a simplified example of an RPC connection between a parent window and an iframe.\n\n### \u003ca name='Iframescriptiframe.ts'\u003e\u003c/a\u003eIframe script (`iframe.ts`)\n\n```ts\nimport {\n  createIframeParentTransport,\n  createRPC,\n  createRPCRequestHandler,\n  type RPCSchema,\n} from \"rpc-anywhere\";\n\n// import the parent's (remote) schema\nimport { type ParentSchema } from \"./parent.js\";\n\n// handle incoming requests from the parent\nconst requestHandler = createRPCRequestHandler({\n  /** Greet a given target. */\n  greet: ({\n    name,\n  }: {\n    /** The target of the greeting. */\n    name: string;\n  }) =\u003e `Hello, ${name}!`, // respond to the parent\n});\n\n// create the iframe's schema\nexport type IframeSchema = RPCSchema\u003c\n  {\n    messages: {\n      buttonClicked: {\n        /** The button that was clicked. */\n        button: string;\n      };\n    };\n  },\n  // request types can be inferred from the handler\n  typeof requestHandler\n\u003e;\n\nasync function main() {\n  // create the iframe's RPC\n  const rpc = createRPC\u003cIframeSchema, ParentSchema\u003e({\n    // wait for a connection with the parent window and\n    // pass the transport to our RPC\n    transport: await createIframeParentTransport({ transportId: \"my-rpc\" }),\n    // provide the request handler\n    requestHandler,\n  });\n\n  // send a message to the parent\n  blueButton.addEventListener(\"click\", () =\u003e {\n    rpc.send.buttonClicked({ button: \"blue\" });\n  });\n\n  // listen for messages from the iframe\n  rpc.addMessageListener(\"userLoggedIn\", ({ name }) =\u003e {\n    console.log(`The user \"${name}\" logged in`);\n  });\n}\n\nmain();\n```\n\n### \u003ca name='Parentwindowscriptparent.ts'\u003e\u003c/a\u003eParent window script (`parent.ts`)\n\n```ts\nimport { createIframeTransport, createRPC, type RPCSchema } from \"rpc-anywhere\";\n\n// import the iframe's (remote) schema\nimport { type IframeSchema } from \"./iframe.js\";\n\n// create the parent window's schema\nexport type ParentSchema = RPCSchema\u003c{\n  messages: {\n    userLoggedIn: {\n      /** The user's name. */\n      name: string;\n    };\n  };\n}\u003e;\n\nasync function main() {\n  // create the parent window's RPC\n  const rpc = createRPC\u003cParentSchema, IframeSchema\u003e({\n    // wait for a connection with the iframe and\n    // pass the transport to our RPC\n    transport: await createIframeTransport(\n      document.getElementById(\"my-iframe\"),\n      { transportId: \"my-rpc\" },\n    ),\n  });\n\n  // use the proxy API as an alias ✨\n  const iframe = rpc.proxy;\n\n  // make a request to the iframe\n  const greeting = await iframe.request.greet({ name: \"world\" });\n  console.log(greeting); // Hello, world!\n\n  // send a message to the iframe\n  onUserLoggedIn((user) =\u003e iframe.send.userLoggedIn({ name: user.name }));\n\n  // listen for messages from the iframe\n  rpc.addMessageListener(\"buttonClicked\", ({ button }) =\u003e {\n    console.log(`The button \"${button}\" was clicked`);\n  });\n}\n\nmain();\n```\n\n## \u003ca name='Gettingstarted'\u003e\u003c/a\u003eGetting started\n\nAn RPC is a connection between two endpoints. In this connection, messages are exchanged in two ways:\n\n- **Requests:** messages sent expecting a response.\n- **Messages:** messages sent without expecting a response.\n\nRPC Anywhere is completely flexible, so there is no \"server\" or \"client\" in the traditional sense. Both endpoints can send or receive requests, responses and messages.\n\nLet's go through an example.\n\n### \u003ca name='Schemas'\u003e\u003c/a\u003eSchemas\n\nFirst, we define the requests and messages supported by each endpoint:\n\n```ts\nimport { type RPCSchema } from \"rpc-anywhere\";\n\ntype ChefSchema = RPCSchema\u003c{\n  requests: {\n    cook: {\n      params: { recipe: Recipe };\n      response: Dish;\n    };\n  };\n  messages: {\n    kitchenOpened: void;\n    kitchenClosed: { reason: string };\n  };\n}\u003e;\n\ntype ManagerSchema = RPCSchema\u003c{\n  requests: {\n    getIngredients: {\n      params: { neededIngredients: IngredientList };\n      response: Ingredient[];\n    };\n  };\n  messages: {\n    shiftStarted: void;\n    shiftEnded: void;\n    takingABreak: { reason: string; duration: number };\n  };\n}\u003e;\n```\n\n### \u003ca name='RPCinstances'\u003e\u003c/a\u003eRPC instances\n\nThen, we create each RPC instance:\n\n```ts\nimport { createRPC } from \"rpc-anywhere\";\n\n// chef-rpc.ts\nconst chefRpc = createRPC\u003cChefSchema, ManagerSchema\u003e({\n  transport: createRestaurantTransport(),\n});\n\n// manager-rpc.ts\nconst managerRpc = createRPC\u003cManagerSchema, ChefSchema\u003e({\n  transport: createRestaurantTransport(),\n});\n```\n\nSchema types are passed as type parameters to `createRPC`. The first one is the local schema, while the second one is the schema of the other endpoint (the \"remote\" schema).\n\nRPC Anywhere is transport-agnostic: you need to specify it. A transport provides the means to send and listen for messages to and from the other endpoint. A common real-world example is communicating with an iframe through `window.postMessage(message)` and `window.addEventListener('message', handler)`.\n\nYou can use [a built-in transport](./docs/2-built-in-transports.md), or [create your own](./docs/4-creating-a-custom-transport.md).\n\n### \u003ca name='Messages'\u003e\u003c/a\u003eMessages\n\nHere's how the chef could listen for incoming messages from the manager:\n\n```ts\n// chef-rpc.ts\nchefRpc.addMessageListener(\"takingABreak\", ({ duration, reason }) =\u003e {\n  console.log(\n    `The manager is taking a break for ${duration} minutes: ${reason}`,\n  );\n});\n```\n\nThe manager can then send a message to the chef:\n\n```ts\n// manager-rpc.ts\nmanagerRpc.send.takingABreak({ duration: 30, reason: \"lunch\" });\n```\n\nWhen the chef receives the message, the listener will be called, and the following will be logged:\n\n```\nThe manager is taking a break for 30 minutes: lunch\n```\n\n### \u003ca name='Requests'\u003e\u003c/a\u003eRequests\n\nTo handle incoming requests, we need to define a request handler:\n\n```ts\n// chef-rpc.ts\nconst chefRpc = createRPC\u003cChefSchema, ManagerSchema\u003e({\n  // ...\n  requestHandler: {\n    cook({ recipe }) {\n      return prepareDish(recipe, availableIngredients);\n    },\n  },\n});\n// ...\n```\n\nNow the chef RPC can respond to `cook` requests. Request handlers can be written in this \"object\" format or as a function (`requestHandler(method, params): response`). All functions that handle requests can be synchronous or asynchronous.\n\nTo make a request, there are two main options:\n\n```ts\n// manager-rpc.ts\n\n// using \".request()\"\nconst dish = await managerRpc.request(\"cook\", { recipe: \"pizza\" });\n// using the request proxy API\nconst dish = await managerRpc.request.cook({ recipe: \"pizza\" });\n```\n\nBoth are functionally equivalent.\n\n\u003e **Note:** requests can fail for various reasons, like an execution error, a missing method handler, or a timeout. Make sure to handle errors appropriately when making requests.\n\n## \u003ca name='Documentation'\u003e\u003c/a\u003eDocumentation\n\nThe documentation contains important details that are skipped or overly simplified in the examples above!\n\nStart with [RPC](./docs/1-rpc.md), then read about your transport of choice on the [Built-in transports](./docs/2-built-in-transports.md) page.\n\n- [RPC](./docs/1-rpc.md)\n- [Built-in transports](./docs/2-built-in-transports.md)\n- [Bridging transports](./docs/3-bridging-transports.md)\n- [Creating a custom transport](./docs/4-creating-a-custom-transport.md)\n\nThe API reference is available at [tsdocs.dev](https://tsdocs.dev/docs/rpc-anywhere/).\n\n**This package is published as both ESM and CommonJS.**\n\n## \u003ca name='Typesafetyandfeatures'\u003e\u003c/a\u003eType safety and features\n\nRPC Anywhere is designed to be as type-safe as possible while maintaining great ergonomics and flexibility. Here are some examples:\n\n- When making requests and sending messages, all data is strictly typed based on the schema types including request parameters, response data and message contents.\n- Similarly, all data involved in handling requests and listening to messages is strictly typed. For example, you can't return the wrong response from a request handler.\n- Most times, you'll get autocomplete suggestions in your IDE, like request or message names.\n- The proxy APIs for requests and messages are fully typed as well based on the schema types. This means you can't call a request or send a message that doesn't exist, or with the wrong data types.\n\nThis library goes to even greater lengths to ensure a smooth developer experience, for example:\n\n- It is possible to infer the request schema types from the runtime request handlers, which prevents duplication by having a single source of truth.\n- The features of an RPC instance are constrained based on the schema types. For example, if the remote schema doesn't declare any requests, the `request` method won't be available in the first place. Similarly, if the local schema doesn't declare any requests, you can't set a request handler or customize the maximum request time. This affects almost all of the methods and options!\n\nBesides these, many other minor type-related details make RPC Anywhere extremely type-safe and a joy to work with.\n\n## \u003ca name='Featuresunderconsideration'\u003e\u003c/a\u003eFeatures under consideration\n\nIf you need any of these, please [file a feature request](https://github.com/DaniGuardiola/rpc-anywhere/issues/new?assignees=\u0026labels=enhancement\u0026projects=\u0026template=feature-request.yaml) or upvote an existing one! 😄\n\n- Transport: Electron ipcMain/ipcRenderer.\n- Transport: WebSockets.\n- Transport: service workers (this is already possible through the message port transport, but it's a low-level API).\n- Transport: WebRTC.\n- Transport: HTTP(S) requests.\n- Transport: UDP.\n- Many-to-one or many-to-many connections.\n- Improved type-safety in general handlers, i.e. the function form of request handlers, the fallback request handler, and the wildcard message handler.\n- A simplified way to wait for connections to be established in any context, like across a chain of bridged transports.\n- Runtime validation support (e.g. through zod or valibot).\n- Better error handling.\n- Support for transferable objects in transports that support it (e.g. workers).\n- Lite version with a much smaller footprint.\n- [File a feature request!](https://github.com/DaniGuardiola/rpc-anywhere/issues/new?assignees=\u0026labels=enhancement\u0026projects=\u0026template=feature-request.yaml)\n\n## \u003ca name='Priorart'\u003e\u003c/a\u003ePrior art\n\nRPC Anywhere is inspired by [JSON-RPC](https://www.jsonrpc.org/), with a few small differences.\n\nFor example, the concept of \"messages\" in RPC Anywhere resembles \"notifications\" in JSON-RPC. Some implementation details (like using an `id` property in requests and responses) are also similar.\n\nA notable difference is that RPC Anywhere is completely flexible, while JSON-RPC is client-server oriented.\n\n## \u003ca name='Contributing'\u003e\u003c/a\u003eContributing\n\nContributions are welcome! Please make sure to create or update any tests as necessary when submitting a pull request.\n\nThe demo is useful for quick manual testing. To start it locally, run `bun demo` and open the local server's address in your browser (probably `localhost:8080`, check the console output). It will automatically reload when you make changes to the source code.\n\nBefore making big changes, consider opening a discussion first to get feedback and make sure the change is aligned with the project's goals.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdaniguardiola%2Frpc-anywhere","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdaniguardiola%2Frpc-anywhere","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdaniguardiola%2Frpc-anywhere/lists"}