Setup
Set up kitcn with Better Auth for Next.js App Router.
Overview
Setting up kitcn with Next.js involves these components:
| Component | Description |
|---|---|
| Caller factory | Creates server-side callers for RSC |
| Client provider | Auth-aware provider with SSR token |
| API route handler | Better Auth API routes |
| RSC helpers | Prefetching and hydration utilities |
Prerequisite: Complete Auth Server first to configure your Convex backend.
Installation
First, install the required packages:
npm install kitcn better-auth@1.5.3 @tanstack/react-query superjsonpnpm add kitcn better-auth@1.5.3 @tanstack/react-query superjsonyarn add kitcn better-auth@1.5.3 @tanstack/react-query superjsonbun add kitcn better-auth@1.5.3 @tanstack/react-query superjsonNote: Complete React Setup first to create your query-client.ts with hydrationConfig.
Environment Variables
Add these to your .env.local:
# WebSocket API (port 3210)
NEXT_PUBLIC_CONVEX_URL=http://localhost:3210
# HTTP routes (port 3211)
NEXT_PUBLIC_CONVEX_SITE_URL=http://localhost:3211
# Better Auth
NEXT_PUBLIC_SITE_URL=http://localhost:3000# Generated by Convex
NEXT_PUBLIC_CONVEX_URL=https://your-project.convex.cloud
# Add manually - replace .cloud with .site
NEXT_PUBLIC_CONVEX_SITE_URL=https://your-project.convex.site
# Better Auth
NEXT_PUBLIC_SITE_URL=http://localhost:3000Caller Factory
First, create a caller factory for Better Auth:
import { api } from '@convex/api';
import { convexBetterAuth } from 'kitcn/auth/nextjs';
export const { createContext, createCaller, handler } = convexBetterAuth({
api,
convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
});See the API Reference below for full details on what convexBetterAuth returns and its options.
Note: Not using Better Auth? See Server Side Calls.
Client Provider
Set up the client provider with Better Auth. The initialToken prop enables authenticated SSR:
'use client';
import { ConvexReactClient } from 'convex/react';
import { ConvexAuthProvider } from 'kitcn/auth/client';
import { inferAdditionalFields } from 'better-auth/client/plugins';
import { createAuthClient } from 'better-auth/react';
import { convexClient } from 'kitcn/auth/client';
import type { Auth } from '@convex/auth-shared';
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_SITE_URL!,
plugins: [inferAdditionalFields<Auth>(), convexClient()],
});
export function ConvexProvider({
children,
token,
}: {
children: React.ReactNode;
token?: string;
}) {
return (
<ConvexAuthProvider
client={convex}
authClient={authClient}
initialToken={token}
>
{children}
</ConvexAuthProvider>
);
}Use in your layout RSC to fetch the token server-side:
import { ConvexProvider } from '@/lib/convex/convex-provider';
import { caller } from '@/lib/convex/rsc';
export async function Layout({ children }: { children: React.ReactNode }) {
const token = await caller.getToken();
return <ConvexProvider token={token}>{children}</ConvexProvider>;
}This pattern ensures authenticated queries work on the initial SSR render.
API Route Handler
Create the Next.js API route for Better Auth:
import { handler } from '@/lib/convex/server';
export const { GET, POST } = handler;RSC Setup
import 'server-only';
import { api } from '@convex/api';
import type { FetchQueryOptions } from '@tanstack/react-query';
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query';
import {
createServerCRPCProxy,
getServerQueryClientOptions,
} from 'kitcn/rsc';
import { headers } from 'next/headers';
import { cache } from 'react';
import { hydrationConfig } from './query-client';
import { createCaller, createContext } from './server';
// RSC context factory - wraps createContext with cache() and next/headers
const createRSCContext = cache(async () =>
createContext({ headers: await headers() })
);
// Server caller - direct calls without caching/hydration
export const caller = createCaller(createRSCContext);
// Server-compatible cRPC proxy (queryOptions only)
export const crpc = createServerCRPCProxy({ api });
// Create server QueryClient with HTTP-based fetching
function createServerQueryClient() {
return new QueryClient({
defaultOptions: {
...hydrationConfig, // SuperJSON serialization for SSR
...getServerQueryClientOptions({
getToken: caller.getToken,
convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
}),
},
});
}
// Cache QueryClient per request
export const getQueryClient = cache(createServerQueryClient);
// Fire-and-forget prefetch for client hydration
export function prefetch<T extends { queryKey: readonly unknown[] }>(
queryOptions: T
): void {
void getQueryClient().prefetchQuery(queryOptions);
}
// Hydration wrapper for client components
export function HydrateClient({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient();
const dehydratedState = dehydrate(queryClient);
return (
<HydrationBoundary state={dehydratedState}>{children}</HydrationBoundary>
);
}
/**
* Preload a query - returns data + hydrates for client.
* Use for server-side data access (metadata, conditionals).
*/
export function preloadQuery<
TQueryFnData = unknown,
TError = Error,
TData = TQueryFnData,
TQueryKey extends readonly unknown[] = readonly unknown[],
>(
options: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>
): Promise<TData> {
return getQueryClient().fetchQuery(options);
}The RSC file above exports these helpers. See the Helpers reference for descriptions of each.
Next Steps
API Reference
convexBetterAuth
convexBetterAuth returns:
| Export | Description |
|---|---|
createContext | Creates RSC context with auth |
createCaller | Creates server-side caller |
handler | Next.js API route handler |
Options:
| Option | Description |
|---|---|
api | Your Convex API object |
convexSiteUrl | Convex site URL (must end in .convex.site) |
auth.jwtCache | Enable/disable JWT caching (default: true) |
auth.isUnauthorized | Custom UNAUTHORIZED error detection |
Helpers
The RSC Setup exports these helpers (see code above for implementations):
| Helper | Description |
|---|---|
caller | Direct server calls - not cached, not hydrated to client. Use for server-only logic. |
prefetch | Fire-and-forget prefetch. Data fetched in background and hydrated to client. |
HydrateClient | Wrapper that dehydrates prefetched queries for client hydration. |
preloadQuery | Awaited fetch that returns data on the server. Equivalent to Convex's preloadQuery. |
Server Proxy
createServerCRPCProxy creates a cRPC proxy for Server Components:
- Only supports
queryOptions(no mutations in RSC) - Generates the same query keys as the client proxy
import { createServerCRPCProxy } from 'kitcn/rsc';
// Server proxy - queryOptions only, no auth config
const crpc = createServerCRPCProxy({ api });
crpc.posts.list.queryOptions({}); // Works
crpc.posts.create.mutationOptions(); // Not available in RSCQuery Client Options
getServerQueryClientOptions configures the QueryClient for server-side HTTP fetching:
import { getServerQueryClientOptions } from 'kitcn/rsc';
// With auth + HTTP routes
const queryClient = new QueryClient({
defaultOptions: {
...getServerQueryClientOptions({
getToken: caller.getToken,
convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
}),
},
});
// Without auth (public queries only)
const queryClient = new QueryClient({
defaultOptions: {
...getServerQueryClientOptions(),
convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
},
});Date support is always on. If you pass a custom transformer, it is additive and should be shared across server, client, and caller boundaries.