DEV Community

Doraemon — RelayHook
Doraemon — RelayHook

Posted on

How to Send GitHub Push Notifications to Discord Without Managing a Server

GitHub webhooks are great for repository automation, but getting useful push notifications into Discord often becomes more work than expected.

You need to receive the GitHub event securely, validate its signature, send it to Discord, and make sure a temporary network error does not silently lose the notification.

This tutorial shows a small, secure approach:

GitHub → Serverless verification bridge → RelayHook → Discord
Enter fullscreen mode Exit fullscreen mode

The bridge verifies that GitHub really sent the event. RelayHook then handles delivery, retries, delivery logs, and failed-message inspection.

There is no always-on server or VPS to maintain—just a small serverless route.

Why not point GitHub directly at RelayHook?

RelayHook production ingress endpoints require an X-API-Key header.

GitHub's native webhook configuration supports a payload URL, content type, secret, and event selection—but it does not let you add arbitrary request headers such as X-API-Key.

The solution is to use a small server-side bridge:

  1. GitHub sends the webhook to your bridge.
  2. The bridge verifies GitHub's X-Hub-Signature-256.
  3. The bridge adds the RelayHook API key safely from its environment variables.
  4. RelayHook delivers the event to your Discord webhook.

Your RelayHook API key never appears in GitHub settings, browser code, or repository source code.

What you need

Before starting, prepare:

  • A GitHub repository where you have admin access.
  • A Discord Incoming Webhook URL.
  • A RelayHook account and project.
  • A project API key and Ingress URL from RelayHook.
  • A serverless application. This example uses a Next.js App Router route and can be deployed on Vercel.

1. Configure RelayHook

Create a RelayHook project, then add your Discord Incoming Webhook URL as a Discord destination.

Next, copy two values from the project:

RELAYHOOK_INGRESS_URL
RELAYHOOK_API_KEY
Enter fullscreen mode Exit fullscreen mode

Keep the API key secret. It must only exist in server-side environment variables.

For this example, keep the project in the default Raw ingress mode. RelayHook will preserve the GitHub event envelope and deliver it as formatted JSON to Discord.

2. Add environment variables

In your Next.js project, add these variables locally and in your hosting provider:

GITHUB_WEBHOOK_SECRET=replace-with-a-long-random-secret
RELAYHOOK_INGRESS_URL=https://relay-hook-api.fluxhub.dev/v1/ingress/your-project-token
RELAYHOOK_API_KEY=rh_your_project_api_key
Enter fullscreen mode Exit fullscreen mode

Generate a long random value for GITHUB_WEBHOOK_SECRET. You will paste the exact same value into GitHub's Webhook Secret field later.

Do not expose any of these variables with a NEXT_PUBLIC_ prefix.

3. Create the verification bridge

Create this file:

app/api/github-webhook/route.ts
Enter fullscreen mode Exit fullscreen mode

Then add:

import { createHmac, timingSafeEqual } from "node:crypto";

function hasValidSignature(body: string, signature: string | null) {
  if (!signature) return false;

  const expected =
    "sha256=" +
    createHmac("sha256", process.env.GITHUB_WEBHOOK_SECRET!)
      .update(body)
      .digest("hex");

  const actualBuffer = Buffer.from(signature);
  const expectedBuffer = Buffer.from(expected);

  return (
    actualBuffer.length === expectedBuffer.length &&
    timingSafeEqual(actualBuffer, expectedBuffer)
  );
}

