ACME RETAIL GROUP · assets.acmeretail.com RUNBOOK
PORTFOLIO SAMPLE
Azure Front Door → Cloudflare R2 CDN

Moving assets.acmeretail.com from Azure Front Door to Cloudflare Worker + R2.

How the new system works, how it runs alongside Azure during the transition, the template cutover plan, and what's left before go-live.

Current (Azure)
$3,000.00/mo
Projected (Cloudflare)
~$62–65/mo
Annual (Azure)
~$36,000
01

Azure vs Cloudflare

What we have today, and what changes once the migration completes.

Category Azure (today) Cloudflare (target)
Monthly cost $3,000.00 ~$62–65 — ~97% reduction
Storage model TTL cache only — no persistent copy. Every expiry re-fetches from originweb01. R2 permanent object storage — a lasting second copy, not a cache.
If originweb01 goes down Cache still expires on schedule. Once expired, images break until originweb01 is back. Anything already requested keeps serving indefinitely from R2 — originweb01 can stay down.
Cache control Passive — wait for TTL to expire, no reliable manual override. Active — two-step purge (delete R2 + purge edge) takes effect immediately.
Origin dependency Every cache miss requires originweb01 to respond, every time, forever. Originweb01 is only needed once per file — after that, R2 is self-sufficient.
Security posture No WAF, no request restrictions on the CDN layer. Path + method restrictions, hardened error handling, WAF available.
The architectural difference — not just cheaper, structurally different
AZURE — TODAY CLOUDFLARE — TARGET EDGE CACHE ONLY Temporary, no 2nd copy on every expiry ORIGINWEB01 The only real copy Originweb01 down = images break EDGE CACHE Fastest, checked first WORKER Decides where to look R2 — PERMANENT 2nd real copy ORIGINWEB01 Fallback only
What the Worker actually does

The Worker is a small program that runs on Cloudflare's network, sitting between every visitor and the storage layer. For every request it makes one decision, in order:

  • 1
    Is it already at the edge cache?Fastest path — serve instantly, nothing else runs.
  • 2
    Is it in R2, the permanent store?Serve from there — this covers almost every request once a file has been seen once.
  • 3
    Not in R2 yet — ask originweb01, just this onceServe the file, and quietly save a permanent copy into R2 in the background so originweb01 is never asked for that file again.
The core shift: Azure has only ever been a temporary cache in front of originweb01 — a single layer with no memory of its own. Cloudflare adds a Worker that actively manages a permanent second copy of every asset. That's the resilience upgrade this migration is really about, cost savings aside.
02

How it works — email open, start to finish

Azure and Cloudflare run side by side. Old campaigns keep working; new campaigns route through the new backbone. Both land on the same storage layer.

Recipient opens email OLD CAMPAIGN LINK azureedge.net URL NEW CAMPAIGN LINK assets.acmeretail.com URL SAFETY NET Azure Front Door CLOUDFLARE Worker checks R2 → falls back to originweb01
Both paths converge here. Old or new link, the request hits the same Worker — check R2 first, fall back to originweb01 if needed, then store it in R2 for next time. One backend, two entry points.
03

Purge process during coexistence

Correcting a new campaign's asset shortly after launch is two steps:

  • 1
    Delete the file from R2R2 is the source of truth — this removes the old version for good.
  • 2
    Purge the Cloudflare edge cacheForces a fresh fetch, which re-populates R2 automatically.
Azure keeps its own separate cache for old campaign links. No extra steps needed — old campaigns are never edited after sending, so Azure's cache never enters the purge workflow.
04

Purge via Dashboard — exact clicks

The actual navigation path in the Cloudflare dashboard, step by step. Two parts — edge cache first, then R2.

Part A — Edge cache
  • 1
    Expand Domain
  • 2
    Click Overview
  • 3
    Click acmeretail.com
  • 4
    Click Caching in the left menu
  • 5
    Click Configuration
  • 6
    Click Custom Purge
  • 7
    Click URL
  • 8
    Enter the folder path matching the client folder on originweb01:
    https://assets.acmeretail.com/email/ClientName
    • Example: https://assets.acmeretail.com/email/quickbite
    • Example: https://assets.acmeretail.com/email/riverside
  • 9
    Click Purge
