Skip to content
oRPC
Esc
navigateopen⌘Jpreview

Changelog

v2.0.0-beta.24

August 3, 2026

Prerelease

   🚀 Features

   🐞 Bug Fixes

   🏎 Performance

    View changes on GitHub
v2.0.0-beta.22

July 29, 2026

Prerelease

   🚀 Features

   🐞 Bug Fixes

   🏎 Performance

    View changes on GitHub
v2.0.0-beta.20

July 25, 2026

Prerelease

   🚀 Features

    View changes on GitHub
v2.0.0-beta.19

July 24, 2026

Prerelease

   🚨 Breaking Changes

   🚀 Features

   🐞 Bug Fixes

    View changes on GitHub
v2.0.0-beta.17

July 14, 2026

Prerelease

   🚀 Features

   🐞 Bug Fixes

    View changes on GitHub
v2.0.0-beta.16

July 10, 2026

Prerelease

   🚨 Breaking Changes

   🚀 Features

   🐞 Bug Fixes

    View changes on GitHub
v1.13.0

December 18, 2025

Release

Smart Coercion now stable docs

Automatically converts input values to match schema types without manually defining coercion logic.

import { OpenAPIHandler } from '@orpc/openapi/fetch'
import { SmartCoercionPlugin } from '@orpc/json-schema'

const handler = new OpenAPIHandler(router, {
  plugins: [
    new SmartCoercionPlugin({
      schemaConverters: [
        new ZodToJsonSchemaConverter(),
        // Add other schema converters as needed
      ],
    })
  ]
})

Rethrow handler plugin docs

The RethrowHandlerPlugin allows you to catch and rethrow specific errors that occur during request handling. This is particularly useful when your framework has its own error handling mechanism (e.g., global exception filters in NestJS, error middleware in Express) and you want certain errors to be processed by that mechanism instead of being handled by the oRPC error handling flow.

import {
  experimental_RethrowHandlerPlugin as RethrowHandlerPlugin,
} from '@orpc/server/plugins'

const handler = new RPCHandler(router, {
  plugins: [
    new RethrowHandlerPlugin({
      // Decide which errors should be rethrown.
      filter: (error) => {
        // Example: Rethrow all non-ORPCError errors
        // This allows unhandled exceptions to bubble up to your framework
        return !(error instanceof ORPCError)
      },
    }),
  ],
})

.$input now available in contract builder docs

Unlike .input, the .$input method lets you redefine the input schema after its initial configuration. This is useful when you need to enforce a void input when no .input is specified.

const base = os.$input(z.void())
const base = os.$input<Schema<void, unknown>>()

   🚀 Features

   🐞 Bug Fixes

    View changes on GitHub
v1.12.0

November 30, 2025

Release

Tanstack Query Default Options docs

You can configure default options for all query/mutation utilities using experimental_defaults. These options are spread merged with user-provided options, allowing you to set defaults while still enabling customization on a per-call basis.

const orpc = createTanstackQueryUtils(client, {
  experimental_defaults: {
    planet: {
      find: {
        queryOptions: {
          staleTime: 60 * 1000, // 1 minute
          retry: 3,
        },
      },
      list: {
        infiniteOptions: {
          staleTime: 30 * 1000,
        },
      },
      create: {
        mutationOptions: {
          onSuccess: (output, input, _, ctx) => {
            ctx.client.invalidateQueries({ queryKey: orpc.planet.key() })
          },
        },
      },
    },
  },
})

// These will automatically use the default options
const query = useQuery(orpc.planet.find.queryOptions({ input: { id: 123 } }))
const mutation = useMutation(orpc.planet.create.mutationOptions())

// User-provided options override defaults
const customQuery = useQuery(orpc.planet.find.queryOptions({
  input: { id: 123 },
  staleTime: 0, // overrides the default
}))

Cloudflare Durable Object Publisher Adapter docs

Building real-time features on Cloudflare Workers has never been easier, thanks to our publisher helpers:

