Build a Full-Stack AI SaaS App with Next.js 16, Clerk, and Supabase 2026
Step-by-step guide to building a production-ready AI SaaS application with authentication, usage limits, subscription billing, and AI features
Build a Full-Stack AI SaaS App with Next.js 16, Clerk, and Supabase 2026
Step-by-step guide to building a production-ready AI SaaS application with authentication, usage limits, subscription billing, and AI features
Complete tutorial for building a full-stack AI SaaS application using Next.js 16, Clerk for authentication, Supabase for database, and OpenAI for AI features. Covers user management, usage metering, stripe billing, and deploying to production.
Build a Full-Stack AI SaaS App with Next.js 16, Clerk, and Supabase 2026
This tutorial builds a complete AI writing tool SaaS from scratch. You'll have a deployable app with authentication, usage limits, paid plans, and AI features by the end.
Architecture Overview
Next.js 16 (App Router)
├── Authentication: Clerk (SSO, magic links, org management)
├── Database: Supabase (Postgres + pgvector)
├── AI: OpenAI API (GPT-4o)
├── Billing: Stripe (subscriptions)
└── Deployment: Vercel
1. Project Setup
bash
npx create-next-app@latest ai-saas --typescript --tailwind --app
cd ai-saas
bun add @clerk/nextjs @supabase/supabase-js openai stripe @stripe/stripe-js ai @ai-sdk/openai
2. Authentication with Clerk
typescript
// src/middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/api/ai(.*)',
'/api/usage(.*)',
]);
export default clerkMiddleware((auth, req) => {
if (isProtectedRoute(req)) auth().protect();
});
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};
typescript
// src/app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs';export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
3. Supabase Database Schema
sql
-- Users table (synced from Clerk)
CREATE TABLE users (
id TEXT PRIMARY KEY, -- Clerk user ID
email TEXT NOT NULL,
name TEXT,
plan TEXT DEFAULT 'free' CHECK (plan IN ('free', 'starter', 'pro')),
created_at TIMESTAMPTZ DEFAULT NOW()
);-- Usage tracking
CREATE TABLE usage_logs (
id BIGSERIAL PRIMARY KEY,
user_id TEXT REFERENCES users(id),
action TEXT NOT NULL, -- 'generate', 'improve', 'translate'
tokens_used INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Monthly usage view
CREATE VIEW monthly_usage AS
SELECT
user_id,
DATE_TRUNC('month', created_at) AS month,
COUNT(*) AS request_count,
SUM(tokens_used) AS total_tokens
FROM usage_logs
GROUP BY user_id, DATE_TRUNC('month', created_at);
-- Plan limits
CREATE TABLE plan_limits (
plan TEXT PRIMARY KEY,
monthly_requests INTEGER,
monthly_tokens INTEGER
);
INSERT INTO plan_limits VALUES
('free', 20, 50000),
('starter', 200, 500000),
('pro', 2000, 5000000);
4. Usage Metering
typescript
// src/lib/usage.ts
import { createClient } from '@/lib/supabase/server';
import { auth } from '@clerk/nextjs/server';export async function checkUsageLimit(action: string): Promise {
const { userId } = await auth();
if (!userId) return false;
const supabase = createClient();
// Get user's plan and current month usage
const { data } = await supabase
.from('users')
.select(`
plan,
plan_limits!users_plan_fkey(monthly_requests)
`)
.eq('id', userId)
.single();
const monthStart = new Date();
monthStart.setDate(1);
monthStart.setHours(0, 0, 0, 0);
const { count } = await supabase
.from('usage_logs')
.select('*', { count: 'exact', head: true })
.eq('user_id', userId)
.gte('created_at', monthStart.toISOString());
const limit = data?.plan_limits?.monthly_requests ?? 20;
return (count ?? 0) < limit;
}
export async function logUsage(action: string, tokensUsed: number) {
const { userId } = await auth();
if (!userId) return;
const supabase = createClient();
await supabase.from('usage_logs').insert({
user_id: userId,
action,
tokens_used: tokensUsed,
});
}
5. AI Features
typescript
// src/app/api/ai/generate/route.ts
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
import { auth } from '@clerk/nextjs/server';
import { checkUsageLimit, logUsage } from '@/lib/usage';export async function POST(req: Request) {
const { userId } = await auth();
if (!userId) {
return new Response('Unauthorized', { status: 401 });
}
// Check usage limits
const canProceed = await checkUsageLimit('generate');
if (!canProceed) {
return new Response(
JSON.stringify({ error: 'Monthly limit reached. Please upgrade your plan.' }),
{ status: 429, headers: { 'Content-Type': 'application/json' } }
);
}
const { prompt, type } = await req.json();
const systemPrompts: Record = {
blog: 'You are an expert blog writer. Create engaging, SEO-optimized content.',
email: 'You are a professional email copywriter. Write clear, effective emails.',
social: 'You are a social media expert. Create engaging posts optimized for each platform.',
};
let tokensUsed = 0;
const result = streamText({
model: openai('gpt-4o'),
system: systemPrompts[type] ?? systemPrompts.blog,
prompt,
onFinish: async ({ usage }) => {
tokensUsed = usage.totalTokens;
await logUsage('generate', tokensUsed);
},
});
return result.toDataStreamResponse();
}
6. Stripe Billing
typescript
// src/app/api/stripe/create-checkout/route.ts
import Stripe from 'stripe';
import { auth } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const PLANS = {
starter: process.env.STRIPE_STARTER_PRICE_ID!,
pro: process.env.STRIPE_PRO_PRICE_ID!,
};
export async function POST(req: Request) {
const { userId, sessionClaims } = await auth();
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { plan } = await req.json() as { plan: 'starter' | 'pro' };
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card'],
line_items: [{ price: PLANS[plan], quantity: 1 }],
success_url: ${process.env.NEXT_PUBLIC_APP_URL}/dashboard?upgraded=true,
cancel_url: ${process.env.NEXT_PUBLIC_APP_URL}/pricing,
customer_email: sessionClaims?.email as string,
metadata: { userId, plan },
});
return NextResponse.json({ url: session.url });
}
typescript
// src/app/api/stripe/webhook/route.ts
import Stripe from 'stripe';
import { createAdminClient } from '@/lib/supabase/admin';const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: Request) {
const body = await req.text();
const sig = req.headers.get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return new Response('Webhook signature verification failed', { status: 400 });
}
const supabase = createAdminClient();
if (event.type === 'checkout.session.completed') {
const session = event.data.object as Stripe.CheckoutSession;
const { userId, plan } = session.metadata as { userId: string; plan: string };
// Update user's plan in database
await supabase.from('users').upsert({ id: userId, plan });
}
if (event.type === 'customer.subscription.deleted') {
// Downgrade to free on cancellation
const subscription = event.data.object as Stripe.Subscription;
const userId = subscription.metadata.userId;
await supabase.from('users').update({ plan: 'free' }).eq('id', userId);
}
return new Response('OK', { status: 200 });
}
7. Pricing Page
typescript
// src/app/pricing/page.tsx
import { auth } from '@clerk/nextjs/server';
import { PricingCards } from '@/components/pricing-cards';export default async function PricingPage() {
const { userId } = await auth();
const plans = [
{
name: 'Free',
price: '$0',
period: 'forever',
features: ['20 AI generations/month', 'Basic templates', 'Email support'],
cta: userId ? 'Current Plan' : 'Get Started',
disabled: true,
},
{
name: 'Starter',
price: '$19',
period: '/month',
features: ['200 AI generations/month', 'All templates', 'Priority support', 'API access'],
cta: 'Upgrade to Starter',
plan: 'starter',
highlighted: true,
},
{
name: 'Pro',
price: '$49',
period: '/month',
features: ['2000 AI generations/month', 'Custom AI personas', 'Team collaboration', 'Analytics'],
cta: 'Upgrade to Pro',
plan: 'pro',
},
];
return (
Simple, transparent pricing
);
}
8. Deployment Checklist
bash
Environment variables needed:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=...
OPENAI_API_KEY=sk-...
STRIPE_SECRET_KEY=sk_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_STARTER_PRICE_ID=price_...
STRIPE_PRO_PRICE_ID=price_...
NEXT_PUBLIC_APP_URL=https://your-app.vercel.app
Conclusion
This architecture handles most AI SaaS requirements: authentication with Clerk, data persistence with Supabase, usage metering, subscription billing with Stripe, and AI features with the Vercel AI SDK. The total backend cost for under 100 users: ~$50/month (Supabase Pro + Vercel Pro + OpenAI usage). Revenue needed to be profitable: 3 Starter plan subscribers.
相关工具
相关教程
Automatically classify, summarize, and draft replies to emails using AI
Build voice AI applications with natural-sounding TTS and custom voice cloning
Transcribe audio files, meetings, and real-time speech with Whisper