export async function POST(request: Request) {
  // GitHub signs the original request body.
  const rawBody = await request.text();

  const signature = request.headers.get("x-hub-signature-256");

  if (!hasValidSignature(rawBody, signature)) {
    return new Response("Invalid GitHub signature", { status: 401 });
  }

  const event = request.headers.get("x-github-event") ?? "unknown";

  // GitHub sends a ping as soon as a webhook is created.
  if (event === "ping") {
    return new Response("pong", { status: 200 });
  }

  const response = await fetch(process.env.RELAYHOOK_INGRESS_URL!, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": process.env.RELAYHOOK_API_KEY!,
    },
    body: JSON.stringify({
      source: "github",
      event,
      // Useful for log correlation and downstream deduplication.
      event_id: request.headers.get("x-github-delivery"),
      data: JSON.parse(rawBody),
    }),
  });

  return response.ok
    ? new Response("Accepted", { status: 200 })
    : new Response("RelayHook rejected the event", { status: 502 });
}
Enter fullscreen mode Exit fullscreen mode

This route does three important things:

  • Verifies the GitHub HMAC signature before trusting the request.
  • Keeps RelayHook credentials on the server.
  • Passes GitHub's delivery ID along with the event.

4. Deploy the route

Deploy your Next.js application to Vercel or another serverless platform.

After deployment, your endpoint will look similar to:

https://your-app.vercel.app/api/github-webhook
Enter fullscreen mode Exit fullscreen mode

Use HTTPS. GitHub webhooks should never target an unsecured endpoint.

5. Configure the GitHub Webhook

In your GitHub repository:

  1. Open SettingsWebhooksAdd webhook.
  2. Set Payload URL to your deployed bridge URL:
   https://your-app.vercel.app/api/github-webhook
Enter fullscreen mode Exit fullscreen mode
  1. Set Content type to:
   application/json
Enter fullscreen mode Exit fullscreen mode
  1. Paste your GITHUB_WEBHOOK_SECRET into Secret.
  2. Choose Let me select individual events.
  3. Enable Pushes.
  4. Save the webhook.

GitHub sends a ping event immediately. Your bridge returns 200 pong, so the GitHub delivery page should show a successful response.

6. Test it

Push a small commit:

git commit --allow-empty -m "Test GitHub webhook notification"
git push
Enter fullscreen mode Exit fullscreen mode

Then check each layer:

  1. GitHub

    Open the webhook's Recent Deliveries page and confirm a 2xx response.

  2. RelayHook

    Open Logs and confirm that an ingress event and a Discord delivery job were created.

  3. Discord

    Confirm that the GitHub push payload arrived in your target channel.

If Discord is temporarily unavailable, RelayHook keeps a durable delivery job, retries the request automatically, and records final failures in the dead-letter queue instead of silently losing the event.

Troubleshooting

GitHub gets a 401 response

The webhook secret and GITHUB_WEBHOOK_SECRET do not match, or the raw request body was modified before validation.

Always calculate the HMAC using the untouched await request.text() body.

GitHub ping succeeds but push events do not arrive

Check that:

  • The webhook is active.
  • The Pushes event is enabled.
  • The deployed route has the latest environment variables.
  • Your deployment logs show incoming requests.

RelayHook rejects the event

Make sure RELAYHOOK_API_KEY belongs to the same RelayHook project as RELAYHOOK_INGRESS_URL.

Discord delivery fails

Open the failed delivery job in RelayHook Logs. A common cause is a revoked or deleted Discord Incoming Webhook URL.

A note about duplicate delivery

Webhook delivery is generally at least once, not exactly once.

GitHub may redeliver events, and a reliable delivery system may retry a request after a temporary failure. If your downstream action must happen exactly once, use GitHub's X-GitHub-Delivery value (sent here as event_id) to deduplicate it in your own application.

Wrap-up

A small serverless bridge is enough to securely connect GitHub to RelayHook:

GitHub verifies → RelayHook persists → Discord receives
Enter fullscreen mode Exit fullscreen mode

RelayHook gives you a central place to fan out events to Discord, Slack, Telegram, or email—with delivery logs, retries, and a dead-letter queue.

Try RelayHook here:

https://relay-hook-app.fluxhub.dev/?utm_source=devto&utm_medium=article&utm_campaign=github_discord

Top comments (0)