# Forms

Take contact forms, sign-ups and feedback without writing a backend. Mark a form in your HTML, deploy, and Deplane receives its submissions, keeps them, and tells you about them.

## Add a form

Add `data-auto-form` and a `name` to a `<form>`:

```html
<form name="contact" data-auto-form data-auto-form-honeypot="company" data-auto-form-thanks="/thanks.html">
  <p hidden><input name="company"></p>
  <input name="email" type="email" required>
  <textarea name="message"></textarea>
  <button>Send</button>
</form>
```

Deploy. Deplane reads the `.html` files in your project, finds every marked form with its fields, and sets where each one posts: it writes `action="/__auto_forms/…"` and `method="POST"` into the form in the build. You never set the action yourself, and none of your app's own paths is ever taken over. Netlify's `netlify` and `data-netlify="true"` attributes work too, so forms from a Netlify site need no change; a Netlify form's `action` becomes its thank-you page.

After a submission, the visitor goes to the page in `data-auto-form-thanks` (a page of your site). Without it, they see a short "Thanks, we got it." page. With `fetch` and `Accept: application/json`, the answer is `{"ok": true}`.

Only fields that are in the deployed form are kept. File fields are not supported: their files are dropped.

## Forms built by JavaScript

Deplane reads only HTML files, never JSX, Vue or Svelte. For a form your JavaScript builds, declare it in `deplane.toml`:

```toml
[[forms]]
name = "signup"
fields = ["email", "plan"]
thanks = "/welcome"   # optional
```

After the deploy, `deplane forms` (or the dashboard's **Forms** tab) shows the path the form posts to. Post to it:

```js
await fetch("/__auto_forms/3f9c…", {       // the path deplane forms shows
  method: "POST",
  headers: { "Accept": "application/json" },
  body: new URLSearchParams({ email, plan }),
});
```

The path stays the same across deploys.

## Versions of a form

Each deploy uses its own version of each form. Rolling back brings the earlier version back too. A field is never deleted: fields the deployed version doesn't have are left out of the inbox, the CLI and the emails, and come back if a later version has them again. The CSV export has every field ever collected.

## Read submissions

In the dashboard, open the app's **Forms** tab. From the CLI:

```sh
deplane forms                              # forms, counts, and where each posts
deplane forms submissions contact          # the newest 100
deplane forms submissions contact --status spam
deplane forms export contact > contact.csv # all of them, every field, as CSV
deplane forms delete contact ID            # one; without ID, all of them
```

Submissions are kept until you delete them. There is no limit on how many a form takes; a form that holds 100,000 stops taking new ones until you delete some, and you get an alert. Spam is deleted after 7 days.

Submissions are your visitors' personal data: they are encrypted at rest with a key of your team's own, and AI tools (MCP, agent tokens) can never read them: they can list a team's forms and counts (`list_forms`), nothing more. Members read submissions; only owners delete them or change notifications and CAPTCHA.

## Notifications

```sh
deplane forms notify contact --email you@example.com,teammate@example.com
pbpaste | deplane forms notify contact --slack -
pbpaste | deplane forms notify contact --webhook -
deplane forms notify contact --slack off
```

- **Email** goes only to members of your team, as a digest of what came in: up to 8 a day on Free (every 3 hours), up to twice an hour on paid plans. When nothing came in, no email is sent.
- **Slack** gets a short message for each submission: the form, its first fields, and a link to the inbox.
- **Webhooks** get each submission as JSON: `{"form", "app", "submissions": [{"data", "created_at"}], "more"}`. The response shows a signing secret once; check that `X-Deplane-Signature` is `sha256=` followed by the HMAC-SHA256 of the body with that secret. Failing deliveries are retried, and after a day of failures you get an alert.
- **Busy forms are collated:** while a message for a form is being sent or retried, new submissions wait and go out together in the next one. Slack says how many came in and shows the newest; a webhook gets up to the 100 newest in `submissions`, with `more` counting the rest. Every submission is in the inbox either way.

Spam never notifies.

## Spam

- **Honeypot:** a hidden field (`data-auto-form-honeypot="NAME"`, or Netlify's `netlify-honeypot`). People leave it empty; bots fill it in. Those submissions go to spam.
- **Rate limits:** 10 a minute from one visitor, 600 an hour per form.
- **CAPTCHA:** Cloudflare Turnstile, per form.

### CAPTCHA

The CAPTCHA is Cloudflare Turnstile, with a free widget in your own Cloudflare account. Your domains do not need to be on Cloudflare.

1. Sign in at [dash.cloudflare.com](https://dash.cloudflare.com) (a free account is enough), open **Turnstile**, and choose **Add widget**.
2. Give it a name and add your app's hostnames: its own address (`<app>.deplane.app`) and each custom domain. A hostname covers its subdomains.
3. Widget mode: **Managed**. Create it. Cloudflare shows a **site key** and a **secret key**.
4. Put the widget inside your form, with your site key:

   ```html
   <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
   <!-- inside the form -->
   <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
   ```

5. Give Deplane the secret key and turn the check on, in the dashboard's **Forms** tab (CAPTCHA card), or from the CLI:

   ```sh
   pbpaste | deplane forms captcha contact on --secret -
   ```

6. Deploy the page with the widget.

`deplane forms captcha contact on` again keeps your key; `deplane forms captcha contact off` turns the check off. A form cannot require the check without a secret key ([captcha_key_required](errors.md#captcha_key_required)).

If Cloudflare cannot be reached, the submission is kept in spam rather than lost.

Turnstile sends your visitors' browser data to Cloudflare. Mention it in your site's privacy notice.
