Building a Reliable CDN for our Library with Cloudflare R2 and Workers

Internet Traffic Illustration

Modern web applications depend on shared JavaScript libraries to ensure consistency and minimise code duplication. At Webmobix Solutions AG, we encountered a challenge with our ChatLeo service: providing users with a reliable and efficient way to integrate our Web Components and libraries without requiring complex server-side builds or dynamic rendering.

Our solution needed to meet several key requirements: immutability for reproducible builds, global low-latency performance, simplicity for developers, and predictable cost efficiency. Traditional options such as npm registries or generic CDNs involved trade-offs we found unacceptable.

Our goal was straightforward: allow users to add our library with just two lines of HTML. To accomplish this, we developed a custom CDN setup using Cloudflare R2 for storage, Workers for intelligent routing, and KV for version mapping. Below is an overview of our CDN architecture.

The Architecture: Decoupling Storage from Delivery

We separate immutable, versioned file storage from dynamic public URLs, ensuring that version updates do not disrupt existing integrations.

graph TB
    User["👤 User<br/>(Web Browser)"]

    subgraph Cloudflare["☁️ Cloudflare Edge Network"]
        Worker["⚡ Cloudflare Worker<br/>(Routing Logic)"]
        KV[("🗄️ KV Store<br/>(Version Mappings)")]
        R2[("📦 R2 Storage<br/>(Module Files)")]
    end

    User -->|"1. Request<br/>/components/latest/c.js"| Worker
    Worker -->|"2. Query<br/>What is 'latest'?"| KV
    KV -->|"3. Response<br/>latest = v1.2.3"| Worker
    Worker -->|"Redirect to <br />/components/1.2.3/c.js"| User
    User -->|"4. Fetch<br/>/components/1.2.3/c.js"| R2
    R2 -->|"5. Return File"| User

    style User fill:#e1f5ff,stroke:#0366d6,stroke-width:2px
    style Cloudflare fill:#f6f8fa
    style Worker fill:#fff3cd,stroke:#856404,stroke-width:2px
    style KV fill:#d1ecf1,stroke:#0c5460,stroke-width:2px
    style R2 fill:#d4edda,stroke:#155724,stroke-width:2px

Immutable Version Storage (Cloudflare R2)

Each module version is stored in Cloudflare R2 using a unique content-addressed path. Files remain unchanged once uploaded.

Storage Path Structure:

/v1.0.0/chatleo/loader.js

Each version’s URL is permanent and always serves the same code, allowing teams to confidently reference specific versions.

Smart Routing (Cloudflare Workers & KV)

For development, we support references such as “latest” or major versions by redirecting requests through a Worker. The Worker retrieves the current mapping from KV and redirects to the corresponding immutable file in R2.

The Implementation

Below is the logic used in our Version Resolver. It handles CORS preflight requests, parses URLs using Regex, and preserves query parameters during redirects.

The Worker Code

interface Env {
  CHATLEO_VERSION_MAP: KVNamespace;
}

// Define standard CORS headers
const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
  'Access-Control-Allow-Headers': '*',
  'Access-Control-Max-Age': '86400', // Cache preflight for 24 hours
};

export default {
  async fetch(request, env, ctx): Promise<Response> {
    const url = new URL(request.url);

    // --- 1. HANDLE PREFLIGHT (OPTIONS) ---
    if (request.method === 'OPTIONS') {
      return new Response(null, {
        headers: corsHeaders,
      });
    }

    // Schema: /v1/chatleo/loader.js
    // Regex:
    // ^/            -> Start
    // (v\d+|latest) -> Group 1: Alias ("v1", "latest")
    // /             -> Separator
    // ([^/]+)       -> Group 2: Library Name ("chatleo", "chatolib")
    // (.*)          -> Group 3: File Path ("/loader.js")
    const match = url.pathname.match(/^\/(v\d+|latest)\/([^/]+)(.*)/);

    if (!match) {
      return new Response('Not found', {
        status: 404,
        headers: corsHeaders,
      });
    }

    const alias = match[1]; // "v1"
    const libName = match[2]; // "chatleo"
    const filePath = match[3]; // "/loader.js"

    // 1. Construct a Key for KV
    // Key example: "v1" or "latest"
    const kvKey = alias;

    // 2. Lookup Exact Version
    // Returns: "v1.2.3"
    const realVersion = await env.CHATLEO_VERSION_MAP.get(kvKey);

    if (!realVersion) {
      return new Response(`Alias '${alias}' for library '${libName}' not found.`, {
        status: 404,
        headers: corsHeaders, // Send CORS even on 404 so JS can read the error
      });
    }

    // 3. Redirect
    // Target: /v1.2.3/chatleo/loader.js
    // Note: We put the version at the ROOT to match your R2 structure
    const newPath = `/${realVersion}/${libName}${filePath}`;

    const targetUrl = new URL(newPath, url.origin);

    // --- COPY QUERY PARAMS ---
    // This takes "?server=xyz" from the incoming request and attaches it to the outgoing 302 redirect.
    targetUrl.search = url.search;

    return new Response(null, {
      status: 302,
      headers: {
        Location: targetUrl.toString(),
        ...corsHeaders,
        'Cache-Control': 'public, max-age=3600, s-maxage=3600',
      },
    });
  },
} satisfies ExportedHandler<Env>;

