Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/cloudflare/agents/llms.txt

Use this file to discover all available pages before exploring further.

A real-time GitHub repository activity monitor built with Cloudflare Agents. Demonstrates how to handle webhooks with Agents, verify signatures, store events in SQLite, and stream updates to connected clients.

What it demonstrates

  • Webhook Handling - Receive and process GitHub webhooks
  • Signature Verification - HMAC-SHA256 verification of webhook payloads
  • Agent-per-Repository - Each repo gets its own isolated agent instance
  • Real-time Updates - WebSocket connection streams events as they arrive
  • Event History - Events stored in SQLite for persistence
  • Beautiful Dashboard - Dark-themed UI with live event feed

Architecture

GitHub → POST /webhooks/github/owner/repo → Worker → RepoAgent (Durable Object)

Browser ← WebSocket ← Agent broadcasts state updates ←─────┘

Server Implementation

src/server.ts
import { Agent, callable, getAgentByName, routeAgentRequest } from "agents";
import type { GitHubWebhookPayload, GitHubEventType, StoredEvent } from "./github-types";

export type RepoState = {
  repoFullName: string;
  stats: {
    stars: number;
    forks: number;
    openIssues: number;
  };
  lastUpdated: string | null;
  webhookConfigured: boolean;
};

export class RepoAgent extends Agent<Env, RepoState> {
  initialState: RepoState = {
    repoFullName: "",
    stats: { stars: 0, forks: 0, openIssues: 0 },
    lastUpdated: null,
    webhookConfigured: false
  };

  async onStart(): Promise<void> {
    // Initialize the events table
    this.sql`
      CREATE TABLE IF NOT EXISTS events (
        id TEXT PRIMARY KEY,
        type TEXT NOT NULL,
        action TEXT,
        title TEXT NOT NULL,
        description TEXT,
        url TEXT,
        actor_login TEXT,
        actor_avatar TEXT,
        timestamp TEXT NOT NULL
      )
    `;

    this.sql`
      CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp DESC)
    `;
  }

  async onRequest(request: Request): Promise<Response> {
    if (request.method !== "POST") {
      return new Response("Method not allowed", { status: 405 });
    }

    const eventType = request.headers.get("X-GitHub-Event") as GitHubEventType;
    if (!eventType) {
      return new Response("Missing X-GitHub-Event header", { status: 400 });
    }

    // Verify the signature
    const signature = request.headers.get("X-Hub-Signature-256");
    const body = await request.text();

    if (this.env.GITHUB_WEBHOOK_SECRET) {
      const isValid = await this.verifySignature(
        body,
        signature,
        this.env.GITHUB_WEBHOOK_SECRET
      );
      if (!isValid) {
        return new Response("Invalid signature", { status: 401 });
      }
    }

    // Parse and process the payload
    const payload = JSON.parse(body) as GitHubWebhookPayload;
    await this.processWebhook(eventType, payload);

    return new Response("OK", { status: 200 });
  }

  private async verifySignature(
    payload: string,
    signature: string | null,
    secret: string
  ): Promise<boolean> {
    if (!signature) return false;

    const encoder = new TextEncoder();
    const key = await crypto.subtle.importKey(
      "raw",
      encoder.encode(secret),
      { name: "HMAC", hash: "SHA-256" },
      false,
      ["sign"]
    );

    const signatureBytes = await crypto.subtle.sign(
      "HMAC",
      key,
      encoder.encode(payload)
    );

    const expectedSignature = `sha256=${Array.from(
      new Uint8Array(signatureBytes)
    )
      .map((b) => b.toString(16).padStart(2, "0"))
      .join("")}`;

    return signature === expectedSignature;
  }

  private async processWebhook(
    eventType: GitHubEventType,
    payload: GitHubWebhookPayload
  ): Promise<void> {
    const repo = payload.repository;
    if (!repo) return;

    // Update stats from repository data
    this.setState({
      ...this.state,
      repoFullName: repo.full_name,
      stats: {
        stars: repo.stargazers_count,
        forks: repo.forks_count,
        openIssues: repo.open_issues_count
      },
      lastUpdated: new Date().toISOString(),
      webhookConfigured: true
    });

    // Create and store the event
    const event = this.createEvent(eventType, payload);
    if (event) {
      this.sql`
        INSERT OR REPLACE INTO events 
        (id, type, action, title, description, url, actor_login, actor_avatar, timestamp)
        VALUES (${event.id}, ${event.type}, ${event.action || null}, 
                ${event.title}, ${event.description}, ${event.url}, 
                ${event.actor.login}, ${event.actor.avatar_url}, ${event.timestamp})
      `;

      // Cleanup old events (keep last 100)
      this.sql`
        DELETE FROM events WHERE id NOT IN (
          SELECT id FROM events ORDER BY timestamp DESC LIMIT 100
        )
      `;
    }
  }

