From 9bca6920105f11f09740fd639a7afd67ec0f86ba Mon Sep 17 00:00:00 2001 From: Mnigos Date: Mon, 7 Sep 2026 01:03:59 +0200 Subject: [PATCH 1/6] docs(server): restore Better Auth integration guide for v2 --- .../content/docs/integrations/better-auth.mdx | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 apps/content/docs/integrations/better-auth.mdx diff --git a/apps/content/docs/integrations/better-auth.mdx b/apps/content/docs/integrations/better-auth.mdx new file mode 100644 index 000000000..2f77fbf53 --- /dev/null +++ b/apps/content/docs/integrations/better-auth.mdx @@ -0,0 +1,113 @@ +--- +title: "Better Auth Integration" +description: "Use Better Auth sessions in oRPC context and protect procedures with typed middleware." +sidebar: + label: "Better Auth" +--- + +Use your configured [Better Auth](https://better-auth.com/) instance with oRPC's [context](/docs/context) and [middleware](/docs/middleware). No additional oRPC package is needed. + +The examples below import that instance from `./auth`. Keep Better Auth's authentication routes mounted through its [framework integration](https://better-auth.com/docs/installation#mount-handler), alongside your oRPC handler. + +## Resolve the Session in Middleware + +Pass the request headers into context and load the session only for procedures that use the authentication middleware. Public procedures can use the same base without looking up a session. + +```ts +import { ORPCError, os } from '@orpc/server' +import { RPCHandler } from '@orpc/server/fetch' +import { auth } from './auth' + +const base = os.$context<{ headers: Headers }>() + +const requireSession = base.middleware(async ({ context, next }) => { + const session = await auth.api.getSession({ headers: context.headers }) + + if (!session) { + throw new ORPCError('UNAUTHORIZED') + } + + return next({ context: { session } }) +}) + +const protectedProcedure = base.use(requireSession) + +const router = { + ping: base.handler(() => ({ message: 'pong' })), + me: protectedProcedure.handler(({ context }) => ({ + id: context.session.user.id, + name: context.session.user.name, + })), +} + +const handler = new RPCHandler(router) + +export async function handleRPC(request: Request): Promise { + const { matched, response } = await handler.handle(request, { + prefix: '/rpc', + context: { headers: request.headers }, + }) + + return matched ? response : new Response('Not Found', { status: 404 }) +} +``` + +Here, `context.session` holds Better Auth's complete session result, including its `user` and `session` fields. Its type is inferred from your configured auth instance, so additional fields remain available in protected handlers. + +Only a missing session becomes `UNAUTHORIZED`. Errors thrown by `getSession`, such as a database failure, propagate to oRPC's [error handling](/docs/error-handling). + +## Reuse a Session from Context + +If your server already resolves the session before calling oRPC, pass that result into context. The middleware can check and narrow the existing value without another lookup. + +This alternative also lets public procedures read an optional session. It resolves the session for every request passed to `handleRPC`, including public procedures; prefer the first approach when only protected procedures need it. + +```ts +import { ORPCError, os } from '@orpc/server' +import { RPCHandler } from '@orpc/server/fetch' +import { auth } from './auth' + +type Session = Awaited> + +const base = os.$context<{ session: Session }>() + +const requireSession = base.middleware(async ({ context, next }) => { + if (!context.session) { + throw new ORPCError('UNAUTHORIZED') + } + + return next({ context: { session: context.session } }) +}) + +const protectedProcedure = base.use(requireSession) + +const router = { + greeting: base.handler(({ context }) => ({ + message: `Hello, ${context.session?.user.name ?? 'guest'}`, + })), + me: protectedProcedure.handler(({ context }) => ({ + id: context.session.user.id, + name: context.session.user.name, + })), +} + +const handler = new RPCHandler(router) + +export async function handleRPC(request: Request): Promise { + const session = await auth.api.getSession({ headers: request.headers }) + const { matched, response } = await handler.handle(request, { + prefix: '/rpc', + context: { session }, + }) + + return matched ? response : new Response('Not Found', { status: 404 }) +} +``` + +`Session` includes `null`. After `requireSession` runs, protected handlers receive a non-null session while public handlers must still check for it. The value passed to `next` is merged into context, preserving other context fields. + +In this version, session lookup happens outside the oRPC handler. Handle lookup failures at your server's error boundary. + +:::info +Server-side Better Auth calls do not automatically forward response headers to the browser. If your session setup needs cookie updates from `getSession`, follow Better Auth's [response headers guidance](https://better-auth.com/docs/concepts/api#getting-headers) and your framework's cookie integration. +::: From 868c7983ac7645b057cc2c3e3301a64a1b4a2d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BB=8Bnh=20L=C3=AA?= Date: Mon, 7 Sep 2026 10:32:43 +0700 Subject: [PATCH 2/6] Revise Better Auth integration details in documentation Updated documentation for Better Auth integration with oRPC, clarifying session handling and middleware usage. --- .../content/docs/integrations/better-auth.mdx | 79 +++++++++---------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/apps/content/docs/integrations/better-auth.mdx b/apps/content/docs/integrations/better-auth.mdx index 2f77fbf53..ba7973be8 100644 --- a/apps/content/docs/integrations/better-auth.mdx +++ b/apps/content/docs/integrations/better-auth.mdx @@ -5,23 +5,24 @@ sidebar: label: "Better Auth" --- -Use your configured [Better Auth](https://better-auth.com/) instance with oRPC's [context](/docs/context) and [middleware](/docs/middleware). No additional oRPC package is needed. - -The examples below import that instance from `./auth`. Keep Better Auth's authentication routes mounted through its [framework integration](https://better-auth.com/docs/installation#mount-handler), alongside your oRPC handler. +Use your [Better Auth](https://better-auth.com/) instance with oRPC's [context](/docs/context) and [middleware](/docs/middleware). No extra package is needed. ## Resolve the Session in Middleware -Pass the request headers into context and load the session only for procedures that use the authentication middleware. Public procedures can use the same base without looking up a session. +The [Request Headers Plugin](/docs/plugins/request-headers) exposes request headers as `context.reqHeaders`. The middleware loads the session from them and rejects unauthenticated calls. Public procedures use the base directly. Each protected call performs its own lookup, including every sub-request of a [batch](/docs/plugins/batch). ```ts +import type { RequestHeadersHandlerPluginContext } from '@orpc/server/plugins' import { ORPCError, os } from '@orpc/server' -import { RPCHandler } from '@orpc/server/fetch' -import { auth } from './auth' -const base = os.$context<{ headers: Headers }>() +interface ServerContext extends RequestHeadersHandlerPluginContext {} + +const base = os.$context() const requireSession = base.middleware(async ({ context, next }) => { - const session = await auth.api.getSession({ headers: context.headers }) + const session = await auth.api.getSession({ + headers: context.reqHeaders ?? new Headers(), + }) if (!session) { throw new ORPCError('UNAUTHORIZED') @@ -39,52 +40,53 @@ const router = { name: context.session.user.name, })), } - -const handler = new RPCHandler(router) - -export async function handleRPC(request: Request): Promise { - const { matched, response } = await handler.handle(request, { - prefix: '/rpc', - context: { headers: request.headers }, - }) - - return matched ? response : new Response('Not Found', { status: 404 }) -} ``` -Here, `context.session` holds Better Auth's complete session result, including its `user` and `session` fields. Its type is inferred from your configured auth instance, so additional fields remain available in protected handlers. +`context.session` is Better Auth's full result with `user` and `session`. Its type is inferred from your auth instance, so additional fields stay available. -Only a missing session becomes `UNAUTHORIZED`. Errors thrown by `getSession`, such as a database failure, propagate to oRPC's [error handling](/docs/error-handling). +Only a missing session becomes `UNAUTHORIZED`. Other errors from `getSession` propagate to oRPC's [error handling](/docs/error-handling). -## Reuse a Session from Context +`reqHeaders` is `undefined` without the plugin, such as in [server-side calls](/docs/client/server-side). The empty fallback makes the session `null` there, since `getSession` rejects missing headers. Pass `reqHeaders` in the initial context to authenticate such calls. -If your server already resolves the session before calling oRPC, pass that result into context. The middleware can check and narrow the existing value without another lookup. +## Lazily Load and Share the Session -This alternative also lets public procedures read an optional session. It resolves the session for every request passed to `handleRPC`, including public procedures; prefer the first approach when only protected procedures need it. +If your server resolves the session itself, pass a lazy getter into the initial context instead of the session. The lookup runs at most once per request and only when a procedure asks for it. This includes [batch](/docs/plugins/batch) requests, where every sub-request shares the getter. The same getter can also serve the rest of your request handling. ```ts import { ORPCError, os } from '@orpc/server' import { RPCHandler } from '@orpc/server/fetch' -import { auth } from './auth' type Session = Awaited> -const base = os.$context<{ session: Session }>() +function once(fn: () => Promise): () => Promise { + let promise: Promise | undefined + + return () => { + promise ??= fn() + return promise + } +} + +const base = os.$context<{ getSession: () => Promise }>() const requireSession = base.middleware(async ({ context, next }) => { - if (!context.session) { + const session = await context.getSession() + + if (!session) { throw new ORPCError('UNAUTHORIZED') } - return next({ context: { session: context.session } }) + return next({ context: { session } }) }) const protectedProcedure = base.use(requireSession) const router = { - greeting: base.handler(({ context }) => ({ - message: `Hello, ${context.session?.user.name ?? 'guest'}`, - })), + greeting: base.handler(async ({ context }) => { + const session = await context.getSession() + + return { message: `Hello, ${session?.user.name ?? 'guest'}` } + }), me: protectedProcedure.handler(({ context }) => ({ id: context.session.user.id, name: context.session.user.name, @@ -93,21 +95,14 @@ const router = { const handler = new RPCHandler(router) -export async function handleRPC(request: Request): Promise { - const session = await auth.api.getSession({ headers: request.headers }) +export async function fetch(request: Request): Promise { + const getSession = once(() => auth.api.getSession({ headers: request.headers })) + const { matched, response } = await handler.handle(request, { prefix: '/rpc', - context: { session }, + context: { getSession }, }) return matched ? response : new Response('Not Found', { status: 404 }) } ``` - -`Session` includes `null`. After `requireSession` runs, protected handlers receive a non-null session while public handlers must still check for it. The value passed to `next` is merged into context, preserving other context fields. - -In this version, session lookup happens outside the oRPC handler. Handle lookup failures at your server's error boundary. - -:::info -Server-side Better Auth calls do not automatically forward response headers to the browser. If your session setup needs cookie updates from `getSession`, follow Better Auth's [response headers guidance](https://better-auth.com/docs/concepts/api#getting-headers) and your framework's cookie integration. -::: From 436957b1621da49fd6f5cc58890a77ee9a02b05a Mon Sep 17 00:00:00 2001 From: Mnigos Date: Mon, 7 Sep 2026 12:37:25 +0200 Subject: [PATCH 3/6] docs: clarify Better Auth session headers and cookies --- apps/content/docs/integrations/better-auth.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/content/docs/integrations/better-auth.mdx b/apps/content/docs/integrations/better-auth.mdx index ba7973be8..bc5856b47 100644 --- a/apps/content/docs/integrations/better-auth.mdx +++ b/apps/content/docs/integrations/better-auth.mdx @@ -46,7 +46,7 @@ const router = { Only a missing session becomes `UNAUTHORIZED`. Other errors from `getSession` propagate to oRPC's [error handling](/docs/error-handling). -`reqHeaders` is `undefined` without the plugin, such as in [server-side calls](/docs/client/server-side). The empty fallback makes the session `null` there, since `getSession` rejects missing headers. Pass `reqHeaders` in the initial context to authenticate such calls. +`reqHeaders` is `undefined` without the plugin, such as in [server-side calls](/docs/client/server-side). The empty `Headers` fallback carries no session cookie, so `getSession` returns `null` and protected calls return `UNAUTHORIZED`. Pass `reqHeaders` in the initial context to authenticate such calls. ## Lazily Load and Share the Session @@ -106,3 +106,5 @@ export async function fetch(request: Request): Promise { return matched ? response : new Response('Not Found', { status: 404 }) } ``` + +Both examples may need to forward session cookies to the browser; see Better Auth's [response headers guidance](https://better-auth.com/docs/concepts/api#getting-headers). From 570b280d44987286eee1e3196ff3a6f78cbbc2d1 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Mon, 7 Sep 2026 20:33:50 +0700 Subject: [PATCH 4/6] docs(server): move the Better Auth cookie forwarding note to the top and name the Response Headers Plugin --- apps/content/docs/integrations/better-auth.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/content/docs/integrations/better-auth.mdx b/apps/content/docs/integrations/better-auth.mdx index bc5856b47..4e2351874 100644 --- a/apps/content/docs/integrations/better-auth.mdx +++ b/apps/content/docs/integrations/better-auth.mdx @@ -7,6 +7,10 @@ sidebar: Use your [Better Auth](https://better-auth.com/) instance with oRPC's [context](/docs/context) and [middleware](/docs/middleware). No extra package is needed. +:::warning +`getSession` may return response headers, such as a refreshed session cookie. The examples below do not forward them. If you do not use the [Batch Plugin](/docs/plugins/batch), the [Response Headers Plugin](/docs/plugins/response-headers) can forward them: call `getSession` with `returnHeaders: true` and append the returned [headers](https://better-auth.com/docs/concepts/api#getting-headers) to `context.resHeaders`. Batched sub-requests cannot set browser cookies. +::: + ## Resolve the Session in Middleware The [Request Headers Plugin](/docs/plugins/request-headers) exposes request headers as `context.reqHeaders`. The middleware loads the session from them and rejects unauthenticated calls. Public procedures use the base directly. Each protected call performs its own lookup, including every sub-request of a [batch](/docs/plugins/batch). @@ -106,5 +110,3 @@ export async function fetch(request: Request): Promise { return matched ? response : new Response('Not Found', { status: 404 }) } ``` - -Both examples may need to forward session cookies to the browser; see Better Auth's [response headers guidance](https://better-auth.com/docs/concepts/api#getting-headers). From 5ffbff6cede261c2b5fd6e211f4a5f609515c0e1 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Mon, 7 Sep 2026 20:37:03 +0700 Subject: [PATCH 5/6] docs(server): shorten the Better Auth response headers hint --- apps/content/docs/integrations/better-auth.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/content/docs/integrations/better-auth.mdx b/apps/content/docs/integrations/better-auth.mdx index 4e2351874..5c1f4ccb9 100644 --- a/apps/content/docs/integrations/better-auth.mdx +++ b/apps/content/docs/integrations/better-auth.mdx @@ -7,8 +7,8 @@ sidebar: Use your [Better Auth](https://better-auth.com/) instance with oRPC's [context](/docs/context) and [middleware](/docs/middleware). No extra package is needed. -:::warning -`getSession` may return response headers, such as a refreshed session cookie. The examples below do not forward them. If you do not use the [Batch Plugin](/docs/plugins/batch), the [Response Headers Plugin](/docs/plugins/response-headers) can forward them: call `getSession` with `returnHeaders: true` and append the returned [headers](https://better-auth.com/docs/concepts/api#getting-headers) to `context.resHeaders`. Batched sub-requests cannot set browser cookies. +:::tip +To forward Better Auth's [response headers](https://better-auth.com/docs/concepts/api#getting-headers), such as a refreshed session cookie, use `returnHeaders: true` with the [Response Headers Plugin](/docs/plugins/response-headers). This does not work for [batched](/docs/plugins/batch) requests. ::: ## Resolve the Session in Middleware From 6bf466c23dcdb0b45353860862be4c1527174e34 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Mon, 7 Sep 2026 20:38:27 +0700 Subject: [PATCH 6/6] docs(server): trim the Better Auth response headers hint --- apps/content/docs/integrations/better-auth.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/content/docs/integrations/better-auth.mdx b/apps/content/docs/integrations/better-auth.mdx index 5c1f4ccb9..350ac92c7 100644 --- a/apps/content/docs/integrations/better-auth.mdx +++ b/apps/content/docs/integrations/better-auth.mdx @@ -8,7 +8,7 @@ sidebar: Use your [Better Auth](https://better-auth.com/) instance with oRPC's [context](/docs/context) and [middleware](/docs/middleware). No extra package is needed. :::tip -To forward Better Auth's [response headers](https://better-auth.com/docs/concepts/api#getting-headers), such as a refreshed session cookie, use `returnHeaders: true` with the [Response Headers Plugin](/docs/plugins/response-headers). This does not work for [batched](/docs/plugins/batch) requests. +You may need to forward Better Auth's [response headers](https://better-auth.com/docs/concepts/api#getting-headers), such as a refreshed session cookie. The [Response Headers Plugin](/docs/plugins/response-headers) can help unless you use the [Batch Plugin](/docs/plugins/batch). ::: ## Resolve the Session in Middleware