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.