Debug Toolbar
The debug toolbar is a self-contained visual overlay injected into the HTML response when debug mode is active. It provides real-time cache diagnostics, per-section purge controls, and a live webhook event feed — all without external dependencies.
For usage instructions, see the Debug Toolbar guide.
Architecture overview
Section titled “Architecture overview”┌──────────────────────────────────────────────────────┐│ Browser (debug mode active) ││ ││ ┌─────────┐ hover ┌────────────┐ ││ │ Pill │ ──────> │ Action Bar │ ││ └─────────┘ └──────┬─────┘ ││ │ ││ ┌──────────────┼──────────────┐ ││ │ │ │ ││ ┌─────┴────┐ ┌─────┴────┐ ┌─────┴──────┐ ││ │ Sections │ │ Events │ │ Purge │ ││ │ Panel │ │ Panel │ │ Actions │ ││ └──────────┘ └─────┬────┘ └─────┬──────┘ ││ │ │ ││ poll 5s POST /api/ ││ │ cache/purge │└────────────────────────────┼──────────────┼─────────┘ │ │┌────────────────────────────┼──────────────┼─────────┐│ Dispatch Worker │ │ ││ │ auth + forward ││ │ │ │└────────────────────────────┼──────────────┼─────────┘ │ │┌────────────────────────────┼──────────────┼─────────┐│ Theme Worker │ │ ││ │ │ ││ ┌──────────────┐ ┌─────┴────┐ ┌─────┴──────┐ ││ │ Page Renderer │ │ Event │ │ Section │ ││ │ (collects │ │ Ring │ │ Cache │ ││ │ debug data) │ │ Buffer │ │ Purge │ ││ └──────────────┘ └──────────┘ └────────────┘ │└─────────────────────────────────────────────────────┘Injection flow
Section titled “Injection flow”The debug toolbar is injected during HTML response construction in the theme worker. The flow is:
- Render:
page-renderer.tsrenders each section, collectingSectionDebugInfowhendebugModeis true - Assemble data:
worker.tsbuilds aDebugBarDataobject with sections, deploy version, cache status, and purge token - Build HTML:
debug-bar.tsassembles<style>+<div id="lk-debug">+<script>from the data - Inject: The HTML block is inserted before
</body>in the response stream
All CSS is scoped under #lk-debug with CSS custom properties. All JS runs as an IIFE that receives the serialized data object. Zero external dependencies.
Data collected per section
Section titled “Data collected per section”interface SectionDebugInfo { id: string; // unique section ID type: string; // section type (e.g. "hero", "header") cached: boolean; // whether served from section cache renderMs: number; // time in executeComponent() (0 for cache hits) sizeBytes: number; // HTML output byte length cacheKey?: string; // exact Cache API URL for purge targeting dynamic?: boolean; // true if section accesses session data sessionDeps?: string[]; // which session deps (cart, customer, etc.)}The cacheKey is the full URL used by the Cache API (e.g. https://section-cache/fmdf/slideshow/2b18d6bd). This is what gets passed to cache.delete() during purge.
Dynamic sections are identified by getSectionMeta() which reads the section registry. These sections access session-scoped data (cart, customer, checkout) and are rendered fresh on every request — they have no cache key.
CDN cache status
Section titled “CDN cache status”The pill also shows the page-level CDN cache status from the dispatch worker’s X-Cache header:
| Value | Meaning |
|---|---|
| HIT | Page served from dispatch CDN cache (zero Worker invocations) |
| MISS | Page rendered by theme worker, now cached at CDN edge |
| bypass | Debug mode skips CDN cache to prevent debug HTML from being cached |
Purge flow
Section titled “Purge flow”Three purge actions are available, each with a different scope:
Individual section purge
Section titled “Individual section purge”Click section purge button → Client JS: POST /api/cache/purge { sectionKeys: ["https://section-cache/..."] } → Dispatch Worker: auth check → forward to theme worker via service binding → Theme Worker: cache.delete(new Request(key)) on Cache API → Theme Worker: record PurgeEvent in ring buffer → Theme Worker: return { sectionsPurged: 1 } → Dispatch Worker: merge response → return { purged: 0, sectionsPurged: 1, themeForwarded: true } → Client JS: toast "Purged 1 cached item(s)"On next page load, the purged section renders fresh (cache miss).
Page purge
Section titled “Page purge”Click "Page" → "Confirm?" → Client JS: POST /api/cache/purge { paths: ["/current-page"], sectionKeys: [all section cache keys on page] } → Dispatch Worker: cache.delete on CDN page entry + forward to theme worker → Theme Worker: cache.delete on each section key → Response merges both countsSite purge
Section titled “Site purge”Same as page purge but sends paths: ["/"] to purge the homepage CDN entry. Section keys from the current page are still sent — this does not purge every section across the entire site (that would require a full cache clear via the CF API).
Auth flow
Section titled “Auth flow”The purge token flows through two workers:
- Theme worker bakes
CACHE_PURGE_TOKENinto the debug bar HTML aspurgeToken - Client JS sends
Authorization: Bearer <purgeToken>with purge requests - Dispatch worker validates the token using timing-safe comparison against its own
CACHE_PURGE_TOKENbinding - Theme worker receives the forwarded request (already authed by dispatch)
Both workers must have the same CACHE_PURGE_TOKEN value. The deploy script reads it from process.env.CACHE_PURGE_TOKEN and bakes it as a secret_text binding. Set the env var before deploying:
CACHE_PURGE_TOKEN="your-token" pnpm deploy:theme <slug> --promoteUsing wrangler secret put after deploy will be overwritten by subsequent deploys — always pass via env var.
Event polling
Section titled “Event polling”The events panel shows a live feed of cache purge events via a polling endpoint.
Server side
Section titled “Server side”purge-events.ts maintains an in-memory ring buffer (max 100 events) per Worker isolate. Each purge action (section, page, webhook) records a PurgeEvent:
interface PurgeEvent { id: string; // UUID type: string; // "path" | "key" | "d1" | "full" paths: string[]; // URLs/keys that were purged kvEvicted: number; // KV keys evicted pathsPurged: number; // cache entries deleted timestamp: number; // Unix ms durationMs: number; // purge duration}The endpoint GET /__dev/purge-events?since=<ts> returns events newer than the given timestamp, gated by the debug cookie.
Client side
Section titled “Client side”The debug bar JS polls every 5 seconds with the last-seen timestamp as cursor:
GET /__dev/purge-events?since=1709341200000→ [{ id, type, paths, timestamp, ... }, ...]New events are prepended to the panel. If the panel is closed, a toast notification appears and the Events button badge increments.
Per-isolate limitation
Section titled “Per-isolate limitation”The ring buffer is in-memory — if a webhook purge hits a different CF isolate than the one serving your debug bar poll, those events won’t appear. This is expected and acceptable for dev/debug. Cross-isolate visibility via KV with short TTL is a potential future upgrade.
Dynamic sections
Section titled “Dynamic sections”Sections that access session-scoped data (cart, customer, checkout) are marked as dynamic in the section registry via getSectionMeta(). These sections:
- Are always rendered fresh (never cached in the section cache)
- Show a yellow DYNAMIC badge in the sections panel
- Have no cache key (purge button shows “No cache key” toast)
- Are excluded from the cached/total ratio in the pill
The sessionDeps field lists which session dependencies triggered the dynamic classification (e.g. ["cart"], ["customer", "checkout"]).
Key files
Section titled “Key files”| File | Role |
|---|---|
cf-client-site/src/lib/debug-bar.ts | Assembles CSS + DOM + JS into injectable HTML |
cf-client-site/src/lib/debug-bar-css.ts | All styles scoped under #lk-debug |
cf-client-site/src/lib/debug-bar-js.ts | Client-side IIFE — pill, panels, events, highlighting |
cf-client-site/src/lib/purge-events.ts | In-memory ring buffer for purge events |
cf-client-site/src/render/page-renderer.ts | Collects SectionDebugInfo per section during render |
cf-client-site/src/worker.ts | Injects debug bar + serves /__dev/purge-events endpoint |
cf-client-dispatch/src/worker.ts | Auth + forward purge requests to theme worker |