Skip to content

Cloudflare Workers Architecture

Cloudflare Workers Architecture

This document describes the two Cloudflare Workers deployments that make up the Bloqr Compiler service, the differences between them, and how they relate to each other.


Overview

The Bloqr Compiler is deployed as two separate Cloudflare Workers from a single GitHub repository. Each has a distinct role:

bloqr-backendbloqr-frontend
Wrangler configwrangler.tomlfrontend/wrangler.toml
Entry pointworker/worker.tsdist/bloqr-backend/server/server.mjs
RoleREST API + compilation engine; also serves the Angular SPA as bundled static assets (CSR only)Angular 21 SSR UI — canonical home URL for the app
Source pathworker/ + src/frontend/
Deploy commanddeno task wrangler:deploysh scripts/deploy-frontend.sh (repo root)
CI deploy triggerdeploy job in ci.yml (main push, when worker/** or src/** changed)deploy-frontend job in ci.yml (main push when frontend/**, worker/**, src/**, wrangler.toml, src/version.ts, or compiler config changes such as deno.json / deno.lock changed; or workflow_dispatch with force_deploy_frontend: true)
Release deploy triggerbuild-binaries job in release.ymldeploy-frontend job in release.yml (tag push)
Local dev port87878787 (via pnpm --filter bloqr-frontend run preview)

bloqr-backend — The API Worker

What It Does

The backend worker is the compilation engine. It:

  • Exposes a REST API (POST /compile, POST /compile/stream, POST /compile/batch, GET /metrics, etc.)
  • Runs adblock/hostlist filter list compilation using the core src/ TypeScript logic (forked from AdguardTeam/HostlistCompiler)
  • Handles async queue-based compilation via Cloudflare Queues
  • Manages caching, rate limiting, and metrics via KV namespaces
  • Stores compiled outputs in R2 and persists state in D1 + Durable Objects
  • Runs scheduled background jobs (cache warming, health monitoring) via Cloudflare Workflows + Cron Triggers
  • Also serves the compiled Angular frontend as static assets via its [assets] binding (bundled deployment mode)

Source

mindmap
  root((bloqr-backend))
    worker["worker/"]
      workerTs["worker.ts — Cloudflare Workers fetch handler"]
    src["src/ — core compilation logic"]
    wrangler["wrangler.toml — deployment configuration (name = bloqr-backend)"]

Key Bindings

BindingTypePurpose
COMPILATION_CACHEKVCache compiled filter lists
RATE_LIMITKVPer-IP rate limiting
METRICSKVMetrics counters
FILTER_STORAGER2Store compiled filter list outputs
DBD1SQLite edge database
BLOQR_COMPILER_DODurable ObjectStateful compilation sessions
HYPERDRIVEHyperdriveAccelerated PostgreSQL access
ANALYTICS_ENGINEAnalytics EngineHigh-cardinality telemetry
ASSETSStatic AssetsServes compiled Angular frontend as static assets (bundled/single-worker mode only)

bloqr-frontend — The UI Worker

What It Does

The frontend worker is the Angular 21 SSR application. It:

  • Server-side renders the Angular application at the Cloudflare edge using AngularAppEngine
  • Serves the home page as a prerendered static page (SSG); all other routes are SSR per-request
  • Serves JS/CSS/font bundles directly from Cloudflare’s CDN via the ASSETS binding (the Worker never handles these requests)
  • Calls the bloqr-backend backend Worker’s REST API for all compilation operations

Source

mindmap
  root((bloqr-frontend))
    frontend["frontend/"]
      src["src/ — Angular 21 application source"]
      server["server.ts — Cloudflare Workers fetch handler (AngularAppEngine)"]
      wrangler["wrangler.toml — deployment configuration (name = bloqr-frontend)"]

Key Bindings

BindingTypePurpose
ASSETSStatic AssetsJS bundles, CSS, fonts — served from CDN before the Worker is invoked
APIService BindingActive — forwards all /api/* requests (browser-originated and SSR-initiated) from bloqr-frontend to bloqr-backend via the internal Cloudflare network. The browser→frontend leg is a normal public request; the frontend→backend leg bypasses the public network and CORS entirely. Sets CF-Worker-Source: ssr on every forwarded request.

SSR Architecture

The server.ts fetch handler uses Angular 21’s AngularAppEngine with the standard WinterCG fetch API — no Express, no Node.js HTTP server:

// Angular 21: setAngularAppEngineManifest() must be called before constructing
// AngularAppEngine. The manifest is a build artifact loaded at runtime via a
// dynamic import so esbuild does not attempt to bundle it.
import { AngularAppEngine, ɵsetAngularAppEngineManifest as setAngularAppEngineManifest } from '@angular/ssr';
// Cached at module scope — setup runs once per isolate, not once per request.
let angularAppPromise: Promise<AngularAppEngine> | null = null;
function getAngularApp(): Promise<AngularAppEngine> {
if (!angularAppPromise) {
angularAppPromise = (async () => {
const manifestPath = './angular-app-engine-manifest.mjs'; // variable path → not bundled
const { default: manifest } = await import(manifestPath);
setAngularAppEngineManifest(manifest);
return new AngularAppEngine();
})();
}
return angularAppPromise;
}
const handler = {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// Forward ALL /api/* requests to the backend via the service binding.
// Browser requests arrive at this worker over the public network; the
// hop from here to the backend is internal (no public round-trip, no CORS).
if (new URL(request.url).pathname.startsWith('/api/')) {
try {
const internalReq = new Request(request, {
headers: { ...Object.fromEntries(request.headers), 'CF-Worker-Source': 'ssr' },
});
return await env.API.fetch(internalReq);
} catch (err) {
return new Response('API unavailable', { status: 502 });
}
}
// Delegate remaining requests to AngularAppEngine for SSR.
const response = await (await getAngularApp()).handle(request);
return response ?? new Response('Not found', { status: 404 });
},
};
export default handler;

This means:

  • Edge-compatible — runs in any WinterCG-compliant runtime (Cloudflare Workers, Deno Deploy, Fastly Compute)
  • Fast cold starts — no Express middleware chain, no Node.js HTTP server initialisation
  • Zero-overhead static assets — JS/CSS/fonts are served by Cloudflare CDN before the Worker is ever invoked

Relationship Between the Two Workers

flowchart TD
    BROWSER["Browser Request"]

    FRONTEND["bloqr-frontend\n(Angular 21 SSR Worker)\n\n• Prerendered home page (SSG)\n• SSR for /compiler, /performance, /admin, /api-docs, /validation\n• Static assets served from CDN via ASSETS binding\n• All /api/* requests forwarded to bloqr-backend via service binding"]

    BROWSER -->|"Public network"| FRONTEND

    subgraph INTERNAL["Cloudflare Internal Network (service binding)"]
        direction TB
        BACKEND["bloqr-backend\n(TypeScript REST API Worker)\n\n• POST /compile\n• POST /compile/stream (SSE)\n• POST /compile/batch\n• GET /metrics  •  GET /health\n• KV, R2, D1, Durable Objects, Queues, Workflows, Hyperdrive"]
    end

    FRONTEND -->|"All /api/* calls via env.API.fetch()\n— no public hop, no CORS"| BACKEND

Two Deployment Modes

The backend worker supports two ways the frontend can be served:

1. Bundled Mode (single worker)

The root wrangler.toml includes an [assets] block pointing to the Angular build output:

[assets]
directory = "./frontend/dist/bloqr-backend/browser"
binding = "ASSETS"

This means a single wrangler deploy from the repo root deploys both the API and the Angular frontend as one unit. The Worker serves API requests; static assets are served by Cloudflare CDN via the binding.

2. Independent SSR Mode (two separate workers)

frontend/wrangler.toml deploys the Angular application as its own Worker with full SSR (AngularAppEngine). This is the bloqr-frontend worker. It runs server-side rendering at the edge and calls the backend API for data.

Bundled ModeIndependent SSR Mode
Workers deployed1 (bloqr-backend)2 (backend + frontend)
Frontend servingStatic assets via CDN bindingAngularAppEngine SSR + CDN for assets
SSR supportNo (SPA only)Yes (prerender + server rendering)
Deploy commanddeno task wrangler:deploy (root)deno task wrangler:deploy (root) + sh scripts/deploy-frontend.sh
Use caseSimpler deployment, CSR onlyFull SSR, edge rendering, independent scaling

Deployment

Both Workers support three environments — local, dev, and production. See Environments for the full reference including deploy commands, URL configuration, and TOML scoping rules.

Backend

Terminal window
# Production
deno task wrangler:deploy
# Dev (deploys to *.workers.dev, ENVIRONMENT=development)
deno task wrangler:deploy:dev

Frontend (Independent SSR mode)

Terminal window
# Production — preferred (builds, injects CF analytics token, deploys)
sh scripts/deploy-frontend.sh
# Dev (Angular development build + *.workers.dev deploy)
deno task ui:deploy:ng:dev
# Production step by step (from repo root):
pnpm --filter bloqr-frontend run build
sh scripts/build-worker.sh # injects/removes {{CF_WEB_ANALYTICS_TOKEN}} in index.html
pnpm --filter bloqr-frontend run deploy

Important: Always run scripts/build-worker.sh after ng build and before wrangler deploy. It rewrites the {{CF_WEB_ANALYTICS_TOKEN}} placeholder in dist/.../browser/index.html (or removes the analytics <script> tag if CF_WEB_ANALYTICS_TOKEN is not set). Skipping this step leaves the placeholder in the deployed HTML.

CI/CD Automatic Deployment

Both Workers are deployed automatically by GitHub Actions:

flowchart LR
    push["Push to main"] --> ci_gate["ci-gate\n(all checks pass)"]
    ci_gate --> deploy_backend["deploy job\n(bloqr-backend)"]
    ci_gate --> frontend_build["frontend-build\n(artifact upload)"]
    frontend_build --> deploy_frontend["deploy-frontend job\n(bloqr-frontend)\nfrontend/** OR worker/** OR src/**\nOR wrangler.toml OR src/version.ts"]
    deploy_frontend --> inject["Inject CF Web\nAnalytics token\n(build-worker.sh)"]
    inject --> wrangler_deploy["pnpm run deploy\n(wrangler deploy)"]
    deploy_backend --> smoke_backend["smoke-test-backend\n/api/health\n/api/version\n/api/auth/providers"]
    wrangler_deploy --> smoke_frontend["smoke-test-frontend\nhomepage\n/api/auth/providers\n/api/health"]
    smoke_backend --> deploy_status["deploy-status\n(final summary)"]
    smoke_frontend --> deploy_status

    tag["Tag push (v*)"] --> validate["validate"]
    validate --> deploy_frontend_rel["deploy-frontend job\n(release.yml)"]
    deploy_frontend_rel --> build_rel["pnpm run build\n(ng build)"]
    build_rel --> inject_rel["Inject CF Web\nAnalytics token\n(build-worker.sh)"]
    inject_rel --> wrangler_rel["pnpm run deploy\n(wrangler deploy)"]
TriggerBackend deployFrontend deploy
Push to main (worker/compiler change)ci.ymldeploy jobci.ymldeploy-frontend job
Push to main (frontend change)ci.ymldeploy-frontend job
Push to main (wrangler.toml / src/version.ts change)ci.ymldeploy jobci.ymldeploy-frontend job
Tag push (v*)release.yml → binary/docker buildsrelease.ymldeploy-frontend job
Manual dispatchci.ymldeploy-frontend job (set force_deploy_frontend: true)

Note: The deploy-frontend job triggers whenever frontend/**, worker/**, src/**, wrangler.toml, or src/version.ts change — so the frontend SSR Worker is always re-deployed alongside the backend when they share a release train.

Manual Force-Redeploy

If the frontend worker shows “Assets have not yet been deployed” or “Unable to reach API”, the bloqr-frontend Worker may be running a stale version. This typically happens when:

  • The deploy-frontend CI job was skipped because no monitored files changed.
  • The worker was first registered before the build artifact was available.
  • A version-bump commit landed but the Worker didn’t pick up the latest env bindings.

To fix it immediately without a code change:

  1. Go to GitHub Actions → CI → Run workflow.
  2. Select the main branch.
  3. Set force_deploy_frontend to true.
  4. Click Run workflow.

This forces frontend-build and deploy-frontend to run regardless of which files changed.

Post-Deploy Smoke Tests

After every successful backend or frontend deploy to main, CI automatically runs smoke test jobs to verify the deployment:

smoke-test-backend (needs deploy)

CheckURLPass condition
/api/healthhttps://bloqr-backend.jk-com.workers.dev/api/healthHTTP 200 + status is "healthy" or "degraded"
/api/versionhttps://bloqr-backend.jk-com.workers.dev/api/versionHTTP 200
/api/auth/providershttps://bloqr-backend.jk-com.workers.dev/api/auth/providersHTTP 200

smoke-test-frontend (needs deploy-frontend)

CheckURLPass condition
Homepagehttps://bloqr-frontend.jk-com.workers.dev/HTTP 200
SSR API proxyhttps://bloqr-frontend.jk-com.workers.dev/api/auth/providersHTTP 200 (confirms the SSR Worker proxies to the backend correctly)
Health via proxyhttps://bloqr-frontend.jk-com.workers.dev/api/healthHTTP 200 + status is "healthy" or "degraded"

Both smoke tests:

  • Run with timeout-minutes: 5 and continue-on-error: false — a failing smoke test fails the CI run and blocks the next commit from deploying on top of a broken state.
  • Wait 15 seconds before probing (allows Cloudflare to propagate the new Worker).
  • Emit a $GITHUB_STEP_SUMMARY table so you can see pass/fail at a glance in the Actions UI.

Interpreting smoke test failures

SymptomLikely cause
smoke-test-frontend SSR proxy step fails (HTTP 000 or 502)frontend/server.ts /api/* proxy block is broken or the backend is unreachable via service binding
smoke-test-backend /api/auth/providers failsBetter Auth middleware conflict (e.g. compress() wrapping auth responses)
Both /api/health checks failWorker deploy failed silently; check Cloudflare dashboard for deployment errors
HTTP 200 but jq parse error on /api/healthcompress() is still registered globally and returning gzip to curl; confirm logger()/compress() are scoped to routes.use('*') only

Local Development

Terminal window
# Backend API
deno task wrangler:dev # → http://localhost:8787
# Frontend (Angular dev server, CSR)
pnpm --filter bloqr-frontend run start # → http://localhost:4200
# Frontend (Cloudflare Workers preview, mirrors production SSR)
pnpm --filter bloqr-frontend run preview # → http://localhost:8787

Renaming Note

These workers were renamed twice. Current names as of this PR:

Old nameInterim name (2026-03-07)Current nameDate
bloqr-backendbloqr-backend-backendbloqr-backend2026-03-07
bloqr-backend-angular-pocbloqr-frontendbloqr-frontend2026-03-07
bloqr-backend-frontendbloqr-frontendbloqr-frontend2026-03-23

The backend was renamed back to bloqr-backend for brevity. If you have workers under old names in your Cloudflare dashboard, they continue to run until manually deleted. The next wrangler deploy creates workers under the current names.


Further Reading