Building For Good: How Civil Society Organizations Are Automating On Cloudflare

If you’re reading this, you’re probably the de facto IT department for your family, your friends, and maybe even your local community. As homelabbers, we love spinning up virtual machines, configuring reverse proxies, and tweaking Docker containers until our power meters spin. But today, I want to talk about taking those skills and applying them to a slightly different arena: Civil Society Organizations (CSOs).

Civil society organizations are the non-profits, mutual aid groups, community pantries, and local charities that keep our neighborhoods functioning. They do incredible work, but they usually operate on shoestring budgets held together by duct tape and goodwill. When they need software, they often resort to clunky spreadsheets, expensive SaaS subscriptions they can barely afford, or worse—nothing at all.

But what if we could build them automated, highly available, and secure tools for practically zero cost? Enter Cloudflare.

While we usually talk about racking servers in our basements, Cloudflare’s developer ecosystem—specifically Workers, Pages, D1, and Zero Trust—offers a unique opportunity to build enterprise-grade infrastructure for local CSOs without racking up a massive AWS bill or maintaining a fragile on-prem server that goes down when the power blinks.

Let’s look at why Cloudflare is the perfect platform for this, and then we’ll walk through a practical, real-world example of building an automated system for a community pantry.

Why Cloudflare for Civil Society?

When you’re building for a non-profit, your priorities shift. High availability and low maintenance become more important than having total hardware control. Here’s why Cloudflare’s ecosystem is a perfect match:

  • Generous Free Tiers: Cloudflare Workers gives you 100,000 requests per day for free. D1 (their SQLite database) gives you 5 million rows read and 100,000 rows written per day for free. For a local non-profit, you will likely never exceed these limits.
  • Global Edge Performance: Code runs close to the user. If a volunteer is logging hours from their phone in the park, the request is processed at the nearest edge data center, making the app feel instantaneous.
  • Zero Server Maintenance: No patching OS vulnerabilities, no renewing Let’s Encrypt certificates, no worrying about a failing hard drive at 2 AM. You write the code, you deploy it, and you’re done.
  • Built-in Zero Trust Security: Cloudflare Access allows you to put authentication in front of any application. You can restrict an admin dashboard to specific email addresses or IP ranges without writing a single line of auth code.

The Homelabber’s Role

You might be thinking, “If there are no servers to manage, how is this a homelab project?”

The truth is, homelabbing isn’t just about hardware. It’s about the skills—networking, scripting, database management, and system architecture. CSOs don’t have the budget to hire a developer. But they do have access to you. By volunteering a few hours of your time to architect and deploy a Cloudflare-based solution, you can give a local organization tools that would normally cost them thousands of dollars a year.

Practical Walkthrough: The Community Pantry Volunteer Logger

Let’s build something real. Imagine a local food bank that relies on volunteers. They need a simple web app where volunteers can log their hours on their way out. At the end of every day, the pantry manager needs an automated summary of who volunteered and for how long, sent directly to their email or a private Discord channel.

We are going to build this using:

  • Cloudflare D1: To store the volunteer logs.
  • Cloudflare Workers: To handle the API logic and run a daily automated cron job.
  • Cloudflare Pages: To host the simple HTML/JS frontend.
  • Cloudflare Access: To secure the admin dashboard.

Step 1: Prerequisites and Database Setup

First, ensure you have Node.js installed and the Cloudflare CLI tool, Wrangler. Open your terminal and log in:

1
2
3
npm create cloudflare@latest volunteer-logger
cd volunteer-logger
npx wrangler login

Let’s create a D1 database to store our volunteer logs.

1
npx wrangler d1 create volunteer_db

Wrangler will output a database ID. Copy that ID, as we need to add it to our wrangler.toml configuration file. Open wrangler.toml and update it to look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
name = "volunteer-logger"
main = "src/index.js"
compatibility_date = "2023-10-30"

