formdata.dev is a pure form ingestion service built on Cloudflare Workers. It accepts form submissions at the edge, validates them, and delivers payloads to configured destinations asynchronously.
POST request to /v1/f/pk_xxx.202 Accepted immediately.Stores all persistent admin data:
D1 is never touched on the ingestion path. It is only used for admin CRUD operations and MCP tool calls.
Stores denormalized form configuration keyed by public key (pk_xxx). Each KV entry contains everything the ingestion Worker needs:
KV is synced from D1 after every admin mutation (create, update, delete).
Cloudflare Queues decouple ingestion from delivery. Configuration from wrangler.jsonc:
| Setting | Value |
|---|---|
| Queue name | formdata-delivery |
| Max batch size | 10 |
| Max retries | 5 |
| Retry delay | 30 seconds |
| Dead letter queue | formdata-delivery-dlq |
Each queue message contains the full submission envelope and a single destination. This enables per-destination retry -- if one destination fails, others are unaffected.
After 5 failed retries, the message is moved to the dead letter queue (formdata-delivery-dlq).
The FormDataMcpAgent Durable Object hosts the MCP server. It authenticates incoming requests with the sk_ secret key and provides 9 tools for account, form, and destination management.
The submission endpoint reads exclusively from KV. This avoids any latency from D1 database queries on every form submission. KV is globally replicated at the edge, so lookups are fast regardless of where the request originates.
formdata.dev does not store submission payloads. The form data exists only as a transient queue message between ingestion and delivery. Once delivered (or moved to the DLQ after exhausting retries), the payload is gone.
Each destination receives its own queue message. If a form has 3 destinations (e.g., email + webhook + Google Sheets), 3 separate messages are enqueued. This means:
Every admin change (create/update/delete form, add/remove destination) writes to D1 first, then syncs the affected form's full config to KV. This ensures the ingestion path always has current configuration without querying D1.
Three connector types are supported:
| Connector | Transport | Config Fields |
|---|---|---|
| webhook | HTTP POST/PUT/PATCH | url, method, headers |
| smtp | Raw TCP sockets (cloudflare:sockets) |
host, port, secureTransport, username, password, from, to[], subjectTemplate |
| google_sheets_webhook | HTTP POST/PUT | url, method, authHeader |
All connectors receive the full SubmissionEnvelope and return a DeliveryResult indicating success or failure with optional response code and body.