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 siteverify endpoint.
  • 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.