diff --git a/.agents/skills/kitcn/references/features/react.md b/.agents/skills/kitcn/references/features/react.md
index e8da9603..aeb5eccd 100644
--- a/.agents/skills/kitcn/references/features/react.md
+++ b/.agents/skills/kitcn/references/features/react.md
@@ -513,7 +513,7 @@ export const { createContext, createCaller, handler } = convexBetterAuth({
|--------|-------------|
| `createContext` | RSC context with auth |
| `createCaller` | Server-side caller factory |
-| `handler` | Next.js API route handler (`export const { GET, POST } = handler;`) |
+| `handler` | Next.js API route handler (`export const { GET, POST, OPTIONS } = handler;`) |
Options: `api`, `convexSiteUrl`, `auth.jwtCache` (default true), `auth.isUnauthorized`.
@@ -530,7 +530,7 @@ return {children};
```ts
// src/app/api/auth/[...all]/route.ts
import { handler } from '@/lib/convex/server';
-export const { GET, POST } = handler;
+export const { GET, POST, OPTIONS } = handler;
```
---
diff --git a/.agents/skills/kitcn/references/setup/next.md b/.agents/skills/kitcn/references/setup/next.md
index 533c83b2..fc0fb96d 100644
--- a/.agents/skills/kitcn/references/setup/next.md
+++ b/.agents/skills/kitcn/references/setup/next.md
@@ -23,7 +23,7 @@ export const { createContext, createCaller, handler } = convexBetterAuth({
```ts
import { handler } from "@/lib/convex/server";
-export const { GET, POST } = handler;
+export const { GET, POST, OPTIONS } = handler;
```
### 8.A.3 RSC helpers
diff --git a/.changeset/next-auth-options-preflight.md b/.changeset/next-auth-options-preflight.md
new file mode 100644
index 00000000..139e63e6
--- /dev/null
+++ b/.changeset/next-auth-options-preflight.md
@@ -0,0 +1,5 @@
+---
+"kitcn": patch
+---
+
+Support `OPTIONS` preflight forwarding in `kitcn/auth/nextjs` route handlers and generated Next auth routes.
diff --git a/bun.lock b/bun.lock
index 3fb69e3e..4413bff0 100644
--- a/bun.lock
+++ b/bun.lock
@@ -119,7 +119,7 @@
},
"packages/kitcn": {
"name": "kitcn",
- "version": "0.15.3",
+ "version": "0.15.4",
"bin": {
"kitcn": "./dist/cli.mjs",
"intent": "./bin/intent.js",
@@ -180,7 +180,7 @@
},
"packages/resend": {
"name": "@kitcn/resend",
- "version": "0.15.3",
+ "version": "0.15.4",
"dependencies": {
"svix": "^1.84.1",
},
diff --git a/docs/solutions/integration-issues/nextjs-auth-proxy-must-forward-options-preflight-20260522.md b/docs/solutions/integration-issues/nextjs-auth-proxy-must-forward-options-preflight-20260522.md
new file mode 100644
index 00000000..0810c557
--- /dev/null
+++ b/docs/solutions/integration-issues/nextjs-auth-proxy-must-forward-options-preflight-20260522.md
@@ -0,0 +1,87 @@
+---
+title: Next.js auth proxy must forward OPTIONS preflight
+date: 2026-05-22
+category: integration-issues
+module: auth-nextjs
+problem_type: integration_issue
+component: authentication
+symptoms:
+ - cross-origin Better Auth clients need `OPTIONS /api/auth/*` preflight support
+ - the generated Next.js auth route only exported `GET` and `POST`
+ - users had to wrap `handler.GET` and `handler.POST` manually to add preflight
+root_cause: wrong_api
+resolution_type: code_fix
+severity: medium
+tags: [auth, nextjs, better-auth, cors, preflight, options, convex, proxy]
+---
+
+# Next.js auth proxy must forward OPTIONS preflight
+
+## Problem
+
+The `kitcn/auth/nextjs` proxy only exposed `GET` and `POST`. That forced
+cross-origin clients, such as local WebViews, to add a custom Next.js
+`OPTIONS` handler even though the Convex auth route already owned CORS policy.
+
+## Symptoms
+
+- `convex/functions/http.ts` had `registerRoutes(http, getAuth, { cors })`.
+- Better Auth `trustedOrigins` included the cross-origin app origin.
+- The Next.js route still needed manual preflight code because
+ `export const { GET, POST } = handler` left `OPTIONS` unhandled.
+
+## What Didn't Work
+
+- Duplicating CORS decisions in the Next.js route is the wrong ownership. The
+ Convex-side `registerRoutes` helper already combines Better Auth trusted
+ origins with explicit allowed origins.
+- Treating a deployment-wide generic Convex HTTP 500 as this proxy bug is also
+ wrong. If a bare `/_debug-auth` HTTP action returns the same generic 500
+ before handler logs, the failure is below KitCN's Better Auth handler.
+
+## Solution
+
+Expose `OPTIONS` from the Next.js proxy and forward it to the Convex site URL.
+Do not attach an empty request body to preflight.
+
+```ts
+const requestCanHaveBody = (method: string) =>
+ method !== "GET" && method !== "HEAD" && method !== "OPTIONS";
+
+const nextJsHandler = (siteUrl: string) => ({
+ GET: (request: Request) => handler(request, siteUrl),
+ OPTIONS: (request: Request) => handler(request, siteUrl),
+ POST: (request: Request) => handler(request, siteUrl),
+});
+```
+
+Generated Next.js auth routes should export all three methods:
+
+```ts
+import { handler } from "@/lib/convex/server";
+
+export const { GET, POST, OPTIONS } = handler;
+```
+
+## Why This Works
+
+The Next.js route is only a proxy. Forwarding `OPTIONS` lets Convex's HTTP
+router answer preflight with the same `registerRoutes` CORS configuration that
+answers direct Convex auth requests.
+
+Keeping preflight bodyless avoids subtle fetch/runtime behavior around methods
+that do not need a body.
+
+## Prevention
+
+1. When adding a server-side auth proxy, expose every method needed by browser
+ CORS, not just the methods Better Auth uses for real work.
+2. Keep CORS allow-list decisions at one layer. For KitCN auth, that layer is
+ Convex `registerRoutes`, not the Next.js proxy.
+3. Test the public `convexBetterAuth(...).handler` surface for `OPTIONS`
+ forwarding and generated route templates for `GET, POST, OPTIONS`.
+
+## Related Issues
+
+- [Next.js auth proxy must forward POST bodies explicitly](/Users/zbeyens/git/better-convex/docs/solutions/integration-issues/nextjs-auth-proxy-must-forward-post-bodies-explicitly-20260410.md)
+- [Convex auth JWKS routes should not trigger Better Auth IP warnings](/Users/zbeyens/git/better-convex/docs/solutions/integration-issues/convex-auth-jwks-routes-should-not-trigger-better-auth-ip-warnings-20260325.md)
diff --git a/fixtures/next-auth/app/api/auth/[...all]/route.ts b/fixtures/next-auth/app/api/auth/[...all]/route.ts
index bf07421f..ad7d45d3 100644
--- a/fixtures/next-auth/app/api/auth/[...all]/route.ts
+++ b/fixtures/next-auth/app/api/auth/[...all]/route.ts
@@ -1,3 +1,3 @@
import { handler } from '@/lib/convex/server';
-export const { GET, POST } = handler;
+export const { GET, POST, OPTIONS } = handler;
diff --git a/packages/kitcn/skills/kitcn/references/features/react.md b/packages/kitcn/skills/kitcn/references/features/react.md
index e8da9603..aeb5eccd 100644
--- a/packages/kitcn/skills/kitcn/references/features/react.md
+++ b/packages/kitcn/skills/kitcn/references/features/react.md
@@ -513,7 +513,7 @@ export const { createContext, createCaller, handler } = convexBetterAuth({
|--------|-------------|
| `createContext` | RSC context with auth |
| `createCaller` | Server-side caller factory |
-| `handler` | Next.js API route handler (`export const { GET, POST } = handler;`) |
+| `handler` | Next.js API route handler (`export const { GET, POST, OPTIONS } = handler;`) |
Options: `api`, `convexSiteUrl`, `auth.jwtCache` (default true), `auth.isUnauthorized`.
@@ -530,7 +530,7 @@ return {children};
```ts
// src/app/api/auth/[...all]/route.ts
import { handler } from '@/lib/convex/server';
-export const { GET, POST } = handler;
+export const { GET, POST, OPTIONS } = handler;
```
---
diff --git a/packages/kitcn/skills/kitcn/references/setup/next.md b/packages/kitcn/skills/kitcn/references/setup/next.md
index 533c83b2..fc0fb96d 100644
--- a/packages/kitcn/skills/kitcn/references/setup/next.md
+++ b/packages/kitcn/skills/kitcn/references/setup/next.md
@@ -23,7 +23,7 @@ export const { createContext, createCaller, handler } = convexBetterAuth({
```ts
import { handler } from "@/lib/convex/server";
-export const { GET, POST } = handler;
+export const { GET, POST, OPTIONS } = handler;
```
### 8.A.3 RSC helpers
diff --git a/packages/kitcn/src/auth-nextjs/index.test.ts b/packages/kitcn/src/auth-nextjs/index.test.ts
index 6302ea16..a4114cf5 100644
--- a/packages/kitcn/src/auth-nextjs/index.test.ts
+++ b/packages/kitcn/src/auth-nextjs/index.test.ts
@@ -1,7 +1,7 @@
import { convexBetterAuth } from './index';
describe('convexBetterAuth', () => {
- test('creates GET/POST handlers that rewrite request URL to convex site', async () => {
+ test('creates GET/POST/OPTIONS handlers that rewrite request URL to convex site', async () => {
const originalFetch = globalThis.fetch;
const calls: Array<{
input: RequestInfo | URL;
@@ -34,8 +34,17 @@ describe('convexBetterAuth', () => {
method: 'POST',
})
);
+ const preflightRequest = {
+ headers: {
+ 'access-control-request-method': 'POST',
+ origin: 'http://localhost:1420',
+ },
+ method: 'OPTIONS',
+ url: 'https://example.com/api/auth/session',
+ } as unknown as Request;
+ await result.handler.OPTIONS(preflightRequest);
- expect(calls).toHaveLength(2);
+ expect(calls).toHaveLength(3);
expect(calls[0]?.input).toBe('https://my-app.convex.site/path?a=1');
expect(calls[0]?.init?.method).toBe('GET');
@@ -66,6 +75,18 @@ describe('convexBetterAuth', () => {
await expect(
new Response(calls[1]?.init?.body as BodyInit).text()
).resolves.toBe(JSON.stringify({ email: 'user@example.com' }));
+
+ expect(calls[2]?.input).toBe(
+ 'https://my-app.convex.site/api/auth/session'
+ );
+ expect(calls[2]?.init?.method).toBe('OPTIONS');
+ expect(calls[2]?.init?.redirect).toBe('manual');
+ const optionsHeaders = new Headers(calls[2]?.init?.headers);
+ expect(optionsHeaders.get('host')).toBe('my-app.convex.site');
+ expect(optionsHeaders.get('x-forwarded-host')).toBe('example.com');
+ expect(optionsHeaders.get('x-forwarded-proto')).toBe('https');
+ expect(optionsHeaders.get('origin')).toBe('http://localhost:1420');
+ expect(calls[2]?.init?.body).toBeUndefined();
} finally {
globalThis.fetch = originalFetch;
}
diff --git a/packages/kitcn/src/auth-nextjs/index.ts b/packages/kitcn/src/auth-nextjs/index.ts
index a520a607..ae02c851 100644
--- a/packages/kitcn/src/auth-nextjs/index.ts
+++ b/packages/kitcn/src/auth-nextjs/index.ts
@@ -10,6 +10,9 @@ import { createCallerFactory } from '../server/caller-factory';
const TRAILING_COLON_RE = /:$/;
+const requestCanHaveBody = (method: string) =>
+ method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS';
+
const handler = async (request: Request, siteUrl: string) => {
const requestUrl = new URL(request.url);
const nextUrl = `${siteUrl}${requestUrl.pathname}${requestUrl.search}`;
@@ -26,10 +29,9 @@ const handler = async (request: Request, siteUrl: string) => {
'x-better-auth-forwarded-proto',
requestUrl.protocol.replace(TRAILING_COLON_RE, '')
);
- const body =
- request.method !== 'GET' && request.method !== 'HEAD'
- ? await request.arrayBuffer()
- : undefined;
+ const body = requestCanHaveBody(request.method)
+ ? await request.arrayBuffer()
+ : undefined;
return fetch(nextUrl, {
body,
@@ -41,6 +43,7 @@ const handler = async (request: Request, siteUrl: string) => {
const nextJsHandler = (siteUrl: string) => ({
GET: (request: Request) => handler(request, siteUrl),
+ OPTIONS: (request: Request) => handler(request, siteUrl),
POST: (request: Request) => handler(request, siteUrl),
});
diff --git a/packages/kitcn/src/cli/cli.commands.ts b/packages/kitcn/src/cli/cli.commands.ts
index 7aff25b2..9bfe1007 100644
--- a/packages/kitcn/src/cli/cli.commands.ts
+++ b/packages/kitcn/src/cli/cli.commands.ts
@@ -1944,7 +1944,9 @@ describe('cli/cli', () => {
expect(routeSource).toContain(
"import { handler } from '@/lib/convex/server';"
);
- expect(routeSource).toContain('export const { GET, POST } = handler;');
+ expect(routeSource).toContain(
+ 'export const { GET, POST, OPTIONS } = handler;'
+ );
const authPageSource = fs.readFileSync(
path.join(dir, 'app', 'auth', 'page.tsx'),
diff --git a/packages/kitcn/src/cli/registry/items/auth/auth-next-route.template.ts b/packages/kitcn/src/cli/registry/items/auth/auth-next-route.template.ts
index 21a34867..102f3820 100644
--- a/packages/kitcn/src/cli/registry/items/auth/auth-next-route.template.ts
+++ b/packages/kitcn/src/cli/registry/items/auth/auth-next-route.template.ts
@@ -1,4 +1,4 @@
export const AUTH_NEXT_ROUTE_TEMPLATE = `import { handler } from '@/lib/convex/server';
-export const { GET, POST } = handler;
+export const { GET, POST, OPTIONS } = handler;
`;
diff --git a/www/content/docs/migrations/auth.mdx b/www/content/docs/migrations/auth.mdx
index 96c8259d..cb65c6b3 100644
--- a/www/content/docs/migrations/auth.mdx
+++ b/www/content/docs/migrations/auth.mdx
@@ -739,7 +739,7 @@ API route:
```ts showLineNumbers title="app/api/auth/[...all]/route.ts"
import { handler } from "@/lib/auth-server";
-export const { GET, POST } = handler;
+export const { GET, POST, OPTIONS } = handler;
```
Root layout:
diff --git a/www/content/docs/nextjs/index.mdx b/www/content/docs/nextjs/index.mdx
index 2b5ceb52..5aa0f5b9 100644
--- a/www/content/docs/nextjs/index.mdx
+++ b/www/content/docs/nextjs/index.mdx
@@ -146,7 +146,7 @@ Create the Next.js API route for Better Auth:
```ts title="src/app/api/auth/[...all]/route.ts" showLineNumbers {3}
import { handler } from '@/lib/convex/server';
-export const { GET, POST } = handler;
+export const { GET, POST, OPTIONS } = handler;
```
## RSC Setup