Skip to main content

Edge Workers Deployment

Deploy and manage Cloudflare Workers for edge computing.

Overview

Cloudflare Workers handle edge-level processing:

WorkerPurposeRoutes
RouterRequest routing*.olympuscloud.ai/*
AuthToken validationapi.olympuscloud.ai/*
Rate LimiterAbuse protectionapi.olympuscloud.ai/*
Static AssetsAsset servingstatic.olympuscloud.ai/*
AI GatewayAI request routingai.olympuscloud.ai/*

Project Setup

Directory Structure

workers/
├── router/
│ ├── src/
│ │ ├── index.ts
│ │ └── handlers/
│ ├── wrangler.toml
│ └── package.json
├── auth/
│ ├── src/
│ │ └── index.ts
│ └── wrangler.toml
├── rate-limiter/
│ ├── src/
│ │ └── index.ts
│ └── wrangler.toml
└── shared/
└── types.ts

Wrangler Configuration

# workers/router/wrangler.toml
name = "olympus-router"
main = "src/index.ts"
compatibility_date = "2026-01-01"
compatibility_flags = ["nodejs_compat"]

# Account configuration
account_id = "0196a94fc8633e5d5c91275cb0ec37a7"

# Production environment
[env.production]
routes = [
{ pattern = "api.olympuscloud.ai/*", zone_name = "olympuscloud.ai" },
{ pattern = "api.restaurantrevolution.ai/*", zone_name = "restaurantrevolution.ai" }
]
vars = { ENVIRONMENT = "production" }

# Staging environment
[env.staging]
routes = [
{ pattern = "staging.api.olympuscloud.ai/*", zone_name = "olympuscloud.ai" }
]
vars = { ENVIRONMENT = "staging" }

# KV Namespaces
[[kv_namespaces]]
binding = "CONFIG"
id = "abc123"
preview_id = "def456"

[[kv_namespaces]]
binding = "CACHE"
id = "ghi789"

# Durable Objects
[[durable_objects.bindings]]
name = "RATE_LIMITER"
class_name = "RateLimiter"

[[durable_objects.migrations]]
tag = "v1"
new_classes = ["RateLimiter"]

# R2 Buckets
[[r2_buckets]]
binding = "ASSETS"
bucket_name = "olympus-static-assets"

# Service Bindings
[[services]]
binding = "AUTH_WORKER"
service = "olympus-auth"
environment = "production"

# Secrets (set via wrangler secret)
# - JWT_SECRET
# - API_GATEWAY_KEY

Worker Implementation

Router Worker

// workers/router/src/index.ts
import { Env, RequestContext } from './types';

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

// Health check
if (url.pathname === '/health') {
return new Response('OK', { status: 200 });
}

// Extract tenant from subdomain
const tenant = extractTenant(url.hostname);
if (!tenant) {
return new Response('Invalid tenant', { status: 400 });
}

// Rate limiting
const rateLimitResult = await checkRateLimit(request, env);
if (rateLimitResult.limited) {
return new Response('Too Many Requests', {
status: 429,
headers: { 'Retry-After': rateLimitResult.retryAfter.toString() },
});
}

// Route to appropriate backend
const response = await routeRequest(request, env, tenant);

// Add security headers
return addSecurityHeaders(response);
},
};

function extractTenant(hostname: string): string | null {
// acme.olympuscloud.ai -> acme
const match = hostname.match(/^([a-z0-9-]+)\.(?:olympuscloud|restaurantrevolution)\.ai$/);
return match ? match[1] : null;
}

async function routeRequest(
request: Request,
env: Env,
tenant: string
): Promise<Response> {
const url = new URL(request.url);

// API routes -> Cloud Run
if (url.pathname.startsWith('/api/v1/')) {
const backendUrl = `${env.API_GATEWAY_URL}${url.pathname}${url.search}`;
const headers = new Headers(request.headers);
headers.set('X-Tenant-ID', tenant);
headers.set('X-Forwarded-Host', url.hostname);

return fetch(backendUrl, {
method: request.method,
headers,
body: request.body,
});
}

// WebSocket -> Durable Objects
if (request.headers.get('Upgrade') === 'websocket') {
return handleWebSocket(request, env, tenant);
}

// Static assets -> R2
if (url.pathname.startsWith('/static/')) {
return serveStaticAsset(url.pathname, env);
}

return new Response('Not Found', { status: 404 });
}

function addSecurityHeaders(response: Response): Response {
const headers = new Headers(response.headers);
headers.set('X-Content-Type-Options', 'nosniff');
headers.set('X-Frame-Options', 'DENY');
headers.set('X-XSS-Protection', '1; mode=block');
headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');

return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
}

Rate Limiter Durable Object

// workers/rate-limiter/src/index.ts
export class RateLimiter implements DurableObject {
private requests: number[] = [];
private readonly limit = 100;
private readonly windowMs = 60000;

constructor(
private state: DurableObjectState,
private env: Env
) {}

async fetch(request: Request): Promise<Response> {
const now = Date.now();

// Load state
this.requests = (await this.state.storage.get('requests')) || [];

// Remove old requests outside window
this.requests = this.requests.filter(t => t > now - this.windowMs);

// Check limit
if (this.requests.length >= this.limit) {
const oldestRequest = Math.min(...this.requests);
const retryAfter = Math.ceil((oldestRequest + this.windowMs - now) / 1000);

return new Response('Rate Limited', {
status: 429,
headers: {
'Retry-After': retryAfter.toString(),
'X-RateLimit-Limit': this.limit.toString(),
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': ((oldestRequest + this.windowMs) / 1000).toString(),
},
});
}

// Record request
this.requests.push(now);
await this.state.storage.put('requests', this.requests);

return new Response('OK', {
headers: {
'X-RateLimit-Limit': this.limit.toString(),
'X-RateLimit-Remaining': (this.limit - this.requests.length).toString(),
},
});
}
}

export default {
async fetch(request: Request, env: Env): Promise<Response> {
const ip = request.headers.get('CF-Connecting-IP') || 'unknown';
const id = env.RATE_LIMITER.idFromName(ip);
const limiter = env.RATE_LIMITER.get(id);
return limiter.fetch(request);
},
};

Deployment

Development

# Start remote development (connects to Cloudflare dev environment)
cd workers/router
wrangler dev --remote

# Test against deployed dev worker
curl https://dev.api.olympuscloud.ai/health

Staging Deployment

# Deploy to staging
wrangler deploy --env staging

# Test staging
curl https://staging.api.olympuscloud.ai/health

Production Deployment

# Deploy to production
wrangler deploy --env production

# Verify deployment
wrangler tail --env production

Secrets Management

# Set secrets
wrangler secret put JWT_SECRET --env production
wrangler secret put API_GATEWAY_KEY --env production
wrangler secret put SENTRY_DSN --env production

# List secrets
wrangler secret list --env production

# Delete secret
wrangler secret delete OLD_SECRET --env production

CI/CD Pipeline

GitHub Actions

# .github/workflows/workers.yml
name: Deploy Workers

on:
push:
branches: [main]
paths:
- 'workers/**'

jobs:
deploy:
runs-on: ubuntu-latest
strategy:
matrix:
worker: [router, auth, rate-limiter]

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: workers/${{ matrix.worker }}/package-lock.json

- name: Install dependencies
run: |
cd workers/${{ matrix.worker }}
npm ci

- name: Run tests
run: |
cd workers/${{ matrix.worker }}
npm test

- name: Deploy to Cloudflare
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
workingDirectory: workers/${{ matrix.worker }}
command: deploy --env production

Rollback

# List deployments
wrangler deployments list

# Rollback to previous version
wrangler rollback --env production

# Rollback to specific version
wrangler rollback --version abc123 --env production

KV Store Usage

Configuration Storage

// Store configuration
await env.CONFIG.put('feature-flags', JSON.stringify({
newCheckout: true,
voiceOrdering: false,
}));

// Retrieve with caching
const flags = await env.CONFIG.get('feature-flags', {
type: 'json',
cacheTtl: 300, // 5 minutes
});

// Store with expiration
await env.CONFIG.put('session:abc123', sessionData, {
expirationTtl: 3600, // 1 hour
});

// List keys
const keys = await env.CONFIG.list({ prefix: 'session:' });

Response Caching

async function getCachedResponse(
cacheKey: string,
env: Env,
fetchFn: () => Promise<Response>
): Promise<Response> {
// Check cache
const cached = await env.CACHE.get(cacheKey, { type: 'arrayBuffer' });
if (cached) {
return new Response(cached, {
headers: { 'X-Cache': 'HIT' },
});
}

// Fetch fresh
const response = await fetchFn();

// Cache response
const body = await response.arrayBuffer();
await env.CACHE.put(cacheKey, body, {
expirationTtl: 300,
});

return new Response(body, {
status: response.status,
headers: {
...Object.fromEntries(response.headers),
'X-Cache': 'MISS',
},
});
}

R2 Static Assets

Serving Assets

async function serveStaticAsset(
pathname: string,
env: Env
): Promise<Response> {
const key = pathname.replace('/static/', '');

const object = await env.ASSETS.get(key);
if (!object) {
return new Response('Not Found', { status: 404 });
}

const headers = new Headers();
headers.set('Content-Type', object.httpMetadata?.contentType || 'application/octet-stream');
headers.set('Cache-Control', 'public, max-age=31536000, immutable');
headers.set('ETag', object.httpEtag);

return new Response(object.body, { headers });
}

Upload Assets

# Upload file
wrangler r2 object put olympus-static-assets/images/logo.png \
--file ./logo.png \
--content-type image/png

# Upload directory
for file in dist/*; do
wrangler r2 object put "olympus-static-assets/app/${file##*/}" \
--file "$file"
done

