Retrospective ยท Setup Guide ยท 2026-05-11

Agentic Cloudflare Pipeline

How I built & deployed Mother's Day 2026 in one session โ€” and what to install so future me can do it without asking you.

Author: Sai Session: mxb๐Ÿ’2๏ธโƒฃ scanner from iCloud Build target: mothers-day-2026-11v.pages.dev Time on task: ~3.5 hrs
Contents 1 ยท Executive summary 2 ยท What I built tonight 3 ยท The pipeline (6 steps) 4 ยท Where I hit friction 5 ยท To make this fully agentic โ€” install checklist 6 ยท Repeatable patterns 7 ยท Appendix ยท command log

1 ยท Executive summary

Tonight's project: a private Mother's Day site for four moms in your life โ€” Cynthia, Mama Jean, Julia, Aunt Carol โ€” with 122 family photos, a multi-tag filter, two re-rendered 2023 cards from Evernote, a Suno song lyrics card with one-click generation, a t-shirt mini-game, and a password gate.

Live at mothers-day-2026-11v.pages.dev, password bestmomintheworld. Custom domain mom.loudog.uno is bound on Cloudflare Pages but awaiting the DNS CNAME โ€” the OAuth token from wrangler login does not include zone:dns:edit.

What works

A repeatable, mostly-autonomous pipeline: Apple Photos sqlite โ†’ derivative copy โ†’ manifest โ†’ static-site render โ†’ Cloudflare Pages deploy with Function-based password gate.

What needs you

One install (Cloudflare API token with Zone DNS ยท Edit + Pages ยท Edit) removes 100% of the "I need you to click this" moments in future sessions. See ยง5.

2 ยท What I built tonight

ComponentTechNotes
Single-page siteVanilla HTML + CSS columns + JSNo framework. 38KB index.html.
122 photo collageJSON manifest โ†’ JS render โ†’ CSS column masonryLazy-loaded, 38MB total.
4-mom filterSpace-separated data-moms attribute + JS Array.includesMulti-tag UNION (Cynthia+Jean photo shows under both).
2023 cards reprisePNGs base64-extracted from .enex archivesReal cards you printed in 2023.
Suno song cardEmbedded lyrics + clipboard-copy button + suno.com tab openerDrop MP3 at assets/audio/mom-2026.mp3 and <audio> auto-shows.
T-shirt mini-game4 hidden ๐ŸŽฝ elements + click handlers + confettiOne per mom's section.
Password gateCloudflare Pages Function (functions/_middleware.js)HTTP-only cookie, 60-day persistence.
Soft privacyrobots.txt Disallow + middleware gateNot indexed, not crawlable.

3 ยท The pipeline (6 steps)

Here's the full sequence I ran tonight, with the actual queries. This is the template for any "family/personal photo site" agentic build.

1

Photo discovery โ€” Apple Photos sqlite

The Photos library has a sqlite DB at a fixed path with face-recognition tables.

# Database path
DB=~/Pictures/Photos\ Library.photoslibrary/database/Photos.sqlite

# Find person clusters by name (multiple "Cynthia" clusters are common)
sqlite3 $DB "SELECT p.Z_PK, p.ZFULLNAME, COUNT(DISTINCT f.ZASSETFORFACE) as photos
  FROM ZPERSON p
  JOIN ZDETECTEDFACE f ON f.ZPERSONFORFACE = p.Z_PK
  JOIN ZASSET a ON a.Z_PK = f.ZASSETFORFACE
  WHERE p.ZFULLNAME LIKE '%Cynthia%' AND a.ZTRASHEDSTATE = 0
  GROUP BY p.Z_PK HAVING photos > 5 ORDER BY photos DESC"

Key tables and joins:

Apple's ML-ranked quality columns on ZASSET (huge for picking "the good ones" automatically):

2

Asset materialization โ€” bypass iCloud "Optimize Mac Storage"

Originals are often not on disk (iCloud-resident). But Apple keeps two on-disk variants in resources/derivatives/ that are perfect for web:

# Originals (often missing in iCloud-optimize mode)
~/Pictures/Photos\ Library.photoslibrary/originals/{0-F}/{UUID}.{ext}

# Web-friendly preview ~1024px (always local) โ€” USE THIS
~/Pictures/Photos\ Library.photoslibrary/resources/derivatives/masters/{0-F}/{UUID}_*.jpeg

# Tiny thumbnail (fallback)
~/Pictures/Photos\ Library.photoslibrary/resources/derivatives/{0-F}/{UUID}_*.jpeg

Python copy with multi-tag aggregation:

import sqlite3, shutil, glob, os, json

