Configuration Reference

Complete reference for all Wrangler bindings, secrets, queue settings, and D1 schema used by formdata.dev.

Wrangler Bindings

These bindings are defined in wrangler.jsonc and available to the Worker at runtime via env.

Binding Type Name/ID Description
DB D1 Database formdata-db Stores organizations, forms, destinations, API keys
FORM_KV KV Namespace Edge cache for form config (keyed by public key)
DELIVERY_QUEUE Queue Producer formdata-delivery Enqueues one message per destination per submission
MCP_AGENT Durable Object FormDataMcpAgent MCP agent for AI-driven tenant management

Secrets

Secrets are set via wrangler secret put <NAME> and are not stored in source control.

Secret Required Description
CF_ZONE_ID No Cloudflare Zone ID for subdomain provisioning
CF_API_TOKEN No Cloudflare API token with Zone > Custom Hostnames > Edit permission
CAPTCHA_VERIFY_URL No URL for captcha token verification (e.g., Cloudflare Turnstile verify endpoint)
Captcha Secrets

Individual form captcha secrets (captchaSecret) are stored in D1 per form, not as Worker secrets. The CAPTCHA_VERIFY_URL is the global verification endpoint used by all forms.

Queue Settings

Configured in the queues.consumers section of wrangler.jsonc:

Setting Value Description
queue formdata-delivery Queue name
max_batch_size 10 Max messages processed per invocation
max_retries 5 Retries before moving to DLQ
dead_letter_queue formdata-delivery-dlq Queue for permanently failed messages
retry_delay 30 Seconds between retries

Tuning Tips

  • Increase max_batch_size (up to 100) if you have high submission volume and want fewer Worker invocations
  • Increase max_retries if your SMTP server or webhook endpoint has frequent transient failures
  • Decrease retry_delay for faster retry cycles (minimum 0 seconds)
  • Monitor the DLQ in the Cloudflare Dashboard under Queues to catch permanently failed deliveries

Durable Object Migration

The migrations section in wrangler.jsonc declares the Durable Object classes:

"migrations": [
  {
    "tag": "v1",
    "new_sqlite_classes": ["FormDataMcpAgent"]
  }
]
Migration Tags

Each migration tag must be unique. If you add new Durable Object classes in the future, add a new migration entry with a new tag (e.g., v2). Never modify existing migration entries.

Compatibility Flags

"compatibility_date": "2026-02-24",
"compatibility_flags": ["nodejs_compat"]
  • nodejs_compat is required for cloudflare:sockets (used by the SMTP connector) and other Node.js APIs

D1 Schema

The database schema is defined in migrations/0000_init.sql. Below is the complete schema reference.

organizations

Column Type Constraints Description
id TEXT PRIMARY KEY NOT NULL UUID
name TEXT NOT NULL Organization display name
slug TEXT NOT NULL UNIQUE URL slug, used for subdomain
owner_email TEXT NOT NULL Account owner email
cf_custom_hostname_id TEXT nullable Cloudflare Custom Hostname ID
created_at TEXT NOT NULL ISO 8601 timestamp
updated_at TEXT NOT NULL ISO 8601 timestamp

organization_api_keys

Column Type Constraints Description
id TEXT PRIMARY KEY NOT NULL UUID
organization_id TEXT NOT NULL, FK → organizations(id) ON DELETE CASCADE Owning organization
key_hash TEXT NOT NULL UNIQUE SHA-256 hash of the tenant key
label TEXT NOT NULL Human label (e.g., primary, rotated-2026-...)
created_at TEXT NOT NULL ISO 8601 timestamp
revoked_at TEXT nullable Set on key rotation; non-null means revoked

forms

Column Type Constraints Description
id TEXT PRIMARY KEY NOT NULL UUID
organization_id TEXT NOT NULL, FK → organizations(id) ON DELETE CASCADE Owning organization
name TEXT NOT NULL Display name
slug TEXT NOT NULL URL-safe identifier
public_key TEXT NOT NULL UNIQUE pk_ prefixed key for ingestion URL
allowed_origins TEXT NOT NULL JSON array of allowed origin URLs
verify_captcha INTEGER NOT NULL DEFAULT 0 1 = captcha required
captcha_secret TEXT nullable Captcha verification secret
is_enabled INTEGER NOT NULL DEFAULT 1 1 = accepting submissions
created_at TEXT NOT NULL ISO 8601 timestamp
updated_at TEXT NOT NULL ISO 8601 timestamp

destinations

Column Type Constraints Description
id TEXT PRIMARY KEY NOT NULL UUID
form_id TEXT NOT NULL, FK → forms(id) ON DELETE CASCADE Parent form
type TEXT NOT NULL webhook, smtp, or google_sheets_webhook
config TEXT NOT NULL JSON config object (varies by type)
is_enabled INTEGER NOT NULL DEFAULT 1 1 = active
created_at TEXT NOT NULL ISO 8601 timestamp
updated_at TEXT NOT NULL ISO 8601 timestamp

Indexes

Index Table Column(s)
idx_organizations_slug organizations slug
idx_forms_public_key forms public_key
idx_forms_org forms organization_id
idx_org_api_keys_org organization_api_keys organization_id
idx_destinations_form destinations form_id

KV Data Format

Each form's config is stored in KV under the key form:<public_key>. The value is a JSON object:

{
  "formId": "f1234567-...",
  "formName": "Contact Form",
  "organizationId": "a1b2c3d4-...",
  "slug": "contact-form",
  "allowedOrigins": ["https://example.com"],
  "verifyCaptcha": false,
  "captchaSecret": null,
  "isActive": true,
  "destinations": [
    {
      "destinationId": "d1234567-...",
      "type": "webhook",
      "config": {
        "url": "https://hooks.example.com/abc",
        "method": "POST",
        "headers": {}
      }
    }
  ]
}

KV is synced automatically after every admin mutation (create, update, delete) via the syncFormToKV and deleteFormFromKV functions. The ingestion endpoint reads exclusively from KV and never touches D1.