Skip to main content

Overview

The Ahh CLI webhook server provides a complete solution for receiving and inspecting HTTP webhooks during development. It automatically creates a local server, tunnels it to the internet, and provides a beautiful web dashboard for viewing incoming requests in real-time.

How It Works

When you run ahh webhook, the CLI:
  1. Starts a local Elysia server on the configured port (default: varies by config)
  2. Creates a Cloudflare tunnel to expose the server publicly
  3. Generates a secure authentication token
  4. Opens a web dashboard at cli.ahh.bet for viewing requests
  5. Streams all incoming HTTP requests to the dashboard via WebSocket
All webhook data is processed locally and streamed only to your authenticated browser session. No data is stored on remote servers.

Basic Usage

Start the Webhook Server

ahh webhook
This will:
  • Start a local webhook server
  • Create a public tunnel URL
  • Open your browser to the dashboard

Example Output

Starting tunnel, this may take a second...
Webhook URL https://abc-xyz-123.trycloudflare.com

Architecture

Server Implementation

The webhook server is built on Elysia with WebSocket support:
export async function createWebhookServer(port: number) {
  const token = randomUUID();
  const wsClients = new Set<ElysiaWS>();

  const server = new Elysia()
    .use(cors({ origin: /cli\\.ahh\\.bet$/ }))
    .get("/ws", () => {
      return "OK";
    })
    .all("*", (ctx) => {
      const path = new URL(ctx.request.url).pathname;
      const logData = {
        id: randomUUID(),
        method: ctx.request.method,
        path: path,
        timestamp: new Date().toISOString(),
        headers: filterHeaders(ctx.headers),
        query: ctx.query,
        body: ctx.body || "",
      };

      wsClients.forEach((ws) => {
        if (ws.readyState === 1) {
          ws.send(JSON.stringify(logData));
        }
      });
      return "OK";
    })
    .listen(port);

  return { token, port, kill: () => server.stop(true) };
}
Reference: /home/daytona/workspace/source/ahh-binary/src/commands/webhook/main.ts:22-69

Header Filtering

To keep the display clean, certain proxy and CDN headers are filtered out:
function filterHeaders(headers: Record<string, string | undefined>) {
  const result: Record<string, string | undefined> = {};
  Object.entries(headers).forEach(([key, value]) => {
    if (
      key.toLowerCase().startsWith("cf-") ||
      key.toLowerCase().startsWith("x-forwarded") ||
      key.toLowerCase() === "cdn-loop"
    ) {
      return;
    }
    result[key] = value;
  });

  return result;
}
Reference: /home/daytona/workspace/source/ahh-binary/src/commands/webhook/main.ts:6-20 This filters out:
  • Cloudflare headers (cf-*)
  • Forwarding headers (x-forwarded-*)
  • CDN loop detection headers

WebSocket Authentication

The server uses a UUID token for secure WebSocket connections:
.ws("/ws", {
  message(ws, message) {
    if (token !== message) {
      ws.terminate();
      return;
    }
    wsClients.add(ws);
    ws.send('"OK"');
  },
  close(ws) {
    wsClients.delete(ws);
  },
})
Reference: /home/daytona/workspace/source/ahh-binary/src/commands/webhook/main.ts:50-62

Request Logging

Captured Data

Each incoming request captures:
const logData = {
  id: randomUUID(),           // Unique request identifier
  method: ctx.request.method, // HTTP method (GET, POST, etc.)
  path: path,                 // Request path
  timestamp: new Date().toISOString(), // ISO 8601 timestamp
  headers: filterHeaders(ctx.headers), // Filtered headers
  query: ctx.query,           // Query parameters
  body: ctx.body || "",      // Request body
};

Real-Time Streaming

Requests are broadcast to all connected WebSocket clients:
wsClients.forEach((ws) => {
  if (ws.readyState === 1) {
    ws.send(JSON.stringify(logData));
  }
});

Use Cases

GitHub Webhooks

Test repository events like pushes, pull requests, and issue comments without deploying.

Payment Integration

Debug payment webhooks from Stripe, PayPal, or other payment processors.

API Development

Inspect incoming API requests to understand client behavior and debug issues.

Third-Party Services

Receive webhooks from Twilio, SendGrid, or any other service that sends HTTP callbacks.

Dashboard Features

The web dashboard provides:
  • Real-time updates: See requests as they arrive
  • Request details: View method, path, headers, query params, and body
  • Timestamp tracking: Know exactly when each request was received
  • Formatted JSON: Pretty-printed JSON bodies for easy reading
  • Multiple requests: View and compare multiple requests side-by-side

Advanced Usage

Custom Port Configuration

Configure the default webhook port in your config file:
// The server reads from config
const { port, token } = await createWebhookServer(
  (await getConfig()).DEFAULT_WEBHOOK_HTTP_PORT
);

Programmatic Usage

Use the webhook server in your own scripts:
import { createWebhookServer } from './src/commands/webhook/main';
import { tunnel } from './src/commands/tunnel/main';

const { port, token, kill } = await createWebhookServer(3000);
const { url } = await tunnel(port);

console.log('Webhook URL:', url);
console.log('Auth token:', token);

// Later, cleanup
kill();

Security Considerations

The webhook server is designed for development use. Always use proper authentication and validation in production.

CORS Configuration

The server only allows WebSocket connections from the official dashboard:
.use(cors({ origin: /cli\\.ahh\\.bet$/ }))

Token-Based Auth

WebSocket connections require the exact token generated at server startup:
if (token !== message) {
  ws.terminate();
  return;
}

Public Exposure

Be aware that:
  • The webhook URL is publicly accessible
  • Anyone with the URL can send requests
  • All requests are logged and visible in your dashboard
  • Don’t use for sensitive production data

Integration Examples

  1. Run ahh webhook
  2. Copy the webhook URL
  3. Go to your GitHub repo Settings → Webhooks
  4. Add webhook with your URL
  5. Select events you want to receive
  6. Save and test the webhook
You’ll see the test payload in your dashboard immediately.

Troubleshooting

The CLI tries to automatically open the dashboard. If it doesn’t work:
  1. Copy the Webhook URL from the terminal
  2. Manually visit https://cli.ahh.bet/webhook
  3. Paste your webhook URL and token when prompted
Check that:
  • The webhook URL is correct and accessible
  • Your browser has WebSocket connections enabled
  • You’re not behind a firewall blocking WebSockets
  • The sender is actually making requests (check sender logs)
Ensure:
  • You’re using the correct authentication token
  • The dashboard domain is cli.ahh.bet
  • Your network allows WebSocket connections

Best Practices

  1. One webhook server at a time - Avoid running multiple instances to prevent port conflicts
  2. Clean up when done - Stop the server when you’re finished testing
  3. Verify signatures - When implementing real webhooks, always verify cryptographic signatures
  4. Use HTTPS - The tunnel provides HTTPS automatically, ensuring secure transmission
  5. Test error cases - Use the webhook server to test how your code handles malformed or unexpected payloads
  • Tunneling - Learn more about the underlying tunnel technology
  • File Serving - Serve static files with similar tunneling

Technical Reference

Return Value

The createWebhookServer function returns:
{
  token: string;      // UUID for WebSocket authentication
  port: number;       // Local port the server is listening on
  kill: () => void;   // Function to stop the server
}

Dependencies

  • Elysia: Fast web framework
  • @elysiajs/cors: CORS middleware
  • WebSocket: For real-time request streaming
  • Cloudflare tunnel: For public URL generation

Build docs developers (and LLMs) love