Part B — R2 bucket
  • 1
    Click Back to Domains or Account home
  • 2
    Click Storage & databases
  • 3
    Expand R2 Object Storage
  • 4
    Click Overview
  • 5
    Click the acme-email-assets bucket
  • 6
    Navigate to the path needed and delete the folder or file
Do both parts. Deleting from R2 alone still leaves the old version served from edge cache. Purging the edge cache alone just re-fetches the same stale version from R2. Order doesn't matter between the two, but both steps are required.
05

Migration plan — template URL change

New campaigns adopt the new URL naturally, one at a time. Existing reusable templates are a separate, finite task.

Existing template inventory to update

Unlike a one-time campaign blast, these get sent or triggered again — so their URLs need to be updated directly, not left to age out naturally.

Template typeQuickBite GrillRiverside Café
Ongoing, Triggers, Adhocs4055
95 templates total, not zero. The "no bulk edits" framing above applies to brand-new one-off campaigns only. These 95 — Ongoing, Triggers, and Adhocs across both clients — are reused, so each needs its URL updated directly as part of this migration, tracked as a discrete checklist rather than left to naturally age out.
Planning the change
PREREQUISITE Worker + R2 ready STEP 1 Sam builds HTML assets.acmeretail.com STEP 2 Paste into CampaignHub / MP LIVE Azure not involved
Purge process after the change
TRIGGER Asset needs a fix STEP 1 + 2 Delete R2 + purge cache LIVE Fresh version served
Same two steps as during coexistence — the only difference is Azure is no longer part of the picture for that campaign. Old campaigns keep working through Azure until they stop being opened.
06

Future improvement — automated purge

Next step beyond this migration: connect Cloudflare's API directly to CampaignHub.

CAMPAIGNHUB Image uploaded API call AUTOMATIC Write to R2 + purge cache NO ORIGINWEB01 STAGING Instantly live, no manual steps
Dependency: QuickBite Grill (MailerPro) needs to move to CampaignHub before this applies. Until then, QuickBite Grill stays on the originweb01 → Worker → R2 pattern.
Originweb01 stays online regardless. This diagram describes the publishing workflow only — new campaigns skip the originweb01 staging step and go straight to R2. It does not mean originweb01 gets shut down. Keeping originweb01 alive as a live serving fallback protects against R2 or Worker-specific problems (these do happen — R2 had a real regional incident in Feb 2026) independent of a full Cloudflare outage. Same logic already applied to Azure during its wind-down: keep the safety net running, retire it deliberately later, not as a side effect of a workflow change.
If Cloudflare (Workers and/or R2) has an outage

Originweb01 being kept online only helps if there's an actual way to route to it. Two levels of fallback, depending on severity:

  • 1
    Worker misbehaving specifically (bug, bad deploy)Remove the Worker route (see Troubleshooting section) — Cloudflare's edge stays in the path, just skips the Worker, proxying straight to originweb01.
  • 2
    Broader Workers/R2 outage — full bypassSwitch assets.acmeretail.com from Proxied (orange cloud) to DNS only (gray cloud) in the Cloudflare dashboard. This routes traffic directly to originweb01's IP, fully bypassing Cloudflare's network for this hostname — the most complete fallback, independent of which specific Cloudflare service is having the problem.
This is the concrete payoff of keeping originweb01 online. A resilience backup only counts if there's a real, fast way to fail over to it — this is that path.
07

Security improvements

What's been hardened, and two real issues caught before they became bigger problems.