import { DurablePublisher, PublisherDurableObject } from '@orpc/experimental-publisher-durable-object'

export class PublisherDO extends PublisherDurableObject {
  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env, {
      resume: {
        retentionSeconds: 60 * 2, // Retain events for 2 minutes to support resume
      },
    })
  }
}

export default {
  async fetch(request, env) {
    const publisher = new DurablePublisher<{
      'something-updated': {
        id: string
      }
    }>(env.PUBLISHER_DO, {
      prefix: 'publisher1', // avoid conflict with other keys
      customJsonSerializers: [] // optional custom serializers
    })
  },
}

[!NOTE] Full example at Cloudflare Worker Playground

NestJS Integration Upgraded docs

NestJS Integration now supports oRPC plugins, custom error responses, custom send-response behavior, and global context type-safe.

declare module '@orpc/nest' {
  /**
   * Extend oRPC global context to make it type-safe inside your handlers/middlewares
   */
  interface ORPCGlobalContext {
    request: Request
  }
}

@Module({
  imports: [
    ORPCModule.forRootAsync({ // or use .forRoot for static config
      useFactory: (request: Request) => ({
        interceptors: [
          onError((error) => {
            console.error(error)
          }),
        ],
        context: { request }, // oRPC context, accessible from middlewares, etc.
        eventIteratorKeepAliveInterval: 5000, // 5 seconds
        customJsonSerializers: [],
        plugins: [
          new SmartCoercionPlugin({
            schemaConverters: [
              new ZodToJsonSchemaConverter(),
            ],
          }),
        ], // almost oRPC plugins are compatible
      }),
      inject: [REQUEST],
    }),
  ],
  controllers: [AuthController, PlanetController, ReferenceController, OtherController],
  providers: [PlanetService, ReferenceService],
})

   🚀 Features

   🐞 Bug Fixes

[!TIP] If you find oRPC valuable and would like to support its development, you can do so here.

    View changes on GitHub
v1.11.3

November 17, 2025

Release

Cloudflare Worker Ratelimit Adapter docs

Adapter for Cloudflare Workers Ratelimit.

import { CloudflareRatelimiter } from '@orpc/experimental-ratelimit/cloudflare-ratelimit'

export default {
  async fetch(request, env) {
    const limiter = new CloudflareRatelimiter(env.MY_RATE_LIMITER)

    return new Response(`Hello World!`)
  }
}

   🚀 Features

    View changes on GitHub
v1.11.2

November 12, 2025

Release

   🚀 Features

   🐞 Bug Fixes

    View changes on GitHub

[!TIP] If you find oRPC valuable and would like to support its development, you can do so here.

v1.11.0

November 9, 2025

Release

Pino Logging Integration docs

Easy add structured logging, request tracking, and error monitoring to your oRPC powered by Pino

const logger = pino()

const handler = new RPCHandler(router, {
  plugins: [
    new LoggingHandlerPlugin({
      logger, // Custom logger instance
      generateId: ({ request }) => crypto.randomUUID(), // Custom ID generator
      logRequestResponse: true, // Log request start/end (disabled by default)
      logRequestAbort: true, // Log when requests are aborted (disabled by default)
    }),
  ],
})

Ratelimit Helpers docs

The Rate Limit package provides flexible rate limiting for oRPC with multiple storage backend support. It includes adapters for in-memory, Redis, and Upstash, along with middleware and plugin helpers for seamless integration.

Ratelimiter

const ratelimiter = new MemoryRatelimiter({
  maxRequests: 10,
  window: 60000,
})

Manually usage

const result = await limiter.limit('user:123')

if (!result.success) {
  throw new ORPCError('TOO_MANY_REQUESTS', {
    data: {
      limit: result.limit,
      remaining: result.remaining,
      reset: result.reset,
    },
  })
}

Built-in middleware

const loginProcedure = os
  .$context<{ ratelimiter: Ratelimiter }>()
  .input(z.object({ email: z.email() }))
  .use(
    createRatelimitMiddleware({
      limiter: ({ context }) => context.ratelimiter,
      key: ({ context }, input) => `login:${input.email}`,
    }),
  )
  .handler(({ input }) => {
    return { success: true }
  })


