kkrpc

February 6, 2026 · View on GitHub

GitHub Docs Ask DeepWiki NPM Version JSR Version Downloads GitHub stars Typedoc Documentation Excalidraw Diagrams LLM Docs

kkRPC Banner

TypeScript-first RPC for runtimes, processes, windows, workers, desktop IPC, and message buses.

kkrpc lets two endpoints call each other through type-safe proxy objects. Expose an API on one side, wrap a transport on the other side, and call remote functions, nested methods, properties, constructors, and callback arguments like local code.

The stable API is built on native Transport<RPCMessage> objects. The main kkrpc entry is browser-safe and feature-light; runtime transports and optional peer dependencies live behind subpath exports.

Why kkrpc

FeaturekkrpctRPCComlink
Type-safe remote callsYesYesYes
Bidirectional APIsYesNoYes
Browser-safe core entryYesMostly HTTP app focusedBrowser focused
Node.js, Deno, Bun stdioYesNoNo
WebSocket and HTTPYesHTTP focusedNo
Workers and iframesYesNoYes
Electron, Tauri, Chrome extensionYesNoNo
Message busesRabbitMQ, Kafka, Redis Streams, NATSNoNo
Callback argumentsEvented transportsNoYes
Optional runtime validationStandard SchemaZod-orientedNo
Middleware/interceptorsYesYesNo
Transferable objectsWhere supportedNoYes
Code generation requiredNoNoNo

Choose kkrpc when the hard part is not just an HTTP API. It is designed for cross-runtime systems: browser to worker, renderer to main process, app to plugin host, web server to child process, or one service connected to another through a message bus.

Bundle Size

The core bundle is tracked with a Bun-based benchmark against equivalent comctx and comlink samples. See the bundle-size benchmark example for the latest raw, gzip, brotli, and feature-scope comparison.

Install

npm install kkrpc
pnpm add kkrpc
bun add kkrpc

For Deno and JSR:

deno add jsr:@kunkun/kkrpc

Install optional peer dependencies only for the transports you use, such as ws, hono, elysia, socket.io, amqplib, kafkajs, ioredis, or @nats-io/transport-node.

Quick Start

import { expose, wrap } from "kkrpc"

const controller = expose(localAPI, serverTransport)
const remote = wrap<RemoteAPI>(clientTransport)

await remote.ping()
controller.dispose()

Use RPCChannel when both sides expose APIs or when explicit channel ownership is useful:

import { RPCChannel } from "kkrpc"

const channel = new RPCChannel<LocalAPI, RemoteAPI>(transport, { expose: localAPI })
const remote = channel.getAPI()

await remote.math.add(1, 2)
channel.destroy()

RPCChannel.getAPI() is typed from the channel generic. For one-way clients, wrap<RemoteAPI>(transport) is the shortest path.

WebSocket Example

import { expose } from "kkrpc"
import { webSocketTransport } from "kkrpc/ws"
import { WebSocketServer } from "ws"

const api = {
	math: {
		add(a: number, b: number) {
			return a + b
		}
	},
	async greet(name: string) {
		return `Hello, ${name}`
	}
}

export type API = typeof api

const wss = new WebSocketServer({ port: 3000 })

wss.on("connection", (socket) => {
	expose(api, webSocketTransport(socket))
})
import { wrap } from "kkrpc"
import { webSocketClientTransport } from "kkrpc/ws"
import type { API } from "./server"

const api = wrap<API>(webSocketClientTransport({ url: "ws://localhost:3000" }))

console.log(await api.greet("World"))
console.log(await api.math.add(1, 2))

Worker Example

Main thread:

import { wrap } from "kkrpc"
import { workerTransport } from "kkrpc/worker"
import type { WorkerAPI } from "./worker"

const worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module" })
const api = wrap<WorkerAPI>(workerTransport(worker))

console.log(await api.ping())

Worker:

import { expose } from "kkrpc"
import { workerSelfTransport } from "kkrpc/worker"

const api = {
	async ping() {
		return "pong"
	}
}

export type WorkerAPI = typeof api

expose(api, workerSelfTransport())

