Skip to content

Migrate from 0.7.x to 1.0

kkrpc 1.0 is a breaking rewrite of the public API. The stable package now uses native Transport<RPCMessage> objects everywhere. The old classic adapter model based on IoInterface, IoMessage, and public *IO classes is not preserved.

This guide is for applications currently using kkrpc 0.7.x or the temporary vNext entries and moving to kkrpc 1.0.

The 1.0 API promotes the previous native architecture into the stable kkrpc package entry.

Major changes:

  • kkrpc now exports the stable native core: RPCChannel, wrap(), expose(), dispose(), transfer(), and core protocol/transport/plugin types.
  • Runtime transports live behind subpath exports such as kkrpc/worker, kkrpc/stdio, kkrpc/http, kkrpc/ws, and kkrpc/electron.
  • Optional integrations and peer dependencies are no longer pulled through the main entry.
  • Validation and middleware are plugins, not top-level classic channel options.
  • Request metadata for tracing and logging is configured with getMetadata and read from plugin or middleware ctx.meta.
  • SuperJSON is an opt-in codec feature, not a core dependency.
  • Async iterable streaming and request/response remote references are opt-in entries. See Migrate from v0.1.0 to v0.2.0 for the feature split.
  • HTTP is explicitly unary request/response. Use WebSocket or another evented transport for bidirectional calls and callback arguments.
  • Temporary kkrpc/next entries were removed because the native API is now stable.
  1. Upgrade the package to kkrpc@1.
  2. Replace old imports with stable imports from kkrpc and transport subpaths.
  3. Replace classic IO adapters with native transport factories.
  4. Use wrap() for client-only proxies and expose() for server-only APIs.
  5. Use new RPCChannel(transport, { expose }) when both sides expose APIs.
  6. Replace validators and interceptors channel options with validationPlugin() and middlewarePlugin().
  7. Move trace, logging, or activity context to request metadata. See Request Metadata.
  8. Move SuperJSON usage to kkrpc/superjson and compose it through createTransport() only where needed.
  9. Remove temporary kkrpc/next, classic-compat, next/io, browser-lite, browser-mini, and electron-ipc imports.
  10. Run type checks and runtime tests for every migrated transport boundary.

These names and entries should not remain in 1.0 applications.

