A working AI generation feature that authenticates users, checks their plan and credit usage before calling Claude, logs each generation to the database, and returns a 402 Payment Required when users exceed their limit.
Configure NextAuth
Set up authentication with Google OAuth. Users will log in with their Google account — no password management required.
import NextAuth from 'next-auth'
import Google from 'next-auth/providers/google'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from '@/lib/prisma'
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!
})
],
callbacks: {
async session({ session, user }) {
session.user.id = user.id
session.user.plan = (user as any).plan
session.user.creditsUsed = (user as any).creditsUsed
session.user.creditsLimit = (user as any).creditsLimit
return session
}
}
})The AI Generation API Route
The generation endpoint does four things: authenticate the user, check their credit limit, call Claude, and log the usage. All four must succeed or the request fails.
import { NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import Anthropic from '@anthropic-ai/sdk'
const claude = new Anthropic()
export async function POST(request: Request) {
// 1. Authenticate
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// 2. Check credits
const user = await prisma.user.findUnique({
where: { id: session.user.id },
select: { plan: true, creditsUsed: true, creditsLimit: true }
})
if (user!.plan === 'FREE' && user!.creditsUsed >= user!.creditsLimit) {
return NextResponse.json(
{ error: 'Credit limit reached. Upgrade to Pro for unlimited generations.', upgrade: true },
{ status: 402 } // Payment Required
)
}
// 3. Call Claude
const { prompt } = await request.json()
const response = await claude.messages.create({
model: 'claude-opus-4-5', max_tokens: 1024,
messages: [{ role: 'user', content: prompt }]
})
const result = response.content[0].text
// 4. Log usage atomically
await prisma.$transaction([
prisma.generation.create({
data: { userId: session.user.id, prompt, result, tokens: response.usage.output_tokens }
}),
prisma.user.update({
where: { id: session.user.id },
data: { creditsUsed: { increment: 1 } }
})
])
return NextResponse.json({ result })
}Dashboard: Show Usage and History
Show users how many credits they've used and their recent generations. This is the core dashboard of any AI SaaS.
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { redirect } from 'next/navigation'
export default async function Dashboard() {
const session = await auth()
if (!session) redirect('/login')
const [user, recentGenerations] = await Promise.all([
prisma.user.findUnique({
where: { id: session.user.id },
select: { plan: true, creditsUsed: true, creditsLimit: true }
}),
prisma.generation.findMany({
where: { userId: session.user.id },
orderBy: { createdAt: 'desc' },
take: 5
})
])
return (
<div>
<h1>Welcome, {session.user.name}</h1>
<p>Plan: {user?.plan} | Credits: {user?.creditsUsed}/{user?.creditsLimit}</p>
</div>
)
}The Generate Page
The AI generation UI: a prompt input, submit button, and result display. Handles the 402 upgrade prompt gracefully.
'use client'
import { useState } from 'react'
export default function GeneratePage() {
const [prompt, setPrompt] = useState('')
const [result, setResult] = useState('')
const [loading, setLoading] = useState(false)
const [showUpgrade, setShowUpgrade] = useState(false)
const generate = async () => {
setLoading(true)
const res = await fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt })
})
const data = await res.json()
if (res.status === 402) { setShowUpgrade(true) }
else { setResult(data.result) }
setLoading(false)
}
return (
<div>
{showUpgrade && <div className="upgrade-banner">You've used all your free credits. <a href="/pricing">Upgrade to Pro</a></div>}
<textarea value={prompt} onChange={e => setPrompt(e.target.value)} />
<button onClick={generate} disabled={loading}>{loading ? 'Generating...' : 'Generate'}</button>
{result && <div className="result">{result}</div>}
</div>
)
}What You Learned Today
- Configured NextAuth with Google OAuth and added plan/credits to the session object
- Built a generation API route that authenticates, checks credits, calls Claude, and logs usage in a transaction
- Displayed real-time usage data and generation history from the database on the dashboard
- Handled the 402 Payment Required response by showing an upgrade prompt in the UI
Go Further on Your Own
- Add a rate limiter using Upstash Redis to prevent a single user from abusing the API within a short window
- Create a /api/usage endpoint that returns the user's current month usage stats as JSON
- Add email verification so users must confirm their email before getting any free credits
Nice work. Keep going.
Day 3 is ready when you are.
Continue to Day 3Want live instruction and hands-on projects? Join the AI bootcamp — 3 days, 5 cities.