# Recipe — "a full-stack edge app" (multiple Workers sharing data planes)

> A **golden path** for the acme-shaped app: a static frontend + one or more API Workers + a background
> processor, all sharing a database, a bucket, and (optionally) AI / vectors / live sessions. This is
> **composition without includes** — each plan stays standalone; this doc only sequences them and shows what
> each Provides to the next. The machine-readable edges live in
> [`catalog.json`](../../services/ephemera-releases/catalog.json).

## What you end up with

A real edge application: a CDN-served frontend, an HTTP API, async processing, and shared state — wired by
**bindings (names), never a shared state file**. The shape (every box is one standalone plan):

```
domain ──Provides cf-zone──► web            (static/SPA frontend)
                              │  calls
                              ▼
              ┌───────────► service  (HTTP API Worker)  ──producer──► task-runner (queue consumer / cron)
              │              │  │  │                                        │
   the shared data planes ───┘  │  └──► inference (Workers AI) ──► vector (Vectorize, RAG)
   are the hand-off:            │                                          ▲
   database (D1) ◄──────────────┴──────────────────────────────────────────┘  (embeddings → index)
   storage  (R2) ◄── service + task-runner both bind the SAME bucket/database_id
   realtime (DO) ◄── live sessions / websockets, addressed by name
```

## ⚠️ The key idea: shared data planes are the hand-off

In `acme`, three Workers (`api`, `forms`, `processor`) bind the **same** D1 `database_id` and the
**same** R2 buckets. That sharing is not a shared *state file* (the `.tfstate` anti-pattern) — it's the **cloud
as the hand-off**: the database/bucket plan **Provides** `d1-db`/`r2-bucket`, and each Worker **Requires** it,
discovering it by name at bind time. **Provision the shared data planes first; the Workers come second.**

## The order

| step | plan | knobs | Provides → | Requires |
|------|------|-------|-----------|----------|
| 1 | `domain.cloudflare.md` | — | `cf-zone(DOMAIN)` | — |
| 2 | `database.cloudflare.md` | `SCHEMA_MODE=migrations`, `ACCESS=shared` | `d1-db(DB, id)` | — |
| 3 | `storage.cloudflare.md` | `ACCESS=private` (+`NOTIFY=queue` to feed the processor) | `r2-bucket(BUCKET)` | `cf-zone?`, `queue?` |
| 4 | `vector.cloudflare.md` *(if RAG)* | `DIMENSIONS` (match the model), `METRIC=cosine` | `vectorize-index(IDX)` | the embedding model's width |
| 5 | `realtime.cloudflare.md` *(if live)* | `PATTERN=websocket-room` | `durable-object(CLASS)` | `cf-zone?` |
| 6 | `service.cloudflare.md` | `ROUTING=route`, `BINDINGS=[d1,r2,ai,vectorize,do]`, `SECRETS`, `BOT_PROTECT=turnstile` | `http-service(WORKER@URL)` | steps 2–5 + `cf-zone` |
| 7 | `inference.cloudflare.md` *(if AI)* | `MODEL_CLASS`, `GATEWAY=ai-gateway`, `PROTECT` | `inference-endpoint` / `embeddings` | `cf-zone?` |
| 8 | `task-runner.cloudflare.md` | `STATE_STORE=d1` (the shared DB), `ENGINE=queue` or `cron` | `task-api` (the processor) | the shared `d1-db` + `queue` |
| 9 | `web.cloudflare.md` | `SITE_TYPE=spa`, `DOMAIN_MODE=apex` | `site(DOMAIN)` | `cf-zone` |

Take **only the steps you need** — a plain API + DB + frontend is steps 1,2,6,9; add 3/4/5/7/8 as the app grows.
Each intent is one binding here (all-Cloudflare), but every step is swappable per the shared contract.

## How to drive it

Plain English, one plan at a time — provision the shared planes, then the Workers that bind them:

```
"register example.com with domain.cloudflare.md"                                   # 1  (🔴 purchase gate)
"create the app database with database.cloudflare.md, migrations, shared"          # 2  → d1-db
"create a private R2 bucket with storage.cloudflare.md"                            # 3  → r2-bucket
"stand up the API with service.cloudflare.md on api.example.com, binding the DB
 and bucket, turnstile on public posts"                                            # 6  (discovers 2 & 3)
"add the background processor with task-runner.cloudflare.md using the same DB"     # 8  (binds the shared d1-db)
"ship the frontend with web.cloudflare.md, spa, apex"                              # 9
```

After each plan, its **acceptance contract** is the gate before the next: db → schema applied + round-trip;
storage → object round-trip + private has no public URL; service → `GET /healthz` 200 + a bound resource
returns live data; task-runner → submit → terminal status; web → `GET / → 200 + EPHEMERA-OK`. When every
step you chose passes, the app is live.

## Provenance across many Workers

Cloudflare has no resource tags, so each Worker carries provenance in its own `[vars]`
(`MANAGED_BY`/`SOURCE`/`PLAN_VERSION`/`ENVIRONMENT`) and the **shared** D1/R2 carry it by **naming**
(`${APP}-db-${ENV}`, `${PREFIX}-${ENV}`). One `ENV` knob threads every name — "stand the whole app up for stg"
is the same plans with `ENV=stg`.

## Deliberately not a meta-plan

This is a doc, not a `stack.app.cloudflare.md` orchestration plan — no new intent, just an ordering of existing
ones and the Provides/Requires they already declare. If a single "run all of it" entry point earns its keep
later, a thin meta-plan can list this same Requires-order; until then, the standalone plans + this recipe are
the lighter answer.