RemovedUse instead
IoInterface, IoMessageTransport<RPCMessage>
Public *IO adapter classesNative transport factories, such as workerTransport() or webSocketTransport()
Classic validators optionvalidationPlugin() from kkrpc/validation
Classic interceptors optionmiddlewarePlugin() from kkrpc/middleware
RPCValidators, classic validation helpersdefineMethod(), defineAPI(), extractValidators(), validationPlugin()
RPCInterceptor from the old APIMiddlewareHandler from kkrpc/middleware
kkrpc/next and kkrpc/next/*Stable kkrpc and stable subpaths
kkrpc/next/classic-compatNative plugins and options
kkrpc/next/ioNative transport implementations
kkrpc/browser-litekkrpc or kkrpc/browser
kkrpc/browser-minikkrpc or kkrpc/browser
kkrpc/electron-ipckkrpc/electron

Do not add compatibility bridges in new 1.0 code. If an old custom adapter still exists, migrate the adapter itself to Transport<RPCMessage>.

The main entry is intentionally small and browser-safe. Runtime-specific code and optional peers live behind subpaths.

EntryPurpose
kkrpcStable core: RPCChannel, wrap, expose, dispose, transfer, core types
kkrpc/browserExplicit browser-safe core entry
kkrpc/denoDeno-friendly core entry
kkrpc/transportTransport, Platform, Codec, createTransport()
kkrpc/codecsBuilt-in object, JSON, and JSON-line codecs
kkrpc/pluginsCore plugin types and helpers
kkrpc/validationStandard Schema validation plugin and schema helpers
kkrpc/middlewareInterceptor middleware plugin
kkrpc/superjsonSuperJSON codec helpers
kkrpc/streamingOpt-in async iterable streaming channel
kkrpc/remote-refsOpt-in explicit proxy() remote references
kkrpc/workerWeb Worker transports
kkrpc/stdioNode.js, Deno, and Bun stdio transports
kkrpc/httpHTTP client transport and request handler
kkrpc/wsPlain WebSocket transports
kkrpc/ws/honoHono WebSocket integration
kkrpc/ws/elysiaElysia WebSocket integration
kkrpc/iframeiframe postMessage transports
kkrpc/chrome-extensionChrome extension port transport
kkrpc/electronElectron IPC and utility process transports
kkrpc/tauriTauri shell stdio transport
kkrpc/socketioSocket.IO transport
kkrpc/rabbitmqRabbitMQ transport
kkrpc/kafkaKafka transport
kkrpc/redis-streamsRedis Streams transport
kkrpc/natsNATS transport
kkrpc/relayTransport relay helper
kkrpc/inspectorNative inspector helpers

In 1.0, the common client path is wrap(remoteTransport).

1.0 client
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"))

Keep the proxy and call dispose(api) when the client lifetime ends.

import { dispose, wrap } from "kkrpc"
const api = wrap<API>(transport)
dispose(api)

Expose a local object with expose(api, transport). The returned controller owns the channel.

1.0 server
import { expose } from "kkrpc"
import { webSocketTransport } from "kkrpc/ws"
const controller = expose(api, webSocketTransport(socket))
controller.dispose()

Use RPCChannel directly 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.ping()
channel.destroy()

Type parameters are ordered as local API first, remote API second.

The old adapter layer exposed IO-like objects. The new layer exposes Transport<RPCMessage> factories. Pass the native transport directly into wrap(), expose(), or RPCChannel.

Use workerTransport() on the parent side and workerSelfTransport() inside the worker.

main.ts
import { wrap } from "kkrpc"
import { workerTransport } from "kkrpc/worker"
const worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module" })
const api = wrap<WorkerAPI>(workerTransport(worker))
worker.ts
import { expose } from "kkrpc"
import { workerSelfTransport } from "kkrpc/worker"
const api = {
async ping() {
return "pong"
}
}
export type WorkerAPI = typeof api
expose(api, workerSelfTransport())

Worker transports can advertise transferable support. Use transfer(value, transferables) only when the underlying platform supports ownership transfer.

Use nodeStdioTransport(), denoStdioTransport(), bunStdioTransport(), or stdioJsonTransport() from kkrpc/stdio.

child.ts
import { expose } from "kkrpc"
import { nodeStdioTransport } from "kkrpc/stdio"
expose(api, nodeStdioTransport({ readable: process.stdin, writable: process.stdout }))
parent.ts
import { spawn } from "node:child_process"
import { wrap } from "kkrpc"
import { nodeStdioTransport } from "kkrpc/stdio"
const child = spawn("node", ["child.js"])
const api = wrap<ChildAPI>(nodeStdioTransport({ readable: child.stdout, writable: child.stdin }))

Stdio transports use newline-delimited JSON. Do not reuse old blocking read()/write() IO classes.

HTTP in 1.0 is unary request/response only. It is appropriate for normal client-initiated value calls, but not server-initiated calls, callback arguments, async iterable streams, remote-reference handles, or raw function values.

server.ts
import { createHttpHandler } from "kkrpc/http"
const handler = createHttpHandler(api)
Bun.serve({
fetch(request) {
return new URL(request.url).pathname === "/rpc"
? handler(request)
: new Response("Not found", { status: 404 })
}
})
client.ts
import { wrap } from "kkrpc"
import { httpClientTransport } from "kkrpc/http"
const api = wrap<API>(httpClientTransport({ url: "http://localhost:3000/rpc" }))

If old HTTP code depended on callbacks, subscriptions, streaming progress, remote references, or server pushes, migrate that boundary to WebSocket or another evented transport instead.

Use webSocketTransport(socket) for accepted sockets and webSocketClientTransport({ url }) for clients.

import { expose, wrap } from "kkrpc"
import { webSocketClientTransport, webSocketTransport } from "kkrpc/ws"

Framework helpers moved to dedicated subpaths:

  • Hono: createHonoWebSocketHandler() and honoWebSocketTransport() from kkrpc/ws/hono.
  • Elysia: createElysiaWebSocketHandler() and elysiaWebSocketTransport() from kkrpc/ws/elysia.

Use iframeParentTransport(), iframeChildTransport(), and readiness helpers from kkrpc/iframe. Use chromePortTransport() from kkrpc/chrome-extension for chrome.runtime.Port boundaries.

These transports use message events. Keep origin checks and extension permission boundaries in application code.

Use kkrpc/electron, not kkrpc/electron-ipc.

Available helpers include:

  • electronIpcTransport() for endpoint-like IPC messaging.
  • electronUtilityProcessTransport() for parent-side utility process communication.
  • electronUtilityProcessChildTransport() for child-side utility process communication.
  • createSecureIpcBridge() for preload-safe bridge construction.

Do not import Electron-specific helpers from the main kkrpc entry.

Use tauriShellStdioTransport() from kkrpc/tauri for Tauri shell child process communication. Keep Tauri plugin dependencies behind this subpath.

Use socketIoTransport(socket) from kkrpc/socketio. Socket.IO remains separate from kkrpc/ws because it uses Socket.IO-specific event semantics and peer dependencies.

Use the native message-bus transports from their dedicated subpaths:

System1.0 helper
RabbitMQrabbitMqTransport() from kkrpc/rabbitmq
KafkakafkaTransport() from kkrpc/kafka
Redis StreamsredisStreamsTransport() from kkrpc/redis-streams
NATSnatsTransport() from kkrpc/nats

Message-bus transports use envelope metadata for peer identity and routing. They may provide at-least-once delivery depending on the broker. Do not assume exactly-once execution unless your application protocol handles idempotency. Configure remotePeerId for point-to-point streaming or remote-reference APIs; broadcast mode is for fan-out messages and does not advertise remote-reference support.

Validation is now an explicit plugin. It accepts Standard Schema-compatible validators, including Zod, Valibot, and ArkType schemas.

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
)
})
expose(api, transport, {
plugins: [validationPlugin(extractValidators(api))]
})

If old code passed validators directly to a classic channel, move those schemas into a validator map or define the API with defineMethod().

Middleware is also a plugin.

import { expose } from "kkrpc"
import { middlewarePlugin, type MiddlewareHandler } from "kkrpc/middleware"
const logger: MiddlewareHandler = async (ctx, next) => {
console.log("rpc:start", ctx.method)
const result = await next()
console.log("rpc:end", ctx.method)
return result
}
expose(api, transport, {
plugins: [middlewarePlugin([logger])]
})

If old code used classic interceptors, migrate each interceptor to the new MiddlewareHandler context and install it with middlewarePlugin().

If your 0.7 code used getMetadata or interceptor ctx.meta for tracing, activity IDs, or logging correlation, keep that context as request metadata in 1.0. The option remains getMetadata, but it now belongs to the native wrap(), expose(), or RPCChannel options.

import { wrap } from "kkrpc"
const api = wrap<API>(transport, {
getMetadata: () => ({
traceparent: currentTraceparent(),
requestId: currentRequestId(),
sessionId: currentSessionId()
})
})

Receive-side plugins and middleware read the metadata from ctx.meta.

import { middlewarePlugin, type MiddlewareHandler } from "kkrpc/middleware"
const logger: MiddlewareHandler = async (ctx, next) => {
console.log("rpc", ctx.method, ctx.meta?.requestId)
return await next()
}
const plugins = [middlewarePlugin([logger])]

See Request Metadata for tracing, logging, and kunkun migration examples.

SuperJSON is opt-in through kkrpc/superjson and transport composition.

import { createTransport } from "kkrpc/transport"
import { superJsonCodec } from "kkrpc/superjson"
const transport = createTransport({ platform, codec: superJsonCodec() })

Do not import SuperJSON helpers from kkrpc. This keeps the core bundle small and avoids pulling SuperJSON into applications that do not need it.

The stable wire protocol uses compact JSON-compatible records:

{ "t": "q", "id": "request-id", "op": "call", "p": ["math", "add"], "a": [1, 2] }

Responses use t: "r"; callback invocations use t: "cb". Async iterable streams use t: "sq" credit/control requests and t: "sr" data/completion/error responses. Explicit remote references use op: "ref" requests inside kkrpc/remote-refs; default-core endpoints reject those operations with an opt-in error. Non-TypeScript implementations should use the language interop references and the skills/interop guide.

ProblemFix
Importing runtime transports from kkrpcImport from runtime subpaths such as kkrpc/ws or kkrpc/electron
Leaving kkrpc/next imports in codeReplace them with kkrpc and stable subpaths
Migrating HTTP code that used callbacksUse WebSocket or another evented transport
Assuming wrap() disposes automaticallyKeep the proxy and call dispose(proxy) when the lifetime ends
Pulling SuperJSON into every bundleUse kkrpc/superjson only at boundaries that need richer values
Preserving old *IO class names in wrappersRename wrappers around native Transport<RPCMessage> factories
Treating transferables as universalCheck transport capabilities and use transfer() only where supported

For this repository, run:

Terminal window
pnpm --filter kkrpc check-types
pnpm --filter kkrpc test
pnpm --filter "./examples/*" check-types

For downstream projects, run the equivalent TypeScript type check, unit tests, and at least one integration test for each migrated transport boundary.

Search for old API remnants before finishing:

Terminal window
rg 'kkrpc/next|next/io|classic-compat|IoInterface|IoMessage|RPCValidators|kkrpc/browser-lite|kkrpc/browser-mini|kkrpc/electron-ipc'
rg '[A-Za-z0-9_]+IO\b'

The second search can produce false positives for application names. Review each match and remove old public kkrpc adapter usage.