Architecture

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.

High-Level Flow

Client Cloudflare Worker Queue Connector │ │ │ │ │ POST /v1/f/pk_xxx │ │ │ │─────────────────────────────>│ │ │ │ │ KV lookup (form config) │ │ │ │ Validate origin, size, │ │ │ │ captcha │ │ │ │ │ │ │ │ Enqueue 1 msg per dest │ │ │ │─────────────────────────>│ │ │ │ │ │ │ 202 Accepted │ │ │ │<─────────────────────────────│ │ │ │ │ │ Dequeue batch │ │ │ │───────────────────>│ │ │ │ │ │ │ │ ack or retry │ │ │ │<───────────────────│
  1. A client submits a POST request to /v1/f/pk_xxx.
  2. The Worker reads form configuration from KV (never D1).
  3. The Worker validates origin, payload size (max 128 KB), and optional captcha.
  4. One queue message is enqueued per destination.
  5. The Worker returns 202 Accepted immediately.
  6. The Queue consumer dequeues messages in batches and dispatches each to its connector.
  7. Successful deliveries are acknowledged; failures are retried.

Components

D1 Database (Admin)

Stores all persistent admin data:

  • Organizations (accounts)
  • API keys (SHA-256 hashed)
  • Forms and their configuration
  • Destinations

D1 is never touched on the ingestion path. It is only used for admin CRUD operations and MCP tool calls.

KV Namespace (Edge Config)

Stores denormalized form configuration keyed by public key (pk_xxx). Each KV entry contains everything the ingestion Worker needs:

  • Form ID, name, organization ID
  • Allowed origins list
  • Captcha settings
  • Active/inactive state
  • Full list of destinations with their configs

KV is synced from D1 after every admin mutation (create, update, delete).

Queues (Async Delivery)

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).

Durable Object (MCP Agent)

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.

Design Principles

KV-Only Ingestion Path

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.

Zero Submission Storage

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.

One Queue Message Per Destination

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:

  • A failing webhook does not block email delivery.
  • Each destination retries independently.
  • Retry counts are per-destination, not per-submission.

Admin Mutations Sync KV

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.

Connectors

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.