Config is baked in the browser; the Worker stores opaque JS and copies the chosen base engine → e/<id>.js. DELETE cascades e/<id>.js + cfg/<id>.js.
2 · Runtime — every visitor, the hot path
1 billed Worker request per pageview (0 KV on a cache hit); the engine and the cfg are R2-direct at $0, edge-cached, free egress.The switch is a host test on the baked url · rotation is planned, the backend is built
The baked POP_CONFIG.window.url decides the mode via /^https:\/\/cfg\.analyytics\.com\/.*\.js$/i: a plain landing url → the engine opens it directly, no cfg request (today's behaviour, backward compatible); a cfg.analyytics.com/<id>.js url → the wrapper loads that cfg via <script> at init, sets window.__ADCFG, and pickAdUrl() rotates. That conditional load is not yet baked into the wrapper — /admin/adconfig + cfg/<id>.js storage are BUILT (DESIGN-ad-rotation.md §3). Triggers stay governed by POP_CONFIG; rotation only picks WHICH url.
3 · Worker — every route & case — read schema = read code
export default { async fetch(request, env, ctx) } in try/catch → 500 "Internal error" (L432-433). Guards short-circuit top-down, exactly like the code.
Match on the query-stripped URL keyUrl (L168-170) so ?cb=1 can't fragment cache. Regex /^https?:\/\/[^/]+\/w\/([\w-]{1,64})\.js$/ (L49) — id capped 1-64. cfg.analyytics.com has NO Worker route → cfg/<id>.js serves R2-direct.
/w/<id>.jsthe ONLY hot path · no auth5 cases · 200 hit 1·0·1 · 200 miss 1·1·1 · 404 neg-60s · 405 · 204L164-209
if method === "OPTIONS"204noContentL172
if method !== "GET"405Method not allowedL173-174
GET →cache.match(keyUrl) hit1·0·1200cached bytes · logStat(1)L179-183
miss →WRAPPERS.get("w:"+id) == null1·1·1404// not found · neg-cache 60s · logStat(0)L185-198
miss → js found1·1·1200bytes · CC max-age=300 · cache.put · logStat(0)L199-208
PUT · request.json() throws400{error:"bad json"}L222-226
PUT · !/^[\w-]+$/.test(id)400{error:"bad id"}L230
PUT · typeof js!==string || !js.length400{error:"missing js"}L231
PUT · engineSrc ∉ ENGINE_VARIANTS400{error:"bad engine"} · 4 variants L39-44L232-233
PUT ok → WRAPPERS.put("w:"+id, js, {metadata:{e,m,b,t}}) + ENGINE.put("e/"+id+".js") + purge()200{ok,id,ttl:300,link,engineCopied,engineKey} · copy graceful if base/binding absent · no cfg seeded here · payload also carries optional adTarget → metadata {e:engineSrc|null, m:isCfg(adTarget)?"r":"s", b:js.length, t:Date.now()} (≤1024B, no ad url)L234-256
GET → WRAPPERS.list({prefix:"w:", cursor}) paginate · read k.metadata1 LIST /1000 · 0 gets200{items:[{id, e, m, b, t}]} · e=engine variant|null · m=r|s · b=size · t=Date.now · ad url NOT here → fetched per-row via /admin/adconfigL≈new
!AE_API_TOKEN || !CF_ACCOUNT_ID501{error:"stats not configured"} · dashboard degrades, /admin/inventory still works—
AE fetch throws/catch502—
GET → 1 aeQuery(buildStatsSQL) POST /accounts/<acct>/analytics_engine/sql200{days, rows:[{id, requests, hits}]} · GROUP BY blob1 · requests=SUM(_sample_interval*double1) · hits=…*double2 · WHERE timestamp > NOW()-INTERVAL 'N' DAYL≈new
* (no match)unknown path — engine + cfg are on R2, deliberately NOT here1 cases · 404 onlyL431
fall through404"Not found"L431
4 · Storage — everything stored
Store
Key
Value
KV WRAPPERS
w:<id>
Baked wrapper JS · 1 row per script · metadata{e,m,b,t} on the same key (e=engine|null · m=r|s · b=size · t=publish ts) · ≤1024 B, no ad url
KV WRAPPERS
presets
Builder settings (not the scripts)
R2 popunder-engine
tasty-*.js
4 base engine builds · uploaded at deploy · served from cloudfroze.com
R2 popunder-engine
e/<id>.js
Per-wrapper engine copy · made on publish, cascade-deleted with the wrapper · ENGINE binding admin-only, serving stays $0
R2 popunder-engine
cfg/<id>.js
Per-wrapper ad config window.__ADCFG={v,mode,urls[]} · written by PUT /admin/adconfig, cascade-deleted with the wrapper · $0 edge-cached, NOT the Worker
Analytics Engine
popunder_stats
1 point / request · SQL read
5 · Cost — per /w/<id>.js · green=free orange=billed
Case
Worker
KV read
AE write
cache HIT
1
0
1
cache MISS
1
1
1
404 cached (60s)
1
0
1
engine e/<id>.js (R2-direct, cloudfroze.com)
0
0
0
ad config cfg/<id>.js (R2-direct, cfg.analyytics.com)
0
0
0
adconfig write + purge (admin, per edit)
0
0
0
Only /w/<id>.js costs a Worker request. Engine and cfg load R2-direct (0·0·0): ~1000 copies ≪ 10 GB-month free, egress free; the admin write is a Class-A PUT (≤1M/mo free) + a free by-URL purge, so rotation adds $0. Caveat: deleting the engine stops the pop at $0, but /w/<id>.js still bills 1 req/hit until the wrapper goes too.
8 helpers. Auth is fail-closed with no ADMIN_TOKEN · logStat never blocks · purge + purgeCfg share one CF_ZONE_ID · validateCfg gates every ad url · isCfg is the rotation switch
authorized(req, env)= !!env.ADMIN_TOKEN AND constant-time Bearer <ADMIN_TOKEN> match. No token set → fail-closed (all admin = 401). L71-74
timingSafeEqual(a,b)XOR-accumulate all bytes, no early exit. Type-guard + length check first. L60-69
logStat(env,ctx,id,hit)SAMPLE gate (1 = every req) → STATS.writeDataPoint{indexes:[id], blobs:[id,"wrapper"], doubles:[SAMPLE,hit]} in waitUntil, try/catch swallow. Never blocks. L77-90
purge(env,ctx,origin,id)if CF_API_TOKEN && CF_ZONE_ID → POST purge_cache {files:[origin+"/w/"+id+".js"]}, fire-and-forget. Else propagate within 300s. Single-domain only. L97-109
validateCfg(cfg)boolean gate for PUT /admin/adconfig: cfg a non-null object; mode ∈ {weighted, roundrobin} (missing defaults weighted); urls a non-empty array; every u a string matching /^https?:\/\//i and NOT containing #; w (if present) a positive finite number. Mandatory — instant propagation makes a bad url an instant fleet-wide incident. L119-132
purgeCfg(env,id)if CF_API_TOKEN && CF_ZONE_ID → POST purge_cache {files:["https://cfg.analyytics.com/cfg/"+id+".js"]}, awaited, returns r.ok; else/catch → false. Reuses the SAME CF_ZONE_ID + token as purge() (cfg.analyytics.com is in the analyytics.com zone — no new CF_ENGINE_ZONE_ID). L138-156
isCfg(u)= /^https:\/\/cfg\.analyytics\.com\/.*\.js$/i.test(u) — the rotation switch (§0.11). On PUT /admin/wrapper it sets the metadata mode m = "r" (rotation cfg url) else "s" (single). L≈new · re-sync
buildStatsSQL(N) · aeQuery(env,sql)for /admin/stats: build ONE SQL over popunder_stats (GROUP BY blob1=id · requests=SUM(_sample_interval*double1) · hits=…*double2 · WHERE timestamp > NOW()-INTERVAL 'N' DAY) → POST to /accounts/<CF_ACCOUNT_ID>/analytics_engine/sql with Bearer AE_API_TOKEN (MODIFICHE-dettaglio.md §1.1). L≈new · re-sync
7 · Gap — CLOSED by the inventory dashboard (HANDOFF §0.12)
w:<id> carries KV metadata{e,m,b,t} on the same key, so one list() returns every id with its metadata — zero per-id reads. /admin/inventory serves it, /admin/stats?days=N joins AE traffic, wrappers-dashboard.html renders the table.
3 alternatives were rejected — meta:<id> costs +1 read per row, client__link ids need naming discipline, GET-single needs a Worker route
Option
Storage
Trade-off
✓ KV metadata on w:<id> (SHIPPED)
none (rides the key)
Chosen: 1 list(), no per-id reads; ≤1024 B (no ad url — fetched on demand)