Edge Workers Deployment
Deploy and manage Cloudflare Workers for edge computing.
Overview
Cloudflare Workers handle edge-level processing:
| Worker | Purpose | Routes |
|---|---|---|
| Router | Request routing | *.olympuscloud.ai/* |
| Auth | Token validation | api.olympuscloud.ai/* |
| Rate Limiter | Abuse protection | api.olympuscloud.ai/* |
| Static Assets | Asset serving | static.olympuscloud.ai/* |
| AI Gateway | AI request routing | ai.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
| Issue | Cause | Solution |
|---|---|---|
| 1101 Worker threw exception | Uncaught error | Check logs, add error handling |
| 1102 Worker exceeded CPU limit | Long computation | Optimize code, use Durable Objects |
| 1015 Worker hit memory limit | Large payloads | Stream data, reduce memory usage |
| Subrequest limit | Too many fetches | Batch 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' },
});
}
Related Documentation
- Cloud Run Deployment - Backend deployment
- Environments - Environment configuration
- Edge Infrastructure - Architecture overview