Context
As part of my portfolio website, I wanted to offer a contact form that is both secure and minimal — without relying on third-party form providers or heavyweight CAPTCHA solutions.
The goal was to build a small but production-ready pipeline that handles spam prevention, validation, rate limiting and email delivery, while still feeling lightweight and maintainable.
Decisions
- Cloudflare Turnstile instead of reCAPTCHA
Turnstile provides bot protection without forcing users through visual challenges. It also fits well with my preference for privacy-friendly defaults. - Separate mail subdomain
The API runs on a dedicated subdomain to keep concerns separated and make CORS and security policies explicit. - No frontend framework
The form logic is written in vanilla JavaScript to stay aligned with the overall philosophy of the portfolio.
Implementation notes
- Turnstile is verified server-side via Cloudflare’s
siteverifyendpoint. - Requests are validated using a schema-based approach before any further processing.
- Rate limiting and a honeypot field provide additional protection against abuse.
- Email delivery is handled via SMTP on a self-managed VPS using Nodemailer.
- Logging was added early to make debugging production issues possible without guesswork.
Lessons learned
- Missing or misconfigured environment variables are a common source of hard-to-diagnose errors — explicit logging helps a lot.
- Running your own mail stack gives you control, but also exposes you to networking and TLS edge cases that hosted solutions abstract away.
- Double execution bugs are easy to introduce when initialization logic is split across multiple files.
Links
- Live: https://michelnauen.dev/contact
- Repo: (private)