const result = await call(
  loginProcedure,
  { email: 'user@example.com' },
  { context: { ratelimiter } }
)

Response Header Plugin

import { RatelimitHandlerPlugin } from '@orpc/experimental-ratelimit'

const handler = new RPCHandler(router, {
  plugins: [
    new RatelimitHandlerPlugin(),
  ],
})

Retry After Plugin docs

The Retry After Plugin automatically retries requests based on server Retry-After headers. This is particularly useful for handling rate limiting and temporary server unavailability.

import { RetryAfterPlugin } from '@orpc/client/plugins'

const link = new RPCLink({
  url: 'http://localhost:3000/rpc',
  plugins: [
    new RetryAfterPlugin({
      condition: (response, options) => {
        // Override condition to determine if a request should be retried
        return response.status === 429 || response.status === 503
      },
      maxAttempts: 5, // Maximum retry attempts
      timeout: 5 * 60 * 1000, // Maximum time to spend retrying (ms)
    }),
  ],
})

Expand support union/interaction in openapi generator

Now you can use union/interaction for define params, query, headers, …

const procedure = os
   .route({ path: '/{type}' })
   .input(z.discriminatedUnion('type', [
      z.object({
        type: z.literal("a"),
        foo: z.number().int().positive(),
      }),
      z.object({
        type: z.literal("b"),
        foo: z.number().int().negative(),
      }),
    ]))

   🚀 Features

[!TIP] If you find oRPC valuable and would like to support its development, you can do so here.

    View changes on GitHub
v1.10.3

November 2, 2025

Release

AI SDK implementTool & createTool helpers

Implement/Convert a procedure/contract -> AI SDK Tool

const getWeatherTool = implementTool(getWeatherContract, {
  execute: async ({ location }) => ({
    location,
    temperature: 72 + Math.floor(Math.random() * 21) - 10,
  }),
})

const getWeatherTool = createTool(getWeatherProcedure, {
  context: {}, // provide initial context if needed
})

   🚀 Features

   🐞 Bug Fixes

    View changes on GitHub
v1.10.2

October 26, 2025

Release

OpenAPI - custom error format docs

By default, OpenAPIHandler, OpenAPIGenerator, and OpenAPILink share the same error response format. You can customize one, some, or all of them based on your requirements.

const handler = new OpenAPIHandler(router, {
  customErrorResponseBodyEncoder(error) {
    return error.toJSON()
  },
})

Native Fastify adapter docs

Previously, oRPC in Fastify used the Node adapter, which didn’t integrate well with Fastify’s ecosystem (e.g., cookies, helpers, middleware). This native adapter supports Fastify’s request/reply APIs directly, enabling full access to Fastify features within oRPC.

import Fastify from 'fastify'
import { RPCHandler } from '@orpc/server/fastify'
import { onError } from '@orpc/server'

const handler = new RPCHandler(router, {
  interceptors: [
    onError((error) => {
      console.error(error)
    })
  ]
})

const fastify = Fastify()

fastify.addContentTypeParser('*', (request, payload, done) => {
  // Fully utilize oRPC feature by allowing any content type
  // And let oRPC parse the body manually by passing `undefined`
  done(null, undefined)
})

fastify.all('/rpc/*', async (req, reply) => {
  const { matched } = await handler.handle(req, reply, {
    prefix: '/rpc',
    context: {} // Provide initial context if needed
  })

  if (!matched) {
    reply.status(404).send('Not found')
  }
})

fastify.listen({ port: 3000 }).then(() => console.log('Server running on http://localhost:3000'))

   🚀 Features

[!TIP] If you find oRPC valuable and would like to support its development, you can do so here.

    View changes on GitHub
v1.10.1

October 24, 2025

Release

Message port transfer docs

