Professional · Backend & security
A spam-resistant contact API that never exposes your inbox
For Batteries Included, LLC I built a small Fastify contact service used by the company site. It verifies an ALTCHA proof-of-work, validates and stores the submission in PostgreSQL, and delivers mail over SMTP, without putting an email address or SMTP credentials anywhere near the browser.
Company
Batteries Included, LLC
Role
Sole developer
Runtime
Node.js + Fastify
Anti-spam
ALTCHA proof-of-work
Problem
Public contact forms are an abuse magnet
Batteries Included needed a contact path for its marketing site that could stay fully static on the frontend. The naive approaches all leak something: a mailto: link exposes the address to harvesters, a third-party form service adds a heavy captcha and a dependency on someone else's uptime, and posting directly to an SMTP server from the browser would expose credentials. The goal was a service where sensitive configuration lives only on the server.
Approach
Two small endpoints with an explicit contract
The service exposes just two endpoints. GET /challenge returns a signed ALTCHA challenge that the browser widget solves as a proof-of-work. POST /contact accepts JSON with name, email, subject, body, and the ALTCHA payload. The frontend stays a handful of fetch calls; the server owns everything that matters.
POST /contact
Content-Type: application/json
{
"name": "Alex Visitor",
"email": "visitor@example.com",
"subject": "Project inquiry",
"body": "Message text…",
"altcha": "<signed solution from GET /challenge>"
}Every request body is validated with explicit rules (required fields, minimum lengths after trimming, a real email check) and sanitized to strip null bytes and most control characters while keeping newlines and tabs. The ALTCHA payload is verified with a shared HMAC secret before any database write or outbound mail. CORS is locked to the single site origin so only the real frontend can call it.
Approach
Persist first, then deliver, with a gated auto-reply in progress
Accepted submissions are inserted into a PostgreSQL emails table (via Drizzle ORM) before any mail leaves the process, giving a durable audit trail. Outbound mail uses Nodemailer with environment-driven SMTP settings, and the transporter is verified at startup so misconfiguration fails loudly rather than silently dropping messages.
A notification always goes to the configured recipient inbox. The optional confirmation to the sender is rate limited: if the same address has submitted three or more times today, or again within the last hour, the auto-reply (in progress) is skipped while the notification still sends. That keeps a misbehaving sender from turning the mail path into a spam amplifier.
Results
What it delivers
- Proof-of-work blocks automated spam without a third-party captcha widget on the page.
- The recipient address and SMTP credentials never reach the client.
- Predictable JSON errors for validation and verification failures; generic 4xx/5xx otherwise.
- Every submission is stored, so there is a record even if mail delivery has a bad day.
Lessons
What I would carry forward
Public endpoints are where small oversights become incidents, so the design bias was toward a fast happy path and a boring unhappy path. Bundling the service with Rspack and typechecking with tsgo kept the deployable small and the feedback loop quick. If I extended it, the next steps would be structured logging with request correlation IDs and a lightweight admin view over the stored submissions.