Deployment Infrastructure

How sites are deployed to Cloudflare Pages with custom subdomains.

Overview

Every demo and final site is deployed to Cloudflare Pages — a global CDN hosting platform with a generous free tier and instant deploys. Each site gets its own Pages project and a custom subdomain attached via a CNAME DNS record.

Demo Subdomain Pattern

Demo sites follow this URL pattern:

https://<slug>.demos.<your-domain>

For example, if your domain is gohippoweb.com and the business slug is smithplumbing, the demo URL is:

https://smithplumbing.demos.gohippoweb.com

The deploy-demo.ps1 Script

Location: tools/deploy-demo.ps1

This script is called by DeployWorker with the -SiteName <slug> parameter. It reads all credentials from environment variables set by the app at runtime (never from files on disk).

What the Script Does

Check for existing Pages project

Calls the Cloudflare API to check if a Pages project named <slug> already exists. Creates it if not.

Deploy with Wrangler

Runs wrangler pages deploy <site_dir> --project-name <slug>. Wrangler uploads the site files and returns a deployment URL.

Attach custom domain

Calls the Cloudflare Pages API to attach the custom domain <slug>.demos.<domain> to the project.

Create DNS record

Creates a CNAME record in the Cloudflare DNS zone pointing <slug>.demos to the Pages project's pages.dev URL.

Environment Variables

The deploy script reads credentials from environment variables, never from files:

VariableSource
CLOUDFLARE_API_TOKENWindows Registry via registry.py
CLOUDFLARE_ACCOUNT_IDWindows Registry
CLOUDFLARE_ZONE_IDWindows Registry
CLOUDFLARE_DOMAINWindows Registry

DeployWorker passes these to the PowerShell process via env= on the subprocess call.

⚠️

Never write these credentials into the deploy script itself or any file on disk. The script reads them from the environment automatically.

Final Hosting — Going Live (host-final.ps1)

Location: tools/host-final.ps1, driven by FinalHostWorker. This is the Step 7 production deploy — it puts the approved site on the client's real domain rather than a demos subdomain.

What the Script Does

Zone check (is the domain on our account?)

Looks the target domain up by name via GET /zones?name=. If it is not a zone on your Cloudflare account, the script exits with a distinct code and the worker reports it plainly — nothing is deployed.

Deploy to a dedicated project

Runs wrangler pages deploy edited_sites/<slug> into a <slug>-live Pages project, keeping production separate from the demo project.

Bind apex + www

Attaches both example.com and www.example.com as custom domains and creates proxied CNAMEs (Cloudflare flattens the apex CNAME).

Refuse to clobber a live site

If the apex or www already has conflicting A/AAAA/CNAME records, the script aborts rather than overwriting them. Re-running the action passes -Force to replace them — an explicit two-step opt-in, so you never silently take down a domain that is still serving.

Email Provisioning & Contact-Form Relay (Purelymail)

Email is handled entirely through Purelymail plus Cloudflare DNS — no third-party form service and no server to run.

Mailbox + DNS (Step 8)

EmailProvisionWorker (via core/purelymail_client.py and core/cloudflare_api.py) adds the domain to Purelymail, fetches the account ownership code, and writes the fixed Purelymail record set into the domain's Cloudflare zone — idempotently, none proxied:

It then creates the contact@<domain> mailbox with a generated password (shown once, stored in the registry). Requires PURELYMAIL_API_TOKEN.

Contact-Form Relay

The generated sites ship a Cloudflare Pages advanced-mode worker (_worker.js) that serves the static files and handles POST /api/contact. It drops bots with an off-screen honeypot field and a fill-time gate, then relays the submission over Purelymail SMTP to the site's contact mailbox. The SMTP app password is stored as a Cloudflare Pages project secret (wrangler pages secret put) — never committed to the repo. If the relay can't send, the form falls back to a pre-filled mailto: so no lead is lost.

Encrypted Document Publishing

When the Host Demo step runs, the app automatically checks for .md files in edited_sites/<slug>/ (typically DESIGN_NOTES.md written by the AI during Demo). If any are found, each is encrypted and published to the shared client-docs Cloudflare Pages project using a fragment URL that never touches the server.

How It Works

Encryption

Each .md file is encrypted with AES-256-GCM using a randomly generated 256-bit key. The ciphertext and IV are stored together as a .enc file in data/encrypted-docs/. The key and a stable UUID are recorded in data/encrypted-docs/docs-index.json, which is local-only and never deployed.

Deployment

tools/deploy-docs.ps1 copies only the .enc files (never docs-index.json) alongside the markdown viewer HTML into a temp directory, then deploys to the client-docs Cloudflare Pages project at client-docs.demos.<domain>.

Sharing

The shareable link for each document is a fragment URL. The fragment is never sent to the server — decryption happens entirely in the client's browser using the WebCrypto API:

https://client-docs.demos.<domain>/#<uuid>:<base64url-key>
ℹ️

All client documents — from all sites — share the same client-docs Cloudflare Pages project. This means every new client adds zero additional projects to your account.

⚠️

Never deploy docs-index.json. It contains all encryption keys in plaintext. The deploy script is configured to upload only .enc files. Keep the index file local.

SEO Report Publishing

The SEO window's Report & Publish tab generates a client-facing seo-report.md and publishes it through the same encrypted pipeline to a fragment URL you can share directly with the client.

Wrangler Authentication

Wrangler authenticates using the CLOUDFLARE_API_TOKEN environment variable. The token must have these permissions:

Create the token in Cloudflare Dashboard → My Profile → API Tokens → Create Token → Custom Token.

DNS Propagation

After deployment, the custom subdomain may take 1–5 minutes to become accessible globally due to DNS propagation. The app marks the step complete as soon as the deploy script exits successfully — it does not wait for DNS propagation.

Cloudflare Pages Free Tier Limits

LimitFree Tier
Projects100 (hard cap)
Deployments per month500
BandwidthUnlimited
Custom domains per project100
File size limit25 MB per file
Total files per deployment20,000

The 100-project cap is why the encrypted docs system centralises all client documents into a single client-docs project rather than creating one per client. At the old rate (1 project per client doc), the limit would have been reached after roughly 23 clients.

Normal agency usage (a few deployments per day) stays well within the free tier.