By default, oRPC serializes request/response messages to string/binary data before sending over message port. If needed, you can define the transfer option to utilize full power of MessagePort: postMessage() method, such as transferring ownership of objects to the other side or support unserializable objects like OffscreenCanvas.

const handler = new RPCHandler(router, {
  experimental_transfer: (message, port) => {
    const transfer = deepFindTransferableObjects(message) // implement your own logic
    return transfer.length ? transfer : null // only enable when needed
  }
})

   🚀 Features

[!TIP] If you find oRPC valuable and would like to support its development, you can do so here.

    View changes on GitHub
v1.10.0

October 20, 2025

Release

Publisher helper docs

The Publisher is a helper that enables you to listen to and publish events to subscribers. Combined with the Event Iterator, it allows you to build streaming responses, real-time updates, and server-sent events with minimal requirements.

const publisher = new MemoryPublisher<{
  'something-updated': {
    id: string
  }
}>()

const live = os
  .handler(async function* ({ input, signal, lastEventId }) {
    const iterator = publisher.subscribe('something-updated', { signal, lastEventId })
    for await (const payload of iterator) {
      // Handle payload here or yield directly to client
      yield payload
    }
  })

const publish = os
  .input(z.object({ id: z.string() }))
  .handler(async ({ input }) => {
    await publisher.publish('something-updated', { id: input.id })
  })

Available Adapters

Name Resume Support Description
MemoryPublisher A simple in-memory publisher
IORedisPublisher Adapter for ioredis
UpstashRedisPublisher Adapter for Upstash Redis

eventIteratorToUnproxiedDataStream util

Prefer using eventIteratorToUnproxiedDataStream over eventIteratorToStream when integrating oRPC with the AI SDK. The AI SDK uses structuredClone internally, which doesn’t support proxied data. Since oRPC may proxy events to attach metadata, you should unproxy them before passing to the AI SDK.

const { messages, sendMessage, status } = useChat({
  transport: {
    async sendMessages(options) {
      return eventIteratorToUnproxiedDataStream(await client.chat({
        chatId: options.chatId,
        messages: options.messages,
      }, { signal: options.abortSignal }))
    },
    reconnectToStream(options) {
      throw new Error('Unsupported')
    },
  },
})

   🚀 Features

   🏎 Performance

[!TIP] If you find oRPC valuable and would like to support its development, you can do so here.

    View changes on GitHub
v1.9.3

October 4, 2025

Release

[!TIP] If you find oRPC valuable and would like to support its development, you can do so here.

Bump dependencies

    View changes on GitHub
v1.9.2

September 29, 2025

Release

[!TIP] If you find oRPC valuable and would like to support its development, you can do so here.

   🚀 Features

   🐞 Bug Fixes

    View changes on GitHub
v1.9.0

September 23, 2025

Release

Durable Iterator docs

Durable Iterator is a complete rewrite of the old Durable Event Iterator. It builds on Event Iterator by offloading streaming to a dedicated service that delivers durable event streams with automatic reconnections and event recovery.

[!NOTE] While not limited to Cloudflare Durable Objects, it’s currently the only supported implementation.

Durable Object

import { DurableIteratorObject } from '@orpc/experimental-durable-iterator/durable-object'

export class ChatRoom extends DurableIteratorObject<{ message: string }> {
  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env, {
      signingKey: 'secret-key', // Replace with your actual signing key
      interceptors: [
        onError(e => console.error(e)), // log error thrown from rpc calls
      ],
      onSubscribed: (websocket, lastEventId) => {
        console.log(`WebSocket Ready id=${websocket['~orpc'].deserializeId()}`)
      }
    })
  }

  someMethod() {
    // publishEvent method inherited from DurableIteratorObject
    this.publishEvent({ message: 'Hello, world!' })
  }
}

Server Side

import { DurableEventIterator } from '@orpc/experimental-durable-iterator'

