Adding authentication to your Claude app with NextAuth
In brief
Protect your API routes, tie conversations to users, and track per-user costs. The full integration from NextAuth setup to authenticated Claude calls.
Contents
Without authentication, anyone who finds your Claude app can use your API key. Adding auth is not optional once you move past localhost — it protects your budget, enables per-user features, and is the foundation for conversation history, rate limiting, and billing.
This guide covers: NextAuth setup, protecting your API routes, passing user identity to Supabase, and tying Claude conversations to authenticated users.
1. Install and configure NextAuth
npm install next-auth @auth/supabase-adapter
Create app/api/auth/[...nextauth]/route.ts:
import NextAuth from 'next-auth'
import Google from 'next-auth/providers/google'
import { SupabaseAdapter } from '@auth/supabase-adapter'
const handler = NextAuth({
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
adapter: SupabaseAdapter({
url: process.env.NEXT_PUBLIC_SUPABASE_URL!,
secret: process.env.SUPABASE_SERVICE_ROLE_KEY!,
}),
callbacks: {
session({ session, user }) {
// Add user ID to session so we can use it in API routes
session.user.id = user.id
return session
},
},
})
export { handler as GET, handler as POST }
Add to .env.local:
GOOGLE_CLIENT_ID=your_client_id
GOOGLE_CLIENT_SECRET=your_client_secret
NEXTAUTH_SECRET=generate_with_openssl_rand_base64_32
NEXTAUTH_URL=http://localhost:3000
2. Extend the session type
NextAuth's default session type does not include id. Extend it:
// types/next-auth.d.ts
import 'next-auth'
declare module 'next-auth' {
interface Session {
user: {
id: string
name?: string | null
email?: string | null
image?: string | null
}
}
}
3. Protect your Claude API route
// app/api/chat/route.ts
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import Anthropic from '@anthropic-ai/sdk'
export async function POST(request: Request) {
// Check authentication first
const session = await getServerSession(authOptions)
if (!session?.user?.id) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
const { messages } = await request.json()
const userId = session.user.id
const anthropic = new Anthropic()
try {
const response = await anthropic.messages.create({
model: 'claude-opus-4-5',
max_tokens: 1024,
messages,
})
return Response.json({
content: response.content[0],
usage: response.usage,
})
} catch (err) {
console.error('Claude API error:', err)
return Response.json({ error: 'Claude call failed' }, { status: 500 })
}
}
4. Middleware to protect entire route groups
Rather than checking auth in every route handler, use Next.js middleware:
// middleware.ts
import { withAuth } from 'next-auth/middleware'
export default withAuth({
callbacks: {
authorized({ token }) {
return !!token
},
},
})
export const config = {
matcher: [
'/chat/:path*',
'/api/chat/:path*',
'/dashboard/:path*',
],
}
This redirects unauthenticated users to the sign-in page automatically.
5. Accessing user ID in client components
// components/Chat.tsx
'use client'
import { useSession } from 'next-auth/react'
export default function Chat() {
const { data: session, status } = useSession()
if (status === 'loading') return <p>Loading...</p>
if (!session) return <p>Please sign in</p>
return (
<div>
<p>Welcome, {session.user.name}</p>
{/* chat UI */}
</div>
)
}
6. Wrap your app with the session provider
// app/layout.tsx
import { getServerSession } from 'next-auth'
import { SessionProvider } from '@/components/SessionProvider'
import { authOptions } from '@/lib/auth'
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const session = await getServerSession(authOptions)
return (
<html>
<body>
<SessionProvider session={session}>
{children}
</SessionProvider>
</body>
</html>
)
}
// components/SessionProvider.tsx
'use client'
import { SessionProvider as NextAuthSessionProvider } from 'next-auth/react'
import type { Session } from 'next-auth'
export function SessionProvider({ session, children }: { session: Session | null; children: React.ReactNode }) {
return <NextAuthSessionProvider session={session}>{children}</NextAuthSessionProvider>
}
7. Sign in / sign out UI
// components/AuthButton.tsx
'use client'
import { signIn, signOut, useSession } from 'next-auth/react'
export default function AuthButton() {
const { data: session } = useSession()
if (session) {
return (
<button onClick={() => signOut()}>
Sign out ({session.user?.email})
</button>
)
}
return (
<button onClick={() => signIn('google')}>
Sign in with Google
</button>
)
}
What comes after auth
Once you have user IDs flowing, you can build:
- Per-user conversation history — store messages in Supabase with
user_idforeign key - Per-user rate limiting — track usage by
user_id(see the rate limiting guide) - Cost attribution — know exactly how many tokens each user is consuming
- Billing — gate features by plan, track usage against billing periods
Authentication is infrastructure. It feels like overhead until you need to refund a user's bill, debug a specific user's conversation, or block an abusive account. Do it early.
Further reading
- Claude API quickstart — the fastest path from zero to a working Claude API integration
- Available models — current Claude model IDs and context window sizes for your API calls
- Prompt caching — reducing API costs on repeated calls by caching system prompts and long conversation histories