Architectures Medium
Next.js SaaS Architecture
A production-ready architecture for a multi-tenant SaaS app using Next.js 15, Postgres, Auth.js, and Cloudflare. Includes a Mermaid diagram and file structure.
nextjstypescriptpostgresprismaauthjscloudflarestripe
Targets: Markdown
Code
Next.js SaaS Architecture
System diagram
graph TD
subgraph Client
Browser["Browser / Mobile"]
end
subgraph Edge["Cloudflare Edge"]
Pages["Cloudflare Pages\n(Next.js SSR)"]
Workers["Cloudflare Workers\n(API / Webhooks)"]
R2["R2 Storage\n(files, assets)"]
end
subgraph Auth
AuthJS["Auth.js v5\n(OAuth + magic link)"]
Session["Edge Session\n(JWT in cookie)"]
end
subgraph Data
Neon["Neon Postgres\n(primary DB)"]
Prisma["Prisma ORM\n(type-safe queries)"]
Redis["Upstash Redis\n(rate limiting, cache)"]
end
subgraph Services
Stripe["Stripe\n(subscriptions)"]
Resend["Resend\n(transactional email)"]
end
Browser --> Pages
Pages --> AuthJS
AuthJS --> Session
Pages --> Workers
Workers --> Prisma
Prisma --> Neon
Workers --> Redis
Workers --> Stripe
Workers --> Resend
Pages --> R2
File structure
src/
├── app/
│ ├── (auth)/
│ │ ├── login/page.tsx
│ │ └── register/page.tsx
│ ├── (dashboard)/
│ │ ├── layout.tsx # org context provider
│ │ ├── [orgSlug]/
│ │ │ ├── page.tsx
│ │ │ ├── settings/
│ │ │ └── billing/
│ ├── api/
│ │ ├── auth/[...nextauth]/route.ts
│ │ ├── webhooks/stripe/route.ts
│ │ └── trpc/[trpc]/route.ts
│ └── layout.tsx
├── components/
│ ├── ui/ # shadcn/ui primitives
│ └── app/ # domain components
├── lib/
│ ├── auth.ts # Auth.js config
│ ├── db.ts # Prisma client singleton
│ ├── stripe.ts # Stripe client
│ └── permissions.ts # RBAC helpers
├── middleware.ts # auth guard + org routing
└── types/
└── index.ts
Middleware (auth guard + org routing)
// middleware.ts
import { auth } from "@/lib/auth";
import { NextResponse } from "next/server";
export default auth((req) => {
const { pathname } = req.nextUrl;
// Public routes
if (pathname.startsWith("/login") || pathname.startsWith("/api/webhooks")) {
return NextResponse.next();
}
// Redirect unauthenticated users
if (!req.auth) {
return NextResponse.redirect(new URL("/login", req.url));
}
return NextResponse.next();
});
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
Row-level security pattern
// lib/db.ts — always scope queries to org
export async function getProjects(orgId: string, userId: string) {
return prisma.project.findMany({
where: {
organizationId: orgId, // always filter by org
organization: {
members: { some: { userId } }, // verify membership
},
},
});
}
Stripe webhook handler
// app/api/webhooks/stripe/route.ts
import Stripe from "stripe";
import { stripe } from "@/lib/stripe";
import { prisma } from "@/lib/db";
export async function POST(req: Request) {
const sig = req.headers.get("stripe-signature")!;
const body = await req.text();
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return new Response("Invalid signature", { status: 400 });
}
if (event.type === "customer.subscription.updated") {
const sub = event.data.object as Stripe.Subscription;
await prisma.organization.update({
where: { stripeCustomerId: sub.customer as string },
data: { plan: sub.status === "active" ? "pro" : "free" },
});
}
return new Response(null, { status: 200 });
}
Environment variables
# Auth
AUTH_SECRET=
AUTH_GITHUB_ID=
AUTH_GITHUB_SECRET=
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
# Database
DATABASE_URL=
# Stripe
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
# Email
RESEND_API_KEY=
A battle-tested architecture for building a multi-tenant SaaS product on Next.js 15. Copy the markdown into Claude or ChatGPT and use it as the starting point for your implementation plan.
Stack
| Layer | Choice |
|---|---|
| Framework | Next.js 15 (App Router) |
| Auth | Auth.js v5 (GitHub + Google OAuth + magic link) |
| Database | Postgres (Neon or Supabase) + Prisma ORM |
| Payments | Stripe (subscriptions + webhooks) |
| Resend | |
| Deployment | Cloudflare Pages + Workers |
| Storage | Cloudflare R2 |
Key decisions
- Multi-tenancy via subdomain or path — each org gets
org.app.comorapp.com/org - Row-level security — every DB query scoped to
organizationId - Middleware-first auth — Next.js middleware blocks unauthenticated requests before route handlers run
- Optimistic UI — React Server Components for initial load, client transitions for mutations