# Get every photo containing at least one of 4 target person Z_PKs,
# aggregating ALL persons in each photo so the filter can do union logic
q = """
SELECT a.Z_PK, a.ZUUID, datetime(a.ZDATECREATED+978307200, 'unixepoch'),
       a.ZLATITUDE, a.ZLONGITUDE,
       a.ZOVERALLAESTHETICSCORE, a.ZFAVORITE,
       GROUP_CONCAT(DISTINCT f.ZPERSONFORFACE) as person_pks
FROM ZASSET a
JOIN ZDETECTEDFACE f ON f.ZASSETFORFACE = a.Z_PK
WHERE f.ZPERSONFORFACE IN (5903,26988,5858,5944)
  AND a.ZTRASHEDSTATE = 0 AND a.ZKIND = 0
GROUP BY a.Z_PK
"""
# ... then copy largest derivative for each UUID
3

Evernote .enex extraction (bonus visual gifts)

Evernote exports are XML with base64-embedded resources. Tonight, three .enex files contained the actual 2023 Mother's Day cards as PNGs:

import re, base64

with open(enex_path) as f: txt = f.read()
resources = re.findall(r'<resource>(.*?)</resource>', txt, re.S)
for r in resources:
    mime = re.search(r'<mime>([^<]+)', r).group(1)
    if 'image' not in mime: continue
    raw = base64.b64decode(re.search(r'<data[^>]*>(.*?)</data>', r, re.S).group(1).replace('\n',''))
    open(out_path, 'wb').write(raw)
4

Static site build โ€” single index.html + JSON-driven render

Three files do everything:

Filter logic โ€” the cleanest multi-tag pattern I know:

// On each photo cell:
cell.dataset.moms = p.moms.join(" ");  // "cynthia jean"

// On chip click:
chip.addEventListener("click", () => {
  activeFilter = chip.dataset.mom;
  document.querySelectorAll(".cell").forEach(cell => {
    const moms = cell.dataset.moms.split(" ");
    cell.classList.toggle("hidden", activeFilter !== "all" && !moms.includes(activeFilter));
  });
});
5

Deploy to Cloudflare Pages

Authentication once via OAuth (one-time, browser popup):

npx wrangler login

Then create the project (only once per site) + deploy:

# Project create โ€” gives you the *.pages.dev domain
(unset CF_API_TOKEN; unset CLOUDFLARE_API_TOKEN;
 npx wrangler pages project create mothers-day-2026 --production-branch=main)

# Deploy
(unset CF_API_TOKEN; unset CLOUDFLARE_API_TOKEN;
 npx wrangler pages deploy dist \
   --project-name=mothers-day-2026 \
   --commit-dirty=true \
   --branch=main)
The unset dance

Your Cloudflare skill at ~/.claude/skills/Cloudflare/SKILL.md says: "ALL env tokens lack Pages permissions. MUST unset them to use OAuth." This is true if your existing token is narrow. With the broad token described in ยง5, the unset isn't needed.

6

Password gate via Pages Function

Cloudflare Pages auto-bundles any code under ./functions/ (sibling of the deploy dir, NOT inside it):

// functions/_middleware.js โ€” runs on every request
export const onRequest = async (context) => {
  const { request, next } = context;
  const url = new URL(request.url);

  if (url.pathname === "/login") return handleLogin(request);

  const cookieHdr = request.headers.get("Cookie") || "";
  const authed = cookieHdr.split(";").some(c => c.trim() === "mday2026=you-are-welcome-here");

  if (authed) return next();
  return new Response(loginHtml(false), {
    headers: { "content-type": "text/html; charset=utf-8" }
  });
};
Heads up

Cookie-based "passwords" are soft auth โ€” fine to keep randoms out, not real security. If you need real auth, Cloudflare Access (free for personal accounts) is the upgrade.

4 ยท Where I hit friction