[[d1_databases]]
binding = "DB"
database_name = "volunteer_db"
database_id = "YOUR_DATABASE_ID_HERE"

[triggers]
# Run the cron job every day at 5:00 PM UTC
crons = ["0 17 * * *"]

Notice the [triggers] section. This is our automation. We are telling Cloudflare to trigger our Worker at 5:00 PM UTC every day.

Now, let’s create the database schema. Create a file named schema.sql:

1
2
3
4
5
6
CREATE TABLE IF NOT EXISTS logs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    volunteer_name TEXT NOT NULL,
    hours_logged REAL NOT NULL,
    timestamp TEXT DEFAULT (datetime('now'))
);

Apply this schema to your D1 database:

1
npx wrangler d1 execute volunteer_db --file=./schema.sql

Step 2: Writing the Worker Logic

Now for the fun part. We need our Worker to do two things:

  1. Accept POST requests to log new volunteer hours.
  2. Handle the scheduled event (our cron trigger) to summarize the day’s logs and send a webhook.

Open src/index.js and let’s write the code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
export default {
  // Handle HTTP requests (from the frontend)
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    // Handle CORS for our frontend
    const corsHeaders = {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    };

    if (request.method === 'OPTIONS') {
      return new Response(null, { headers: corsHeaders });
    }

    // Route: Log a new volunteer
    if (url.pathname === '/api/log' && request.method === 'POST') {
      try {
        const { volunteer_name, hours_logged } = await request.json();
        
        if (!volunteer_name || !hours_logged) {
          return new Response('Missing data', { status: 400, headers: corsHeaders });
        }

        await env.DB.prepare(
          "INSERT INTO logs (volunteer_name, hours_logged) VALUES (?, ?)"
        ).bind(volunteer_name, hours_logged).run();

        return new Response('Logged successfully!', { status: 200, headers: corsHeaders });
      } catch (err) {
        return new Response('Error logging hours', { status: 500, headers: corsHeaders });
      }
    }

    // Route: Get all logs (for admin dashboard)
    if (url.pathname === '/api/logs' && request.method === 'GET') {
      const { results } = await env.DB.prepare(
        "SELECT * FROM logs ORDER BY timestamp DESC LIMIT 100"
      ).all();
      
      return new Response(JSON.stringify(results), {
        headers: { 'Content-Type': 'application/json', ...corsHeaders }
      });
    }

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

  // Handle the scheduled cron job (automation)
  async scheduled(event, env, ctx) {
    // Get today's logs
    const { results } = await env.DB.prepare(
      "SELECT volunteer_name, hours_logged FROM logs WHERE date(timestamp) = date('now')"
    ).all();

    if (results.length === 0) {
      return; // No volunteers today, do nothing
    }

    // Build a summary message
    let summary = "Today's Volunteer Summary:\n";
    let totalHours = 0;
    
    results.forEach(log => {
      summary += `- ${log.volunteer_name}: ${log.hours_logged} hours\n`;
      totalHours += log.hours_logged;
    });
    
    summary += `\nTotal Hours Today: ${totalHours}`;

    // Send the summary to a Discord webhook (or Slack, or an email API)
    // You would store this webhook URL in a Worker Secret
    const webhookUrl = env.DISCORD_WEBHOOK_URL;
    
    if (webhookUrl) {
      await fetch(webhookUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ content: summary })
      });
    }
  }
};

This code is straightforward. The fetch handler manages our API routes. The scheduled handler runs automatically based on the cron we set in wrangler.toml. It queries the database, tallies the hours, and fires off a webhook to notify the pantry manager.

Before deploying, add your webhook URL as a secret so it isn’t hardcoded:

1
npx wrangler secret put DISCORD_WEBHOOK_URL

Deploy the worker:

1
npx wrangler deploy

Step 3: The Frontend (Cloudflare Pages)

