The 2026 SaaS Boilerplate: Auth, Billing, Multi-Tenancy
The stack, the patterns, and the code you need to ship a production SaaS - without spending 3 months on auth and billing plumbing.
The modern SaaS stack: Next.js 15 + Drizzle + Better Auth + Stripe
The Recommended Stack
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 15 (App Router) | RSC, server actions, middleware, massive ecosystem |
| ORM | Drizzle | Type-safe, SQL-like API, zero overhead, great migrations |
| Database | PostgreSQL (Neon / Supabase) | Serverless-friendly, branching, generous free tiers |
| Auth | Better Auth | Self-hosted, free, multi-tenancy built-in |
| Payments | Stripe | Industry standard, best docs, subscription support |
| Resend | Developer-friendly API, React Email templates | |
| Styling | Tailwind CSS + shadcn/ui | Copy-paste components, full control, no vendor lock-in |
| Deployment | Vercel / Railway | Zero-config deploys, preview environments |
Auth: The Comparison That Matters
| Feature | Better Auth | Clerk | Auth.js (NextAuth) |
|---|---|---|---|
| Pricing | Free (self-hosted) | Free to 10K MAU, then $0.02/MAU | Free (self-hosted) |
| Email/Password | ✅ Built-in | ✅ Built-in | ⚠️ Credentials adapter (limited) |
| OAuth Providers | ✅ 20+ providers | ✅ 20+ providers | ✅ 80+ providers |
| Magic Links | ✅ Built-in | ✅ Built-in | ✅ Email provider |
| Multi-Tenancy | ✅ Organizations plugin | ✅ Organizations | ❌ Build yourself |
| RBAC | ✅ Plugin | ✅ Built-in | ❌ Build yourself |
| 2FA / MFA | ✅ TOTP plugin | ✅ Built-in | ❌ Build yourself |
| Session Management | ✅ JWT + DB sessions | ✅ Managed | ✅ JWT or DB |
| UI Components | ⚠️ Headless (BYO UI) | ✅ Pre-built components | ⚠️ Headless |
| Data Ownership | ✅ Your database | ❌ Clerk's servers | ✅ Your database |
| Vendor Lock-in | None | High | None |
| Setup Effort | Medium (2-4 hours) | Low (30 min) | Medium-High (4-8 hours) |
Verdict: Better Auth for most teams. Clerk if you want zero auth code and have budget. Auth.js only if you need a very specific OAuth provider it supports.
Multi-Tenancy with Drizzle
Row-level tenancy is the right pattern for 95% of SaaS apps. Every table gets a tenantId column, and every query filters by it. Drizzle makes this type-safe:
// schema.ts - Row-level multi-tenancy
import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
export const tenants = pgTable("tenants", {
id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(),
slug: text("slug").notNull().unique(),
plan: text("plan").notNull().default("free"),
stripeCustomerId: text("stripe_customer_id"),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
export const projects = pgTable("projects", {
id: uuid("id").primaryKey().defaultRandom(),
tenantId: uuid("tenant_id").notNull().references(() => tenants.id),
name: text("name").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
// Tenant-scoped query helper
export function forTenant(db: DrizzleDB, tenantId: string) {
return {
projects: {
list: () =>
db.select().from(projects).where(eq(projects.tenantId, tenantId)),
create: (data: { name: string }) =>
db.insert(projects).values({ ...data, tenantId }).returning(),
},
};
}
// Usage in a server action
export async function getProjects() {
const session = await auth();
const tenantId = session.user.activeTenantId;
return forTenant(db, tenantId).projects.list();
}
tenantId, RLS prevents data leaks from raw SQL queries, admin tools, or bugs.
Stripe Webhook Handler
This handles the critical subscription lifecycle events. Put it in app/api/webhooks/stripe/route.ts:
import { headers } from "next/headers";
import Stripe from "stripe";
import { db } from "@/lib/db";
import { tenants } from "@/lib/schema";
import { eq } from "drizzle-orm";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: Request) {
const body = await req.text();
const sig = (await 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 "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
await db.update(tenants)
.set({
stripeCustomerId: session.customer as string,
plan: "pro",
})
.where(eq(tenants.id, session.metadata!.tenantId));
break;
}
case "customer.subscription.updated": {
const sub = event.data.object as Stripe.Subscription;
const plan = sub.status === "active" ? "pro" : "free";
await db.update(tenants)
.set({ plan })
.where(eq(tenants.stripeCustomerId, sub.customer as string));
break;
}
case "customer.subscription.deleted": {
const sub = event.data.object as Stripe.Subscription;
await db.update(tenants)
.set({ plan: "free" })
.where(eq(tenants.stripeCustomerId, sub.customer as string));
break;
}
}
return new Response("OK", { status: 200 });
}
Project Structure
├── app/
│ ├── (auth)/
│ │ ├── login/page.tsx
│ │ └── signup/page.tsx
│ ├── (dashboard)/
│ │ ├── layout.tsx # Auth guard + tenant context
│ │ ├── projects/page.tsx
│ │ └── settings/
│ │ ├── billing/page.tsx
│ │ └── team/page.tsx
│ ├── api/
│ │ ├── auth/[...all]/route.ts # Better Auth handler
│ │ └── webhooks/stripe/route.ts
│ ├── layout.tsx
│ └── page.tsx # Landing page
├── components/
│ ├── ui/ # shadcn/ui components
│ └── dashboard/
├── lib/
│ ├── auth.ts # Better Auth config
│ ├── db.ts # Drizzle client
│ ├── schema.ts # Drizzle schema
│ └── stripe.ts # Stripe helpers
├── drizzle/
│ └── migrations/
├── drizzle.config.ts
├── tailwind.config.ts
└── package.json
Deployment Cost Comparison
| Platform | Free Tier | $50/mo Gets You | $200/mo Gets You | Best For |
|---|---|---|---|---|
| Vercel | 100GB BW, serverless | 1TB BW, 1000 build min | Team features, analytics | Next.js apps, speed to market |
| Railway | $5 credit/mo | 2 services, 8GB RAM | HA, multiple envs | Full-stack with background jobs |
| AWS (ECS + RDS) | 12-mo free tier | t4g.small + db.t4g.micro | Multi-AZ, autoscaling | Scale, compliance, control |
| Fly.io | 3 shared VMs | 2 dedicated VMs, 4GB | HA, global edge | Global latency, Docker apps |
Recommendation: Start on Vercel (free) + Neon (free). Move to Railway or AWS when you need background jobs, cron, or more control. Don't over-engineer deployment for an MVP.
Open-Source Boilerplate Comparison
| Boilerplate | Stack | Auth | Billing | Multi-Tenant | License |
|---|---|---|---|---|---|
| next-saas-starter | Next.js 15 + Drizzle | Better Auth | Stripe | ✅ | MIT |
| Taxonomy | Next.js + Prisma | NextAuth | Stripe | ❌ | MIT |
| Saas UI | Next.js + Chakra | Supabase Auth | Stripe | ✅ | MIT |
| Supastarter | Next.js + Supabase | Supabase Auth | Stripe/Lemon | ✅ | $299 (paid) |
| Shipfast | Next.js + MongoDB | NextAuth | Stripe/Lemon | ❌ | $199 (paid) |
What NOT to Build Yourself
- Payment processing - Use Stripe. Period. Don't touch PCI compliance.
- Email delivery - Use Resend or Postmark. Building email infrastructure is a full-time job.
- File uploads - Use UploadThing or S3 presigned URLs. Don't stream files through your server.
- Search - Use Meilisearch or Algolia. Building full-text search on PostgreSQL works until it doesn't.
- Analytics - Use PostHog (self-hostable) or Plausible. Don't build event tracking from scratch.
- Feature flags - Use LaunchDarkly or Unleash. A JSON config file is not a feature flag system.
The Bottom Line
The 2026 SaaS stack is remarkably good. Next.js 15 + Drizzle + Better Auth + Stripe gives you auth, billing, multi-tenancy, and type safety out of the box. You can go from npx create-next-app to a deployed MVP with user signups and Stripe checkout in a weekend. The hard part isn't the boilerplate anymore - it's building something people want to pay for.