Three assets, three paths. All edited remotely — the publisher's <script src> never changes.
| Asset | Wrapper remote | Rotation cfg remote | Engine not remote |
|---|---|---|---|
| Served at | the loader at analyytics.com/w/<id>.js |
ad list + targeting at cfg.analyytics.com/cfg/<id>.js |
the popunder engine at cloudfroze.com/e/<id>.js |
| Lives in | KV (w:<id>) |
R2 (cfg/<id>.js) |
R2, a per-wrapper copy made at publish |
| Holds | baked POP_CONFIG: frequency, triggers, ad URL (single mode), behavior | ad URLs, weights, device + country targeting (general + per-ad) | the popunder logic (its behavior is driven by the wrapper's POP_CONFIG) |
| Update it | edit in the builder → Publish (PUT /admin/wrapper, same id) |
Ad-rotation editor → Save (PUT /admin/adconfig) |
no admin route — needs delete R2 base + redeploy + re-publish |
| Cost of edit | re-bakes the whole wrapper bytes | tiny — no wrapper re-bake | heavy / manual (see caveats) |
w:<id>, cfg → R2 cfg/<id>.js.CF_API_TOKEN.<script src=".../w/<id>.js"> is never touched. Same id = same URL = no re-embed, ever.Read a row across: the same layer, for each asset. Numbers are exact from the code — WRAPPER_TTL=300, NEGATIVE_TTL=60, cfg Cache Rule edge 86400 / browser 60.
| Layer | 🧩 Wrapper /w/ | 🔀 CFG /cfg/ | ⚡ Engine /e/ |
|---|---|---|---|
| Store & origin write | KV w:<id> · last-write-wins · read-your-write in origin colo (0s) |
R2 cfg/<id>.js · strong-ish · next origin read = new bytes |
R2 e/<id>.js copy made at publish · base variants write-once (deploy never overwrites) |
| Cross-colo lag | up to ~60s KV replicates async · a far colo can re-cache stale KV and pin it +300s | none R2 strong-ish at origin | none R2 strong-ish at origin |
| Edge cache TTL (if not purged) | 300s (WRAPPER_TTL) |
86400s / 1 day via cfg/* Cache Rule · without the rule → R2/CDN default |
no code TTL Cloudflare R2 custom-domain default — never overridden here |
| Edge purge on update | ~250ms purge() files:[/w/<id>.js] — needs CF_API_TOKEN+CF_ZONE_ID |
~250ms purgeCfg() — only when r.ok && success===true |
none nothing purges cloudfroze.com/e/<id>.js — ever |
| If purge missing / refused | edge holds up to 300s | edge holds up to 86400s (1 day), then re-pulls last R2 write | always waits the R2 default (no purge exists to reconcile) |
| Browser cache (max-age) | 300s public, max-age=300 |
60s Cache Rule browser_ttl=60 |
none set no Cache-Control in code → R2 default |
?v= cache-bust |
no hot path strips ? before cache.match → same edge entry; busts the browser only |
n/a the src is fixed by the baked prelude |
n/a URL is baked into wrapper bytes — can't add ?v without a re-publish |
| Negative cache (unknown/deleted id) | 404 max-age=60s (NEGATIVE_TTL) · first Publish's purge clears it ~250ms |
N/A NEGATIVE_TTL is /w/-only — R2-direct 404 rides the edge default |
N/A NEGATIVE_TTL n/a — R2-direct 404 until the default TTL |
| Publisher-domain CDN | bypassed cross-origin content isn't proxied | bypassed cross-origin content isn't proxied | bypassed unless a WP plugin localizes it (see below) |
| WP full-page HTML cache | yes caches page + tag, not content | yes prelude runs each view, fetches cfg fresh | yes engine URL lives inside wrapper bytes → still fetched client-side |
| WP defer / async / delay-JS | yes only changes when it loads (pop fires later) | yes only when cfg is fetched; moot in baked mode | yes load-timing only |
| WP combine / minify / inline | no baked into a bundle → frozen until WP cache cleared | yes runtime-injected src — static passes can't fold it |
no if inlined into a bundle → frozen until WP cache regen |
| WP self-host / load-locally | no stale local copy — remote updates never arrive | usually runtime src normally not localized; if it is → freezes at copy time |
no double-freeze: captures bytes and bypasses cloudfroze.com (worst case) |
| Two updates in a row | #2 in ~250ms (each PUT purges) · no purge → #1 pins up to 300s | #2 in ~250ms (both purge) · failed 2nd purge → up to 86400s · R2 ends on last write | R2 last-write-wins, but no purge → edge may keep the earlier copy until the default TTL |
| Delete / cascade (DELETE /admin/wrapper) | KV del + purge() → 404 neg 60s · no purge → old 200 serves up to 300s (can still fire pops) |
R2 del + purgeCfg() → ~250ms · else edge up to 86400s (NEGATIVE_TTL n/a) |
R2 del (cascade) but no engine purge → stale from edge/browser until default TTL · base variants never touched |
| Client geo cache (targeting input) | — | 12h localStorage __cc_<id> = 43200000ms · caches the visitor country, not the cfg body · empty cc fails open (ad shown) |
— |
| Runtime mode | — (it is the wrapper) | baked-snapshot: if(BAKED)apply(BAKED) → never fetches cfg → needs a wrapper re-publish (300s+purge). external-fetch (BAKED null): the only mode where remote cfg edits propagate. |
bytes immutable after publish · only swapping among the 4 fixed variants changes bytes |
| Actually live end-to-end? | yes freq / triggers / single-ad URL are baked bytes | not yet the wrapper doesn't consume window.__ADCFG yet (TODO config.html 786 & 1487) — cfg reaches R2/edge/browser but changes nothing on screen |
content immutable — a variant swap is the only "change" |
t=0 save #1 → purge → live ~250ms → edge caches #1
t=10s save #2 → purge (drops #1) → live ~250ms → edge caches #2
↳ #2 wins · #1 was live only ~10s
CF_API_TOKEN, wrong CF_ZONE_ID, or no Cache Rule. It is the only case where "the first stays for a day" happens.cf-cache-status: HIT = old edge copy · grep the baked url":" → NEW = live, OLD = still cached / KV.curl -sI https://analyytics.com/w/<id>.js | grep -i cf-cache-status # HIT = old edge copy curl -s https://analyytics.com/w/<id>.js | grep -o 'url":"[^"]*' # NEW = live · OLD = still cached / KV
HIT with old bytes → wait the TTL or re-purge. New bytes but you still see old → it's your browser cache (hard-refresh / incognito).
Purges never reach browsers — already-cached visitors always wait out their own max-age.
| Step | Action | Latency |
|---|---|---|
| 1 | Ensure CF_API_TOKEN + CF_ZONE_ID are set, then Publish (PUT /admin/wrapper) — it auto-purges. | ~250ms |
| 2 | Or manual: POST /zones/<CF_ZONE_ID>/purge_cache with files:[https://<domain>/w/<id>.js] on every serving domain (purge() only hits the request origin). | ~250ms / domain |
| 3 | Re-purge ~60s later to evict any colo that re-cached stale KV during cross-colo lag. | +~60s |
| 4 | If combine / self-host is in play on the publisher, clear that WP plugin cache. | until cleared |
| 5 | Browsers can't be forced — already-cached visitors wait out max-age=300; only a hard-refresh / incognito bypasses it. | up to 300s |
| Step | Action | Latency |
|---|---|---|
| 1 | Save (PUT /admin/adconfig) → purgeCfg() fires; PUT returns {purged:true} only when success===true. | ~250ms |
| 2 | If {purged:false}: confirm CF_API_TOKEN/CF_ZONE_ID and that the URL is inside the analyytics.com zone — else the edge holds old bytes. | up to 86400s |
| 3 | Verify: GET /admin/adconfig?id= reads R2 directly (bypasses the edge) → shows the true last write immediately. | immediate |
| 4 | Returning browsers re-serve their copy for the Browser TTL. | 60s |
| 5 | Country targeting can lag: __cc_<id> caches it 12h (43200000ms) — clear the visitor's localStorage to force fresh geo. | up to 12h |
| 6 | Reality check: baked-snapshot wrappers never fetch cfg, and the wrapper doesn't consume __ADCFG yet — to actually change the ad now, re-publish the wrapper instead. | wrapper path (300s+purge) |
| Step | Action | Latency |
|---|---|---|
| 1 | No admin route edits engine bytes — the change is out-of-band. | — |
| 2 | Replace the base object in R2 (deploy is write-once and won't overwrite): delete it, then wrangler r2 object put --remote <variant> (or the dashboard). | manual |
| 3 | Re-publish every wrapper using that variant (PUT /admin/wrapper) — that's the only path that copies new bytes to e/<id>.js. | R2 write immediate |
| 4 | No engine purge exists → edge/browser keep the old copy until the R2 custom-domain default TTL (no code number). Purge it manually in the Cloudflare dashboard if you must. | until default TTL |
| 5 | Faster alternative: at publish, pick a different whitelisted variant (one of the 4) — its bytes are copied to e/<id>.js immediately at origin. | R2 write immediate |
| 6 | If the publisher self-hosts / inlines the engine, clear that WP plugin cache too. | until cleared |
analyytics.com/w/, cfg.analyytics.com/cfg/ and cloudfroze.com/e/ to every "combine / minify / inline JS", "load third-party JS locally / self-host" and "delay JS execution" exclusion — then verify on the publisher page's served bytes, not only on the origin domains.Combine/minify/inline bakes it into a WP bundle (frozen until that cache clears); self-host / "load JS locally" copies it onto the WP server, so remote publishes never arrive. Full-page cache, CDN and defer/async/delay only touch the tag or its timing.
src is injected at runtime. Moot today: the wrapper doesn't consume __ADCFG yet.Static combine/minify/inline passes can't fold a document.createElement('script') URL, and full-page cache is fine (the prelude re-requests cfg each view). A self-host / localize plugin that snapshots the runtime URL freezes cfg at copy time until re-localized. Upstream reality dwarfs the plugin layer: baked-snapshot wrappers never fetch cfg at all.
The URL is baked into the wrapper bytes and fetched straight from cloudfroze.com, so the publisher CDN never sees it. Combine/inline bakes it into a WP bundle; self-host double-freezes it — captures the bytes and bypasses cloudfroze.com, so even a variant-swap re-publish never reaches those visitors.
window.__ADCFG (wrapper JS pending) — a cfg edit reaches R2 and the edge but changes nothing visitors see.e/<id>.js copy — deliberate: deleting a wrapper 404s its engine, so a direct flood on one id costs $0. Patching means updating the R2 base + re-publishing the wrappers.Domains: wrapper analyytics.com · engine cloudfroze.com · rotation cfg cfg.analyytics.com · panel panel.analyytics.com.