Convex GitHub
Sync GitHub issues and pull requests into Convex reactively. Open, comment on, and merge them directly from Convex functions.
git clone https://github.com/sholajegede/convex-github.git
cd convex-githubThis upstream package has not been adapted or qualified for Bijection. Read the original documentation before adopting it.
Description
Sync GitHub issues and pull requests into Convex reactively. Open, comment on, and merge them directly from Convex functions.
Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.
Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.
Getting started
- Get the source at the revision above and read its package documentation.
- Check the required Bijection SDK version, dependencies, and external service credentials.
- Follow the package setup instructions and the guide to using components.
- Exercise the component in your deployment according to your project’s verification rules.
Open source
Maintained source: sholajegede/convex-github. License: See repository. Consult the repository for license terms, notices, and contributor attribution.
From the upstream publisher
Sync GitHub issues and pull requests into Convex reactively, and open issues, comment, and merge PRs from Convex functions.
Explore the demo ↗Upstream overview
From the upstream package documentation.
convex-github maintains a live, queryable mirror of GitHub issues and pull requests inside an isolated Convex component schema, kept current via verified GitHub webhooks. It exposes actions for creating issues, posting comments, closing issues, and merging pull requests through the GitHub REST API, with changes written to Convex immediately rather than waiting for webhook round-trips. Webhook deliveries are deduplicated by X-GitHub-Delivery ID and verified via HMAC-SHA256, making retries safe.
Use cases
From the upstream package documentation.
- Building an internal developer dashboard that surfaces open issues and PRs reactively in a React UI without polling GitHub's API
- Automating issue triage or PR merges from Convex mutations triggered by your own application logic, such as closing a bug report when a user marks a problem resolved
- Auditing webhook delivery history for a repo by querying the stored webhookEvents table from within Convex functions
- Syncing GitHub project state into a Convex-backed app so useQuery re-renders immediately when issues are labeled, assigned, or closed on GitHub
- Bootstrapping a lightweight GitHub integration without standing up a separate webhook server or managing your own database schema for issues and PRs
How it works
From the upstream package documentation.
After installing the package, you register the component in convex/convex.config.ts with app.use(convexGithub), then instantiate a GitHub client with your token and webhook secret. The client exposes a webhookHandler httpAction that you mount on any HTTP route in convex/http.ts. GitHub delivers issues and pull_request events to that endpoint, which verifies the X-Hub-Signature-256 header, deduplicates by X-GitHub-Delivery, and upserts the current issue or PR state into the component's isolated tables.
The component schema includes three tables: issues, pullRequests, and webhookEvents. These live entirely within the component's own namespace and never appear in your app's convex/schema.ts. Issues are keyed by GitHub's globally unique numeric id (as a string) rather than by number, which remains stable across repo renames and transfers.
For write operations, the GitHub class provides actions: createIssue, createIssueComment, closeIssue, and mergePullRequest. These call the GitHub REST API and immediately patch the corresponding Convex record without waiting for the next webhook delivery. mergePullRequest accepts an optional mergeMethod parameter supporting merge, squash, or rebase strategies.
Read operations include listIssuesByRepo, listPullRequestsByRepo, getIssue, getPullRequest, and cross-repo helpers like listRecentIssues. These work from Convex queries, mutations, or actions and can be called directly from useQuery in a React component to get reactive updates whenever GitHub sends a webhook event.
Upstream package FAQ
Does convex-github add tables to my app's schema?
No. convex-github is a Convex component, so its issues, pullRequests, and webhookEvents tables live in an isolated schema namespace separate from your app's convex/schema.ts. You access them only through the functions the component exposes, and they will never collide with your own table names.
What happens if GitHub retries a webhook delivery that already succeeded?
convex-github records every delivery by its X-GitHub-Delivery header ID in the webhookEvents table before processing it. If a retry arrives with the same delivery ID, the handler recognizes it as a duplicate and skips the write, so no issue or pull request row is double-processed.
Which GitHub webhook events does convex-github handle?
The handler processes issues events (opened, edited, labeled, assigned, closed, reopened, and others) and pull_request events (opened, edited, closed, reopened, synchronize, and others). Other event types such as push, star, and release pass signature verification and idempotency recording but are otherwise ignored, so you can subscribe to additional events on the same webhook without needing a new endpoint.
What GitHub token scopes does convex-github require?
convex-github accepts a personal access token, a fine-grained token, or a GitHub App installation token. The token needs issues and pull_requests scopes on the repositories you want to manage. You set it as a Convex environment variable named GITHUB_TOKEN using npx convex env set.
How are issues and pull requests identified internally in convex-github?
Records are keyed by GitHub's globally unique numeric id field, stored as a string in issueId and pullRequestId fields. The issue or PR number is also stored but is only unique within a single repository, so convex-github uses the global id for indexing and deduplication while the number field is available for display and lookup by repo plus number.
Use with agents and CLI
Give your coding agent the component’s source identity and setup guidance.
Help me integrate Convex GitHub with Bijection.
Read: https://bijection.com/components/convex-github/SKILL.md
Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.Upstream README.md
View repository ↗Documentation from sholajegede/convex-github, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.
What this does
convex-github gives your Convex app a live, queryable mirror of GitHub issues and pull requests for the repos you care about, kept up to date by GitHub webhooks, plus a small set of actions for driving GitHub from your own backend logic:
- Reactive issue & PR tracking — every
issuesandpull_requestwebhook event updates a Convex table, souseQueryin your React app re-renders the instant something changes on GitHub. - Open, comment, close, merge — call
createIssue,createIssueComment,closeIssue, andmergePullRequestfrom Convex actions using the GitHub REST API. - Idempotent by delivery ID — every webhook delivery is recorded by its
X-GitHub-DeliveryID, so retried deliveries (GitHub retries failed webhooks) never double-process. - Signature verified — every inbound webhook is verified against
X-Hub-Signature-256with a constant-time HMAC-SHA256 comparison before anything is written.
This is a Convex component: its issues, pullRequests, and webhookEvents tables live in an isolated schema, not your app's schema, and are only reachable through the functions this component exposes.
Table of Contents
- Install
- Quick Start
- Setup
- Usage
- API Reference
- Type Reference
- Webhook Events
- Database Schema
- Example App
- Testing
- Limitations
- Troubleshooting
- Contributing
- Changelog
Install
npm install convex-github
Quick Start
1. Add the component
// convex/convex.config.ts
import { defineApp } from "convex/server";
import convexGithub from "convex-github/convex.config";
const app = defineApp();
app.use(convexGithub);
export default app;
2. Set environment variables
npx convex env set GITHUB_TOKEN ghp_...
npx convex env set GITHUB_WEBHOOK_SECRET whsec_...
GITHUB_TOKEN is a personal access token, a fine-grained token, or a GitHub App installation token with issues and pull_requests scopes on the repos you want to manage. GITHUB_WEBHOOK_SECRET is the secret you set on the webhook in step 4.
3. Mount the webhook handler
// convex/http.ts
import { httpRouter } from "convex/server";
import { components } from "./_generated/api";
import { GitHub } from "convex-github";
const github = new GitHub(components.convexGithub, {
token: process.env.GITHUB_TOKEN!,
webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
});
const http = httpRouter();
http.route({
path: "/webhooks/github",
method: "POST",
handler: github.webhookHandler,
});
export default http;
4. Register the webhook in GitHub
In your repo (or organization) settings, add a webhook pointing at https://<your-deployment>.convex.site/webhooks/github, content type application/json, with the same secret as GITHUB_WEBHOOK_SECRET. Subscribe to the Issues and Pull requests events.
5. Initialize the client
// convex/example.ts
import { action, query } from "./_generated/server";
import { components } from "./_generated/api";
import { GitHub } from "convex-github";
import { v } from "convex/values";
const github = new GitHub(components.convexGithub, {
token: process.env.GITHUB_TOKEN!,
webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
});
export const listIssuesByRepo = query({
args: { repo: v.string() },
handler: async (ctx, args) => {
return await github.listIssuesByRepo(ctx, args);
},
});
Setup
The component needs no schema changes in your app — its tables (issues, pullRequests, webhookEvents) live entirely inside the component's own isolated schema. All you need is the webhook mounted (step 3 above) and a GitHub client instance wherever you call its methods.
Repos are identified throughout by their "owner/repo" full name (e.g. "sholajegede/the-convex-reactor"), matching GitHub's own repository.full_name field, so there's no separate repo-registration step — the first webhook event or createIssue call for a repo is enough for it to start showing up in queries.
Usage
Create an issue
export const fileIssue = action({
args: { owner: v.string(), repo: v.string(), title: v.string(), body: v.optional(v.string()) },
handler: async (ctx, args) => {
return await github.createIssue(ctx, args);
},
});
Returns { number, url } and immediately records the issue in Convex — you don't have to wait for the webhook round-trip to see it in a query.
Comment on an issue
export const comment = action({
args: { owner: v.string(), repo: v.string(), issueNumber: v.number(), body: v.string() },
handler: async (ctx, args) => {
return await github.createIssueComment(ctx, args);
},
});
Close an issue
export const resolveIssue = action({
args: { owner: v.string(), repo: v.string(), issueNumber: v.number() },
handler: async (ctx, args) => {
await github.closeIssue(ctx, args);
return null;
},
});
closeIssue only patches the issue's state in Convex — it never overwrites the issue's stored title, labels, or url with stale data, so it's safe to call even if your local copy of those fields is out of date.
Merge a pull request
export const merge = action({
args: { owner: v.string(), repo: v.string(), pullNumber: v.number() },
handler: async (ctx, args) => {
await github.mergePullRequest(ctx, args);
return null;
},
});
Pass mergeMethod: "merge" | "squash" | "rebase" to control the merge strategy (defaults to "merge"). mergePullRequest looks the pull request up first to get its id (GitHub's merge response doesn't include it), merges it, then immediately patches its merged and state fields in Convex — like closeIssue, it doesn't wait for the pull_request webhook to arrive.
Read issues and pull requests reactively
const issues = useQuery(api.example.listIssuesByRepo, { repo: "sholajegede/the-convex-reactor" });
Every issues and pull_request webhook event patches or inserts a row, so this query re-renders live as issues are opened, labeled, assigned, or closed on GitHub — no polling.
API Reference
Actions (need ctx from an action)
| Method | Description |
|---|---|
createIssue(ctx, { owner, repo, title, body?, labels?, assignees? }) | Opens a new issue via the REST API and records it. Returns { number, url }. |
createIssueComment(ctx, { owner, repo, issueNumber, body }) | Posts a comment on an issue or PR. Returns { url }. |
closeIssue(ctx, { owner, repo, issueNumber }) | Closes an issue on GitHub and patches its stored state. |
mergePullRequest(ctx, { owner, repo, pullNumber, mergeMethod? }) | Merges a pull request via the REST API. |
Queries (work from actions, queries, or mutations)
| Method | Description |
|---|---|
getIssue(ctx, { issueId }) | Fetch one issue by its GitHub numeric ID (as a string). |
listIssuesByRepo(ctx, { repo, limit? }) | Most recently updated issues for a repo, newest first. |
getPullRequest(ctx, { pullRequestId }) | Fetch one pull request by its GitHub numeric ID (as a string). |
listPullRequestsByRepo(ctx, { repo, limit? }) | Most recently updated pull requests for a repo, newest first. |
getStats(ctx) | Counts of issues, pull requests, and webhook deliveries recorded so far. |
listRecentIssues(ctx, { limit? }) | Most recently updated issues across every repo, newest first. |
listRecentPullRequests(ctx, { limit? }) | Most recently updated pull requests across every repo, newest first. |
listRecentWebhookEvents(ctx, { limit? }) | Most recent raw webhook deliveries across every repo, newest first. |
Webhook
| Property | Description |
|---|---|
webhookHandler | An httpAction that verifies, deduplicates, and processes issues and pull_request webhook deliveries. Mount it at any route. |
Type Reference
type GitHubOptions = {
token: string; // personal access token, fine-grained token, or GitHub App installation token
webhookSecret: string; // the secret configured on the GitHub webhook
};
type CreateIssueArgs = {
owner: string;
repo: string;
title: string;
body?: string;
labels?: string[];
assignees?: string[];
};
type Issue = {
issueId: string; // GitHub's numeric issue id, as a string
repo: string; // "owner/repo"
number: number;
title: string;
state: "open" | "closed";
authorLogin?: string;
labels?: string[];
assignees?: string[];
url: string;
createdAt: number;
updatedAt: number;
};
type PullRequest = {
pullRequestId: string; // GitHub's numeric PR id, as a string
repo: string;
number: number;
title: string;
state: "open" | "closed";
merged: boolean;
authorLogin?: string;
headRef?: string;
baseRef?: string;
url: string;
createdAt: number;
updatedAt: number;
};
Webhook Events
The webhook handler subscribes to two GitHub event types (set these when creating the webhook):
issues— any action (opened,edited,labeled,assigned,closed,reopened, ...) upserts the issue's current state into theissuestable.pull_request— any action (opened,edited,closed,reopened,synchronize, ...) upserts the pull request's current state into thepullRequeststable, includingmerged.
Every delivery is checked against X-Hub-Signature-256 (HMAC-SHA256 of the raw request body, hex-encoded, prefixed sha256=) with a constant-time comparison, and deduplicated by X-GitHub-Delivery before any table write — GitHub's automatic webhook retries on timeout or 5xx are safe to receive twice.
Other event types (push, star, release, etc.) are accepted by the route but ignored after signature verification and idempotency recording, so you can subscribe to more events later without needing a new endpoint.
Database Schema
issues: {
issueId: string; // indexed: by_issueId
repo: string; // indexed: by_repo
number: number;
title: string;
state: "open" | "closed";
authorLogin?: string;
labels?: string[];
assignees?: string[];
url: string;
createdAt: number;
updatedAt: number;
}
pullRequests: {
pullRequestId: string; // indexed: by_pullRequestId
repo: string; // indexed: by_repo
number: number;
title: string;
state: "open" | "closed";
merged: boolean;
authorLogin?: string;
headRef?: string;
baseRef?: string;
url: string;
createdAt: number;
updatedAt: number;
}
webhookEvents: {
eventId: string; // indexed: by_eventId — GitHub's X-GitHub-Delivery
eventType: string; // X-GitHub-Event
repo?: string;
payload: string; // raw JSON body, for auditing/replay
receivedAt: number;
}
This schema lives entirely inside the component's isolated namespace — it will never collide with tables in your app's own convex/schema.ts. getStats, listRecentIssues, listRecentPullRequests, and listRecentWebhookEvents scan across every repo to power a dashboard-style view — they're not indexed by repo, so they're meant for demos and internal tooling, not high-volume production use.
Customer IDs
Issues and pull requests are keyed by GitHub's own numeric id field (converted to a string), not by number — number is only unique within a single repo, while id is globally unique and stable even if a repo is renamed or transferred. Always look issues and pull requests up by repo + number for UI purposes, but store and index on issueId/pullRequestId.
Example App
The example/ app is a full interactive demo, not just a form:
- Issues — open an issue (with suggestion chips), comment on it, and close it, all reactively reflected below.
- Pull Requests — merge an open pull request with your choice of merge method; Convex's copy updates immediately, it doesn't wait for the webhook.
- Webhooks — a live, expandable feed of every raw delivery to
/webhooks/github, across every repo, so you can watch signature verification and deduplication happen in real time. - History — every issue and pull request this component has ever recorded, newest first, across every repo, with a one-click "open follow-up issue" action.
- A repo switcher, with a "Connect your own repo" field so you can point the demo at any throwaway
owner/repoof your own, plus a sidebar Activity console logging every action call this demo makes, with its result or error.
Run it from the repo root (not example/):
npm install --legacy-peer-deps
npx convex env set GITHUB_TOKEN ghp_...
npx convex env set GITHUB_WEBHOOK_SECRET whsec_...
npm run dev
Then register a webhook on a scratch repo pointed at https://<your-dev-deployment>.convex.site/webhooks/github (see Quick Start) to see live deliveries land in the Webhooks tab.
Testing
npm run test
npm run typecheck
Tests use convex-test and cover recordIssue/recordPullRequest upsert behavior (including that closeIssue's updateIssueState path never blanks out an issue's title or url), updatePullRequestState marking a PR merged without touching its title, listIssuesByRepo/listPullRequestsByRepo scoping by repo, webhook idempotency via checkAndRecordEvent, and the cross-repo getStats/listRecentIssues/listRecentPullRequests/listRecentWebhookEvents queries.
Limitations
- Only
issuesandpull_requestwebhook events are processed; review, check-run, and deployment events are out of scope for this component. createIssue/createIssueComment/closeIssue/mergePullRequestuse the REST API (api.github.com), not GraphQL — for GraphQL-only features (e.g. Projects v2 fields) you'll need to call the GitHub GraphQL API directly.- The component does not manage webhook registration for you — you create the webhook once in GitHub's UI or via the REST API yourself.
- Rate limits are GitHub's own (5,000 requests/hour for most tokens, higher for GitHub Apps) — this component does not implement its own rate limiting or backoff.
Troubleshooting
Webhook returns 401 — the signature didn't match. Double check GITHUB_WEBHOOK_SECRET matches exactly what's configured on the GitHub webhook (not your GITHUB_TOKEN), and that GitHub is set to send application/json, not application/x-www-form-urlencoded.
Issues never appear in queries — confirm the webhook's "Recent Deliveries" tab in GitHub shows a 200 response, and that the webhook is subscribed to the Issues and Pull requests events specifically (not just "Just the push event").
closeIssue throws a 404 — the issue number must belong to the same owner/repo the token has access to; fine-grained tokens scoped to a different repo will fail here even if the issue ID exists in your Convex table from a webhook on another repo.
mergePullRequest throws a 404 before merging anything — that's the id lookup (GET .../pulls/{pullNumber}) that runs first, not the merge itself; same cause and fix as the closeIssue 404 above.
Contributing
See CONTRIBUTING.md.
Changelog
See CHANGELOG.md.