← All work

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.

TypeScriptFastifyALTCHAPostgreSQLDrizzle ORMNodemailer

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.