export const router = {
  onMessage: base.handler(({ context }) => {
    return new DurableEventIterator<ChatRoom>('some-room', {
      tags: ['tag1', 'tag2'],
      signingKey: 'secret-key', // Replace with your actual signing key
    })
  }),

  sendMessage: base
    .input(z.object({ message: z.string() }))
    .handler(async ({ context, input }) => {
      const id = context.env.CHAT_ROOM.idFromName('some-room')
      const stub = context.env.CHAT_ROOM.get(id)

      await stub.publishEvent(input)
    }),
}

Client Side

const iterator = await client.onMessage()

for await (const { message } of iterator) {
  console.log('Received message:', message)
}

await client.sendMessage({ message: 'Hello, world!' })

experimental_streamedOptions is removed in old tanstack query

If you depend on this, please try NEW Tanstack Query Interagration


[!TIP] If you find oRPC valuable and would like to support its development, you can do so here.

   🚀 Features

    View changes on GitHub
v1.8.9

September 15, 2025

Release

Request Validation Plugin docs

The Request Validation Plugin ensures that only valid requests are sent to your server. This is especially valuable for applications that depend on server-side validation.

You can simplify your frontend by removing heavy form validation libraries and relying on oRPC’s validation errors instead, since input validation runs directly in the browser and is highly performant.

export function ContactForm() {
  const [error, setError] = useState()

  const handleSubmit = async (form: FormData) => {
    try {
      const output = await client.someProcedure(parseFormData(form))
      console.log(output)
    }
    catch (error) {
      setError(error)
    }
  }

  return (
    <form action={handleSubmit}>
      <input name="user[name]" type="text" />
      <span>{getIssueMessage(error, 'user[name]')}</span>

      <input name="user[emails][]" type="email" />
      <span>{getIssueMessage(error, 'user[emails][]')}</span>

      <button type="submit">Submit</button>
    </form>
  )
}

[!NOTE] If you find oRPC valuable and would like to support its development, you can do so here.

   🚀 Features

   🐞 Bug Fixes

  • contract: Cannot be named without a reference AsyncIteratorClass  -  by @dinwwwh (c68ce)
    View changes on GitHub
v1.8.8

September 11, 2025

Release

consumeEventIterator utility docs

oRPC provides a utility function consumeEventIterator to consume an event iterator with lifecycle callbacks.

import { consumeEventIterator } from '@orpc/client'

const cancel = consumeEventIterator(client.streaming(), {
  onEvent: (event) => {
    console.log(event.message)
  },
  onError: (error) => {
    console.error(error)
  },
  onSuccess: (value) => {
    console.log(value)
  },
  onFinish: (state) => {
    console.log(state)
  },
})

setTimeout(async () => {
  // Stop the stream after 1 second
  await cancel()
}, 1000)

optimize withEventMeta to avoid unnecessary proxies event iterator data

This prevents structuredClone from failing on proxy event iterator data, which the AI SDKdepends on.

const { messages, sendMessage, status } = useChat({
  transport: {
    async sendMessages(options) {
      return eventIteratorToStream(await client.chat({
        chatId: options.chatId,
        messages: options.messages,
      }, { signal: options.abortSignal }))
    },
    reconnectToStream(options) {
      throw new Error('Unsupported')
    },
  },
})

[!NOTE] If you find oRPC valuable and would like to support its development, you can do so here.

   🚀 Features

   🐞 Bug Fixes

    View changes on GitHub
v1.8.6

September 1, 2025

Release

Response Validation Plugin docs

The Response Validation Plugin validates server responses against your contract schema, ensuring that data returned from your server matches the expected types defined in your contract.

import { RPCLink } from '@orpc/client/fetch'
import { ResponseValidationPlugin } from '@orpc/contract/plugins'

const link = new RPCLink({
  url: 'http://localhost:3000/rpc',
  plugins: [
    new ResponseValidationPlugin(contract),
  ],
})

const client: ContractRouterClient<typeof contract> = createORPCClient(link)