Monitoring

Worker Analytics

// Log to Analytics Engine
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const start = Date.now();

try {
const response = await handleRequest(request, env);

// Log success
env.ANALYTICS.writeDataPoint({
blobs: [request.url, response.status.toString()],
doubles: [Date.now() - start],
indexes: [request.headers.get('CF-Connecting-IP') || ''],
});

return response;
} catch (error) {
// Log error
env.ANALYTICS.writeDataPoint({
blobs: [request.url, 'error', error.message],
doubles: [Date.now() - start],
indexes: [request.headers.get('CF-Connecting-IP') || ''],
});

throw error;
}
},
};

Tail Logs

# Stream logs in real-time
wrangler tail --env production

# Filter by status
wrangler tail --env production --status error

# Filter by search term
wrangler tail --env production --search "rate limit"

# JSON output
wrangler tail --env production --format json

Troubleshooting

Common Issues

IssueCauseSolution
1101 Worker threw exceptionUncaught errorCheck logs, add error handling
1102 Worker exceeded CPU limitLong computationOptimize code, use Durable Objects
1015 Worker hit memory limitLarge payloadsStream data, reduce memory usage
Subrequest limitToo many fetchesBatch requests, use cache

Debug Tips

// Add detailed logging
console.log(JSON.stringify({
event: 'request',
url: request.url,
method: request.method,
headers: Object.fromEntries(request.headers),
cf: request.cf,
}));

// Return debug info in development
if (env.ENVIRONMENT === 'development') {
return new Response(JSON.stringify({
url: request.url,
cf: request.cf,
headers: Object.fromEntries(request.headers),
}), {
headers: { 'Content-Type': 'application/json' },
});
}