This guide covers building a production SaaS from scratch: user accounts, a dashboard, file uploads, optional AI-powered document processing, and subscription billing. The stack works for any B2B or B2C SaaS: Next.js 16, React 19, TypeScript, Tailwind CSS v4, Prisma with libSQL/Turso, NextAuth, Stripe, Claude AI, and deployment on Railway with Docker.
Follow these steps and commands to launch your own SaaS.
Prerequisites
- Node.js 20+ and npm
- Accounts: Turso, Stripe, Railway, Anthropic
- Basic familiarity with React and SQL
Step 1: Scaffold the Next.js App
npx create-next-app@latest my-saas-app \ --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" cd my-saas-app npm run dev # Open http://localhost:3000
Step 2: Install Core Dependencies
npm install @prisma/client @auth/prisma-adapter next-auth bcryptjs npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-select npm install stripe @stripe/stripe-js resend nodemailer npm install @anthropic-ai/sdk pdfkit pdf-parse xlsx npm install -D prisma @types/bcryptjs @types/pdfkit
Step 3: Database with Prisma and Turso
Initialize Prisma with the libSQL provider for Turso (edge-compatible SQLite):
npx prisma init --datasource-provider sqlite
# schema.prisma — use libsql adapter
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
# Create a Turso database:
turso db create my-saas-app
turso db show my-saas-app --url
turso db tokens create my-saas-app
# .env
DATABASE_URL="libsql://my-saas-app-<org>.turso.io?authToken=<token>"
Define your models (users, organizations, documents, subscriptions, and whatever domain entities your product needs):
npx prisma migrate dev --name init npx prisma generate
Step 4: Authentication with NextAuth v4
We use NextAuth v4 here because it is what we run in production on TradeGuard and it is battle-tested. If you are starting a brand-new project, evaluate Auth.js v5 first — it is the current successor with first-class App Router support; the concepts below carry over directly.
// src/app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"
import bcrypt from "bcryptjs"
export const authOptions = {
adapter: PrismaAdapter(prisma),
providers: [
CredentialsProvider({
name: "credentials",
credentials: { email: {}, password: {} },
async authorize(credentials) {
const user = await prisma.user.findUnique({
where: { email: credentials!.email! }
})
if (!user || !await bcrypt.compare(credentials!.password!, user.passwordHash))
return null
return user
}
})
],
session: { strategy: "jwt" },
pages: { signIn: "/login" }
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }
Hash passwords on registration:
const hash = await bcrypt.hash(password, 12)
await prisma.user.create({ data: { email, passwordHash: hash, name } })
Step 5: Stripe Subscription Billing
Create products and prices in the Stripe Dashboard, then wire checkout:
// src/app/api/stripe/checkout/route.ts
import Stripe from "stripe"
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(req: Request) {
const { priceId, userId } = await req.json()
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_URL}/dashboard?success=1`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
metadata: { userId }
})
return Response.json({ url: session.url })
}
Add a webhook handler for checkout.session.completed and customer.subscription.updated to sync subscription status to your database.
stripe listen --forward-to localhost:3000/api/stripe/webhook
Step 6: Customer Portal and Document Upload
Build API routes for file upload, store files on disk or S3-compatible storage, and save metadata in Prisma. A customer or guest portal can live at a separate route group (/portal/[token]) with magic-link access — no full account required.
// Upload handler — save file, create Document record
export async function POST(req: Request) {
const formData = await req.formData()
const file = formData.get("file") as File
const buffer = Buffer.from(await file.arrayBuffer())
const path = `uploads/${crypto.randomUUID()}-${file.name}`
await fs.writeFile(path, buffer)
await prisma.document.create({ data: { path, type: "upload", userId } })
return Response.json({ ok: true })
}
Step 7: AI Document Processing with Claude
import Anthropic from "@anthropic-ai/sdk"
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! })
async function analyzeDocument(pdfText: string) {
const msg = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 1024,
messages: [{
role: "user",
content: `Analyze this document and summarize key fields:\n\n${pdfText}`
}]
})
return msg.content[0].type === "text" ? msg.content[0].text : ""
}
Extract PDF text with pdf-parse, then store AI analysis results alongside the document record.
Step 8: PDF Reports and Email Alerts
npm install pdfkit resend
// Generate a PDF report with PDFKit
const doc = new PDFDocument()
doc.text("Monthly Report", 50, 50)
// pipe to buffer, attach to email via Resend
await resend.emails.send({
from: "alerts@yourdomain.com",
to: user.email,
subject: "Your subscription renews soon",
html: "<p>Your plan renews in 7 days.</p>"
})
Step 9: UI with Tailwind v4 and Radix
Build dashboard pages with Tailwind utility classes and accessible Radix primitives for dialogs, dropdowns, and data tables. Use server components for data fetching where possible:
// src/app/dashboard/page.tsx
import { getServerSession } from "next-auth"
import { redirect } from "next/navigation"
export default async function DashboardPage() {
const session = await getServerSession(authOptions)
if (!session) redirect("/login")
const docs = await prisma.document.findMany({ where: { orgId: session.user.orgId } })
return <DocumentTable documents={docs} />
}
Step 10: Docker and Railway Deployment
# Dockerfile FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npx prisma generate RUN npm run build FROM node:20-alpine WORKDIR /app COPY --from=builder /app/.next ./.next COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./ EXPOSE 3000 CMD ["npm", "start"]
# Deploy to Railway npm install -g @railway/cli railway login railway init railway add --plugin turso railway variables set DATABASE_URL="..." STRIPE_SECRET_KEY="..." NEXTAUTH_SECRET="..." railway up
Configure Litestream for continuous SQLite replication to S3 as a backup layer if self-hosting the database file.
Step 11: Environment Variables Checklist
- DATABASE_URL — Turso libSQL connection string
- NEXTAUTH_SECRET — random 32+ byte secret
- STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET
- ANTHROPIC_API_KEY
- RESEND_API_KEY
- NEXT_PUBLIC_URL — production domain
Tech Stack Summary
Next.js 16 • React 19 • TypeScript • Tailwind CSS v4 • Radix UI • Prisma • libSQL/Turso • NextAuth v4 • bcryptjs • Stripe • Resend • Claude AI • PDFKit • Docker • Railway • Litestream
This is the exact stack behind TradeGuard Pro, our production compliance SaaS. If you would rather have it built than build it, that is what we do.
