Skip to content

CDN Service Architecture

Technical architecture of the CDN Service.

System Overview

┌─────────────────────────────────────────────────────────────────────────────┐
│                              CDN Service                                     │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐                   │
│  │   Request    │───▶│   Subdomain  │───▶│   Central    │                   │
│  │   Handler    │    │   Router     │    │   Directory  │                   │
│  └──────────────┘    └──────────────┘    │   Loader     │                   │
│                                          └───────┬──────┘                   │
│                                                  │                          │
│                                          ┌───────▼──────┐                   │
│                                          │    KV Cache  │                   │
│                                          │   (24h TTL)  │                   │
│                                          └───────┬──────┘                   │
│                                                  │                          │
│                                          ┌───────▼──────┐                   │
│                                          │     R2       │                   │
│                                          │   Storage    │                   │
│                                          └───────┬──────┘                   │
│                                                  │                          │
│  ┌──────────────┐    ┌──────────────┐    ┌───────▼──────┐                   │
│  │   Response   │◀───│   MIME Type  │◀───│    File      │                   │
│  │   Handler    │    │   Detection  │    │   Extractor  │                   │
│  └──────────────┘    └──────────────┘    └──────────────┘                   │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Component Details

Request Handler

Entry point for all incoming requests:

typescript
app.get('/*', async (c) => {
  const hostname = c.req.header('host') || '';
  const projectId = extractProjectId(hostname);
  const path = c.req.path;

  // Route to appropriate handler
  return handleStaticFile(c, projectId, path);
});

Subdomain Router

Extracts project ID from subdomain:

typescript
function extractProjectId(hostname: string): string | null {
  // Pattern: view-{projectId}.domain.com
  const match = hostname.match(/^view-([^.]+)\./);
  return match ? match[1] : null;
}

Central Directory Loader

Loads ZIP metadata with KV caching:

typescript
async function loadCentralDirectory(projectId: string) {
  const cacheKey = `cd:${projectId}.zip`;

  // Try KV cache first
  let metadata = await kv.get(cacheKey, 'json');

  if (!metadata) {
    // Load from R2 using range request
    metadata = await loadFromR2(projectId);
    await kv.put(cacheKey, JSON.stringify(metadata), {
      expirationTtl: 86400 // 24 hours
    });
  }

  return metadata;
}

File Extractor

Extracts individual files from ZIP:

typescript
async function extractFile(projectId: string, path: string) {
  const metadata = await loadCentralDirectory(projectId);
  const entry = findEntry(metadata, path);

  if (!entry) {
    return notFound();
  }

  // Fetch only the file's bytes
  const data = await fetchRange(projectId, entry.offset, entry.size);

  // Decompress if needed
  if (entry.compressionMethod === 8) {
    return pako.inflate(data);
  }

  return data;
}

MIME Type Detection

Automatic content type detection:

typescript
const MIME_TYPES = {
  '.html': 'text/html',
  '.js': 'application/javascript',
  '.css': 'text/css',
  '.json': 'application/json',
  '.svg': 'image/svg+xml',
  '.png': 'image/png',
  // ... more types
};

function getMimeType(path: string): string {
  const ext = path.match(/\.[^.]+$/)?.[0] || '';
  return MIME_TYPES[ext] || 'application/octet-stream';
}

Response Handler

Builds response with proper headers:

typescript
function buildResponse(data: ArrayBuffer, path: string) {
  return new Response(data, {
    headers: {
      'Content-Type': getMimeType(path),
      'Cache-Control': 'public, max-age=31536000, immutable',
      'Access-Control-Allow-Origin': '*',
    }
  });
}

Data Flow

First Request (Cache Miss)

1. Request arrives at edge
2. Subdomain parsed for project ID
3. Check KV for central directory → MISS
4. Range request to R2 for ZIP end-of-file
5. Parse central directory from response
6. Store central directory in KV
7. Locate requested file in directory
8. Range request to R2 for file bytes
9. Decompress if deflate-compressed
10. Return with headers

Subsequent Requests (Cache Hit)

1. Request arrives at edge
2. Subdomain parsed for project ID
3. Check KV for central directory → HIT
4. Locate requested file in directory
5. Range request to R2 for file bytes
6. Return with headers

Storage Structure

R2 Bucket

static-sites/
├── project-a/
│   ├── latest.zip
│   ├── v1.0.0.zip
│   └── pr-123.zip
├── project-b/
│   └── latest.zip
└── ...

KV Namespace

cd:project-a.zip → { central directory metadata }
cd:project-b.zip → { central directory metadata }

ZIP Structure

Each archive contains:

{project}-{version}.zip
├── index.html           # Storybook entry point
├── iframe.html          # Storybook iframe
├── static/
│   ├── main.js
│   ├── styles.css
│   └── ...
├── assets/
│   └── ...
└── [other files]

Error Handling

File Not Found

typescript
if (!entry) {
  // Try SPA fallback
  const fallback = findEntry(metadata, 'index.html');
  if (fallback) {
    return extractAndServe(fallback);
  }
  return notFound();
}

Invalid Project

typescript
if (!projectId) {
  return new Response('Invalid subdomain', { status: 400 });
}

Storage Errors

typescript
try {
  const data = await r2.get(key);
  if (!data) {
    return notFound();
  }
  return serve(data);
} catch (error) {
  console.error('R2 error:', error);
  return serverError();
}

Caching Strategy

CacheTTLPurpose
Cloudflare Edge1 yearStatic file caching
KV24 hoursCentral directory metadata
Browser1 yearImmutable assets

Cache Headers

Cache-Control: public, max-age=31536000, immutable

Cache Invalidation

  • Re-uploading a ZIP doesn't immediately invalidate cache
  • Wait for KV TTL to expire (24 hours)
  • Or call clearCentralDirectoryCache() programmatically

Performance Optimizations

Range Requests

Only fetch needed bytes:

typescript
const response = await r2.get(key, {
  range: { offset, length }
});

Streaming

Stream large files without buffering:

typescript
return new Response(stream, {
  headers: { 'Content-Type': mimeType }
});

Compression

Files are stored compressed in ZIP; we decompress on the fly for supported types.

Next Steps

Released under the MIT License.