Built in from the start
  • Least-privilege accessEvery deploy token scoped narrowly and time-limited — deleted right after use, never left standing
  • Path & method restrictionsWorker only ever accepts /Email/* paths and GET/HEAD requests — everything else is rejected outright
  • Origin response validationRedirects, cookies, and unsafe cache signals from originweb01 are rejected rather than trusted and passed through
  • Security headers on every responseIncluding a strict content policy on HTML pages specifically, since those render directly in a browser
  • Reliability hardeningOrigin requests now time out properly on a stalled transfer, not just a slow first response
Caught in Testing Phase — real issues, found and fixed
R2 backfill was silently failing, 100% of the time. Every image still loaded correctly for recipients, but the background save into permanent storage was failing on every single file — meaning the resilience layer wasn't actually building anything. Found by reading logs, reproduced, fixed, and verified against real traffic. No recipient was ever affected.
Customer data was landing in system logs. Personalization data — name, mailing address — rides in the link when someone opens a "View in Browser" email. Cloudflare's automatic logging was capturing that in full. Turned off immediately once found; the Worker's own diagnostic logging was unaffected and already excluded this data by design.
Why this matters: both issues were caught because logging was already in place and someone was actually reading it — not because anything visibly broke. That's the same discipline applied throughout this project: verify with real evidence, don't assume something works just because nothing looks wrong.
08

Next steps

Four actions before this goes live.

1
Schedule a test window Pick a low-traffic time to run the live test, stakeholders aware in advance.
Action: schedule
2
Test that the Worker works Confirm it serves from R2, falls back to originweb01 when needed, behaves as designed.
Action: verify
3
Migrate files from originweb01 to R2 Populate R2 with existing assets ahead of cutover, instead of relying on gradual backfill.
Action: migrate
4
Confirm Azure still works side by side Validate Azure keeps serving old links correctly through the new backend.
Action: validate
09

Troubleshooting — if the Worker isn't working

A safe, fast fallback exists if something goes wrong. No DNS changes, no waiting.

How to tell
  • Images or templates failing to loadon assets.acmeretail.com specifically
  • Error responses instead of assetsa blank page, a 500, or a plain-text error where an image should be
  • Worker errors visible in CloudflareWorkers & Pages → assets-r2-primary-router → Metrics or Logs showing failed requests
The fix — fall back to edge cache only
TRIGGER Worker isn't working ACTION Remove the Worker route RESULT Standard Cloudflare edge cache → originweb01
  • 1
    Workers & Pages → assets-r2-primary-router → Domains tabDelete the assets.acmeretail.com/Email/* route
  • 2
    Traffic instantly goes straight to originweb01Through Cloudflare's standard proxy — no Worker, no R2, no custom logic in the way
  • 3
    Site keeps workingExactly like Azure does today — just through Cloudflare instead — while the Worker gets fixed
Takes under 60 seconds. No DNS changes, no waiting for propagation. This is a deliberate design choice — removing the route is the single fastest lever available if anything about the Worker needs attention.
What's temporarily paused: new files won't get backfilled into R2 while the route is off, and originweb01 becomes the sole source of truth again — same as before this migration. What's not lost: anything already saved in R2 stays there, untouched, ready the moment the route is restored.
10

Deploy Guide — assets-r2-primary-router

Step-by-step reference for deploying, testing, and maintaining the Worker.

Files required

Place all four in the same folder on your machine.

  • JS
    assets-r2-primary-worker-cache.jsThe Worker code — cache-enabled, current production version
  • TOML
    wrangler.tomlDeploys the cache-enabled version — this is the one you'll normally use
  • JS
    assets-r2-primary-worker-cache-bypass.jsAlternate Worker code, no edge caching
  • TOML
    wrangler-cache-bypass.tomlDeploys the bypass version — only if you deliberately switch
One-time setup (already done — for reference)
  • R2 bucket acme-email-assets created
  • SSL/TLS mode: Full (strict)
  • Always Use HTTPS: on
  • R2 bucket confirmed no public custom domain
To redeploy the Worker
  • 1
    Confirm Node.js and Wrangler are installed, update if needed Check both in Command Prompt:
    node --version
    wrangler --version
    Node: v16.17.0 or higher — install from nodejs.org if missing. Wrangler: the same command handles both a fresh install and an update — no separate "update" command needed:
    npm install -g wrangler
    Run this if wrangler --version says "command not found," or the version looks out of date. Safe to run any time — if already current, it just confirms that.
  • 2
    Create a scoped API token dash.cloudflare.com → profile icon → My Profile → API Tokens → Create Token.
    Under API token templates, find "Edit Cloudflare Workers" specifically — the list includes several similarly-named options (Workers AI, Read billing info, WordPress, etc.), so look for that exact wording.
    Use template → Account Resources → Include → Acme. Zone Resources → Specific zone → acmeretail.com.
    Set a TTL — a short window (e.g. today → tomorrow). A backstop in case manual deletion (Step 4) is ever forgotten — complements, doesn't replace, deleting it after use.
    Continue to summary → Create Token → copy it now, shown once.
  • 3
    Deploy via Command Prompt Navigate to your folder, set the token, then deploy:
    cd C:\path\to\your\folder
    set CLOUDFLARE_API_TOKEN=paste-your-token-here
    wrangler deploy --config wrangler.toml
    To deploy the cache-bypass version instead:
    wrangler deploy --config wrangler-cache-bypass.toml
    Do not rename this file to wrangler.toml — that reintroduces the exact ambiguity risk this setup avoids. The --config flag exists specifically so you never need to rename anything. Keep the rule simple: wrangler.toml is always the cache-enabled version; switching to bypass is always a deliberate flag typed at deploy time.
  • 4
    Delete the token My Profile → API Tokens → find it → delete. Generate a fresh one next time — never reuse.
  • 5
    Verify the deploy actually took the config Some Observability settings have previously only persisted correctly via a Wrangler-based deploy, not the dashboard code editor. After every deploy, check:
    • Workers & Pages → assets-r2-primary-router → Domains/Routes → route shows assets.acmeretail.com/Email/* (not /*)
    • Observability → Logs → "Include Invocation logs" is OFF (SendGrid personalization data rides in the query string on WebLink.html requests)
    Known recurring issue — if either setting has reverted, re-check and re-fix until a Wrangler-based deploy is confirmed to hold both settings through a subsequent redeploy.
Test after deploying
Enabling debug headers — edit assets-r2-primary-worker-cache.js → find exposeDebugHeaders: false near the top → change to true → redeploy → test → set back to false → redeploy again before real campaign traffic runs through it.
TestURLExpected x-served-from
R2 hitAny known existing file under /Email/r2
Originweb01 fallbackA file not yet in R2originweb01-backfilling first, r2 on reload
Not foundA path that doesn't existnot-found (404)
Outside scopeAny path not starting with /Email/not-found (404)
Wrong methodA POST request405 Method Not Allowed
Also check Workers Logs — confirm R2 backfill stored: <key> appears, confirming the resilience layer is actually writing to R2, not just serving from originweb01 every time.
Cache purge reference (for dev team)
  • 1
    Delete from R2Cloudflare dashboard → R2 → acme-email-assets → browse to the file → delete.
  • 2
    Purge the Cloudflare edge cache acmeretail.com → Caching → Configuration → Purge Cache → Custom Purge → Prefix → enter the affected path:
    https://assets.acmeretail.com/Email/quickbite/
Both steps required — purging edge cache alone re-fetches the stale R2 version. (Cache-bypass variant: only Step 1 needed — there's no edge cache to purge.)
Rollback
  • 1
    Workers & Pages → assets-r2-primary-router → Domains/Routes
  • 2
    Delete the assets.acmeretail.com/Email/* route
  • 3
    Traffic falls back to Cloudflare's standard proxy → originweb01 directly
No DNS changes needed. Under 60 seconds.
Known open items
  • Rate Limiting Rule — not yet configured. Last item before Phase 3 CDN cutover.
  • Observability setting drift — re-check after every deploy until confirmed stable.
  • ORIGIN_AUTH_TOKEN — optional, not currently configured. Originweb01 has no matching check. Not required for launch.
Key reference
Worker nameassets-r2-primary-router
R2 bucketacme-email-assets
Worker routeassets.acmeretail.com/Email/*
Cloudflare zoneacmeretail.com
Default config filewrangler.toml (cache-enabled)
Alternate config filewrangler-cache-bypass.toml (bypass)