Project Overview
In this comprehensive tutorial, we'll build a full-stack AI-powered content generation platform that allows users to generate text content using OpenAI's GPT models. The application will feature user authentication, a credit system, and a clean UI for generating and managing AI-generated content.
Tech Stack
- Frontend: Next.js 14 with TypeScript, Tailwind CSS
- Backend: Next.js API Routes (Node.js)
- Database: PostgreSQL with Prisma ORM
- Authentication: Next-Auth with Google OAuth
- AI Integration: OpenAI GPT-4 API
- Deployment: Vercel (frontend/API) and Railway (database)
- Payments: Stripe (for credit purchases)
Project Structure
ai-content-platform/
├── app/
│ ├── api/
│ │ ├── auth/
│ │ ├── generate/
│ │ ├── webhooks/
│ │ └── ...
│ ├── auth/
│ ├── dashboard/
│ ├── generate/
│ ├── layouts/
│ ├── components/
│ └── ...
├── lib/
│ ├── auth.ts
│ ├── db.ts
│ ├── openai.ts
│ └── ...
├── prisma/
│ └── schema.prisma
└── ...
Prerequisites
- Node.js 18+ installed
- PostgreSQL database
- OpenAI API account
- Google OAuth credentials
- Stripe account (optional)
Step 1: Setting Up the Project
Create a new Next.js application:
npx create-next-app@latest ai-content-platform --typescript --tailwind --eslint --app
cd ai-content-platform
Install additional dependencies:
npm install prisma @prisma/client next-auth @next-auth/prisma-adapter openai stripe @stripe/stripe-js
npm install -D @types/stripe
Step 2: Database Configuration
Initialize Prisma:
npx prisma init
Update prisma/schema.prisma
:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
credits Int @default(10)
accounts Account[]
sessions Session[]
generations Generation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Generation {
id String @id @default(cuid())
prompt String
content String
model String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
Set up your environment variables in .env.local
:
DATABASE_URL="your_postgresql_connection_string"
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="your_nextauth_secret"
OPENAI_API_KEY="your_openai_api_key"
GOOGLE_CLIENT_ID="your_google_oauth_client_id"
GOOGLE_CLIENT_SECRET="your_google_oauth_client_secret"
STRIPE_SECRET_KEY="your_stripe_secret_key"
STRIPE_WEBHOOK_SECRET="your_stripe_webhook_secret"
Generate the Prisma client and push the schema:
npx prisma generate
npx prisma db push
Step 3: Authentication Setup
Create lib/auth.ts
:
import { NextAuthOptions } from "next-auth";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import GoogleProvider from "next-auth/providers/google";
import { prisma } from "./db";
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
}),
],
callbacks: {
session({ session, user }) {
if (session.user) {
session.user.id = user.id;
session.user.credits = (user as any).credits;
}
return session;
},
},
};
Create lib/db.ts
:
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
Create the API route at app/api/auth/[...nextauth]/route.ts
:
import NextAuth from "next-auth";
import { authOptions } from "@/lib/auth";
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
Step 4: OpenAI Integration
Create lib/openai.ts
:
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
export const generateContent = async (prompt: string, model: string = "gpt-4") => {
try {
const completion = await openai.chat.completions.create({
model,
messages: [
{
role: "system",
content: "You are a helpful assistant that generates high-quality content.",
},
{
role: "user",
content: prompt,
},
],
max_tokens: 1000,
});
return completion.choices[0]?.message?.content || "No content generated";
} catch (error) {
console.error("OpenAI API error:", error);
throw new Error("Failed to generate content");
}
};
Step 5: Create the Generation API
Create app/api/generate/route.ts
:
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { prisma } from "@/lib/db";
import { generateContent } from "@/lib/openai";
export async function POST(req: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { prompt, model } = await req.json();
if (!prompt) {
return NextResponse.json({ error: "Prompt is required" }, { status: 400 });
}
// Check user credits
const user = await prisma.user.findUnique({
where: { id: session.user.id },
});
if (!user || user.credits < 1) {
return NextResponse.json({ error: "Insufficient credits" }, { status: 402 });
}
// Generate content
const content = await generateContent(prompt, model);
// Deduct credit and save generation
await prisma.$transaction([
prisma.user.update({
where: { id: user.id },
data: { credits: { decrement: 1 } },
}),
prisma.generation.create({
data: {
prompt,
content,
model: model || "gpt-4",
userId: user.id,
},
}),
]);
return NextResponse.json({ content });
} catch (error) {
console.error("Generation error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
Step 6: Create the Main Layout
Create app/layouts/RootLayout.tsx
:
import { ReactNode } from 'react';
import Navbar from '@/app/components/Navbar';
import { AuthProvider } from '@/app/components/AuthProvider';
interface RootLayoutProps {
children: ReactNode;
}
export default function RootLayout({ children }: RootLayoutProps) {
return (
<AuthProvider>
<div className="min-h-screen bg-gray-50">
<Navbar />
<main className="container mx-auto px-4 py-8">
{children}
</main>
</div>
</AuthProvider>
);
}
Create app/components/Navbar.tsx
:
'use client';
import { useSession, signIn, signOut } from "next-auth/react";
import Link from "next/link";
export default function Navbar() {
const { data: session } = useSession();
return (
<nav className="bg-white shadow-sm border-b">
<div className="container mx-auto px-4">
<div className="flex justify-between items-center h-16">
<Link href="/" className="text-xl font-bold text-indigo-600">
AI Content Generator
</Link>
<div className="flex items-center space-x-4">
{session ? (
<>
<Link href="/dashboard" className="text-gray-700 hover:text-indigo-600">
Dashboard
</Link>
<Link href="/generate" className="text-gray-700 hover:text-indigo-600">
Generate
</Link>
<span className="text-gray-700">
Credits: {session.user.credits}
</span>
<button
onClick={() => signOut()}
className="bg-gray-100 hover:bg-gray-200 text-gray-800 px-4 py-2 rounded-md"
>
Sign Out
</button>
</>
) : (
<button
onClick={() => signIn("google")}
className="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-md"
>
Sign In
</button>
)}
</div>
</div>
</div>
</nav>
);
}
Create app/components/AuthProvider.tsx
:
'use client';
import { SessionProvider } from "next-auth/react";
export function AuthProvider({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}
Update the main app/layout.tsx
:
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import RootLayout from './layouts/RootLayout';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'AI Content Generator',
description: 'Generate high-quality content with AI',
};
export default function Layout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<RootLayout>{children}</RootLayout>
</body>
</html>
);
}
Step 7: Create the Home Page
Create app/page.tsx
:
import Link from "next/link";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
export default async function Home() {
const session = await getServerSession(authOptions);
return (
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900 mb-6">
AI-Powered Content Generation
</h1>
<p className="text-xl text-gray-600 mb-8 max-w-2xl mx-auto">
Generate high-quality content for your blog, social media, or marketing
materials using advanced AI models. Save time and create engaging content
with just a few clicks.
</p>
{session ? (
<div className="space-y-4">
<Link
href="/generate"
className="inline-block bg-indigo-600 text-white px-6 py-3 rounded-lg text-lg font-medium hover:bg-indigo-700"
>
Start Generating Content
</Link>
<p className="text-gray-600">
You have {session.user.credits} credits remaining
</p>
</div>
) : (
<div className="space-y-4">
<Link
href="/api/auth/signin"
className="inline-block bg-indigo-600 text-white px-6 py-3 rounded-lg text-lg font-medium hover:bg-indigo-700"
>
Get Started - Sign In
</Link>
<p className="text-gray-600">
Sign up now and get 10 free credits to start generating content
</p>
</div>
)}
<div className="mt-16 grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="bg-white p-6 rounded-lg shadow-md">
<h3 className="text-xl font-semibold mb-3">Blog Content</h3>
<p className="text-gray-600">
Create engaging blog posts, articles, and long-form content on any topic.
</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h3 className="text-xl font-semibold mb-3">Social Media</h3>
<p className="text-gray-600">
Generate captions, posts, and content ideas for all social media platforms.
</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h3 className="text-xl font-semibold mb-3">Marketing Copy</h3>
<p className="text-gray-600">
Create compelling product descriptions, ads, and email marketing content.
</p>
</div>
</div>
</div>
);
}
Step 8: Create the Generation Page
Create app/generate/page.tsx
:
'use client';
import { useState } from 'react';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
export default function GeneratePage() {
const [prompt, setPrompt] = useState('');
const [model, setModel] = useState('gpt-4');
const [isGenerating, setIsGenerating] = useState(false);
const [generatedContent, setGeneratedContent] = useState('');
const [error, setError] = useState('');
const { data: session, status } = useSession();
const router = useRouter();
if (status === 'loading') {
return <div className="text-center">Loading...</div>;
}
if (!session) {
router.push('/');
return null;
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsGenerating(true);
setError('');
try {
const response = await fetch('/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ prompt, model }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to generate content');
}
setGeneratedContent(data.content);
} catch (err: any) {
setError(err.message);
} finally {
setIsGenerating(false);
}
};
return (
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold text-gray-900 mb-6">Generate Content</h1>
<div className="bg-white p-6 rounded-lg shadow-md mb-6">
<p className="text-lg mb-4">
Credits remaining: <span className="font-semibold">{session.user.credits}</span>
</p>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="model" className="block text-sm font-medium text-gray-700 mb-1">
AI Model
</label>
<select
id="model"
value={model}
onChange={(e) => setModel(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
<option value="gpt-4">GPT-4</option>
<option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
</select>
</div>
<div>
<label htmlFor="prompt" className="block text-sm font-medium text-gray-700 mb-1">
Your Prompt
</label>
<textarea
id="prompt"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
placeholder="Describe what you want to generate. For example: 'Write a blog post about the benefits of renewable energy'"
required
/>
</div>
<button
type="submit"
disabled={isGenerating || session.user.credits < 1}
className="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{isGenerating ? 'Generating...' : `Generate (1 credit)`}
</button>
</form>
{error && (
<div className="mt-4 p-3 bg-red-100 text-red-700 rounded-md">
{error}
</div>
)}
</div>
{generatedContent && (
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">Generated Content</h2>
<div className="prose max-w-none">
{generatedContent.split('\n').map((paragraph, index) => (
<p key={index}>{paragraph}</p>
))}
</div>
<div className="mt-6 flex space-x-4">
<button
onClick={() => navigator.clipboard.writeText(generatedContent)}
className="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200"
>
Copy to Clipboard
</button>
<button
onClick={() => setGeneratedContent('')}
className="bg-indigo-100 text-indigo-700 px-4 py-2 rounded-md hover:bg-indigo-200"
>
Generate Again
</button>
</div>
</div>
)}
</div>
);
}
Step 9: Create the Dashboard Page
Create app/dashboard/page.tsx
:
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { prisma } from "@/lib/db";
import Link from "next/link";
export default async function DashboardPage() {
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return (
<div className="text-center">
<p>You need to be signed in to view this page.</p>
<Link href="/api/auth/signin" className="text-indigo-600 hover:text-indigo-800">
Sign In
</Link>
</div>
);
}
const generations = await prisma.generation.findMany({
where: { userId: session.user.id },
orderBy: { createdAt: "desc" },
take: 10,
});
return (
<div className="max-w-6xl mx-auto">
<h1 className="text-3xl font-bold text-gray-900 mb-6">Dashboard</h1>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-2">Credits Available</h2>
<p className="text-3xl font-bold text-indigo-600">{session.user.credits}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-2">Total Generations</h2>
<p className="text-3xl font-bold text-indigo-600">{generations.length}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-2">Get More Credits</h2>
<button className="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">
Purchase Credits
</button>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">Recent Generations</h2>
{generations.length === 0 ? (
<p className="text-gray-600">You haven't generated any content yet.</p>
) : (
<div className="space-y-4">
{generations.map((generation) => (
<div key={generation.id} className="border-b border-gray-200 pb-4 last:border-0">
<div className="flex justify-between items-start mb-2">
<h3 className="font-medium text-gray-900">
{generation.prompt.length > 60
? `${generation.prompt.substring(0, 60)}...`
: generation.prompt}
</h3>
<span className="text-sm text-gray-500">
{new Date(generation.createdAt).toLocaleDateString()}
</span>
</div>
<p className="text-gray-600 text-sm">
{generation.content.length > 120
? `${generation.content.substring(0, 120)}...`
: generation.content}
</p>
<div className="text-xs text-gray-500 mt-1">
Model: {generation.model}
</div>
</div>
))}
</div>
)}
<div className="mt-6">
<Link
href="/generate"
className="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700"
>
Generate New Content
</Link>
</div>
</div>
</div>
);
}
Step 10: Add Global Styles
Update app/globals.css
:
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
.prose p {
margin-bottom: 1rem;
line-height: 1.7;
}
Step 11: Deployment
Create next.config.js
:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverComponentsExternalPackages: ['@prisma/client', 'bcrypt'],
},
}
module.exports = nextConfig
Create vercel.json
for deployment configuration:
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/next"
}
],
"env": {
"DATABASE_URL": "@database_url",
"NEXTAUTH_URL": "@nextauth_url",
"NEXTAUTH_SECRET": "@nextauth_secret",
"OPENAI_API_KEY": "@openai_api_key",
"GOOGLE_CLIENT_ID": "@google_client_id",
"GOOGLE_CLIENT_SECRET": "@google_client_secret"
}
}
Step 12: Running the Application
- Start the development server:
npm run dev
- Open http://localhost:3000 in your browser.
Conclusion
You've now built a complete AI-powered content generation platform with user authentication, credit management, and integration with OpenAI's GPT models. This application provides a solid foundation that you can extend with additional features like:
- More AI model options
- Content templates for different use cases
- Team collaboration features
- Advanced content editing capabilities
- Content analytics and performance tracking
The platform demonstrates modern full-stack development practices with Next.js 14, TypeScript, Prisma, and NextAuth, making it a great addition to your portfolio.
Top comments (0)