# Playbook — builder / developer on a console client

Use this playbook when the user is a developer or solo builder and you are running in a client that can execute outbound HTTP requests (e.g. `curl`), read and write local files, and stage ZIP archives. Current examples: Claude Code, Cursor agents, Codex CLI, Gemini CLI, Claude Desktop with a shell MCP server attached (e.g. Desktop Commander), Aider, Continue. If your client cannot run `curl` or cannot read local files, switch to `guide(topic="builder_web_chat")` instead.

This is the unsqueezed cell — every upload path the platform exposes is reachable from where you're running. The playbook is short because the primitive set is wide, the shared reference (`llm.txt`, already loaded via `guide(topic="orient")`) is written for a reader like you, and this profile knows what it wants.

## Orient

State the shape once: static per-project hosting, `<slug>.uat-beam.page` subdomains, `/app/` SPA-rewrite slot, snapshot / restore with a 30-day window on Google-owned projects, eventual-consistency writes with a 1–2 second read lag. Full reference is already in your context via `guide(topic="orient")`; fall back to `https://uat-beam.page/llm.txt` only when you're not on MCP.

Stay direct — imperatives, not hedges. `run this`, `zip that`, `PUT these bytes to this URL`.

## Auth — Google first

Go straight to `GET /auth/google`, hand over the URL, wait for the paste. Guest is available but rarely what this profile wants — it has no snapshots, it self-deletes in two hours, and the conversion path only matters if they somehow started guest. If they are already on MCP, the connector has done this for them (the "Connected via MCP" block of the shared reference has the details).

## Project — user picks the slug, hand over the URL

State the slug rules (`[a-z0-9-]`, 2–63 chars, globally unique), then `POST /projects {"slug", "context"}`. Surface any error verbatim; don't guess a substitute — this profile wants explicit control. Once the call returns 200, give the user the `https://<slug>.uat-beam.page` URL so they can open it in a browser.

## Static pages or SPA?

Pick the shape silently before you start iterating:

- **Mostly read-only content that updates infrequently and needs to rank on Google** (marketing sites, restaurants, portfolios, agencies, most small-business sites) → **static pages** — one HTML file per page, SEO works by default, `POST /edit` and the upload methods below cover every iteration.
- **User-interactive tool, dashboard, filterable catalogue, map with saved state, anything with meaningful client-side state** → **SPA under `/app/*`** (see the `/app/` pattern in the Iterate section below, plus `spa-react.md` / `spa-flutter.md`).
- **Hybrid** (content marketing + app functionality) → **both**: static pages for `/`, `/menu`, `/about`; SPA at `/app/*`. The `/` → `/app/*` redirect is optional and usually not what you want.

**Static does not mean read-only.** The `email` action is a server-side primitive the platform provides, so a static site with a booking / quote / RSVP form works fine — the static-plus-action combo covers most small-business use cases without the SPA complexity.

## First content — pick the upload method by shape

Four upload paths, all available to you:

| Shape | Method |
|---|---|
| Text file you can build in-process (HTML, CSS, JS, JSON) | `PUT .../files/<name> {"content": "..."}` |
| Public HTTPS URL already hosts the bytes | `PUT .../files/<name> {"url": "https://..."}` |
| Binary on disk (image, PDF, font) | `POST .../files/<name>/upload-url` → `curl -X PUT --data-binary @file "<uploadUrl>"` echoing every response header |
| Folder tree (SPA build, gallery, multi-page site) | `POST .../pages/<slug>/upload-zip-url` → `curl -X PUT --data-binary @archive.zip "<uploadUrl>"` |

`--data-binary` is required — `-d` / `--data` strips newlines and corrupts HTML. Every custom `x-amz-meta-*` header from the response must be echoed verbatim on the PUT call; mismatch fails with `SignatureDoesNotMatch`. ZIP upload replaces the target page's full file set — anything on the page not in the archive is deleted, same shape as Netlify / Vercel / GitHub Pages.

For the limits (4 MB inline content, 500 MB decompressed ZIP, 100× compression ratio, `max_files_per_page` from `GET /me`, `index.html` at the ZIP root for shell pages) the "Bulk upload via ZIP" section of the shared reference has the detail. Don't re-derive it here.

## SEO — head tags, auto-sitemap, metadata distinction

Sitemap is auto-generated per project at `/sitemap.xml`; `robots.txt` is synthesised permissively by the platform on every subdomain — you don't create either. Search-engine metadata lives in the HTML: `<title>`, `<meta name="description">`, OpenGraph tags, and a `<script type="application/ld+json">` block with schema.org `LocalBusiness` (pick a subtype — `Restaurant`, `HomeAndConstructionBusiness`, `BeautySalon`, `FinancialService`, `ProfessionalService`, or the base `LocalBusiness`).

The `PUT /pages/<slug>/metadata` primitive is a different concern — it's for **data-surface** metadata that the rest of the site (or `cross_page_metadata.json`) reads from client-side JS. Head tags are for search engines and link previews; `metadata` is for the site's own JavaScript. Don't conflate them.

## Email action — the default stance for forms

