How sites are deployed to Cloudflare Pages with custom subdomains.
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 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
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).
Calls the Cloudflare API to check if a Pages project named <slug> already exists. Creates it if not.
Runs wrangler pages deploy <site_dir> --project-name <slug>. Wrangler uploads the site files and returns a deployment URL.
Calls the Cloudflare Pages API to attach the custom domain <slug>.demos.<domain> to the project.
Creates a CNAME record in the Cloudflare DNS zone pointing <slug>.demos to the Pages project's pages.dev URL.
The deploy script reads credentials from environment variables, never from files:
| Variable | Source |
|---|---|
CLOUDFLARE_API_TOKEN | Windows Registry via registry.py |
CLOUDFLARE_ACCOUNT_ID | Windows Registry |
CLOUDFLARE_ZONE_ID | Windows Registry |
CLOUDFLARE_DOMAIN | Windows 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.
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.
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.
Runs wrangler pages deploy edited_sites/<slug> into a <slug>-live Pages project, keeping production separate from the demo project.
Attaches both example.com and www.example.com as custom domains and creates proxied CNAMEs (Cloudflare flattens the apex CNAME).
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 is handled entirely through Purelymail plus Cloudflare DNS — no third-party form service and no server to run.
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:
MX → mailserver.purelymail.comTXT SPF → v=spf1 include:_spf.purelymail.com ~allTXT ownership → purelymail_ownership_proof=… (one account-wide code)CNAMEs → key1/2/3.dkimroot.purelymail.com_dmarc CNAME → dmarcroot.purelymail.comIt then creates the contact@<domain> mailbox with a generated password (shown once, stored in the registry). Requires PURELYMAIL_API_TOKEN.
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.
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.
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.
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>.
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.
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 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.
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.
| Limit | Free Tier |
|---|---|
| Projects | 100 (hard cap) |
| Deployments per month | 500 |
| Bandwidth | Unlimited |
| Custom domains per project | 100 |
| File size limit | 25 MB per file |
| Total files per deployment | 20,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.