Cloudflare Workers: The 20-Minute Solution to Our 72-Hour Azure Nightmare

by Alien Brain Trust AI Learning
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:

  1. Workers & Pages → learn-labs-enrollment → Settings
  2. Variables and Secrets → Add
  3. Add AIRTABLE_API_KEY (encrypt)
  4. 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

AspectAzure Static Web AppsCloudflare Workers
Time to working endpoint72+ hours (failed)20 minutes
Configuration files6+2
Lines of config100+10
Deployment complexityGitHub Actions + Portal + TokensOne CLI command
CORS setup4 different locations, none worked3 lines of code
Secret managementKey Vault + Managed IdentityDashboard UI
Error messagesCrypticClear
DocumentationSprawling, inconsistentFocused, accurate

Lessons for Choosing Infrastructure

  1. Start with the simplest option. We chose Azure because we knew it. We should have asked “what’s the minimum viable infrastructure?”

  2. Complexity compounds. Every abstraction layer (Functions, Static Web Apps, managed deployments) adds potential failure points.

  3. 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.

  4. Test the deploy pipeline first. Before writing business logic, confirm you can deploy a “hello world” and see it change.

  5. 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).