okhp3-cloudflare-worker-api-proxy
OverKill Hill P³ · overkillhill.com · github.com/OKHP3
A Cloudflare Worker is a serverless function that runs at Cloudflare's edge. It handles the API call server-side, holds the API key in secrets, and returns
Scope
| In scope | Out of scope |
|---|---|
| A narrow static-frontend-to-keyed-API proxy, CORS policy, secret binding, request validation, and client fetch example | Hiding a public endpoint, replacing authentication or authorization, rate-abuse prevention by assumption, or promising current plan limits without verification |
| the result to your browser. The key never appears in your JavaScript bundle. |
When to use this
Use this pattern when:
- Your static site needs to call an API that requires a key (Anthropic, OpenAI, SendGrid, etc.)
- Exposing the key in client JavaScript would be a security risk or ToS violation
- You don't want to set up a full backend server
The 30-line Worker
// worker.js — paste this into the Cloudflare Worker editor
export default {
async fetch(request, env) {
// Handle CORS preflight
if (request.method === 'OPTIONS') {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': env.ALLOWED_ORIGIN || '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Max-Age': '86400',
},
});
}
if (request.method !== 'POST') {
return new Response('Method Not Allowed', { status: 405 });
}
// Parse the request body from the client
let body;
try {
body = await request.json();
} catch {
return new Response('Invalid JSON', { status: 400 });
}
// Call the upstream API with your secret key
const upstream = await fetch(env.UPSTREAM_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Add your API-specific auth header here:
'Authorization': `Bearer ${env.API_SECRET_KEY}`,
// Or for Anthropic:
// 'x-api-key': env.API_SECRET_KEY,
// 'anthropic-version': '2023-06-01',
},
body: JSON.stringify(body),
});
const data = await upstream.json();
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': env.ALLOWED_ORIGIN || '*',
},
});
},
};
Setup steps (dashboard, no CLI required)
Create the Worker
- Cloudflare dashboard → Workers & Pages → Create Worker
- Name it (e.g.,
my-api-proxy) - Paste the Worker code above
- Edit
env.UPSTREAM_API_URLreference and auth header to match your API
Add secrets (not visible in code)
- Worker → Settings → Variables → Add variable
- Add as Secret (encrypted at rest):
API_SECRET_KEY= your actual API keyUPSTREAM_API_URL= the API endpoint you're proxying (e.g.,https://api.anthropic.com/v1/messages)ALLOWED_ORIGIN= your frontend's domain (e.g.,https://username.github.io) — or*for open access
Deploy
- Click Deploy
- Worker URL:
https://my-api-proxy.your-account.workers.dev
Add the Worker URL to your frontend
- Store as an environment variable:
VITE_PROXY_URL=https://my-api-proxy.your-account.workers.dev - In Replit: add to Secrets panel
- Store as an environment variable:
Client-side fetch pattern
// In your React app — the key never appears here
const PROXY_URL = import.meta.env.VITE_PROXY_URL;
async function callApi(payload: object) {
const res = await fetch(PROXY_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`Proxy error: ${res.status}`);
return res.json();
}
Anthropic-specific variant
For proxying api.anthropic.com/v1/messages:
// In the Worker, replace the fetch call with:
const upstream = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': env.ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01',
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: body.model || 'YOUR_CURRENT_MODEL_ID',
max_tokens: body.max_tokens || 1000,
system: body.system,
messages: body.messages,
}),
});
Current Cloudflare limits
Do not hard-code plan limits or a permanence claim in an implementation. Check the current official Workers pricing and limits documentation for the account, plan, runtime, and deployment date, then record the verified limits in the project deployment notes.
CORS configuration
The ALLOWED_ORIGIN secret controls which domains can call your Worker:
ALLOWED_ORIGIN = https://username.github.io
Setting it to * allows any domain to call your Worker. Use your specific
frontend domain in production to prevent others from using your proxy.
Security checklist
- API key stored as a Worker Secret (encrypted), not a plain variable
-
ALLOWED_ORIGINset to your specific frontend domain (not*) - Worker only accepts POST (or the method your API uses)
- Worker validates the request body before forwarding
- Worker URL is in your frontend as an env var, not hardcoded
What this does NOT protect against
- Someone hitting your Worker URL directly (anyone can POST to it if ALLOWED_ORIGIN is
*) - Rate abuse (add request counting logic if needed)
- The Worker URL being discovered from your frontend bundle (the URL is public — the key is not)
For higher-security scenarios: add an additional shared secret that the frontend sends and the Worker validates before proxying.
Output contract
Return the Worker source, secret names, allowed-origin policy, request/response shape, deployment steps, and a verification checklist. Never place a real key in source, logs, examples, or client code. Treat the proxy URL as public and state what abuse controls are still missing.
About
Built by Jamie Hill · OverKill Hill P³ Published at github.com/OKHP3 Part of the OKHP3/skillz Agent Skill library. MIT License -- free to use, fork, and adapt. A nod to the source is appreciated.