# Deploy hand-off

The build produces one canonical source tree at `<root>/pages/<project>/`, where
`<root>` is `bin/resolve-cf.sh site-root` and `<project>` is the site slug. That
tree holds the built output directory, `wrangler.toml`, and
`functions/api/<name>.ts` for any form the site captures. There is exactly one
tree per site; never deploy from a `.bak` clone or a stale sibling.

This reference composes existing deploy mechanics. It does not fork them.

## First: on a multi-tenant install, a client account publishes through the broker

If this session is a **client account** on an install where the house
holds the sole Cloudflare master (the post-1631 posture), neither path below will
work: `cf-token.sh` denies the `pages` scope with `reason=not-house`, because the
master is account-wide. Publish with `storage-pages-deploy` instead — it names the
site folder and the project, and the deploy runs house-side. The project must
already be recorded to this account by the house (`storage-pages-adopt`); if the
tool answers `unregistered`, ask the house to record it rather than trying to mint
a token. The two paths below are the **house account** and the **standalone**
editions.

## Inside maxy-code: hand off to `cloudflare:site-deploy`

When the host carries the `cloudflare` plugin, hand the canonical tree to the
`cloudflare:site-deploy` skill and let it run unchanged. It resolves its own
account Pages-and-D1 token, runs `wrangler pages deploy` to the production alias,
attaches the custom domain, wires a contact or waitlist form to D1, checks Open
Graph correctness, and gates "done" on a cache-busted live `200` on the custom
domain. Do not re-implement any of that here — `site-deploy` is the deploy
authority; this skill only produces the tree it consumes.

## Standalone: the generic Cloudflare Pages sequence

When `cloudflare:site-deploy` is not present (the marketplace edition), publish
with `wrangler` directly, following the public mechanics in the Cloudflare
references. Resolve the token through the seam — the resolver prints the **key
name**, never the value:

```bash
KEY=$(sh bin/resolve-cf.sh token-key)      # -> CLOUDFLARE_API_TOKEN, standalone
eval "export CLOUDFLARE_API_TOKEN=\"\$$KEY\""
cd "$(sh bin/resolve-cf.sh site-root)/pages/<project>"
wrangler pages deploy <output-dir> --project-name <project> --branch=main
```

Then attach the custom domain to the Pages project and confirm a DNS record points
the hostname at `<project>.pages.dev` (a subdomain needs a `CNAME`; an apex needs
a proxied flattened `CNAME` plus a `www` `CNAME`). The token that carries
**Account · Cloudflare Pages · Edit** and **Account · D1 · Edit** is the one the
form work below also needs.

## Forms and buckets: D1

A form on the site POSTs to a Pages Function (`functions/api/<name>.ts`) that
`INSERT`s into a D1 table bound in `wrangler.toml` as `[[d1_databases]]`. The token
must carry **both** Pages Edit and D1 Edit — a Pages-only token cannot touch D1,
and even a `SELECT` needs D1 Edit. One form is one table; additional form "buckets"
(a waitlist, a callback request, a survey) are additional tables and additional
`functions/api/<name>.ts` handlers on the same pattern. The full mechanics — table
schema, the `swept` read-cursor, the token discipline, and the D1 round-trip
proof — are in the host's `cloudflare/references/d1-data-capture.md`; follow it
rather than restating it.

## Done-gate

"Done" is a live behavioural signal on the production custom domain, surfaced in
chat: a cache-busted `HTTP/2 200`, a content marker for this site (not a stale one
from a previous brand), brand-correct `og:*` tags, and — for a form-bearing site —
a test POST that appears in D1 unswept. Inside maxy-code this gate is
`site-deploy`'s; standalone it is the same curl-plus-D1-SELECT check.
