DEV Community

Cover image for How I Built a Security-First SaaS Boilerplate with 100% Test Coverage
Hasan Kemal Demirci
Hasan Kemal Demirci

Posted on

How I Built a Security-First SaaS Boilerplate with 100% Test Coverage

"We'll add security later."

If you've ever said this, you're not alone. I've been there too. But after seeing countless data breaches, emergency security patches at 2 AM, and the $4.45 million average cost of a data breach, I decided to take a different approach.

This is the story of how I built ShipSecure — a Next.js boilerplate where security isn't an afterthought, it's the foundation.

The Problem: Security as a TODO Item

Most developers approach security like this:

  • ✅ Build landing page
  • ✅ Add authentication
  • ✅ Integrate payments
  • ⬜ Security (we'll get to it later)

Sound familiar? The issue is that "later" often means "after the first incident." And by then, you've already:

  • Lost customer trust
  • Faced potential regulatory fines (GDPR, CCPA, SOC 2)
  • Spent weeks retrofitting security into an architecture that wasn't designed for it

The Solution: Security-First Development

Instead of treating security as a feature, I treated it as architecture. Every line of code was written with security in mind, and every security feature was covered by tests.

Let me walk you through the key concepts.


1. Security Headers: Your First Line of Defense

Most developers know about Content-Security-Policy, but how many actually implement it correctly? Here's the thing — it's not just about adding one header. You need a complete security header strategy:

Header What It Prevents
Content-Security-Policy XSS attacks, code injection
Strict-Transport-Security Protocol downgrade attacks
X-Content-Type-Options MIME sniffing vulnerabilities
X-Frame-Options Clickjacking attacks
Referrer-Policy Information leakage
Permissions-Policy Unauthorized browser feature access

The tricky part? Getting these headers to work together without breaking your app. CSP alone has dozens of directives that need careful configuration — one wrong setting and your OAuth flow breaks, your analytics stop working, or your styles don't load.

The approach I took: A centralized getSecurityHeaders() function that returns a properly configured header object, applied consistently across all routes through middleware.


2. Rate Limiting: The Art of Saying "Slow Down"

Without rate limiting, your API is an open invitation for:

  • 🔓 Brute force attacks on login
  • 💥 DDoS attempts
  • 🎭 Credential stuffing

The challenge? Production and development have different needs.

In production, you need distributed rate limiting (Upstash Redis) so limits work across serverless instances. But in development, you don't want to set up Redis just to run locally.

My solution: A hybrid architecture with automatic fallback.

Production: Upstash Redis (distributed, persistent)
     ↓ (automatic fallback if Redis not configured)
Development: In-memory Map (simple, no setup)
Enter fullscreen mode Exit fullscreen mode

And here's the key insight: use sliding window, not fixed window.

Why? With fixed window rate limiting, an attacker can make 20 requests in 2 seconds by timing it at the window boundary (10 at :59, 10 at :01). Sliding window eliminates this vulnerability entirely.


3. Test-Driven Security: The Game Changer

Here's what truly sets a security-first approach apart: every security feature is backed by tests.

Why does this matter?

  1. Tests prove claims. Anyone can say "we use CSP." Tests prove it.
  2. Tests prevent regression. That intern can't accidentally remove security headers.
  3. Tests document behavior. Security requirements become executable specifications.

I use a three-layer testing strategy:

Layer 1: Unit Tests (Vitest)
├── Individual security functions
├── Validation logic
└── Edge cases

Layer 2: Integration Tests  
├── Middleware behavior
├── Auth flows
└── API protection

Layer 3: E2E Tests (Playwright)
├── Real browser behavior
├── Header verification
└── User journey security
Enter fullscreen mode Exit fullscreen mode

The result: 75+ tests covering every security mechanism. CI/CD runs them on every PR. No security feature ships without test coverage.


4. Input Validation: Where Most Breaches Start

SQL injection, XSS, command injection — they all share one thing in common: unvalidated input.

The traditional approach looks like this:

// ❌ This is asking for trouble
const { email, name } = await req.json();
await db.users.create({ email, name });
Enter fullscreen mode Exit fullscreen mode

No validation. No type checking. No protection.

The security-first approach:

  1. Define schemas for every input
  2. Validate at the boundary (API routes, form submissions)
  3. Fail fast with clear error messages
  4. Never trust - even authenticated users can send malicious data

I use Zod for this — type-safe at compile time, validated at runtime. But the tool matters less than the discipline of validating everything, everywhere.


5. Authentication Done Right

Authentication is where most security vulnerabilities live. Password storage, session management, CSRF protection, OAuth flows — each is a potential attack vector.

My approach: Don't reinvent the wheel. I use Auth.js v5 because:

  • ✅ HttpOnly, secure cookies by default
  • ✅ Built-in CSRF protection
  • ✅ Battle-tested OAuth implementations
  • ✅ Session management that actually works

The key insight: authentication isn't just about login/logout. It's about:

  • How sessions are stored and validated
  • How cookies are configured
  • How tokens are handled
  • How logout actually invalidates sessions

Getting any of these wrong creates vulnerabilities. Auth.js handles them correctly by default.


The Hard-Won Lessons

After building this, here's what I wish I knew earlier:

1. Security is 10x cheaper when designed in

Retrofitting security into an existing codebase is painful. Every decision you made without security in mind becomes a constraint.

2. Tests are your best security documentation

When someone asks "how do you prevent X?", point them to the test file. It's proof, not promises.

3. Developer experience matters

If security measures are annoying, developers find workarounds. The in-memory fallback for rate limiting? That's about DX. The centralized header function? That's about DX. Security should be invisible to developers using your codebase.

4. Automate everything

Security tests in CI/CD mean no one can accidentally (or intentionally) ship insecure code. It's not about trust — it's about systems.


The Bottom Line

Here's the reality:

  • 60% of startups shut down within 6 months of a security breach
  • 73% of enterprise customers require security certifications
  • SOC 2 compliance can shorten sales cycles by 40%

Security isn't a feature — it's a competitive advantage.


Want the Full Implementation?

I've packaged everything I learned into ShipSecure — a Next.js 15 boilerplate that's secure from day one:

  • 🛡️ All 7 security headers pre-configured and working
  • ⚡ Rate limiting with Redis + in-memory fallback
  • 🔐 Auth.js v5 with secure defaults
  • ✅ 75+ tests for 100% security coverage
  • 📦 Stripe integration included
  • 📚 Complete documentation
  • 🔄 Lifetime updates

One-time purchase. Skip the security research. Start building.

You build. We secure.

👉 shipsecure.dev


Have questions about security in Next.js? Drop a comment — I read every one.

Top comments (0)