# Recipe — "a contact-form site"

> A **golden path**: which plans to run, in what order, with which knobs, so a stranger gets a working
> static site with a contact form that sends email — without reverse-engineering the Provides/Requires graph.
> This is **composition without includes**: each plan stays standalone; this doc only sequences them and
> shows what each one Provides to the next. The machine-readable edges live in [`catalog.json`](../../services/ephemera-releases/catalog.json).

## What you end up with

A static website on HTTPS + a small form handler that receives `POST /contact` and sends an authenticated
email to you. Four intents compose:

```
domain  ──Provides delegated-zone/cf-zone──►  web        (the site)
   │                                           │
   └──Provides cf-zone──►  email  ◄────────────┘  (the sender the handler uses)
                             ▲
        task-runner ─────────┘  (the form handler: POST /contact → calls the email sender)
```

## ⚠️ The one surprise to know first

**Every `email.*` binding — including the AWS/SES one — Requires a *Cloudflare* zone** for its DKIM/SPF/DMARC
DNS records (correct for this stack; surprising for a pure-AWS adopter). So if you want the contact form to
send mail, the domain's **DNS zone must be on Cloudflare**, even if the site itself is on AWS or GCP. Decide
this at the **domain** step, not after you've built the site. Two clean ways:

- **All-Cloudflare DNS:** run [`domain.cloudflare.md`](../../domain.cloudflare.md) → Provides `cf-zone`. Simplest.
- **Register on AWS, DNS on Cloudflare:** run [`domain.aws.md`](../../domain.aws.md) for registration but point the
  domain's nameservers at a Cloudflare zone — so you still get `cf-zone` for email. (web.aws's `DNS_MODE=external`
  expects exactly this.)

## The order

| step | plan | knobs | Provides → | Requires |
|------|------|-------|-----------|----------|
| 1 | `domain.cloudflare.md` (or `domain.aws.md` + CF DNS) | — | `cf-zone(DOMAIN)` | — |
| 2 | `web.{aws,cloudflare,gcp}.md` | `SITE_TYPE=spa` or `static`, `DOMAIN_MODE=apex/subdomain` | `site(DOMAIN)` | `cf-zone` (custom domain) |
| 3 | `email.{resend,cloudflare,aws}.md` | — | `email-sender(DOMAIN)` | `cf-zone` |
| 4 | `task-runner.{cloudflare,aws}.md` | `STATE_STORE`, `ENGINE` | `task-api` (the `/contact` handler) | the email sender's send call/keys |

Pick **one binding per intent** — they're interchangeable per the shared contract. A pragmatic low-cost combo:
`domain.cloudflare` → `web.cloudflare` → `email.resend` → `task-runner.cloudflare` (all on one account, very low cost,
fastest to first-send). An all-AWS combo works too, but step 3 still needs the zone on Cloudflare (the surprise above).

## How to drive it

Talk to your agent in plain English, one plan at a time — Ephemera is conversational:

```
"register example.com — use domain.cloudflare.md"          # step 1, stop at the 🔴 purchase gate
"stand up the site with web.cloudflare.md, static, apex"   # step 2
"set up sending from example.com with email.resend.md"     # step 3, add the DKIM/SPF records to the zone
"build the /contact handler with task-runner.cloudflare.md, that calls the email sender"   # step 4
```

After each plan, its **acceptance contract** (in the plan + summarized in `catalog.json`) is the gate before moving on:
domain → zone delegated; web → `GET / → 200 + EPHEMERA-OK`; email → DKIM/SPF/DMARC present + a test send received;
task-runner → `POST /contact → accepted`, status reaches terminal. When all four pass, the contact-form site is live.

## Deliberately not a meta-plan

This is a doc, not a `stack.contact-form.*.md` orchestration plan — there is no new intent here, just an ordering of
existing ones. If a single "run all four" entry point earns its keep later, a thin meta-plan can list this same
Requires-order and call each plan; until then, the standalone plans + this recipe are the lighter answer.
