Open Source ·

Edge Form

Serverless form inbox for static sites — submit endpoint, D1 archive, abuse controls, email/webhook delivery, and admin export.

  • Cloudflare Workers
  • D1
  • TypeScript
  • Serverless
  • Open Source

Case study snapshot

Role
Solo product engineer across architecture, Worker API, D1 schema, abuse controls, delivery adapters, and deployment documentation.
Scope
Public form endpoint, CORS, validation, spam checks, D1 inbox, email/webhook delivery, admin list/detail routes, and CSV export.
Stack
Cloudflare Workers, D1, TypeScript, Serverless, Open Source
Status
Functional MVP ready for static sites, portfolios, landing pages, and client projects.
Proof
Durable D1 submission archive with spam checks, delivery adapters, and CSV export.
Outcome
Self-hostable form backend that gives static sites a reliable serverless inbox.

edge-form-inbox gives static websites a production-ready contact form backend without running a server. Submissions are validated at the edge, stored in D1, protected from basic abuse, optionally forwarded to email or webhooks, and exportable from protected admin routes.

At a glance

  • Role: Solo product engineer across architecture, Worker API, D1 schema, abuse controls, delivery adapters, and deployment documentation.
  • Scope: Public form endpoint, CORS, validation, spam checks, D1 inbox, delivery event logging, admin list/detail routes, and CSV export.
  • Stack: Cloudflare Workers, D1, TypeScript, Wrangler, Resend API, signed webhooks.
  • Status: Functional MVP ready for static sites, portfolios, landing pages, and client projects.
  • Proof: Serverless submit flow, durable submission archive, hashed IP rate limiting, adapter audit trail, and bearer-token admin access.

The problem

Static sites are fast, cheap, and easy to deploy, but contact forms still need a backend. Teams usually end up choosing between third-party form services, email-only submissions, or a custom server that is too much infrastructure for a simple site.

That creates a few practical problems:

  • A static site cannot receive form submissions by itself.
  • Hosted form services add lock-in, recurring cost, and external data retention.
  • Email-only delivery loses the source of truth when a message bounces or gets filtered.
  • Public forms need basic abuse controls, even on small projects.
  • Client sites need a simple operational handoff, not a full application server.

The goal was to build a small, self-hostable form inbox that can sit behind any static website, store every accepted submission, and forward notifications without requiring traditional backend infrastructure.

What I built

edge-form-inbox is a Cloudflare-native form backend with one public submission API and a protected admin/export API.

Core features:

  • Public submission APIPOST /api/submit accepts JSON, application/x-www-form-urlencoded, and multipart form submissions.
  • Payload normalization — common fields like name, email, subject, and message are normalized while extra safe fields are preserved as metadata.
  • Spam and abuse controls — honeypot detection, message length validation, excessive-link checks, body-size limits, and D1-backed rate limiting.
  • Privacy-aware IP handling — raw IP addresses are not stored. IPs are hashed with a deployment secret before being used for rate limits.
  • D1 submission inbox — accepted, flagged, and rejected submissions are persisted with timestamps, metadata, user agent, spam status, delivery status, and delivery errors.
  • Delivery adapters — Resend email notifications and signed webhook delivery can be enabled independently.
  • Admin and export API — bearer-token protected routes list submissions, fetch a single submission with delivery events, and export CSV.

How it works

Static site / HTML form

        │ POST /api/submit

Cloudflare Worker
parse · validate · CORS · honeypot · rate limit · hash IP

        ├── D1: submissions, rate_limits, delivery_events

        └── waitUntil(): Resend email and signed webhook delivery

Admin API
list · detail + events · CSV export

The system is intentionally small: one Worker, one D1 database, environment variables for configuration, and optional delivery integrations. There are no queues, containers, or long-running servers to operate.

Request pipeline

  1. Parse the body as JSON, URL-encoded, or multipart form data.
  2. Enforce size and content-type limits before trusting the payload.
  3. Hash the client IP using IP_HASH_SECRET so raw addresses are not persisted.
  4. Apply rate limiting through the D1-backed rate_limits table.
  5. Check spam signals: honeypot field and excessive links.
  6. Normalize and validate the submission.
  7. Persist the record to D1.
  8. Dispatch delivery adapters with ctx.waitUntil() so the client gets a fast response while email/webhook work continues.
  9. Record adapter results and update the final delivery status.

Key decisions

Store the truth before delivery

Email and webhook delivery are best-effort. D1 persistence happens first, so a submission is not lost if Resend is down, a webhook fails, or an adapter is missing configuration.

Keep privacy defaults practical

The Worker never stores raw IP addresses. Rate limiting still works, but the stored value is a salted hash. That is enough for basic abuse protection without keeping direct identifiers in the inbox.

Accept common static-site payloads

The endpoint supports the formats developers actually use for simple forms: JSON, URL-encoded bodies, and multipart form data. Custom fields are preserved as metadata so each site can add fields without a schema migration.

Make adapter failure visible, not fatal

Delivery adapters return sent, failed, or skipped. Each attempt is recorded in delivery_events, and the submission receives a final delivery status. The public form flow does not crash just because an optional notification channel is misconfigured.

API surface

RoutePurpose
GET /healthHealth check for deployments and uptime checks
POST /api/submitPublic submission endpoint for browser forms
GET /api/admin/submissionsBearer-token protected list/search endpoint
GET /api/admin/submissions/:idSubmission detail plus delivery event history
GET /api/admin/export.csvCSV export for spreadsheet or client handoff

Configuration

AreaConfig
Rate limitRATE_LIMIT_MAX, RATE_LIMIT_WINDOW_SECONDS
Spam checksHONEYPOT_FIELD, MAX_LINKS
ValidationMESSAGE_MIN_LENGTH, MESSAGE_MAX_LENGTH, MAX_BODY_BYTES
CORSALLOWED_ORIGINS
Admin authADMIN_TOKEN
PrivacyIP_HASH_SECRET
EmailRESEND_API_KEY, MAIL_FROM, MAIL_TO
WebhookWEBHOOK_URL, WEBHOOK_SECRET

Outcome

The MVP proves a complete serverless form workflow for static websites: submissions can be accepted safely, stored durably, forwarded through optional adapters, inspected through protected admin routes, and exported for handoff.

It is intentionally small enough to self-host, adapt for client projects, and use as a reference architecture for edge-native form handling.

What I would add next

  • Optional Cloudflare Turnstile verification for stronger bot protection.
  • A small admin dashboard UI on top of the existing admin API.
  • Retry controls for failed delivery events.
  • Per-form configuration for multi-site deployments.
  • Retention policies for old submissions and rate-limit rows.
  • Tests around request parsing, validation, CORS, rate limiting, and adapter status transitions.

Have a similar system to build?

Send the product goal, timeline, and current blockers. I’ll help you turn it into a practical build plan.

Start a conversation

All works