We need a simple interface for volunteers. You can build this with basic HTML and vanilla JavaScript. Create an index.html file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Volunteer Logger</title>
</head>
<body>
    <h2>Log Your Hours</h2>
    <form id="logForm">
        <input type="text" id="name" placeholder="Your Name" required><br><br>
        <input type="number" id="hours" placeholder="Hours" step="0.5" required><br><br>
        <button type="submit">Submit</button>
    </form>
    <p id="status"></p>

    <script>
        // Replace with your Worker URL
        const API_URL = 'https://volunteer-logger.YOUR_SUBDOMAIN.workers.dev';

        document.getElementById('logForm').addEventListener('submit', async (e) => {
            e.preventDefault();
            const name = document.getElementById('name').value;
            const hours = document.getElementById('hours').value;

            const res = await fetch(`${API_URL}/api/log`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ volunteer_name: name, hours_logged: parseFloat(hours) })
            });

            if (res.ok) {
                document.getElementById('status').innerText = 'Thank you for your time!';
                document.getElementById('logForm').reset();
            } else {
                document.getElementById('status').innerText = 'Something went wrong.';
            }
        });
    </script>
</body>
</html>

You can deploy this directly to Cloudflare Pages by connecting a GitHub repository, or by using the direct upload CLI:

1
npx wrangler pages deploy . --project-name=volunteer-frontend

Now the food bank has a fast, globally distributed web app where volunteers can log their hours from their phones.

Step 4: Securing the Admin Dashboard with Zero Trust

Right now, anyone can hit the /api/logs endpoint and see who volunteered. For a small food bank, that might be fine, but privacy is important. Instead of writing a complex login system, we can use Cloudflare Access (Zero Trust) to protect a separate admin route.

If you build out a separate admin page (e.g., admin.html) that calls the /api/logs endpoint, you can wrap that specific page or API route in Cloudflare Access.

  1. Go to your Cloudflare Dashboard and navigate to Zero Trust.
  2. Go to Access > Applications and click Add an application.
  3. Select Self-hosted.
  4. Set the application domain to your Pages domain (e.g., volunteer-frontend.pages.dev/admin.html).
  5. Create a policy that only allows specific email addresses (like the food bank manager’s email).

Now, when someone tries to visit the admin page, they will be prompted to log in via Google, GitHub, or an email OTP. Cloudflare validates their identity at the edge before the request ever reaches your Pages deployment. It takes five minutes to set up and provides enterprise-grade security for free.

Why This Beats a Traditional Homelab Setup

Don’t get me wrong—I love a good self-hosted Proxmox cluster. But if you set this up on a VPS or a Raspberry Pi in the back office of a community center, what happens when the power goes out? What happens when the SD card corrupts? Who updates the OS?

By leveraging Cloudflare’s edge platform, you are giving civil society organizations a tool with 100% uptime, automatic scaling, and zero maintenance. You get to be the architect and the hero, without becoming the permanent, on-call sysadmin for a non-profit that can’t afford to pay you.

Summary

Civil society organizations do the heavy lifting in our communities, but they are often left behind when it comes to modern technology. By bridging our homelab skills with platforms like Cloudflare, we can build them automated, secure, and resilient tools for free.

In our walkthrough, we built a volunteer logging system that:

  • Uses D1 to store data locally and efficiently.
  • Uses Workers to process API requests at the edge.
  • Uses Cron Triggers to automate daily summary reports.
  • Uses Pages to host a fast, simple frontend.
  • Uses Zero Trust to secure administrative access.

Call to Action

I challenge you to look around your local community. Is there a mutual aid group, an animal shelter, or a community garden that is struggling with spreadsheets and manual data entry? Reach out to them. Offer to build them a simple tool. It might only take you a weekend, but it could save them hundreds of hours a year.

If you end up building something for a local organization using Cloudflare, homelab tools, or local AI, I want to hear about it! Drop a comment below or tag us on social media @SelfHostedTech. Let’s use our tech skills to build for good. Until next time, keep tinkering!