Next.js SaaS Tech Stack Guide 2026: Full Picks
Complete Next.js SaaS Tech Stack Guide 2026: Auth, DB, Payments, Email
Most "tech stack" articles hedge every decision. This one doesn't. These are the specific packages, services, and configurations that ship production SaaS in 2026, with enough setup code to get running in each category immediately.
The stack assumes: Next.js 15 App Router, TypeScript, Vercel deployment, early-stage SaaS (pre-Series A), one to three engineers.
TL;DR
| Category | Pick | Why |
|---|---|---|
| Auth | Better Auth | Free, self-hosted, passkeys + 2FA + orgs built in |
| Database | Neon (Postgres) | Serverless, branching per PR, $0 free tier |
| ORM | Drizzle | Smallest bundle, fastest cold start, native Neon adapter |
| Payments | Stripe | Universal support, best API, Stripe Tax for compliance |
| Email (transactional) | Resend | Developer-first, React Email templates, reliable delivery |
| Email (marketing) | Loops | SaaS-specific sequences, Resend-compatible |
| File uploads | UploadThing | Next.js native, type-safe, free tier covers early stage |
| Background jobs | Trigger.dev | Reliable queues, retries, dashboard, self-hostable |
| Analytics | PostHog | Free self-hosted or affordable cloud, session replays |
| Error tracking | Sentry | Industry standard, generous free tier |
| Deploy | Vercel | Zero-config Next.js, preview deployments per branch |
Authentication: Better Auth
Better Auth is the 2026 consensus pick for self-hosted Next.js auth. MIT license, batteries included (2FA, passkeys, organizations, RBAC), and no per-MAU costs that compound as you grow.
Setup
npm install better-auth
// lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { organization } from "better-auth/plugins";
import { twoFactor } from "better-auth/plugins";
import { passkey } from "better-auth/plugins";
import { db } from "./db";
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "pg" }),
emailAndPassword: { enabled: true, requireEmailVerification: true },
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
plugins: [
organization(),
twoFactor(),
passkey(),
],
});
// app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);
When to choose Clerk instead
Clerk ($0.02/MAU after 10K free) is the better choice if: you need pre-built UI components (no custom login pages), you're moving fast and don't want to maintain auth infrastructure, or your SaaS stays under 10,000 MAUs (free tier). Better Auth wins when you scale past 10K MAUs or have data residency requirements.
Database: Neon Serverless Postgres
Neon is the serverless Postgres platform built for exactly this stack. Key advantages: scale-to-zero (free tier never expires), database branching per GitHub PR, and a native HTTP driver for Edge deployments.
Setup
npm install @neondatabase/serverless drizzle-orm
npm install -D drizzle-kit
// lib/db.ts
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
Environment
# .env.local
DATABASE_URL=postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/neondb?sslmode=require
Database branching workflow
# Each PR automatically gets a preview database
# Configure in Neon Dashboard → Integrations → Vercel
# Result:
# main branch → production database
# feature/new-billing → isolated preview database
# PR #42 → pr-42-billing database (auto-cleaned on merge)
ORM: Drizzle
Drizzle's pure TypeScript architecture means no native binary in your bundle — critical for Vercel serverless cold starts. The ~7KB runtime overhead vs Prisma's ~600KB+ WASM engine is a meaningful difference at scale.
Schema definition
// lib/schema.ts
import {
pgTable, text, timestamp, boolean, integer, pgEnum
} from "drizzle-orm/pg-core";
export const planEnum = pgEnum("plan", ["free", "pro", "enterprise"]);
export const users = pgTable("users", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
email: text("email").notNull().unique(),
name: text("name"),
plan: planEnum("plan").default("free"),
stripeCustomerId: text("stripe_customer_id"),
createdAt: timestamp("created_at").defaultNow(),
});
export const organizations = pgTable("organizations", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text("name").notNull(),
slug: text("slug").notNull().unique(),
plan: planEnum("plan").default("free"),
stripeSubscriptionId: text("stripe_subscription_id"),
});
Migrations
# Generate migration from schema
npx drizzle-kit generate
# Apply to database
npx drizzle-kit migrate
# Push directly (for dev rapid iteration)
npx drizzle-kit push
Payments: Stripe
Stripe is the universal standard. Every SaaS boilerplate supports it, every accountant knows it, and Stripe Tax automates VAT/sales tax collection in 2026. For indie hackers who want zero tax setup, Lemon Squeezy is the alternative (see full billing comparison).
Setup
npm install stripe
// lib/stripe.ts
import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2025-01-27.acacia",
typescript: true,
});
// Create checkout session
export async function createCheckout(params: {
customerId: string;
priceId: string;
successUrl: string;
cancelUrl: string;
}) {
return stripe.checkout.sessions.create({
customer: params.customerId,
payment_method_types: ["card"],
line_items: [{ price: params.priceId, quantity: 1 }],
mode: "subscription",
success_url: params.successUrl,
cancel_url: params.cancelUrl,
subscription_data: {
trial_period_days: 14,
},
});
}
// Customer portal
export async function createPortalSession(customerId: string, returnUrl: string) {
return stripe.billingPortal.sessions.create({
customer: customerId,
return_url: returnUrl,
});
}
Webhook handler
// app/api/stripe/webhook/route.ts
import { stripe } from "@/lib/stripe";
import { db } from "@/lib/db";
import { users } from "@/lib/schema";
import { eq } from "drizzle-orm";
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("Invalid signature", { status: 400 });
}
switch (event.type) {
case "customer.subscription.created":
case "customer.subscription.updated": {
const sub = event.data.object as Stripe.Subscription;
await db
.update(users)
.set({
plan: sub.status === "active" ? "pro" : "free",
stripeSubscriptionId: sub.id,
})
.where(eq(users.stripeCustomerId, sub.customer as string));
break;
}
case "customer.subscription.deleted": {
const sub = event.data.object as Stripe.Subscription;
await db
.update(users)
.set({ plan: "free" })
.where(eq(users.stripeCustomerId, sub.customer as string));
break;
}
}
return new Response("OK");
}
Transactional Email: Resend
Resend is the 2026 default for developer-built transactional email. React Email templates, great deliverability, and a free tier covering 3,000 emails/month.
Setup
npm install resend @react-email/components
// lib/email.ts
import { Resend } from "resend";
import WelcomeEmail from "@/emails/welcome";
const resend = new Resend(process.env.RESEND_API_KEY!);
export async function sendWelcomeEmail(email: string, name: string) {
return resend.emails.send({
from: "noreply@yourdomain.com",
to: email,
subject: "Welcome to Your SaaS",
react: <WelcomeEmail name={name} />,
});
}
// emails/welcome.tsx
import { Html, Text, Button, Section } from "@react-email/components";
export default function WelcomeEmail({ name }: { name: string }) {
return (
<Html>
<Section>
<Text>Hi {name},</Text>
<Text>Welcome! Your account is ready.</Text>
<Button href="https://yourdomain.com/dashboard">
Go to Dashboard
</Button>
</Section>
</Html>
);
}
Marketing email: Loops
For product emails (onboarding sequences, feature announcements, win-back campaigns), Loops is the SaaS-specific alternative to Mailchimp. It uses Resend under the hood, so your transactional and marketing email stack shares the same sending infrastructure.
File Uploads: UploadThing
UploadThing gives you type-safe file uploads in Next.js with one API route. Handles S3 under the hood, no bucket management required.
npm install uploadthing @uploadthing/react
// lib/uploadthing.ts
import { createUploadthing, type FileRouter } from "uploadthing/next";
import { auth } from "@/lib/auth-server";
const f = createUploadthing();
export const ourFileRouter = {
avatarUploader: f({ image: { maxFileSize: "4MB" } })
.middleware(async () => {
const session = await auth();
if (!session) throw new Error("Unauthorized");
return { userId: session.user.id };
})
.onUploadComplete(async ({ metadata, file }) => {
await db
.update(users)
.set({ avatarUrl: file.url })
.where(eq(users.id, metadata.userId));
}),
} satisfies FileRouter;
export type OurFileRouter = typeof ourFileRouter;
Background Jobs: Trigger.dev
Trigger.dev handles long-running tasks, scheduled jobs, and webhook processing with automatic retries and a monitoring dashboard.
npm install @trigger.dev/sdk
// trigger/send-onboarding.ts
import { task, schedules } from "@trigger.dev/sdk/v3";
import { sendEmail } from "@/lib/email";
export const sendOnboardingSequence = task({
id: "send-onboarding",
run: async (payload: { userId: string; email: string }) => {
// Day 1: welcome
await sendEmail.welcome(payload.email);
// Day 3: feature discovery
await new Promise((r) => setTimeout(r, 3 * 24 * 60 * 60 * 1000));
await sendEmail.featureHighlight(payload.email);
// Day 7: check-in
await new Promise((r) => setTimeout(r, 4 * 24 * 60 * 60 * 1000));
await sendEmail.checkIn(payload.email);
},
});
// Trigger from your signup handler
await sendOnboardingSequence.trigger({ userId, email });
Analytics: PostHog
PostHog is the all-in-one product analytics platform with a generous free cloud tier (1M events/month) or self-hosted option. Replaces Mixpanel, FullStory, and LaunchDarkly in one product.
npm install posthog-js posthog-node
// app/providers.tsx
"use client";
import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
import { useEffect } from "react";
export function PHProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: "/ingest", // proxy through your domain
capture_pageview: true,
capture_pageleave: true,
});
}, []);
return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
}
Error Tracking: Sentry
Sentry captures exceptions, performance traces, and session replays. The free tier covers 5,000 errors/month — sufficient through early-stage SaaS.
npm install @sentry/nextjs
npx @sentry/wizard@latest -i nextjs
// sentry.server.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 0.1, // 10% of transactions
environment: process.env.NODE_ENV,
});
The Sentry wizard auto-instruments API routes and React Server Components. Key SaaS events to capture manually:
// Tag errors with user context
Sentry.setUser({ id: session.user.id, email: session.user.email });
// Capture billing failures explicitly
try {
await stripe.subscriptions.create(params);
} catch (err) {
Sentry.captureException(err, { tags: { context: "stripe-subscription" } });
throw err;
}
PostHog and Sentry are complementary — PostHog tracks what users do, Sentry tracks what breaks.
Environment Variables Checklist
# .env.local — full stack checklist
# Auth
BETTER_AUTH_SECRET= # openssl rand -hex 32
BETTER_AUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Database
DATABASE_URL= # Neon connection string
# Payments
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
# Email
RESEND_API_KEY=re_...
# File uploads
UPLOADTHING_SECRET=sk_live_...
UPLOADTHING_APP_ID=
# Background jobs
TRIGGER_SECRET_KEY=tr_...
# Analytics
NEXT_PUBLIC_POSTHOG_KEY=phc_...
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
# Error tracking
NEXT_PUBLIC_SENTRY_DSN=
Total Monthly Cost at Pre-Revenue Stage
| Service | Free Tier | Paid Trigger |
|---|---|---|
| Vercel | $0 (100 deploys/day) | $20/month (Pro) |
| Neon | $0 (0.5 GB, scale-to-zero) | $19/month (Launch) |
| Better Auth | $0 (self-hosted) | $0 always |
| Stripe | $0 (2.9% + $0.30/txn) | Same |
| Resend | $0 (3K emails/month) | $20/month (50K) |
| UploadThing | $0 (2 GB) | $10/month |
| Trigger.dev | $0 (dev environment) | $25/month (Starter) |
| PostHog | $0 (1M events) | $0 for most SaaS |
| Sentry | $0 (5K errors/month) | $26/month |
| Total pre-revenue | $0 | — |
The entire stack runs free until you're generating meaningful revenue. This is the key structural advantage of the 2026 SaaS infrastructure market — infrastructure costs no longer prevent product experimentation.
Browse all Next.js SaaS boilerplates or see the ShipFast review for the fastest way to get this stack pre-assembled.
Stack Alternatives and When to Deviate
The stack above is the 2026 default. There are cases where deviating from each component makes sense.
Better Auth is the default auth choice, but Clerk is the right call in two scenarios. First, if you're in pre-validation mode and want zero auth maintenance — Clerk's pre-built components (sign-in, sign-up, user profile, organization management) save 2–3 days of UI work and Clerk handles all session infrastructure. Second, if your product will stay under 10,000 MAU and you're confident of that, Clerk's free tier costs nothing. The threshold for switching back to Better Auth: when your monthly MAU bill on Clerk ($0.02/MAU) exceeds what you'd spend on the engineering time to maintain Better Auth (~2 hours/month for dependency updates). For most products, that crossover is around 15,000–20,000 MAU.
Neon is the default database, but Supabase is the better choice if you need any of: built-in auth (running auth and database from one provider simplifies the stack for small teams), Supabase Realtime (WebSocket-based subscriptions without adding Pusher or Ably), Supabase Storage (object storage without adding S3/R2), or pgvector for AI features. Supabase adds more infrastructure opinions to the stack — you're less likely to need the individual services from Clerk, Cloudflare R2, or a dedicated realtime service. For teams building AI-first products or needing realtime features, Supabase's all-in-one model is better than Neon's pure-database approach.
Drizzle is the default ORM, but Prisma is worth considering for teams with junior developers or those who prioritize database tooling over bundle size. Prisma Studio (visual database browser), Prisma's descriptive error messages, and the extensive Prisma documentation are genuine advantages for teams learning PostgreSQL patterns. The ~600KB WASM bundle penalty matters for Vercel Edge Functions but not for Railway or standard Vercel serverless. If your team has never used Drizzle and you're not deploying to the edge, the learning curve difference may outweigh the performance difference.
Infrastructure Cost Projection at Scale
The stack above runs at $0/month pre-revenue. Here's what it costs as you grow.
At 1,000 MAU with light usage: the free tiers cover everything. Vercel Hobby plan handles the traffic. Neon's free tier (0.5GB, scale-to-zero) is sufficient. Resend's 3,000 emails/month is enough. Better Auth is always free. PostHog's 1M events covers this usage comfortably. Total: $0.
At 10,000 MAU with moderate usage: Vercel Pro at $20/month becomes necessary (the Hobby plan is not for commercial use). Neon's Launch plan at $19/month provides 10GB and removes the scale-to-zero compute limits. Resend may hit the free tier limit if you send onboarding sequences — the $20/month Pro plan (50K emails) handles it. Trigger.dev's $25/month Starter plan covers background job needs. Total: ~$85/month.
At 50,000 MAU with high engagement: Vercel Pro at $20/month plus potential bandwidth overages. Neon's Scale plan at $69/month for more compute hours and replicas. Resend Pro at $20/month. Trigger.dev at $25/month. PostHog free tier (1M events) may need upgrading to the $450/month plan if you're tracking detailed usage events. Sentry's free tier (5K errors/month) likely needs upgrading to $26/month. Total: ~$200–300/month plus Stripe's 2.9% transaction fees.
The key structural advantage of this stack: each component has a generous free tier and predictable paid pricing. You pay for services as you grow into them, not upfront. Compare this to building on a VM-based stack (Render or Railway for everything) at $50–100/month from day one, or Supabase's Pro at $25/month plus Clerk's MAU fees from month one. The free-tier-first approach extends the zero-infrastructure-cost runway significantly.
Configuration and Environment Validation
Production SaaS applications should validate their environment configuration at startup, not at runtime when a misconfigured variable causes a user-facing error.
The T3 Stack's createEnv from the @t3-oss/env-nextjs package provides this pattern for the recommended stack. It validates environment variables at build time using Zod schemas, giving you type-safe access to env vars throughout the application and a clear error message at build time (not production) when a required variable is missing.
// lib/env.ts
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
BETTER_AUTH_SECRET: z.string().min(32),
STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
STRIPE_WEBHOOK_SECRET: z.string().startsWith("whsec_"),
RESEND_API_KEY: z.string().startsWith("re_"),
},
client: {
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith("pk_"),
NEXT_PUBLIC_POSTHOG_KEY: z.string().startsWith("phc_"),
},
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,
// ... etc
},
});
A deployment with a missing STRIPE_SECRET_KEY fails at build time with a clear error message rather than succeeding and then throwing a runtime error when the first user attempts checkout. This pattern is especially valuable for teams with multiple deployment environments (development, staging, production) where environment configuration drift is common.
Compare alternative stack choices in the best SaaS boilerplates guide — different boilerplates make different component choices.
See which boilerplates pre-assemble this exact stack: best Next.js boilerplates 2026.