Wrangler config

In your wrangler.jsonc file fill the routes section with the excat patterns to match all calls that are not matching a full version like /v1.0.1/file.js. This ensures that the worker is only called for these specific requests. I have chosen here to only route client requests by major version as minor upgrades and bugfixes will not brewak functionality.

You have to deploy the worker again though when introducing a new major version of your library. The v0 branch was used for testing in development. A future enhancement can be a much shorter cache timeout for sepecific versions like v0 to support rapid development changes.

"routes": [
	{
		"pattern": "cdn.chat-leo.com/latest/*",
		"zone_name": "chat-leo.com",
	},
	{
		"pattern": "cdn.chat-leo.com/v0/*",
		"zone_name": "chat-leo.com",
	},
	{
		"pattern": "cdn.chat-leo.com/v1/*",
		"zone_name": "chat-leo.com",
	},
],

Why This Approach Works

The Power of Browser-Based Caching

Performance depends on predictable, layered caching.

  • The 302 redirect response is cached for one hour, allowing updates to reach users quickly.

  • Versioned URLs are immutable and can be cached long-term. We cache them for one year, which significantly reduces origin requests.

Zero Downtime & Atomic Updates

By separating the alias from the file, we can publish new versions without coordination or downtime. New files are uploaded to R2, and the KV mapping for v1 is updated. Existing versioned references remain unchanged, while the latest references receive the new version as caches expire.

Global Scale and Cost Control

Cloudflare’s network delivers modules from nearby edge locations for fast load times. With R2 storage, we pay only for storage and operations, not bandwidth, which reduces costs.

The drawback for long caching

One significant drawback of this approach is the inability to immediately force clients to fetch a patched version if a broken library is deployed. Because the Worker responds with a Cache-Control: public, max-age=3600 header for the 302 redirect, browsers will aggressively cache the routing decision for a full hour.

Consequently, even if you quickly update the KV store to point v1 or latest to a hotfixed version, clients who have already loaded the broken script within the last hour will not check back with the Worker to discover the update. You cannot use traditional cache-busting techniques (like appending a new query string) on the client side since the initial <script> tag is hardcoded on the host website, leaving affected users with the broken experience until their local browser caxche expires.

The Strategic Value of Fast Web Component Distribution

For a third-party service like our ChatLeo service, the delivery mechanism is as critical as the application code. Distributing the library as a Web Component ensures our chat widget functions consistently across any framework, including React, Vue, or plain HTML, while keeping our styles isolated from the host website.

Providing a “drop-in” widget imposes strict performance requirements. Our edge architecture addresses these needs for the following reasons:

  • Frictionless “Two-Line” Integration: By offloading complexity to Cloudflare’s edge, users can integrate your-library with just two lines of code, eliminating the need for complex tooling:
<script src="https://cdn.your-library.com/v1/your-library/loader.js"><script>
<your-library-app></your-library-app>
  • Gradual Update Propagation: Our routing allows us to push changes to thousands of live chat widgets simultaneously without requiring our customers to change a single line of code or redeploy their websites.

Lessons Learned & Future Enhancements

Developing this solution provided several valuable lessons about JavaScript module distribution:

  • Immutability is powerful: Committing to immutable versioned URLs eliminated cache invalidation issues, stale content concerns, and race conditions during deployments.

  • Edge computing transforms distribution: Making intelligent routing decisions closer to the user fundamentally changes performance characteristics.

  • Simplicity scales: By focusing purely on two patterns (versioned and latest), we created a system that is remarkably easy to operate and extend.

While our current implementation meets our needs, we are considering future enhancements, such as automated version bumping via CI/CD, semantic version resolution, and subresource integrity verification, to improve security.

Conclusion: Partner With Us to Scale Your Vision

Combining Cloudflare R2, Workers, and KV provided an effective solution for our module distribution needs, supporting both careful change management and rapid iteration.

At Webmobix Solutions AG, we build for impact, momentum, and growth. Whether you need support with JavaScript module distribution, want to advance your workflows with Custom AI Agents, or require Full Stack Development for your next SaaS platform, our team has the expertise to help you succeed.

We provide the tools, talent, and technology to shape your success story, from the initial release to long-term impact.

Are you ready to optimise your infrastructure or build your next high-impact software?

Visit our website to book a free discovery call, and let us explore how we can turn your potential into progress.