Building an AI Image Generator SaaS — Complete Stack Guide
AI image generators are one of the highest-converting SaaS categories right now. Tools like Midjourney, Leonardo.ai, and NightCafe have proven the market — but there's room for vertical, niche, and white-label solutions. This guide walks you through building your own, from architecture to deployment.
We'll use Next.js for the frontend, PixelAPI as the AI backend, and standard tools for auth, payments, and storage. No GPUs required.
Architecture Overview
The Tech Stack
🖥️ Frontend
Next.js 14+ (App Router), Tailwind CSS, React Query for data fetching
⚡ AI Backend
PixelAPI — generation, upscaling, background removal, face restoration
🔐 Auth
NextAuth.js (or Clerk) — Google, GitHub, email/password
💳 Payments
Stripe — subscriptions + one-time credit packs
🗄️ Database
PostgreSQL via Prisma ORM — users, credits, generation history
📦 Storage
Cloudflare R2 or AWS S3 — generated images, user uploads
Step 1: Project Setup
Terminalnpx create-next-app@latest ai-image-saas --typescript --tailwind --app
cd ai-image-saas
npm install pixelapi @prisma/client next-auth stripe
npm install -D prisma
Database Schema
prisma/schema.prismagenerator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
image String?
credits Int @default(50) // free starter credits
plan String @default("free")
generations Generation[]
transactions Transaction[]
createdAt DateTime @default(now())
}
model Generation {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
prompt String
model String
width Int
height Int
imageUrl String
thumbnailUrl String?
creditCost Int
status String @default("completed")
metadata Json?
createdAt DateTime @default(now())
@@index([userId, createdAt])
}
model Transaction {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
type String // "purchase", "usage", "bonus"
amount Int // positive = credit, negative = debit
details String?
createdAt DateTime @default(now())
@@index([userId])
}
Step 2: The Generation API Route
This is the core of your app — the API route that takes a user's prompt, checks credits, calls PixelAPI, saves the result, and returns the image.
app/api/generate/route.tsimport { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { prisma } from '@/lib/prisma';
import PixelAPI from 'pixelapi';
import { uploadToR2 } from '@/lib/storage';
const pixel = new PixelAPI({ apiKey: process.env.PIXELAPI_KEY! });
// Credit costs per model
const CREDIT_COSTS: Record<string, number> = {
'flux-schnell': 1,
'sdxl': 2,
'flux-schnell-hd': 3, // with upscaling
};
export async function POST(req: NextRequest) {
// 1. Authenticate
const session = await getServerSession();
if (!session?.user?.email) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// 2. Parse & validate request
const { prompt, model = 'flux-schnell', width = 1024, height = 1024 } = await req.json();
if (!prompt || prompt.length > 1000) {
return NextResponse.json({ error: 'Invalid prompt' }, { status: 400 });
}
const creditCost = CREDIT_COSTS[model] || 1;
// 3. Check & deduct credits (atomic)
const user = await prisma.user.update({
where: {
email: session.user.email,
credits: { gte: creditCost }, // only if enough credits
},
data: {
credits: { decrement: creditCost },
},
}).catch(() => null);
if (!user) {
return NextResponse.json({ error: 'Insufficient credits' }, { status: 402 });
}
try {
// 4. Generate image via PixelAPI
const result = await pixel.image.generate({
prompt,
model,
width,
height,
steps: model === 'flux-schnell' ? 4 : 30,
});
// 5. Upload to your storage
const imageUrl = await uploadToR2(
result.imageBytes,
`generations/${user.id}/${Date.now()}.png`
);
// 6. Save generation record
const generation = await prisma.generation.create({
data: {
userId: user.id,
prompt,
model,
width,
height,
imageUrl,
creditCost,
},
});
// 7. Log transaction
await prisma.transaction.create({
data: {
userId: user.id,
type: 'usage',
amount: -creditCost,
details: `Generated image: ${model}`,
},
});
return NextResponse.json({
id: generation.id,
imageUrl,
creditsRemaining: user.credits - creditCost,
});
} catch (error) {
// Refund credits on failure
await prisma.user.update({
where: { id: user.id },
data: { credits: { increment: creditCost } },
});
console.error('Generation failed:', error);
return NextResponse.json({ error: 'Generation failed' }, { status: 500 });
}
}
Step 3: The Frontend
Image Generator Component
components/ImageGenerator.tsx'use client';
import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
export function ImageGenerator() {
const [prompt, setPrompt] = useState('');
const [model, setModel] = useState('flux-schnell');
const queryClient = useQueryClient();
const generate = useMutation({
mutationFn: async () => {
const res = await fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, model, width: 1024, height: 1024 }),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error);
}
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['gallery'] });
queryClient.invalidateQueries({ queryKey: ['credits'] });
},
});
return (
<div className="max-w-2xl mx-auto p-6">
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe the image you want to create..."
className="w-full h-32 p-4 bg-gray-900 border border-gray-700
rounded-lg text-white resize-none focus:ring-2
focus:ring-purple-500 focus:border-transparent"
maxLength={1000}
/>
<div className="flex gap-3 mt-4">
<select
value={model}
onChange={(e) => setModel(e.target.value)}
className="px-4 py-2 bg-gray-900 border border-gray-700
rounded-lg text-white"
>
<option value="flux-schnell">FLUX Schnell (1 credit)</option>
<option value="sdxl">SDXL (2 credits)</option>
</select>
<button
onClick={() => generate.mutate()}
disabled={!prompt.trim() || generate.isPending}
className="px-6 py-2 bg-purple-600 hover:bg-purple-700
rounded-lg text-white font-semibold
disabled:opacity-50 disabled:cursor-not-allowed
transition-colors"
>
{generate.isPending ? 'Generating...' : 'Generate'}
</button>
</div>
{generate.isError && (
<p className="mt-3 text-red-400">{generate.error.message}</p>
)}
{generate.data && (
<div className="mt-6">
<img
src={generate.data.imageUrl}
alt={prompt}
className="rounded-lg w-full"
/>
<p className="mt-2 text-sm text-gray-400">
Credits remaining: {generate.data.creditsRemaining}
</p>
</div>
)}
</div>
);
}
Gallery Component
components/Gallery.tsx'use client';
import { useQuery } from '@tanstack/react-query';
export function Gallery() {
const { data: generations, isLoading } = useQuery({
queryKey: ['gallery'],
queryFn: async () => {
const res = await fetch('/api/generations');
return res.json();
},
});
if (isLoading) return <div className="text-gray-400">Loading gallery...</div>;
return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 p-6">
{generations?.map((gen: any) => (
<div key={gen.id} className="group relative">
<img
src={gen.imageUrl}
alt={gen.prompt}
className="rounded-lg w-full aspect-square object-cover
group-hover:ring-2 ring-purple-500 transition-all"
/>
<div className="absolute inset-0 bg-black/60 opacity-0
group-hover:opacity-100 transition-opacity
rounded-lg p-3 flex flex-col justify-end">
<p className="text-sm text-white line-clamp-3">{gen.prompt}</p>
<p className="text-xs text-gray-400 mt-1">{gen.model}</p>
</div>
</div>
))}
</div>
);
}
Step 4: Credit System & Payments
The credit model is the most common monetization approach for AI image tools. Here's a clean implementation:
| Plan | Price | Credits / Month | Cost per Image |
|---|---|---|---|
| Free | $0 | 50 | $0.00 |
| Starter | $9/mo | 500 | $0.018 |
| Pro | $29/mo | 2,000 | $0.0145 |
| Business | $79/mo | 8,000 | $0.0099 |
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { prisma } from '@/lib/prisma';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
const PLAN_CREDITS: Record<string, number> = {
'price_starter': 500,
'price_pro': 2000,
'price_business': 8000,
};
export async function POST(req: NextRequest) {
const body = await req.text();
const sig = req.headers.get('stripe-signature')!;
const event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
if (event.type === 'invoice.payment_succeeded') {
const invoice = event.data.object as Stripe.Invoice;
const priceId = invoice.lines.data[0]?.price?.id;
const customerId = invoice.customer as string;
const credits = PLAN_CREDITS[priceId!] || 0;
if (!credits) return NextResponse.json({ received: true });
// Find user by Stripe customer ID and add credits
const user = await prisma.user.findFirst({
where: { stripeCustomerId: customerId },
});
if (user) {
await prisma.$transaction([
prisma.user.update({
where: { id: user.id },
data: { credits: { increment: credits } },
}),
prisma.transaction.create({
data: {
userId: user.id,
type: 'purchase',
amount: credits,
details: `Subscription renewal: ${credits} credits`,
},
}),
]);
}
}
return NextResponse.json({ received: true });
}
Step 5: Why API vs Self-Hosting GPUs
The build-vs-buy decision for AI infrastructure is more clear-cut than most:
| Factor | Self-Hosted GPUs | PixelAPI |
|---|---|---|
| Upfront cost | $5,000–$30,000+ per GPU | $0 |
| Monthly infra | $500–$3,000+ (cloud GPU) | Pay per image ($0.002+) |
| Time to launch | Weeks–months | Hours |
| Scaling | Manual provisioning | Automatic |
| Model updates | You download, test, deploy | Available instantly |
| Cold starts | 30s–2min on serverless GPU | None |
| Maintenance | CUDA, drivers, OOM errors | None |
| Break-even | ~50,000+ images/month | Always cost-effective below that |
Self-hosting only makes financial sense when you're generating 50,000+ images per month consistently. Even then, you're trading capital cost for engineering complexity. Most successful AI SaaS companies start with APIs and only bring inference in-house after proving product-market fit.
Step 6: Deployment
The full deployment is straightforward:
- Frontend + API: Deploy to Vercel with
vercel deploy. The API routes run as serverless functions. - Database: Neon (serverless Postgres) or Supabase. Both have generous free tiers.
- Storage: Cloudflare R2 — S3-compatible, no egress fees. Perfect for serving images.
- Domain: Point your domain, set up SSL (automatic on Vercel).
- Monitoring: Sentry for errors, Vercel Analytics for performance.
# .env.local
PIXELAPI_KEY=px_live_...
DATABASE_URL=postgresql://...
NEXTAUTH_SECRET=...
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
R2_ACCESS_KEY_ID=...
R2_SECRET_ACCESS_KEY=...
R2_BUCKET=your-bucket
R2_ENDPOINT=https://xxx.r2.cloudflarestorage.com
Launch Checklist
- ☐ Rate limiting on generation endpoint (10 req/min per user)
- ☐ Content moderation — filter NSFW prompts before they hit the API
- ☐ Error handling with credit refunds on failed generations
- ☐ Image CDN with proper cache headers on R2
- ☐ Stripe webhook idempotency (prevent double-crediting)
- ☐ Email notifications for low credits
- ☐ Privacy policy & terms of service
- ☐ GDPR compliance — user data export & deletion
Further Reading
- 📖 PixelAPI Documentation — complete API reference
- 🎨 AI Editor — test PixelAPI's capabilities interactively
- 🚀 AI Image Generation Tutorial — deep dive into generation parameters
- 🔧 Automate Image Processing — add upscaling, BG removal, and more to your SaaS
Start Building Your AI SaaS Today
Free API access, excellent margins, and zero GPU headaches. Ship your product this weekend.
Get Your API Key →