Skip to content

Webhooks & events

GitHub tells the world what happened to a repository through webhooks — POSTs to a URL on every push, pull request, release, and so on. Alchemy gives you two levels of abstraction: the Webhook resource manages the webhook itself and points it at any URL, and consumeRepositoryEvents subscribes a Cloudflare Worker to a repository with the webhook, routing, and signature verification handled for you.

GitHub.Webhook manages a repository webhook’s lifecycle — created on the first deploy, updated in place on subsequent deploys, deleted when the stack is destroyed:

import * as GitHub from "alchemy/GitHub";
yield* GitHub.Webhook("ci-webhook", {
owner: "my-org",
repository: "my-repo",
url: "https://example.com/github",
events: ["push", "pull_request"],
secret,
});

events defaults to ["push"]; pass ["*"] to receive every event GitHub emits. The secret (a Redacted string, e.g. from Config.redacted — see Secrets & env) makes GitHub sign each delivery with HMAC-SHA256 in the X-Hub-Signature-256 header so your receiver can verify the payload came from GitHub.

Credentials come from GitHub.providers() — env, stored PAT, or the gh CLI (see setup). The token needs repo scope (admin access to the repository) to manage webhooks.

url accepts an Output<string>, so the webhook can target a resource provisioned in the same stack:

const worker = yield* Cloudflare.Worker("Api", { ... });
yield* GitHub.Webhook("repo-webhook", {
owner: "my-org",
repository: "my-repo",
url: worker.url!,
events: ["*"],
});

At this level the receiving end is yours to build: parsing the payload, routing the delivery path, and verifying signatures. Most of the time you don’t want that — read on.

GitHub.consumeRepositoryEvents subscribes a Cloudflare Worker to a repository and replaces all the plumbing: it provisions the webhook pointing at the Worker, binds the signing secret onto it, claims the delivery path in fetch, verifies each delivery’s signature, and hands your handler a typed payload:

src/worker.ts
import * as Cloudflare from "alchemy/Cloudflare";
import * as GitHub from "alchemy/GitHub";
import * as Effect from "effect/Effect";
export default Cloudflare.Worker(
"Worker",
{ main: import.meta.url },
Effect.gen(function* () {
yield* GitHub.consumeRepositoryEvents(
{
owner: "my-org",
repository: "my-repo",
events: ["push", "pull_request"],
secret,
},
(event) => {
switch (event.name) {
case "push":
return Effect.log(`push to ${event.payload.ref}`);
case "pull_request":
return Effect.log(`PR: ${event.payload.pull_request.title}`);
}
},
);
// ...your own fetch handler is untouched
}).pipe(Effect.provide(Cloudflare.Workers.GitHubRepositoryEventSourceLive)),
);

event is Octokit’s EmitterWebhookEvent, narrowed to the events you selected — a switch on event.name narrows event.payload exhaustively. The stack provides both clouds’ providers: Layer.mergeAll(Cloudflare.providers(), GitHub.providers()).

For the full walkthrough — runtime layer, secret verification, deploy, and a release-blogging agent built on this event source — see React to GitHub events from a Worker.

Cloudflare.Workers.GitHubRepositoryEventSourceLive is currently the only implementation of the RepositoryEventSource contract — there is no AWS Lambda event source yet. On other hosts you can still provision a GitHub.Webhook directly with any delivery URL, but routing and signature verification are yours to implement.

Reference: