Claude Code Boilerplate
FeaturesPricingBlogDocs
Get started →

Product

  • Features
  • Pricing
  • Skills

Compare

  • vs ShipFast
  • vs MakerKit
  • vs supastarter

Resources

  • Docs
  • Blog
  • Discord

Legal

  • License
  • Privacy Policy
  • Terms of Service
Claude Code Boilerplate

© 2026 Claude Code Boilerplate. All rights reserved.

← All posts

Next.js SaaS Security Checklist -- 7 Things to Lock Down Before You Launch

September 9, 2026
nextjssaassecurityauthboilerplate

You are one day from launching your SaaS. You have working auth, a Stripe integration, and a database. But how do you know it is actually secure?

Most founders find out their app has a gap when a user reports it -- or when an unexpected bill arrives. This checklist covers the seven things worth verifying before your first real customer signs up. If you are starting from the Next.js SaaS Boilerplate, most of these are already handled for you. Use this list to confirm, not to rebuild.

1. Passwords Are Hashed, Not Stored as Plain Text

If your database is ever exposed and passwords are stored as plain text, every account is compromised immediately. Passwords must be hashed using a slow, purpose-built algorithm before they are written to disk.

The boilerplate uses bcryptjs with a cost factor of 12. You can verify this in modules/user/user.service.ts -- bcrypt.hash() is called before any insert, and bcrypt.compare() is used at login. No direct string comparison ever touches a password.

Check: search for any code path that writes a password field without calling the hash helper first.

2. JWT Secrets Are Random and High-Entropy

JSON Web Tokens are only as secure as the secret that signs them. A predictable secret or a token that never expires means a stolen token is a permanent backdoor into any account.

The boilerplate signs tokens with JWT_SECRET from your environment variables and sets an expiry on every token. The getUserFromRequest() helper in lib/auth.ts validates the signature and expiry on every protected request -- a missing or tampered token returns 401.

Check: confirm JWT_SECRET in your production environment is a random string. If it looks like mysecret or dev, replace it now with openssl rand -base64 32.

3. Every Input Is Validated Before It Reaches the Database

Unexpected data shapes and injection attacks are still the most common vulnerability class. The fix is validating every input at the boundary before anything reaches your database.

The boilerplate runs every incoming request body through a Zod schema before the service layer is called. If a field is missing or wrong, the route returns 400 and the service never runs. Combined with Drizzle ORM's parameterized queries, there is no path for a user to inject raw SQL.

Check: open any file in app/api/ and confirm schema.safeParse(body) is called and its result checked before the service function runs. See the auth docs for the expected pattern.

4. Rate Limiting Covers Auth and Paid Endpoints

Without rate limits, anyone can hammer your login endpoint trying to guess passwords, or call your AI endpoint until your bill is enormous. Neither failure mode requires a sophisticated attacker.

The boilerplate includes a sliding-window rate limiter that runs per user and per IP using Drizzle ORM -- no Redis required, works across serverless instances.

Check: confirm rate limiting is applied to /api/auth/login, /api/auth/register, and any route that calls an external paid API.

5. Password Hashes Are Stripped From API Responses

Even a properly hashed password should never appear in an API response. One accidental console.log(user) in production logs, or a client-side state dump, exposes it.

The boilerplate sanitizes every user object before returning it:

// strip the hash before any response -- never return it
const { passwordHash: _, ...safeUser } = user;
return NextResponse.json(safeUser);

This pattern is enforced at the type level too -- the SafeUser type does not include passwordHash, so TypeScript will catch any slip.

Check: search for any response that spreads a full user object. If passwordHash can reach a response body, fix it before launch.

6. Authorization Runs on the Server, Not Just in the UI

Hiding a button is not authorization. A user who knows your endpoint can call it directly from their browser console. Real authorization means the server checks on every call whether the caller is allowed to perform this action.

The boilerplate checks ownership in the service layer. Every mutation confirms the authenticated user owns the resource before proceeding. A direct API call from the wrong user gets a 403 -- no exceptions.

The RBAC guide covers how role-level checks layer on top of ownership checks for admin-only operations.

Check: for every route that accepts an id in the URL or body, trace the call into the service and confirm the service verifies ownership before mutating.

7. Environment Variables Are Not in Version Control

A .env file committed to a public repository is a permanent credential leak. Removing the file later does not erase git history -- the secret is still retrievable.

Check: open .gitignore and confirm .env and .env.local are listed. Run this command:

git log --all --full-history -- .env

If it returns commits, the file was previously tracked. Rotate every secret that appears in those commits immediately, then remove the file from history.

For production, set environment variables through your hosting dashboard (Vercel has a dedicated env panel). Never commit them to the repository.


What the Boilerplate Covers

If you are starting from boilerplate.iteam-company.com, items 1 through 6 are built in as defaults -- bcrypt password hashing, signed and expiring JWTs, Zod validation on every route, rate limiting, response sanitization, and service-layer authorization are all present from day one.

Item 7 is always on you. No boilerplate can stop a committed secret from reaching version control, so check .gitignore before your first push and confirm secrets are set in your hosting dashboard.

This checklist takes about 30 minutes. Run it before your first real user signs up -- not after. Get the boilerplate with security defaults already in place at boilerplate.iteam-company.com.