I stored user passwords in plain text for 3 weeks. Here's what I learned about security-first architecture
I'm going to be honest about a mistake that still makes me cringe: I stored user passwords in plain text for three weeks.
Not because I didn't understand hashing. Not because I lacked the skills. Because I told myself "I'll add proper security later." That decision cost me 12 hours of emergency fixes, invalidating all user sessions, implementing bcrypt under pressure, and sending embarrassing "please reset your password" emails to our beta users.
Had I done it right from the start? Twenty minutes. Maybe thirty if I read the bcrypt docs carefully.
That experience changed how I approach SaaS development. When I built the NextSaaS template (a production-ready SaaS starter with authentication, payments, admin panel, and 127 passing tests), I committed to security-first architecture from line one of code. Six months later: zero security incidents, zero emergency patches, zero trust-destroying breaches.
Here's what I learned that might save you from my mistakes.
Security is Architectural (Not Optional)
The biggest trap developers fall into is thinking security is a feature you add later. It's not. It's architectural. It affects your middleware structure, your authentication flow, how you validate inputs, how you handle errors, and how you test everything.
I watched a friend's startup nearly collapse when they discovered SQL injection vulnerabilities after reaching 5,000 users. The fix wasn't a quick patch. It required auditing every database query in the codebase, rewriting dozens of endpoints, data migration with downtime risk, and praying they caught everything. Three weeks of developer time. Three years of aging overnight.
The lesson: Adding security after features are built takes 3-4x longer than building secure from the beginning. I've measured this across multiple projects. It's consistent.
The Security Middleware Stack
In NextSaaS, the security middleware stack runs before any application logic touches a request. This isn't my innovation—it's how production systems work—but implementing it from day one changed everything.
First layer: Helmet for security headers. Helmet sets HTTP headers that tell browsers how to behave with your content. It enables HTTP Strict Transport Security (forces HTTPS), sets Content Security Policy (prevents XSS attacks), blocks clickjacking attempts (X-Frame-Options), and prevents MIME type sniffing. This is literally one line of middleware configuration, but it addresses four of the OWASP Top 10 vulnerabilities.
Second layer: CORS configuration. I spent two hours getting CORS right because I wanted to understand it, not just copy-paste a permissive wildcard config from Stack Overflow. My configuration explicitly whitelists the frontend URL, allows credentials for cookie-based auth, and restricts HTTP methods to exactly what the API needs. No wildcards. No "I'll restrict this later."
Third layer: Rate limiting at three levels. General API rate limiting at 100 requests per 15 minutes stops casual abuse. Authentication endpoint rate limiting at 5 attempts per 15 minutes prevents brute force attacks. OAuth endpoints get a more lenient 30 requests because OAuth flows involve multiple redirects. Each rate limiter serves a specific security purpose.
Fourth layer: Request size limiting. Ten megabytes maximum. Why? Because without limits, someone can send a 500MB request and crash your server by exhausting memory. I learned this when a "security researcher" (read: bored hacker) found my staging environment and tried uploading a 2GB file. The server ran out of memory. Not a fun debugging session.
This middleware stack runs before authentication. Before business logic. Before anything. If a request is malicious or malformed, it never reaches your application code.
Authentication and Authorization Done Right
Authentication middleware in NextSaaS verifies every protected request with a deliberate flow: Check for token in cookie first (preferred method), fallback to Authorization header for backward compatibility, verify JWT signature using the secret, look up user in database to confirm they exist and are active, then attach user object to request for downstream use.
This handles edge cases that simpler implementations miss. What if someone's token is valid but their account was disabled? Rejected. What if the token signature is forged? Rejected. What if the token is expired? Rejected with a specific error message so the frontend can trigger a refresh flow.
Authorization sits on top of authentication with role-based access control. Some endpoints need authentication only (any logged-in user). Others need specific roles. The `requireRole` middleware handles this with a simple decorator pattern. Need admin access? Add `requireRole('admin')` to the route. The implementation is eight lines of code, but it prevents privilege escalation attacks.
Input Validation: Trust Nothing
The scariest production code I've seen isn't sophisticated exploits. It's missing validation. A payment endpoint that trusts the client to send the correct price. An admin endpoint that doesn't verify the user is actually an admin. A file upload that doesn't check the file type.
Input validation in NextSaaS happens at the route layer using express-validator. Every endpoint defines its validation schema before the handler runs. Email must be valid email format. Password must be at least 8 characters. User ID must be a valid UUID. Amount must be a positive number.
But validation isn't just about format. It's about business rules. When a user updates their profile, I validate the email format is correct AND that the email isn't already taken by another user. When someone makes a payment, I validate the amount is positive AND that it matches the actual subscription price in the database, not the price the client sent.
This sounds paranoid until you realize: your frontend is not trusted. Anyone can open DevTools, modify the JavaScript, and send whatever payload they want. Your backend is the only source of truth.
Testing Security Features
I didn't test security initially. I tested features—login works, registration works, password reset works. But I didn't test authentication failures, rate limiting enforcement, or input validation edge cases.
NextSaaS has dedicated security tests that verify Helmet middleware is configured and active, rate limiting kicks in after the specified number of requests, JWT tokens expire correctly, expired tokens get rejected, and disabled user accounts can't authenticate even with valid tokens.
Testing security is weird because you're testing that things fail correctly. A good authentication test confirms that invalid tokens are rejected. A good authorization test verifies that users without proper roles can't access restricted endpoints.
The Real Impact
Building security-first slowed down my initial development. That first week, I shipped fewer features than I could have if I'd skipped security. But here's what happened next: I never had a security incident. Not during beta. Not at launch. Not six months later. Every endpoint I built inherited the security middleware automatically. I never spent a weekend retrofitting authentication.
More importantly, security-first thinking changed how I write code. I instinctively think about validation now. I question trust boundaries. When I write a new endpoint, I automatically consider: who should access this, what inputs are valid, what could go wrong, and how do I test failures?
The time investment upfront was maybe 20% more than building without security. The time saved avoiding security retrofits? Easily 70%.
Has anyone else experienced the "I'll add security later" trap? What security practices have saved your projects?
Written with AI assistance. All experiences and code examples are from real production projects.
Expected Q&A
Q1: What's the performance impact of all that middleware?
A: Negligible. Helmet, CORS, and rate limiting add maybe 1-2ms per request. JWT verification adds another 1-3ms. This is completely acceptable for web APIs. The security benefits far outweigh the microseconds of overhead.
Q2: Why HTTP-only cookies instead of localStorage for JWT tokens?
A: HTTP-only cookies can't be accessed by JavaScript, which makes them resistant to XSS attacks. If someone injects malicious JavaScript into your page, they can't steal the authentication token from an HTTP-only cookie. With localStorage, they absolutely can.
Q3: Isn't 5 login attempts too strict for rate limiting?
A: It's aggressive, yes. But legitimate users rarely fail login 5 times in 15 minutes. If someone does, they can wait 15 minutes or use password reset. Meanwhile, this completely prevents automated brute force attacks. I'd rather annoy one forgetful user than let an attacker try 10,000 passwords.
Q4: How do you handle rate limiting with load balancers?
A: Great question. In production behind a load balancer, you need distributed rate limiting using Redis. The basic express-rate-limit uses in-memory storage which doesn't work across multiple server instances. I use rate-limit-redis in production but kept it simple for the starter template.
Q5: What about OAuth security? You mentioned OAuth rate limiting.
A: OAuth adds complexity because the flow involves multiple redirects (user to provider, provider back to your app). Each redirect counts as a request, so you need higher limits than regular auth. But you still want limits to prevent someone from spamming your OAuth callback endpoint.
Q6: How do you test rate limiting without waiting 15 minutes?
A: Two approaches: In tests, I skip rate limiting entirely (check the code—it skips in test environment). For manual testing, I temporarily reduce the window to 1 minute instead of 15. Never reduce limits in production testing though.
Q7: Is Prisma really enough to prevent SQL injection?
A: Prisma auto-parameterizes all queries, which prevents SQL injection in normal usage. BUT if you write raw queries (Prisma allows this), you can still create injection vulnerabilities. My rule: code review every raw query, use parameterized queries even in raw mode, and test with malicious inputs.
Q8: What about HTTPS? You mentioned forcing HTTPS.
A: HSTS (HTTP Strict Transport Security) header tells browsers "only connect to this domain via HTTPS." But this only works if your server is already serving HTTPS. In production, you need: HTTPS certificate (Let's Encrypt is free), server configured for HTTPS, HSTS header enabled, and optionally HSTS preload for max security.
Q9: How do you handle security for WebSockets?
A: WebSockets don't send cookies by default, so you need to authenticate differently. I send the JWT token in the connection handshake, verify it before accepting the connection, and then associate the socket with the authenticated user. Same principles, different implementation.
Q10: What's your biggest security regret?
A: Not implementing security logging from day one. When something suspicious happens (multiple failed logins, unusual access patterns, etc.), you want logs. I added logging later but missed some early incidents because I had no record of them. Log security events from the start.
Posted Mar 4, 2026, 7:30 AM UTC