Skip to main content

Next.js SaaS Tech Stack Guide 2026: Full Picks

·StarterPick Team
nextjstech-stacksaasauthdatabasestriperesenddrizzle2026

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

CategoryPickWhy
AuthBetter AuthFree, self-hosted, passkeys + 2FA + orgs built in
DatabaseNeon (Postgres)Serverless, branching per PR, $0 free tier
ORMDrizzleSmallest bundle, fastest cold start, native Neon adapter
PaymentsStripeUniversal support, best API, Stripe Tax for compliance
Email (transactional)ResendDeveloper-first, React Email templates, reliable delivery
Email (marketing)LoopsSaaS-specific sequences, Resend-compatible
File uploadsUploadThingNext.js native, type-safe, free tier covers early stage
Background jobsTrigger.devReliable queues, retries, dashboard, self-hostable
AnalyticsPostHogFree self-hosted or affordable cloud, session replays
Error trackingSentryIndustry standard, generous free tier
DeployVercelZero-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

ServiceFree TierPaid 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.

Comments