Skip to content
You are reading the v2 docs, currently in beta.V1 docs
oRPC
Esc
↑↓navigate↵open⌘Jpreview
On this page

Better Auth Integration

Protect oRPC procedures with typed Better Auth sessions, using your framework's Better Auth integration or the request and response headers plugins.

Use Better Auth with oRPC middleware. No extra package is needed.

Session Middleware

Use the Request Headers Plugin to read the session and the Response Headers Plugin to forward Better Auth’s cookies.

import type { RequestHeadersHandlerPluginContext, ResponseHeadersHandlerPluginContext } from '@orpc/server/plugins'
import { ORPCError, os } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
import { RequestHeadersHandlerPlugin, ResponseHeadersHandlerPlugin } from '@orpc/server/plugins'

interface ServerContext extends RequestHeadersHandlerPluginContext, ResponseHeadersHandlerPluginContext {}

const base = os.$context<ServerContext>()

const requireSession = base.middleware(async ({ context, next }) => {
  const { headers, response: session } = await auth.api.getSession({
    headers: context.reqHeaders ?? new Headers(),
    returnHeaders: true,
  })

  for (const cookie of headers.getSetCookie()) {
    context.resHeaders?.append('set-cookie', cookie)
  }

  if (!session) {
    throw new ORPCError('UNAUTHORIZED')
  }

  return next({ context: { session } })
})

const router = {
  me: base.use(requireSession).handler(({ context }) => context.session.user),
}

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

Server-Side Calls

In server-side calls, pass reqHeaders, and optionally resHeaders, yourself:

import { call } from '@orpc/server'

const resHeaders = new Headers()

const user = await call(router.me, undefined, {
  context: { reqHeaders: request.headers, resHeaders },
})

Deduplication

To avoid repeated lookups when the middleware runs more than once in a call, see the Dedupe Middleware recipe.

To look up the session once per request, including batch sub-requests, pass a lazy getter in the initial context:

import { ORPCError, os } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'

type Session = typeof auth.$Infer.Session

function once<T>(fn: () => Promise<T>): () => Promise<T> {
  let promise: Promise<T> | undefined

  return () => {
    promise ??= fn()
    return promise
  }
}

const base = os.$context<{ getSession: () => Promise<Session | null> }>()

const requireSession = base.middleware(async ({ context, next }) => {
  const session = await context.getSession()

  if (!session) {
    throw new ORPCError('UNAUTHORIZED')
  }

  return next({ context: { session } })
})

const router = {
  me: base.use(requireSession).handler(({ context }) => context.session.user),
}

const handler = new RPCHandler(router)

export async function fetch(request: Request): Promise<Response> {
  const setCookies: string[] = []

  const getSession = once(async () => {
    const { headers, response } = await auth.api.getSession({
      headers: request.headers,
      returnHeaders: true,
    })

    setCookies.push(...headers.getSetCookie())

    return response
  })

  const { matched, response } = await handler.handle(request, {
    prefix: '/rpc',
    context: { getSession },
  })

  if (!matched) {
    return new Response('Not Found', { status: 404 })
  }

  for (const cookie of setCookies) {
    response.headers.append('set-cookie', cookie)
  }

  return response
}

Last updated on

Was this page helpful?