Cloudflare Workers: The 20-Minute Solution to Our 72-Hour Azure Nightmare
Cloudflare Workers: The 20-Minute Solution to Our 72-Hour Azure Nightmare
Meta Description: 72 hours with Azure, 0 working endpoints. 20 minutes with Cloudflare Workers, fully functional API. Here’s the exact implementation.
After three days of Azure Static Web Apps failures, we asked the obvious question we should have asked on day one: what’s the simplest possible solution?
20 minutes later, we had a working enrollment API on Cloudflare Workers.
The Setup (5 Minutes)
Step 1: Install Wrangler CLI
npm install -g wrangler
Step 2: Login to Cloudflare
wrangler login
This opens a browser, you click authorize, done.
Step 3: Create Project Structure
workers/enrollment/
├── wrangler.toml
└── src/
└── index.js
wrangler.toml:
name = "learn-labs-enrollment"
main = "src/index.js"
compatibility_date = "2024-01-01"
[vars]
AIRTABLE_BASE_ID = "your-base-id-here"
That’s the entire configuration file. Compare this to Azure’s staticwebapp.config.json, host.json, function.json, workflow YAML, and portal settings.
The Code (10 Minutes)
Here’s the complete worker:
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
export default {
async fetch(request, env) {
// Handle CORS preflight
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: corsHeaders });
}
// Only allow POST
if (request.method !== 'POST') {
return new Response(JSON.stringify({ error: 'Method not allowed' }), {
status: 405,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
try {
const { email, name, github_username, company_role, referral_source } = await request.json();
// Validate required fields
if (!email || !name || !github_username) {
return new Response(JSON.stringify({
error: 'Missing required fields',
required: ['email', 'name', 'github_username']
}), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
// Submit to Airtable
const airtableResponse = await fetch(
`https://api.airtable.com/v0/${env.AIRTABLE_BASE_ID}/Learn%20Labs%20Enrollments`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${env.AIRTABLE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
fields: {
'Email Address': email,
'Name': name,
'GitHub User Name': github_username,
'Company or Role': company_role || '',
'Referral Source': referral_source || '',
'Status': 'New'
}
})
}
);
if (!airtableResponse.ok) {
const errorText = await airtableResponse.text();
return new Response(JSON.stringify({ error: 'Failed to save enrollment', details: errorText }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
const airtableData = await airtableResponse.json();
// Invite to GitHub repository
let githubInviteSuccess = false;
if (env.GITHUB_TOKEN) {
const githubResponse = await fetch(
`https://api.github.com/repos/base-bit/secure-prompt-vault/collaborators/${github_username}`,
{
method: 'PUT',
headers: {
'Authorization': `Bearer ${env.GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'Learn-Labs-Enrollment'
},
body: JSON.stringify({ permission: 'pull' })
}
);
githubInviteSuccess = githubResponse.ok || githubResponse.status === 201 || githubResponse.status === 204;
}
return new Response(JSON.stringify({
success: true,
message: 'Enrollment submitted successfully',
enrollment_id: airtableData.id,
github_invite_sent: githubInviteSuccess
}), {
status: 200,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
} catch (error) {
return new Response(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
}
};
~100 lines. All the logic in one file. CORS headers that actually work.
The Deploy (2 Minutes)
cd workers/enrollment
wrangler deploy
Output:
Uploaded learn-labs-enrollment (3.73 sec)
Deployed learn-labs-enrollment triggers (1.35 sec)
https://learn-labs-enrollment.base-b71.workers.dev
That’s it. Working endpoint. Globally distributed. Automatic HTTPS.
Adding Secrets (3 Minutes)
In the Cloudflare dashboard:
- Workers & Pages → learn-labs-enrollment → Settings
- Variables and Secrets → Add
- Add
AIRTABLE_API_KEY(encrypt) - Add
GITHUB_TOKEN(encrypt)
No Key Vault. No Managed Identity. No IAM roles. Just encrypted secrets in a UI that makes sense.
Why Cloudflare Workers Won
CORS Actually Works
In Cloudflare Workers, you control the response. You set headers, they appear in the response. There’s no middleware, no proxy, no hidden configuration layer overriding your settings.
return new Response(body, {
headers: { 'Access-Control-Allow-Origin': '*' }
});
That header will be in the response. Guaranteed.
Deployment Is Deterministic
When you run wrangler deploy, the code you deployed is the code that runs. There’s no:
- Build pipeline that might use cached artifacts
- Deployment tokens that might not match
- Portal settings that override workflow settings
- Mystery “content servers” rejecting requests
Error Messages Are Useful
When something breaks, Cloudflare tells you what broke:
INVALID_VALUE_FOR_COLUMN: Cannot parse value "testuser" for field GitHub User Name
Compare to Azure’s:
The content server has rejected the request with: BadRequest
The Free Tier Is Actually Free
Cloudflare Workers free tier:
- 100,000 requests/day
- 10ms CPU time per request
- Unlimited deployments
- Global distribution
For our enrollment form (maybe 100 requests/day max), this is years of free hosting.
The Final Comparison
| Aspect | Azure Static Web Apps | Cloudflare Workers |
|---|---|---|
| Time to working endpoint | 72+ hours (failed) | 20 minutes |
| Configuration files | 6+ | 2 |
| Lines of config | 100+ | 10 |
| Deployment complexity | GitHub Actions + Portal + Tokens | One CLI command |
| CORS setup | 4 different locations, none worked | 3 lines of code |
| Secret management | Key Vault + Managed Identity | Dashboard UI |
| Error messages | Cryptic | Clear |
| Documentation | Sprawling, inconsistent | Focused, accurate |
Lessons for Choosing Infrastructure
-
Start with the simplest option. We chose Azure because we knew it. We should have asked “what’s the minimum viable infrastructure?”
-
Complexity compounds. Every abstraction layer (Functions, Static Web Apps, managed deployments) adds potential failure points.
-
Monolithic beats microservices for small things. One file with all the logic is easier to debug than a function framework with config files and implicit behaviors.
-
Test the deploy pipeline first. Before writing business logic, confirm you can deploy a “hello world” and see it change.
-
Don’t fight the tool. After the first day of Azure issues, we should have switched. Instead, we kept trying to make a broken tool work.
When Azure Static Web Apps Might Make Sense
To be fair, Azure Static Web Apps probably works for:
- Greenfield projects where Azure creates the entire repo/workflow
- Teams already deep in Azure ecosystem with existing IAM
- Apps that need built-in auth (Azure AD integration)
- Projects where you want everything in one portal
But for a simple API endpoint on an existing GitHub Pages site? Overkill. Way too much overkill.
Tomorrow: The broader lessons about choosing the right tool for the job (and recognizing when you’ve chosen wrong).