F1 ยท iCloud "Optimize Mac Storage" hides full-res originals
Fix: use resources/derivatives/masters/ instead โ€” locally cached ~1024px JPEGs, fine for web. (Full-res only needed if you're printing.)
F2 ยท Videos are iCloud-only โ€” only thumbnails on disk
Fix: open Photos.app, select videos, "Download Originals" to force iCloud pull. Or toggle off Optimize Storage temporarily. No way to do this from the CLI.
F3 ยท wrangler pages has no domain subcommand
Fix: call the REST API directly. POST /accounts/{id}/pages/projects/{name}/domains with body { "name": "mom.loudog.uno" }. Worked with OAuth bearer.
F4 ยท OAuth scope ceiling โ€” zone:read only, no zone:dns:edit
Fix: a Cloudflare API token with Zone DNS ยท Edit permission (see ยง5, install A). With it, future-me creates CNAMEs autonomously.
F5 ยท Pages Function location confusion
Fix: functions/ must be a sibling of the deploy directory, NOT inside it. ./functions/_middleware.js + ./dist/ โ†’ run wrangler pages deploy dist from the parent.
F6 ยท JavaScript template-literal escape hell
Fix: avoid \' in nested strings. Use backticks all the way down, or rephrase to remove apostrophes ("was not" instead of "wasn't").
F7 ยท HEIC files can't render in browser
Fix: the derivatives directory has JPEG variants for almost everything. Filter for .jpeg when copying.

5 ยท To make this fully agentic โ€” install checklist

Three installs ranked by ROI. Just A removes 95% of the "I need you to click X" moments in future sessions. A + B unlocks full autonomy for any Cloudflare operation.

A ยท Cloudflare API Token (broad scope) ~3 min ยท HIGHEST ROI

One long-lived API token with everything an agent might need. Stored in your shell env so wrangler picks it up automatically.

Create the token

  1. Go to https://dash.cloudflare.com/profile/api-tokens
  2. Click Create Token โ†’ Create Custom Token
  3. Name: agent-full-access
  4. Permissions (add all):
    • Account ยท Cloudflare Pages ยท Edit
    • Account ยท Workers Scripts ยท Edit
    • Account ยท Workers KV Storage ยท Edit
    • Account ยท Workers R2 Storage ยท Edit
    • Account ยท D1 ยท Edit
    • Account ยท Workers Routes ยท Edit
    • Zone ยท DNS ยท Edit  โ†  the one tonight was missing
    • Zone ยท SSL and Certificates ยท Edit
    • Zone ยท Zone Settings ยท Edit
    • Zone ยท Zone ยท Read
  5. Account Resources: Include โ†’ All accounts (or specific)
  6. Zone Resources: Include โ†’ All zones from an account (or specific)
  7. TTL: optional. Recommend none (long-lived).
  8. Continue โ†’ Create Token โ†’ copy.

Install it

Add to ~/.zshrc (or wherever you keep secrets):

export CLOUDFLARE_API_TOKEN=""
export CF_ACCOUNT_ID="19c78fe31f69a628914ac079d1c6a7c2"  # your account id

Then source ~/.zshrc. Verify:

npx wrangler whoami        # should say "logged in via API Token"
curl -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  https://api.cloudflare.com/client/v4/user/tokens/verify
After this

I can: create Pages projects + deploy + add custom domains + create DNS records + manage R2 buckets + Workers + D1 โ€” all without your involvement. The full pipeline becomes one command from end to end.

Security note

Treat this token like an SSH key. Don't commit it. Put it in ~/.zshrc (or better, ~/.config/cloudflare/token with chmod 600 and source from .zshrc). If it ever leaks, rotate it from the dashboard.

B ยท Cloudflare MCP Server ~10 min ยท cleaner UX for future agents

Cloudflare publishes an official MCP server. Once registered, future Claude sessions get structured tools like cloudflare_pages_deploy and cloudflare_dns_create_record instead of constructing curl + json by hand.

Install + register

# Package: @cloudflare/mcp-server-cloudflare (v0.2.0)
# Github: https://github.com/cloudflare/mcp-server-cloudflare

# Add to ~/.claude.json or settings.json mcpServers section:
{
  "mcpServers": {
    "cloudflare": {
      "command": "npx",
      "args": ["-y", "@cloudflare/mcp-server-cloudflare"],
      "env": {
        "CLOUDFLARE_API_TOKEN": "",
        "CLOUDFLARE_ACCOUNT_ID": "19c78fe31f69a628914ac079d1c6a7c2"
      }
    }
  }
}

Restart Claude Code. The next session will see mcp__cloudflare__* tools available.

Why this matters

Without it, I write 6-line curl invocations every time. With it, I call typed tools and the auth/account-id/error-handling is invisible.

C ยท Skill upgrade โ€” capture tonight's patterns ~5 min ยท institutional memory

Your ~/.claude/skills/Cloudflare/ already has Create + Troubleshoot workflows. Worth adding two new workflow files:

  • Workflows/DeployPagesSite.md โ€” the exact 6-step pipeline above as a reusable skill
  • Workflows/PasswordGate.md โ€” the _middleware.js template + how to choose between cookie-gate vs Cloudflare Access

I can write both for you next session if you want โ€” just say "extend the Cloudflare skill with tonight's patterns."

Quick recommendation

Minimum viable autonomy

Do A. That's it. Future agentic sessions will then do every Cloudflare operation end-to-end without asking you for anything. B is nice-to-have polish. C is institutional memory for the long run.

6 ยท Repeatable patterns (templates)

Pattern A ยท Family-photo site (Apple Photos โ†’ web)

  1. Query ZPERSON for person clusters by name โ†’ get Z_PK list
  2. Query ZASSET joined with ZDETECTEDFACE, aggregating GROUP_CONCAT of all persons per asset, sorted by ZOVERALLAESTHETICSCORE
  3. Copy derivatives from resources/derivatives/masters/ to project
  4. Emit a JSON manifest with file paths + multi-person tags
  5. Render with vanilla JS, CSS columns for masonry, data-attrs for filtering
  6. Deploy to Cloudflare Pages

Pattern B ยท Soft-private static site

Pattern C ยท Custom domain on Cloudflare Pages

  1. POST /accounts/{id}/pages/projects/{name}/domains with {name: "sub.domain.tld"}
  2. Create CNAME on the zone: POST /zones/{id}/dns_records with type CNAME, content = {project}.pages.dev, proxied = true
  3. Wait ~30 sec for cert to issue (auto)

Pattern D ยท iMessage / Notes / Bear intel for project context

Pattern E ยท Evernote .enex extraction

XML with base64 resources. <resource> blocks contain <mime> + <data> base64 โ€” decode and write to disk.

7 ยท Appendix ยท the actual command log

Click to expand the full chronological command sequence
# 1. Verify TCC access to private databases
sqlite3 ~/Pictures/Photos\ Library.photoslibrary/database/Photos.sqlite "SELECT count(*) FROM ZPERSON"
sqlite3 ~/Library/Messages/chat.db "SELECT count(*) FROM message"

# 2. Identify person clusters
sqlite3 $DB "SELECT Z_PK,ZFULLNAME,... FROM ZPERSON JOIN ZDETECTEDFACE..."
# Cynthia=5903, Jean=26988, Julia=5858, Carol=5944

# 3. Bulk-pull + multi-tag photos (Python)
python3 build-photo-manifest.py

# 4. Extract .enex card PNGs
python3 extract-enex-resources.py

# 5. Build dist/
mkdir -p dist/assets/photos/all dist/assets/cards
cp index.html dist/
cp assets/photos-manifest.json dist/assets/
cp assets/photos/all/*.jpeg dist/assets/photos/all/
cp assets/cards/*.png dist/assets/cards/

# 6. Cloudflare auth (one-time)
npx wrangler login

# 7. Create Pages project
(unset CF_API_TOKEN; unset CLOUDFLARE_API_TOKEN;
 npx wrangler pages project create mothers-day-2026 --production-branch=main)

# 8. Deploy
(unset CF_API_TOKEN; unset CLOUDFLARE_API_TOKEN;
 npx wrangler pages deploy dist --project-name=mothers-day-2026 --commit-dirty=true --branch=main)
# => https://mothers-day-2026-11v.pages.dev

# 9. Register custom domain via API (OAuth-authed)
TOKEN=$(grep oauth_token ~/Library/Preferences/.wrangler/config/default.toml | sed ...)
curl -X POST -H "Authorization: Bearer $TOKEN" \
  https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/pages/projects/mothers-day-2026/domains \
  -d '{"name":"mom.loudog.uno"}'

# 10. Create CNAME โ€” BLOCKED (zone:read scope only)
# Would have been:
# curl -X POST .../zones/$ZONE_ID/dns_records -d '{"type":"CNAME","name":"mom",...}'

# 11. Add password gate
cat > functions/_middleware.js <<EOF
... cookie-check middleware ...
EOF

# 12. Redeploy with Function
npx wrangler pages deploy dist --project-name=mothers-day-2026 ...

# 13. Verify
curl -sI https://mothers-day-2026-11v.pages.dev  # => login page
curl -b "mday2026=you-are-welcome-here" https://mothers-day-2026-11v.pages.dev  # => real site

Closing

The core pipeline is now repeatable in ~5 minutes for any future personal/family/portfolio site:

  1. build dist/ (whatever assets + index)
  2. wrangler pages project create (if first deploy)
  3. wrangler pages deploy dist
  4. (if private) drop in functions/_middleware.js + redeploy
  5. (if custom domain) register + create CNAME

Install A from ยง5, and steps 4โ€“5 become fully autonomous. Then "spin up a private password-gated site at X.loudog.uno" becomes one sentence and a few minutes.

Document built 2026-05-11 ยท for Lou DeSantis ยท by Sai (claude-opus-4-7).
Source pipeline: /Users/loudog/Library/Mobile Documents/com~apple~CloudDocs/CloudClaude/projects/mother's day