// app/ConvexClientProvider.tsx
'use client';
import { ConvexProvider, ConvexReactClient } from 'convex/react';
import { ReactNode } from 'react';
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export function ConvexClientProvider({ children }: { children: ReactNode }) {
return <ConvexProvider client={convex}>{children}</ConvexProvider>;
}
// app/layout.tsx
import { ConvexClientProvider } from './ConvexClientProvider';
export default function RootLayout({ children }) {
return (
<html>
<body>
<ConvexClientProvider>{children}</ConvexClientProvider>
</body>
</html>
);
}// app/posts/page.tsx (Server Component)
import { preloadQuery } from 'convex/nextjs';
import { api } from '@/convex/_generated/api';
import PostList from './PostList';
export default async function PostsPage() {
// Fetch on server
const preloadedPosts = await preloadQuery(api.posts.list);
return (
<div>
<h1>Posts</h1>
<PostList preloadedPosts={preloadedPosts} />
</div>
);
}
// app/posts/PostList.tsx (Client Component)
'use client';
import { usePreloadedQuery, Preloaded } from 'convex/react';
import { api } from '@/convex/_generated/api';
export default function PostList({
preloadedPosts,
}: {
preloadedPosts: Preloaded<typeof api.posts.list>;
}) {
// Hydrates with server data, then stays reactive
const posts = usePreloadedQuery(preloadedPosts);
return (
<div>
{posts.map(post => (
<article key={post._id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</article>
))}
</div>
);
}
// With arguments
export default async function UserPage({ params }) {
const preloaded = await preloadQuery(api.users.get, { id: params.userId });
return <UserProfile preloaded={preloaded} />;
}// app/posts/[id]/page.tsx
import { fetchQuery } from 'convex/nextjs';
import { api } from '@/convex/_generated/api';
export default async function PostPage({ params }) {
const post = await fetchQuery(api.posts.get, { id: params.id });
if (!post) return <div>Post not found</div>;
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}
// Generate static params
export async function generateStaticParams() {
const posts = await fetchQuery(api.posts.list, {});
return posts.map(post => ({ id: post._id }));
}
// API Route
// app/api/stats/route.ts
import { fetchQuery } from 'convex/nextjs';
import { api } from '@/convex/_generated/api';
import { NextResponse } from 'next/server';
export async function GET() {
const stats = await fetchQuery(api.stats.get, {});
return NextResponse.json(stats);
}// app/posts/create/page.tsx
import { fetchMutation } from 'convex/nextjs';
import { api } from '@/convex/_generated/api';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export default function CreatePostPage() {
async function createPost(formData: FormData) {
'use server';
const title = formData.get('title') as string;
const body = formData.get('body') as string;
const postId = await fetchMutation(api.posts.create, { title, body });
revalidatePath('/posts');
redirect(`/posts/${postId}`);
}
return (
<form action={createPost}>
<input name="title" placeholder="Title" required />
<textarea name="body" placeholder="Body" required />
<button type="submit">Create Post</button>
</form>
);
}
// Alternative: separate server action file
// app/actions.ts
'use server';
import { fetchMutation } from 'convex/nextjs';
import { api } from '@/convex/_generated/api';
export async function deletePost(postId: string) {
await fetchMutation(api.posts.delete, { id: postId });
revalidatePath('/posts');
}
// Use in client component
'use client';
import { deletePost } from './actions';
function DeleteButton({ postId }: { postId: string }) {
return (
<button onClick={() => deletePost(postId)}>
Delete
</button>
);
}'use server';
import { fetchAction } from 'convex/nextjs';
import { api } from '@/convex/_generated/api';
export async function sendEmail(to: string, subject: string, body: string) {
await fetchAction(api.emails.send, { to, subject, body });
}
export async function generateReport(userId: string) {
return await fetchAction(api.reports.generate, { userId });
}// Server Component
import { auth } from '@clerk/nextjs';
import { preloadQuery } from 'convex/nextjs';
import { api } from '@/convex/_generated/api';
export default async function PrivatePage() {
const { getToken } = auth();
const token = await getToken({ template: 'convex' });
const preloadedData = await preloadQuery(
api.users.getCurrent,
{},
{ token }
);
return <UserProfile preloaded={preloadedData} />;
}
// Server Action
async function updateProfile(formData: FormData) {
'use server';
const { getToken } = auth();
const token = await getToken({ template: 'convex' });
const bio = formData.get('bio') as string;
await fetchMutation(
api.users.updateProfile,
{ bio },
{ token }
);
revalidatePath('/profile');
}
// Client Provider
// app/ConvexClientProvider.tsx
'use client';
import { ConvexProviderWithClerk } from 'convex/react-clerk';
import { ConvexReactClient } from 'convex/react';
import { ClerkProvider, useAuth } from '@clerk/nextjs';
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export function ConvexClientProvider({ children }) {
return (
<ClerkProvider>
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
{children}
</ConvexProviderWithClerk>
</ClerkProvider>
);
}// Similar pattern with Auth0
import { ConvexProviderWithAuth0 } from 'convex/react-auth0';
import { Auth0Provider } from '@auth0/auth0-react';
// In client provider
<Auth0Provider domain="..." clientId="...">
<ConvexProviderWithAuth0 client={convex}>
{children}
</ConvexProviderWithAuth0>
</Auth0Provider>// app/api/webhooks/stripe/route.ts
import { fetchMutation } from 'convex/nextjs';
import { api } from '@/convex/_generated/api';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const body = await request.json();
// Verify webhook signature
const signature = request.headers.get('stripe-signature');
if (!verifySignature(signature, body)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
// Process via Convex
await fetchMutation(api.webhooks.processStripe, body);
return NextResponse.json({ success: true });
}
// app/api/data/route.ts - With auth
import { auth } from '@clerk/nextjs';
export async function GET() {
const { getToken } = auth();
const token = await getToken({ template: 'convex' });
const data = await fetchQuery(api.data.getPrivate, {}, { token });
return NextResponse.json(data);
}// All server-side functions support these options
const options = {
token: 'jwt-token-string', // For user authentication
adminToken: process.env.CONVEX_DEPLOY_KEY, // For admin operations
url: 'https://custom.convex.cloud', // Override deployment URL
skipConvexDeploymentUrlCheck: true, // For development
};
await preloadQuery(api.fn, args, options);
await fetchQuery(api.fn, args, options);
await fetchMutation(api.fn, args, options);
await fetchAction(api.fn, args, options);// app/layout.tsx
import { ConvexClientProvider } from './ConvexClientProvider';
export default function RootLayout({ children }) {
return (
<html>
<body>
<ConvexClientProvider>{children}</ConvexClientProvider>
</body>
</html>
);
}
// app/posts/page.tsx (Server Component)
import { preloadQuery } from 'convex/nextjs';
import { api } from '@/convex/_generated/api';
import PostList from './PostList';
import CreatePostForm from './CreatePostForm';
export default async function PostsPage() {
const preloadedPosts = await preloadQuery(api.posts.list);
return (
<main>
<h1>Posts</h1>
<CreatePostForm />
<PostList preloadedPosts={preloadedPosts} />
</main>
);
}
// app/posts/PostList.tsx (Client Component - reactive)
'use client';
import { usePreloadedQuery } from 'convex/react';
import { api } from '@/convex/_generated/api';
export default function PostList({ preloadedPosts }) {
const posts = usePreloadedQuery(preloadedPosts);
return (
<div>
{posts.map(post => (
<article key={post._id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</article>
))}
</div>
);
}
// app/posts/CreatePostForm.tsx (Client Component with Server Action)
'use client';
import { createPost } from './actions';
export default function CreatePostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" required />
<textarea name="body" placeholder="Body" required />
<button type="submit">Create</button>
</form>
);
}
// app/posts/actions.ts (Server Actions)
'use server';
import { fetchMutation } from 'convex/nextjs';
import { api } from '@/convex/_generated/api';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const body = formData.get('body') as string;
const postId = await fetchMutation(api.posts.create, { title, body });
revalidatePath('/posts');
redirect(`/posts/${postId}`);
}
export async function deletePost(postId: string) {
await fetchMutation(api.posts.delete, { id: postId });
revalidatePath('/posts');
}