Cloudflare popunder stackArchitecture, data flow & every Worker route — schematic

Synced with worker/src/index.js (436 lines) · every guard row cites its Lxxx · update rules in the MAINTENANCE MAP (HTML source top).

You · config.html Worker KV R2 engine R2 ad config · cfg.analyytics.com Analytics Engine Client · pickAdUrl()
every request async / on miss $0 Worker (R2 direct) planned · wrapper JS pending
wrapper analyytics.com/w/<id>.js engine cloudfroze.com/e/<id>.js (per-id copy) ad config cfg.analyytics.com/cfg/<id>.js panel panel.analyytics.com KV cool-brook-03cb-WRAPPERS AE popunder_stats

1 · Authoring — you build & publish (rare)

YOU · config.html ad url · engine · id WORKER validate · bearer auth KV WRAPPERS w:<id> = baked JS CLIENT snippet in <head> PUT {id, js} put w:<id> embed R2 · ENGINE binding copy → e/<id>.js · admin-only copy base → e/<id>.js
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

VISITOR browser · runs wrapper WORKER · KV wrapper cache.match → bytes 1 req/pageview · ONLY billed item live KV WRAPPERS read on miss only · 0 on hit Analytics Engine 1 write / req ① GET /w/<id>.js ② bytes (0 KV on hit) miss: get w:<id> log $0 Worker · R2-direct · free egress · NOT the Worker · AD TARGET decides the mode R2 · cloudfroze.com e/<id>.js · engine copy the pop mechanism · loads always live AD TARGET — switch POP_CONFIG.window.url the baked url decides the mode plain → open · cfg.js → rotate R2 · cfg.analyytics.com cfg/<id>.js → window.__ADCFG <script> load · CONDITIONAL · at init planned pickAdUrl() rotate ad URL client-side · $0 planned POP → AD URL opens in browser single ad · or rotated pick ③ <script> e/<id>.js reads url plain → direct = cfg.analyytics.com/*.js __ADCFG ④ rotated
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.

2xx success 204 OPTIONS preflight 4xx client / auth / validation 5xx proxy 501/502 500 uncaught throw cost = Worker·KV·AE
if-chain order WRAPPER_RE → /w/<id>.js /admin/wrapper /admin/adconfig /admin/wrappers /admin/inventory /admin/presets /admin/usage /admin/domain /admin/stats 404

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>.js the ONLY hot path · no auth 5 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) hit 1·0·1200cached bytes · logStat(1)L179-183
miss → WRAPPERS.get("w:"+id) == null 1·1·1404// not found · neg-cache 60s · logStat(0)L185-198
miss → js found 1·1·1200bytes · CC max-age=300 · cache.put · logStat(0)L199-208
/admin/wrapper PUT publish + engine copy · DELETE cascade · bearer 10 cases · 200 · 400×4 · 401 · 405 · 204L216-283
method === "OPTIONS" 204L217
!authorized(request, env) 401{error:"unauthorized"}L218
PUT · request.json() throws 400{error:"bad json"}L222-226
PUT · !/^[\w-]+$/.test(id) 400{error:"bad id"}L230
PUT · typeof js!==string || !js.length 400{error:"missing js"}L231
PUT · engineSrc ∉ ENGINE_VARIANTS 400{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
DELETE · bad id (?id=) 400{error:"bad id"}L261
DELETE ok → WRAPPERS.delete + ENGINE.delete("e/"+id+".js") + ENGINE.delete("cfg/"+id+".js") cascade + purge() + purgeCfg() 200{ok,id,engineDeleted,cfgDeleted,note}L262-279
other method 405L282
/admin/adconfig GET/PUT cfg/<id>.js · bearer 10 cases · 200×2 · 400×4 · 401 · 501 · 405 · 204L289-328
method === "OPTIONS" 204L290
!authorized(request, env) 401{error:"unauthorized"}L291
!env.ENGINE 501{error:"engine binding not configured"}L292
GET · !/^[\w-]{1,64}$/.test(id) 400{error:"bad id"}L296
GET ok → ENGINE.get("cfg/"+id+".js") 200null → {id,cfg:null} · else strip "window.__ADCFG=" prefix + JSON.parse → {id,cfg} · parse fail → {id,cfg:null,raw:true}L297-306
PUT · request.json() throws 400{error:"bad json"}L311-315
PUT · !/^[\w-]{1,64}$/.test(id) 400{error:"bad id"}L318
PUT · !validateCfg(cfg) 400{error:"bad cfg"} · mode weighted|roundrobin · urls[] non-empty · every u = absolute http(s), no "#" · w > 0L319
PUT ok → ENGINE.put("cfg/"+id+".js", "window.__ADCFG="+JSON.stringify(cfg)) + purgeCfg() 200{ok:true, purged} · ct application/javascript · purgeCfg reuses CF_ZONE_ID (analyytics.com zone)L320-324
other method 405L327
/admin/wrappers GET list published ids · bearer 4 cases · 200 · 401 · 405 · 204L331-344
method === "OPTIONS" 204L332
method !== "GET" 405L333-334
!authorized 401L335
GET → WRAPPERS.list({prefix:"w:"}) paginate 200{ids:[...]} · slice(2) strips "w:"L336-343
/admin/inventory ids + KV metadata in ONE list() · bearer · powers wrappers-dashboard 4 cases · 200 · 401 · 405 · 204 · 1 LIST/1000, 0 getsL≈new · re-sync
method === "OPTIONS" 204
method !== "GET" 405
!authorized 401
GET → WRAPPERS.list({prefix:"w:", cursor}) paginate · read k.metadata 1 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
/admin/presets GET/PUT shared builder-settings blob · bearer 6 cases · 200×2 · 400 · 401 · 405 · 204L349-370
method === "OPTIONS" 204L350
!authorized 401L351
GET → WRAPPERS.get("presets") 200s || "{}"L352-357
PUT · JSON.parse(txt) throws 400{error:"bad json"}L358-365
PUT ok → WRAPPERS.put("presets", txt) 200{ok:true}L366-367
other method 405L369
/admin/usage?days=N CORS-safe CF GraphQL proxy · bearer + MGMT_API_TOKEN 6 cases · 200 · 401 · 405 · 501 · 502 · 204L375-389
OPTIONS204L376
method !== "GET" 405L377
!authorized 401L378
!MGMT_API_TOKEN || !CF_ACCOUNT_ID 501{error:"mgmt not configured"}L379
fetch CF graphql throws/catch 502{error:"usage proxy failed"}L388
ok · days clamped 1–90 200{days, gql} · invocations + kvOpsL380-387
/admin/domain?action=… kill switch: status | detach | attach · bearer + MGMT_API_TOKEN 10 cases · 200×3 · 400 · 404×2 · 401 · 501 · 502 · 204L393-429
OPTIONS204L394
!authorized 401L395
!MGMT_API_TOKEN || !CF_ACCOUNT_ID 501{error:"mgmt not configured"}L396
action=status → list workers/domains 200{ok,errors,attached}L407-409
action=detach · domain not found 404{error:"not attached"}L412
action=detach → DELETE domains/{id} 200{ok,detached}L413-415
action=attach · zone not found (apex) 404{error:"zone not found"}L422
action=attach → PUT domains {hostname,service,zone_id} 200{ok,attached}L423-425
action = anything else 400{error:"bad action"}L427
any fetch throws/catch 502{error:"domain proxy failed"}L428
/admin/stats?days=N per-id traffic · 1 SQL over popunder_stats · bearer + AE_API_TOKEN 6 cases · 200 · 401 · 405 · 501 · 502 · 204L≈new · re-sync
method === "OPTIONS" 204
method !== "GET" 405
!authorized 401
!AE_API_TOKEN || !CF_ACCOUNT_ID 501{error:"stats not configured"} · dashboard degrades, /admin/inventory still works
AE fetch throws/catch 502
GET → 1 aeQuery(buildStatsSQL) POST /accounts/<acct>/analytics_engine/sql 200{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 here 1 cases · 404 onlyL431
fall through 404"Not found"L431

4 · Storage — everything stored

StoreKeyValue
KV WRAPPERSw:<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 WRAPPERSpresetsBuilder settings (not the scripts)
R2 popunder-enginetasty-*.js4 base engine builds · uploaded at deploy · served from cloudfroze.com
R2 popunder-enginee/<id>.jsPer-wrapper engine copy · made on publish, cascade-deleted with the wrapper · ENGINE binding admin-only, serving stays $0
R2 popunder-enginecfg/<id>.jsPer-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 Enginepopunder_stats1 point / request · SQL read

5 · Cost — per /w/<id>.js · green=free orange=billed

CaseWorkerKV readAE write
cache HIT101
cache MISS111
404 cached (60s)101
engine e/<id>.js (R2-direct, cloudfroze.com)000
ad config cfg/<id>.js (R2-direct, cfg.analyytics.com)000
adconfig write + purge (admin, per edit)000
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.

6 · Helpers & guards — worker/src/index.js L60-156

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 rejectedmeta:<id> costs +1 read per row, client__link ids need naming discipline, GET-single needs a Worker route
OptionStorageTrade-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)
meta:<id> separate key+1 KV row/scriptRicher payload but +1 read per row
id = client__linknoneZero change; needs naming discipline
GET single wrappernoneParse POP_CONFIG live; needs Worker route