[!TIP] Beyond response validation, this plugin also serves special purposes such as Expanding Type Support for OpenAPI Link.

 import { RPCLink } from '@orpc/client/fetch'
 const link = new RPCLink({
   url: `${typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000'}/rpc`,
   headers: async () => {
     if (typeof window !== 'undefined') {
       return {}
     }

    const { headers } = await import('next/headers')
-    return Object.fromEntries(await headers())
+    return await headers()
   },
 })

   🚀 Features

    View changes on GitHub
v1.8.2

August 20, 2025

Release

React SWR Integration docs

import useSWR from 'swr'

const { data, error, isLoading } = useSWR(
  orpc.planet.find.key({ input: { id: 123 } }),
  orpc.planet.find.fetcher(),
)

   🚀 Features

   🐞 Bug Fixes

    View changes on GitHub
v1.8.1

August 14, 2025

Release

   🚀 Features

   🐞 Bug Fixes

    View changes on GitHub
v1.8.0

August 11, 2025

Release

First-class OpenTelemetry Support docs

import { NodeSDK } from '@opentelemetry/sdk-node'
import { ORPCInstrumentation } from '@orpc/otel'

const sdk = new NodeSDK({
  instrumentations: [
    new ORPCInstrumentation(), 
  ],
})

sdk.start()

   🚀 Features

    View changes on GitHub
v1.7.2

July 16, 2025

Release

AI SDK Integration docs

AI SDK is a free open-source library for building AI-powered products. You can seamlessly integrate it with oRPC without any extra overhead.

const { messages, sendMessage, status } = useChat({
  transport: {
    async sendMessages(options) {
      return eventIteratorToStream(await client.chat({
        chatId: options.chatId,
        messages: options.messages,
      }, { signal: options.abortSignal }))
    },
  },
})

   🚀 Features

    View changes on GitHub
v1.7.1

July 15, 2025

Release

ORPCMeta update for tRPC docs

 const example = t.procedure
-  .meta({ path: '/hello', summary: 'Hello procedure' }) 
+  .meta({ route: { path: '/hello', summary: 'Hello procedure' } }) 
   .input(z.object({ name: z.string() }))
   .query(({ input }) => {
     return `Hello, ${input.name}!`
   })

   🚀 Features

   🐞 Bug Fixes

  • client: Missing export createSafeClient  -  by @dinwwwh (cf4ae)
    View changes on GitHub
v1.7.0

July 14, 2025

Release

Safe Client docs

If you often use safe for error handling, createSafeClient can simplify your code by automatically wrapping all procedure calls with safe. It works with both server-side and client-side clients.

import { createSafeClient } from '@orpc/client'

const safeClient = createSafeClient(client)

const [error, data] = await safeClient.doSomething({ id: '123' })

Smart Coercion Plugin docs

Automatically converts input values to match schema types without manually defining coercion logic. This plugin can support almost any standard schema.

import { OpenAPIHandler } from '@orpc/openapi/fetch'
import {
  experimental_SmartCoercionPlugin as SmartCoercionPlugin
} from '@orpc/json-schema'

const handler = new OpenAPIHandler(router, {
  plugins: [
    new SmartCoercionPlugin({
      schemaConverters: [
          new ZodToJsonSchemaConverter(), // <-- if you use Zod
          new ValibotToJsonSchemaConverter(), // <-- if you use Valibot
          new ArkTypeToJsonSchemaConverter(), // <-- if you use ArkType
      ],
    })
  ]
})

Promote stable

aws lambda, websocket, message port, zod 4 converter now stable


   🚀 Features

   🐞 Bug Fixes

  • server: Add missing export for utils in standard adapter  -  by @dinwwwh (29120)
  • trpc: Add missing exports  -  by @dinwwwh (c465e)
    View changes on GitHub
v1.6.4

July 3, 2025

Release

Live Query Options docs

Use .liveOptions to configure live queries for Event Iterator. Unlike .streamedOptions which accumulates chunks, live queries replace the entire result with each new chunk received

const query = useQuery(orpc.live.experimental_liveOptions({
  input: { id: 123 }, // Specify input if needed
  context: { cache: true }, // Provide client context if needed
  // additional options...
}))

   🚀 Features

    View changes on GitHub