If the site has a form of any kind — contact, booking, RSVP, quote request, application, survey, waitlist, donation pledge — **default to wiring it to the `email` action** unless the user has an explicit reason to do otherwise (Slack webhook, custom handler, CRM). The primitive is `POST /actions/<project-slug>/email` with an arbitrary JSON body; the platform rate-limits to 10 emails per project per hour, SES delivers to the authenticated owner, and spam basics are handled.

The action is not restricted to "contact us" — RSVPs, bookings, applications, quotes, surveys, and waitlist signups are all the same shape. The existing `contact-form.md` scenario guide documents the HTML pattern, Alpine.js scaffold, rate limit, and pre-filling via query params — but the file name narrows the framing: **the same pattern applies to every form on the site, not only contact forms.**

Builder overrides: if the user specifies Slack, a custom webhook, or a CRM, do that instead — the `email` action is a default stance, not a mandate.

## Iterate — partial edits and the SPA slot

For small textual changes use `POST /edit` with `{edits: [{old, new}], page, file}` or `{edits, asset}`, or the bulk-target form `{edits, targets: [...]}` that validates every edit against every target before any write. Matching is byte-exact UTF-8 (the "Common pitfalls" section of the shared reference): curly vs straight quotes, `&mdash;` vs `—`, ` ` vs space all silently fail — `GET` the file and copy the exact bytes into `old` rather than retyping.

The SPA slot under `/app/` is the one non-obvious pattern worth stating here — any extensionless URL under `/app/` resolves to `<slug>/app/index.html`, so a client-side router owns the deeper routes. Workflow:

1. Build with the right base — `vite.config.js` `base: "/app/"` for React, `flutter build web --base-href=/app/` for Flutter.
2. `POST /projects/<id>/pages {"slug": "app"}`.
3. ZIP from *inside* the build output (`cd dist && zip -r ../build.zip .`) so `index.html` sits at the ZIP root — not at `app/index.html`.
4. `POST /projects/<id>/pages/app/upload-zip-url` → `curl` PUT.
5. Add `POST /projects/<id>/redirects {"from": "/", "to": "/app/"}` so visitors to the bare subdomain land on the SPA.

Depth targets: `spa-react.md`, `spa-flutter.md`, and the "Hosting a SPA under `/app/`" section of the shared reference. For redirect-on-rename patterns, chains, and deletion rules, the "Redirects" section of the shared reference is the source of truth.

## Durability — snapshot before risky changes

Every Google-owned project has nightly snapshots at 00:00 UTC, an initial snapshot at project creation, and on-demand `POST /projects/<id>/snapshots` (rate-limited to 1 per project per minute). Before a risky refactor, take one explicitly:

> "Take a snapshot labelled `pre-redeploy` first? `POST /projects/<slug>/snapshots {"label": "pre-redeploy"}`."

Restore is `POST /projects/<id>/restore {"snapshot_id": "..."}`, rate-limited to 1 per project per 5 minutes, and replaces the full file set. Failure modes (`404 Snapshot not found`, `410 Snapshot manifest is no longer available`, `500 Restore failed on <key>`, `429`) are in the "Snapshots and point-in-time restore" section of the shared reference; consult it on any non-200 rather than guessing.

Custom domains are coming but not available — keep to the "coming soon" phrasing from the shared reference.

## Images and binaries — a line, not a paragraph

Ask the user for the path, `POST .../files/<name>/upload-url`, `curl -X PUT --data-binary @path "<uploadUrl>"` echoing every returned header. Same flow for PDFs, video, fonts, anything binary. If the file already has a public URL, URL-fetch is the one-liner alternative.

## Do not

- Default the slug — this profile wants explicit control over the subdomain; surface errors verbatim instead of silently retrying candidates.
- Silently switch upload methods mid-task — if the user said _"I'll push a ZIP"_, don't suddenly route to per-file PUTs because one file looks small.
- Wrap imperatives in hedges — _"your assistant might be able to run `curl`"_, _"if your tool supports ZIP"_ — this reader is an imperative reader; say `run this`, `zip that`.
- Retype `old` for `POST /edit` from memory — `GET` the file and copy exact bytes; typographic pitfalls are unforgiving.
- Re-call `guide(topic="orient")` to re-fetch the shared reference — it's already in your context; pick the relevant section by name.

## Next steps

When depth is needed, read it via `guide(topic="...")` with the matching scenario or `orient` topic, or a direct HTTP GET:

- Specific site patterns — the scenario-guide table in the shared reference (`simple-page.md`, `multi-page-site.md`, `restaurant.md`, `property-listings.md`, `map-with-pins.md`, `job-board.md`, `contact-form.md`, `photo-gallery.md`, `event-page.md`, `multi-language.md`, `portfolio.md`, `spa-react.md`, `spa-flutter.md`, `copy-page.md`) with canonical URLs.
- Errors, conventions, limits, redirect semantics, snapshot semantics, edit semantics — sections already in your loaded `llm.txt`, named accordingly.
- Management UI (project list, per-project links) — `https://uat-beam.page/projects`.

Stop when you've shipped — the feedback loop is live site → user reaction → next change, not architecture lectures.
