Setup
Set up kitcn with Better Auth for Next.js App Router.
In this guide, we'll set up kitcn with Better Auth for Next.js App Router. You'll configure environment variables, create the caller factory, set up the client provider, and prepare RSC helpers for server-side rendering.
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 |
Let's set it up.
Prerequisite: Complete Auth Server first to configure your Convex backend.
Installation
First, install the required packages:
bun 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
Now let's create the RSC helpers file:
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.