Open Source ·
Edge Form
Serverless form inbox for static sites — submit endpoint, D1 archive, abuse controls, email/webhook delivery, and admin export.
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 API —
POST /api/submitaccepts JSON,application/x-www-form-urlencoded, and multipart form submissions. - Payload normalization — common fields like
name,email,subject, andmessageare 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
- Parse the body as JSON, URL-encoded, or multipart form data.
- Enforce size and content-type limits before trusting the payload.
- Hash the client IP using
IP_HASH_SECRETso raw addresses are not persisted. - Apply rate limiting through the D1-backed
rate_limitstable. - Check spam signals: honeypot field and excessive links.
- Normalize and validate the submission.
- Persist the record to D1.
- Dispatch delivery adapters with
ctx.waitUntil()so the client gets a fast response while email/webhook work continues. - 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
| Route | Purpose |
|---|---|
GET /health | Health check for deployments and uptime checks |
POST /api/submit | Public submission endpoint for browser forms |
GET /api/admin/submissions | Bearer-token protected list/search endpoint |
GET /api/admin/submissions/:id | Submission detail plus delivery event history |
GET /api/admin/export.csv | CSV export for spreadsheet or client handoff |
Configuration
| Area | Config |
|---|---|
| Rate limit | RATE_LIMIT_MAX, RATE_LIMIT_WINDOW_SECONDS |
| Spam checks | HONEYPOT_FIELD, MAX_LINKS |
| Validation | MESSAGE_MIN_LENGTH, MESSAGE_MAX_LENGTH, MAX_BODY_BYTES |
| CORS | ALLOWED_ORIGINS |
| Admin auth | ADMIN_TOKEN |
| Privacy | IP_HASH_SECRET |
RESEND_API_KEY, MAIL_FROM, MAIL_TO | |
| Webhook | WEBHOOK_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