HTTP Example

HTTP is unary request/response. It is useful for normal web APIs, but it cannot carry callback arguments and the server cannot initiate calls back to the client.

import { createHttpHandler } from "kkrpc/http"

const api = {
	async add(a: number, b: number) {
		return a + b
	}
}

export type API = typeof api

const handler = createHttpHandler(api)

Bun.serve({
	port: 3000,
	fetch(request) {
		const url = new URL(request.url)
		if (url.pathname === "/rpc") return handler(request)
		return new Response("Not found", { status: 404 })
	}
})
import { wrap } from "kkrpc"
import { httpClientTransport } from "kkrpc/http"
import type { API } from "./server"

const api = wrap<API>(httpClientTransport({ url: "http://localhost:3000/rpc" }))

console.log(await api.add(2, 3))

Stdio Example

Stdio transports are useful for plugin hosts, subprocess workers, command-line tools, and language interop tests.

import { expose } from "kkrpc"
import { nodeStdioTransport } from "kkrpc/stdio"

const api = {
	async version() {
		return process.version
	}
}

export type ChildAPI = typeof api

expose(api, nodeStdioTransport({ readable: process.stdin, writable: process.stdout }))
import { spawn } from "node:child_process"
import { wrap } from "kkrpc"
import { nodeStdioTransport } from "kkrpc/stdio"
import type { ChildAPI } from "./child"

const child = spawn("node", ["child.js"])
const api = wrap<ChildAPI>(nodeStdioTransport({ readable: child.stdout, writable: child.stdin }))

console.log(await api.version())

Property Access

Remote proxies can read and write exposed properties. Remote reads are asynchronous.

interface SettingsAPI {
	counter: number
	settings: {
		theme: string
	}
}

const api = wrap<SettingsAPI>(transport)

console.log(await api.counter)
console.log(await api.settings.theme)

api.counter = 42
api.settings.theme = "dark"

Callback Arguments

Evented transports can pass callback arguments by reference. The channel stores the callback locally and sends a callback marker to the remote endpoint.

type RemoteAPI = {
	onProgress(taskId: string, callback: (percent: number) => void): Promise<void>
}

const api = wrap<RemoteAPI>(transport)

await api.onProgress("build", (percent) => {
	console.log(`Progress: ${percent}%`)
})

Async Iterable Streaming

Bidirectional transports can stream async iterable results with windowed pull backpressure. The remote caller can consume an async generator directly with for await; kkrpc grants bounded credit to the producer and breaking early calls return() on the source iterator.

type RemoteAPI = {
	tailLogs(service: string): AsyncIterable<string>
}

for await (const line of api.tailLogs("worker")) {
	console.log(line)
	if (line.includes("ready")) break
}

Async iterables can also be passed as top-level method arguments. HTTP remains unary request/response and does not support async iterable streams.

Transferable Objects

Use transfer() when a transport supports zero-copy ownership transfer, such as Web Workers.

import { transfer } from "kkrpc"

const buffer = new ArrayBuffer(1024 * 1024)
await api.processBuffer(transfer(buffer, [buffer]))

The transport advertises support through its capabilities. Unsupported transports fall back to normal serialization behavior.

Validation

Runtime validation is opt-in and uses Standard Schema compatible validators such as Zod, Valibot, and ArkType.

import { expose } from "kkrpc"
import { defineAPI, defineMethod, extractValidators, validationPlugin } from "kkrpc/validation"
import { z } from "zod"

const api = defineAPI({
	add: defineMethod(
		{ input: z.tuple([z.number(), z.number()]), output: z.number() },
		async (a, b) => a + b
	)
})

export type API = typeof api

expose(api, transport, {
	plugins: [validationPlugin(extractValidators(api))]
})

Middleware

Middleware wraps local handler execution with an onion-style interceptor chain.

import { expose } from "kkrpc"
import { middlewarePlugin, type RPCInterceptor } from "kkrpc/middleware"

const logger: RPCInterceptor = async (ctx, next) => {
	console.log("rpc:start", ctx.method, ctx.args)
	const result = await next()
	console.log("rpc:end", ctx.method)
	return result
}

