← All packages

Developer tools · Convex component

convex-github

Sync GitHub issues and pull requests into Convex reactively, and open issues, comment, and merge PRs from Convex functions.

npm install convex-github
23downloads last week
862downloads last 12 months
v0.0.6latest, Sep 18, 2026
6releases since Sep 13, 2026

Downloads

Download history

Loading download history…

Build this

Build with convex-github

The install and setup steps from the package documentation.

  1. 1

    Install

    sh
    npm install convex-github
  2. 2

    Quick Start

    1. Add the component

    ts
    // 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

    sh
    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

    ts
    // 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

    ts
    // 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);
      },
    });
  3. 3

    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.

  4. 4

    Usage

    Create an issue

    ts
    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

    ts
    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

    ts
    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

    ts
    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

    tsx
    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.