Every week I read about another app that got compromised because the developer skipped security fundamentals. Not sophisticated zero-day exploits. Basic stuff. API keys committed to GitHub. No rate limiting on login endpoints. SQL injection through unsanitised form inputs. Environment variables exposed in client-side bundles.
The common thread: developers who treated security as something to add later. Phase 2. Post-launch. When there's time. There's never time. So it never gets added. And then someone finds the .env file in the public repo.
The Vibe Coding Problem
The rise of AI-assisted development has made it incredibly easy to build things fast. It's also made it incredibly easy to build things that are fast and insecure. When you're prompting an AI to 'build me a login system,' the output will probably work. It will probably not have rate limiting, constant-time password comparison, secure cookie flags, or CSRF protection. Because you didn't ask for those things. And the AI optimised for what you asked for: a login system that works.
This is the fundamental problem with vibe coding. The output matches the input. If your input is vague on security, the output will be vague on security. The developer's job isn't to write the code — it's to know what the code should do, including the parts that aren't exciting, aren't visible, and aren't fun to implement.
What I Ship by Default
Every project I deploy — client work, personal products, everything — ships with the same security baseline. This isn't a checklist I consult sometimes. It's the starting configuration. The default. The floor below which nothing ships.
- Content Security Policy headers — restricting which scripts, styles, fonts, images, and connections the browser allows. If a malicious script somehow gets injected into the page, CSP prevents it from executing or phoning home.
- HTTP Strict Transport Security (HSTS) — forcing HTTPS with a max-age of 2 years, including subdomains, with preload. Once a browser sees this header, it will never attempt an HTTP connection to your domain again.
- X-Content-Type-Options: nosniff — preventing the browser from MIME-type sniffing, which can turn an innocent-looking file into an executable script.
- X-Frame-Options: DENY — preventing your site from being embedded in an iframe, which blocks clickjacking attacks entirely.
- Referrer-Policy: strict-origin-when-cross-origin — controlling what URL information is sent when navigating away from your site.
- Permissions-Policy — explicitly disabling browser APIs you don't use: camera, microphone, geolocation. If your site doesn't need the camera, say so. Don't leave it as an available attack surface.
Content Security Policy — The Most Ignored Header
CSP is the single most effective defence against cross-site scripting (XSS) and it's the header I see missing most often. A proper CSP tells the browser exactly which sources are trusted for scripts, styles, fonts, images, and network connections. Everything else gets blocked.
For the 88 Badminton House project, the CSP allows scripts only from the site itself and specific trusted sources. Styles from the site and the font provider. Images from the site, Supabase storage, and Unsplash (for default product images). Connections to the payment gateway, email service, and real-time database. Frame sources: none. Object sources: none. Everything else: denied by default.
Writing a CSP that works without breaking your site takes effort. You need to know every external resource your application loads. Every font CDN. Every analytics script. Every payment gateway callback URL. Most developers skip it because it's tedious and because a misconfigured CSP breaks visible functionality. But a missing CSP means any XSS vulnerability becomes a full compromise. The tedium is worth it.
Rate Limiting — Your First Line of Defence
Every public-facing endpoint that accepts user input gets rate limited. Login: 5 attempts per 15 minutes per IP. Contact form: 5 submissions per hour. Newsletter signup: 3 per hour. Checkout: 10 per hour. These limits are generous enough that legitimate users never hit them and restrictive enough that automated attacks are throttled before they cause damage.
The implementation uses in-memory rate limiting — simple, effective, zero external dependencies. For a solo developer's client projects running on Vercel's serverless infrastructure, this is pragmatic. For a high-traffic application running across multiple instances, you'd want Redis-backed rate limiting. I know when to use which. That decision-making is what separates a developer from someone who copies a tutorial.
Authentication Done Right
The 88BH admin authentication is custom-built. Not because off-the-shelf auth is bad — NextAuth and Supabase Auth are both excellent. But because this admin panel manages customer PII, financial records, and inventory data. I wanted full control and full understanding of every line in the auth flow.
Passwords hashed with bcrypt at 12 salt rounds. JWT tokens signed with HS256, carrying user ID, email, role, and a unique token ID for revocation. HTTP-only cookies — JavaScript cannot read them, which makes them immune to XSS theft. SameSite=Strict — the cookie is never sent on cross-site requests, blocking CSRF. Secure flag in production — the cookie only transmits over HTTPS. 8-hour expiry — long enough for a work session, short enough to limit damage if compromised.
One detail most developers miss: constant-time password comparison. If a user submits an email that doesn't exist, the normal flow would return immediately — 'user not found.' But the timing difference between 'user not found' (instant) and 'wrong password' (bcrypt comparison takes ~100ms) leaks information about which emails are registered. So I always run a bcrypt comparison, even on non-existent users, using a dummy hash. The response time is identical regardless of whether the email exists. This prevents timing-based user enumeration.
Compliance Is Not Optional
Malaysia's Personal Data Protection Act (PDPA) isn't a suggestion. It's law. The 2024 amendments introduced mandatory breach notification, penalties up to RM 1 million, and enhanced requirements for data processing disclosure. If your website collects a name and an email address — which every contact form does — you have obligations.
For both my portfolio site and the 88BH client project, I built PDPA-compliant privacy policies. Not templates downloaded from the internet — policies written specifically for each site's actual data collection. I audited every form, every analytics service, every third-party integration, every cookie and localStorage entry. Then I documented what data is collected, why, how it's stored, who it's shared with, how long it's retained, and how users can exercise their rights. The PDPA requires a 21-day response window for data access requests. The policy states this explicitly.
I used Claude Code to help research the PDPA requirements — fetching the actual legislation text and the 2024 amendment details from official Malaysian government sources. But the decision about what to include, how to frame the third-party disclosures, and how to structure the data retention policy came from understanding the specific context of each site. The same tool, applied with domain knowledge, produces compliance. Applied without it, it produces a generic template that may not cover your actual obligations.
The Checklist
Every project ships with this verified: CSP headers configured and tested. HSTS enabled with preload. Rate limiting on all public endpoints. Authentication with secure cookie flags and constant-time comparison. Input validation with strict schemas. CSRF protection on state-changing operations. Environment variables for all secrets — never hardcoded, never committed. Privacy policy matching actual data collection. Gitignore preventing internal tooling from leaking.
This isn't impressive. This is the minimum. The fact that it would be impressive at most agencies and for most freelancers tells you everything about the state of web security in 2026.
“Security is not something you add to finished software. It's something you build finished software on top of.”