expose(api, transport, {
	plugins: [middlewarePlugin([logger])]
})

SuperJSON

The core protocol is JSON-compatible. Use the SuperJSON codec when you need richer values such as Date, Map, Set, or BigInt on transports created through createTransport().

import { superJsonCodec } from "kkrpc/superjson"
import { createTransport } from "kkrpc/transport"

const transport = createTransport({ platform, codec: superJsonCodec() })

Relay

relayTransport() forwards messages between two native transports without knowing either side's API. This is useful when a process is only a bridge.

import { relayTransport } from "kkrpc/relay"

const relay = relayTransport(rendererTransport, workerTransport)

relay.dispose()

Inspector

Inspector plugins observe requests, responses, and errors without changing the API implementation.

import { RPCChannel } from "kkrpc"
import { createInspector, MemoryBackend } from "kkrpc/inspector"

const backend = new MemoryBackend()
const inspector = createInspector({ backends: [backend] })
const channel = new RPCChannel(transport, { expose: api, plugins: [inspector.plugin("server")] })

Supported Transports

TransportEntryNotes
Web Workerkkrpc/workerMain-thread and worker-global helpers
stdiokkrpc/stdioNode.js, Deno, and Bun process pipes
HTTPkkrpc/httpUnary request/response only
WebSocketkkrpc/wsBidirectional socket transport
Hono WebSocketkkrpc/ws/honoFramework handler for Hono
Elysia WebSocketkkrpc/ws/elysiaFramework handler for Elysia
iframekkrpc/iframeParent/child postMessage transport
Chrome extensionkkrpc/chrome-extensionchrome.runtime.Port transport
Electronkkrpc/electronIPC endpoint and utility process transports
Taurikkrpc/tauriTauri event and shell process transports
Socket.IOkkrpc/socketioSocket.IO-backed event transport
RabbitMQkkrpc/rabbitmqAMQP transport
Kafkakkrpc/kafkaKafka topic transport
Redis Streamskkrpc/redis-streamsRedis stream transport
NATSkkrpc/natsNATS subject transport

Entry Points

EntryPurpose
kkrpcStable browser-safe core
kkrpc/browserExplicit browser-safe core entry
kkrpc/denoDeno-friendly core entry
kkrpc/transportTransport composition primitives
kkrpc/codecsBuilt-in JSON/object codecs
kkrpc/pluginsPlugin types and helpers
kkrpc/workerWeb Worker transports
kkrpc/stdioNode/Deno/Bun stdio transports
kkrpc/httpHTTP client and handler helpers
kkrpc/wsWebSocket transports
kkrpc/ws/honoHono WebSocket integration
kkrpc/ws/elysiaElysia WebSocket integration
kkrpc/iframeiframe transports
kkrpc/chrome-extensionChrome extension port transports
kkrpc/electronElectron transports
kkrpc/tauriTauri transports
kkrpc/socketioSocket.IO transports
kkrpc/rabbitmqRabbitMQ transports
kkrpc/kafkaKafka transports
kkrpc/redis-streamsRedis Streams transports
kkrpc/natsNATS transports
kkrpc/validationStandard Schema validation plugin
kkrpc/middlewareMiddleware plugin
kkrpc/superjsonSuperJSON codec
kkrpc/relayTransport relay helper
kkrpc/inspectorNative inspector helpers
PlatformPackageLink
npmkkrpchttps://www.npmjs.com/package/kkrpc
JSR@kunkun/kkrpchttps://jsr.io/@kunkun/kkrpc
DocumentationStarlight docshttps://docs.kkrpc.kunkun.sh/
API referenceTypedochttps://kunkunsh.github.io/kkrpc/
ExamplesSource exampleshttps://github.com/kunkunsh/kkrpc/tree/main/examples

Migration

The stable native API removed the classic IoInterface adapter model and the temporary next entries. Use native Transport<RPCMessage> factories from the subpath exports listed above.

See the 0.7.x to 1.0 migration guide for detailed migration notes. AI coding assistants can also use skills/kkrpc-migration.

License

MIT © kunkunsh