DEV Community

seller-mind
seller-mind

Posted on

How I Built a SaaS Boilerplate That Handles Auth, Billing, and Deployment

How I Built a SaaS Boilerplate That Handles Auth, Billing, and Deployment

FTC Disclosure: I'm the developer of SaaSLaunch, the SaaS boilerplate discussed in this article.


Every time I start a new SaaS project, I spend the first two weeks building the same things: authentication, subscription billing, email templates, deployment pipelines. It's tedious, error-prone, and takes time away from the actual product logic that makes each SaaS unique.

Six months ago, I decided to fix this for good. I built SaaSLaunch — a production-ready Next.js boilerplate that handles the boring stuff so you can focus on your product.

In this article, I'll walk you through the architecture decisions, the hard-won lessons, and how the three core pillars (auth, billing, deployment) actually work under the hood.

The Problem With Existing Boilerplates

Before building SaaSLaunch, I tried every SaaS starter kit I could find. Here's what I ran into:

  • Too minimal: Some starters give you a login page and call it a day. You still need to wire up email verification, password resets, session management, and role-based access control.
  • Too opinionated: Others lock you into specific database schemas, UI frameworks, or deployment targets. If your project doesn't fit their mold, you're fighting the framework.
  • Outdated dependencies: Many popular starters haven't been updated in months. You're importing packages with known vulnerabilities before you even write your first line of code.

I needed something that was comprehensive but flexible, modern but battle-tested. So I built it myself.

Pillar 1: Authentication That Actually Works

Authentication seems simple until you've deployed it to production. Then you discover edge cases: What happens when a user's session expires mid-action? How do you handle OAuth token refresh? What about rate limiting on login attempts?

SaaSLaunch uses NextAuth.js (now Auth.js) with a custom adapter layer. Here's the architecture:

// lib/auth/config.ts
export const authConfig = {
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    CredentialsProvider({
      name: "credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        const user = await verifyCredentials(credentials);
        if (!user) throw new Error("Invalid credentials");
        if (!user.emailVerified) throw new Error("Email not verified");
        return user;
      },
    }),
  ],
  session: { strategy: "jwt", maxAge: 30 * 24 * 60 * 60 },
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.role = user.role;
        token.plan = user.subscription?.plan || "free";
      }
      return token;
    },
    async session({ session, token }) {
      session.user.role = token.role;
      session.user.plan = token.plan;
      return session;
    },
  },
  pages: {
    signIn: "/login",
    error: "/auth/error",
  },
};
Enter fullscreen mode Exit fullscreen mode

Key features baked in:

  • Email verification with magic links and OTP fallback
  • Password reset flow with rate limiting (max 3 attempts per hour)
  • Role-based access control — define roles in a central config, protect routes with middleware
  • Session management — JWT-based with configurable expiry and refresh
  • OAuth providers — Google and GitHub pre-configured, easily extensible

Pillar 2: Billing With Stripe

This is where most boilerplates fall short. SaaSLaunch includes a complete billing system:

// lib/stripe/webhooks.ts
export async function handleWebhook(event: Stripe.Event) {
  switch (event.type) {
    case "checkout.session.completed":
      await activateSubscription(event.data.object);
      break;
    case "customer.subscription.updated":
      await syncSubscriptionChanges(event.data.object);
      break;
    case "invoice.payment_failed":
      await handleFailedPayment(event.data.object);
      break;
    case "customer.subscription.deleted":
      await deactivateSubscription(event.data.object);
      break;
  }
}
Enter fullscreen mode Exit fullscreen mode

What you get out of the box:

  • Three pricing tiers (Free, Pro, Team) with annual/monthly toggle
  • Stripe Checkout integration with success/cancel pages
  • Webhook handling for all subscription lifecycle events
  • Usage-based billing support for metered features
  • Customer portal link for self-service subscription management
  • Invoice generation and email receipts

The pricing page component reads directly from your Stripe product catalog, so you never have hardcoded prices in your frontend. Change a price in Stripe Dashboard, and it reflects immediately.

Pillar 3: One-Click Deployment

The deployment story matters more than people think. If deploying takes 30 minutes of configuration, you've already lost momentum.

SaaSLaunch ships with:

  • Vercel deployment config (vercel.json with proper rewrites and headers)
  • Docker setup for self-hosting (Dockerfile + docker-compose.yml)
  • GitHub Actions CI/CD pipeline with lint, test, build, and deploy stages
  • Environment variable templates for all supported platforms
  • Database migration scripts that run automatically on deploy
# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"
      - run: npm ci
      - run: npm run lint
      - run: npm run test -- --coverage
      - run: npm run build
      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: "--prod"
Enter fullscreen mode Exit fullscreen mode

What's Inside the Box

Here's the complete feature list:

Category Features
Frontend Next.js 14 App Router, Tailwind CSS, shadcn/ui components
Backend API routes, Server Actions, middleware
Database Prisma ORM with PostgreSQL, migrations included
Auth NextAuth.js with OAuth, credentials, magic links
Billing Stripe Checkout, webhooks, customer portal
Email Resend with transactional templates
Monitoring Sentry integration, error boundaries
SEO Sitemap, robots.txt, metadata, OG images
Testing Vitest + Playwright for unit and E2E tests

Lessons Learned

After maintaining this boilerplate for six months and watching dozens of developers build on top of it, here are my biggest takeaways:

  1. Don't abstract too early. The boilerplate started with too many abstraction layers. I've since simplified — it's easier to add abstraction than remove it.

  2. Error handling is a feature. Every API route has consistent error responses. Every form validates on both client and server. The error boundaries actually catch real errors.

  3. Documentation beats cleverness. I spent more time writing docs than writing code. The result? Fewer support questions and faster onboarding.

  4. Keep dependencies minimal. Every dependency is a maintenance burden. SaaSLaunch uses about 30 packages total, each one carefully vetted.

What's Next

I'm currently working on:

  • Multi-tenancy support for B2B SaaS
  • AI integration templates (OpenAI, Anthropic)
  • Analytics dashboard with PostHog
  • Admin panel with user management and analytics

If you're building a SaaS and want to skip the boilerplate phase, check out SaaSLaunch. It's designed to get you from idea to production in hours, not weeks.


What's your experience building SaaS from scratch? What boilerplate features do you wish existed? Let me know in the comments.

Top comments (0)