  @callable()
  getEvents(limit = 20): StoredEvent[] {
    const rows = [
      ...this.sql<{
        id: string;
        type: string;
        action: string | null;
        title: string;
        description: string;
        url: string;
        actor_login: string;
        actor_avatar: string;
        timestamp: string;
      }>`SELECT * FROM events ORDER BY timestamp DESC LIMIT ${limit}`
    ];

    return rows.map((row) => ({
      id: row.id,
      type: row.type as GitHubEventType,
      action: row.action || undefined,
      title: row.title,
      description: row.description,
      url: row.url,
      actor: {
        login: row.actor_login,
        avatar_url: row.actor_avatar
      },
      timestamp: row.timestamp
    }));
  }
}

// Worker entry point
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    // Webhook endpoint: POST /webhooks/github/:owner/:repo
    if (
      url.pathname.startsWith("/webhooks/github/") &&
      request.method === "POST"
    ) {
      const clonedRequest = request.clone();
      const payload = (await clonedRequest.json()) as {
        repository?: { full_name?: string };
      };

      const repoFullName = payload.repository?.full_name;
      if (!repoFullName) {
        return new Response("Missing repository in payload", { status: 400 });
      }

      // Get the agent for this specific repository
      const agentName = sanitizeRepoName(repoFullName);
      const agent = await getAgentByName(env.RepoAgent, agentName);

      return agent.fetch(request);
    }

    // Default agent routing for WebSocket connections
    return (
      (await routeAgentRequest(request, env)) ||
      new Response("Not found", { status: 404 })
    );
  }
} satisfies ExportedHandler<Env>;

function sanitizeRepoName(fullName: string): string {
  return fullName
    .toLowerCase()
    .replace(/\//g, "-")
    .replace(/[^a-z0-9-]/g, "");
}

How It Works

1

GitHub sends webhook

When an event occurs (push, PR, issue, etc.), GitHub POSTs to /webhooks/github/owner/repo with a signed payload.
2

Worker routes to agent

The Worker extracts the repository name and routes to the appropriate RepoAgent Durable Object (one per repo).
3

Agent verifies signature

The agent verifies the HMAC-SHA256 signature using the webhook secret to prevent spoofing.
4

Event stored in SQLite

The agent parses the event, updates repository stats, and stores the event in SQLite for persistence.
5

State broadcast to clients

The agent’s state is automatically broadcast to all connected WebSocket clients, updating the UI in real-time.

Supported Events

Event TypeDescription
pushCommits pushed to a branch
pull_requestPR opened, closed, merged, etc.
issuesIssue opened, closed, labeled, etc.
issue_commentComment on an issue or PR
starRepository starred/unstarred
forkRepository forked
releaseRelease published
pingWebhook configured

Setup Instructions

1

Install dependencies

npm install
2

Configure webhook secret

Copy .dev.vars.example to .dev.vars:
cp .dev.vars.example .dev.vars
Edit .dev.vars:
GITHUB_WEBHOOK_SECRET=your-secret-here
3

Start development server

npm start
4

Expose local server

Since GitHub needs to reach your webhook endpoint, use ngrok:
ngrok http 5173
Copy the ngrok URL (e.g., https://abc123.ngrok.io).
5

Configure GitHub webhook

  1. Go to your GitHub repository → SettingsWebhooks
  2. Click Add webhook
  3. Configure:
    • Payload URL: https://your-ngrok-url.ngrok.io/webhooks/github/owner/repo
    • Content type: application/json
    • Secret: Same value as GITHUB_WEBHOOK_SECRET
    • Events: Select which events to receive
  4. Click Add webhook
6

Connect to your repo

Open http://localhost:5173, enter your repository name (e.g., cloudflare/agents), and click Connect.

Key Patterns

Webhook Routing

const agentName = sanitizeRepoName(payload.repository.full_name);
const agent = await getAgentByName(env.RepoAgent, agentName);
return agent.fetch(request);
Each repository gets its own agent instance, identified by the sanitized repo name.

Signature Verification

const key = await crypto.subtle.importKey(
  "raw",
  secret,
  { name: "HMAC", hash: "SHA-256" },
  false,
  ["sign"]
);
const signature = await crypto.subtle.sign("HMAC", key, payload);
GitHub signs every webhook with HMAC-SHA256. Always verify signatures to prevent spoofing.

Event Storage in SQLite

this.sql`INSERT INTO events (id, type, title, ...) VALUES (...)`;
Events are stored in SQLite and automatically persist across hibernation.

Real-time State Broadcasting

When setState() is called, the new state is automatically broadcast to all connected clients via WebSocket.

Deployment

npm run deploy
After deploying:
  1. Set the webhook secret in Cloudflare:
    wrangler secret put GITHUB_WEBHOOK_SECRET
    
  2. Update your GitHub webhook URL to your deployed worker URL:
    https://your-worker.workers.dev/webhooks/github/owner/repo
    

Extending This Example

Ideas for enhancements:
  • AI PR Summaries - Use OpenAI to summarize PR diffs
  • Slack Notifications - Forward important events to Slack
  • Multi-Repo Dashboard - Monitor all your repos in one view
  • Custom Alerts - Schedule reminders for stale PRs
  • Webhook Replay - Re-send events for testing

Email Agent

Process emails with secure routing

x402 Payments

HTTP payment gating with verification

Workflows

Multi-step workflows with approval gates

Webhooks Guide

In-depth guide to webhook handling

Build docs developers (and LLMs) love