Illuminated server racks in a data center

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

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.