GitHub Apps · Bot Architecture · 2026

How GitHub Apps Work

The complete mental model for building a bot you can @mention on an issue to create a PR or review one — app identity, the three tokens, installations, webhooks, and the full server architecture.

0 / 6 sections
1
Why a GitHub App (and not a PAT or OAuth App)?
Three ways to call the GitHub API — only one gives your bot its own identity

There are three ways your server can act on GitHub. A personal access token is a human's credential — everything the bot does shows up as you, and a leak exposes everything you can touch. An OAuth App can only ever act as a signed-in user, with coarse scopes and long-lived tokens. A GitHub App is a first-class actor: it has its own identity (prbot[bot]), fine-grained permissions, per-repository installation, short-lived tokens, and built-in webhooks.

For your use case — a bot users can tag on an issue to open or review PRs — a GitHub App is the only good fit: comments and PRs are attributed to the bot, not to you or the user, and each customer grants it access to only their chosen repos.
Demo — pick an actor, compare the blast radius of a leaked credential
Token lifetime
Repo reach
Permission grain
Click an actor card to see its leak blast radius.
Your bot comments on an issue. With a GitHub App, who does the comment appear to come from?
2
The Three Tokens
App JWT · installation access token (ghs_) · user access token (ghu_)

Everything in a GitHub App reduces to which of three credentials you're holding. Mixing them up is the #1 source of confusion.

A JWT you sign yourself with the app's RSA private key (downloaded once when you register the app). It proves "I am app #12345". Max lifetime 10 minutes (exp claim), iss = your app ID. It can only call app-management endpoints — list installations, mint tokens. It cannot touch repo content.

// payload of the JWT you sign with the private key (RS256)
{ "iat": 1718100000, "exp": 1718100600, // ≤ 10 min
  "iss": "12345" } // your App ID

The workhorse. Exchange your JWT at POST /app/installations/{installation_id}/access_tokens to get a ghs_… token that lives 1 hour and is scoped to that installation's repos and permissions. This is the token your bot uses to create branches, open PRs, and post reviews. Activity is attributed to yourapp[bot].

POST /app/installations/42183077/access_tokens
Authorization: Bearer <your JWT>

// → { "token": "ghs_abc…", "expires_at": "+1h" }

For "user signs in on my server". Standard web flow: redirect to github.com/login/oauth/authorize, exchange the callback code at /login/oauth/access_token. You get a ghu_… token (8 hours) plus a ghr_… refresh token (6 months). Its effective permission is the intersection of what the app may do and what the user may do — the app can never escalate past the user.

// 1. redirect the browser
https://github.com/login/oauth/authorize?client_id=…&state=…
// 2. exchange code server-side
POST https://github.com/login/oauth/access_token
// → { "access_token": "ghu_…", "refresh_token": "ghr_…" }
Demo — mint the bot's credentials in order
🔏 App JWT
eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiIxMjM0NSJ9.sig…
expires in ≤ 10 min · proves app identity · app-management endpoints only
🎫 Installation access token
ghs_16C7e42F292c6912E7710c838347Ae178B4a
expires in 1 hour · scoped to installation 42183077 · this is what the bot acts with
Your server holds the app's private key. Start by signing a JWT.
Your bot needs to open a PR on a customer's repo. Which credential goes in the Authorization header?
3
Installation: How Your Bot Gets Access to a Repo
Users install the app on chosen repos — you store the installation_id

Registering the app gives it an identity, but zero access. Access comes when a user installs the app on their account or org and picks all repositories or only select repositories. GitHub reviews the app's requested permissions (e.g. Contents: read & write, Pull requests: read & write, Issues: read & write) at install time.

Each install creates an installation with a numeric installation_id. Your server learns it from the installation.created webhook (or GET /repos/{owner}/{repo}/installation) and should persist the mapping account → installation_id → repos — it's the key you'll need every time you mint a ghs_ token.

Demo — install "prbot" like a customer would
prbot requests: Contents: R/W Pull requests: R/W Issues: R/W · subscribe: issue_comment pull_request
Choose repos, then install.
Store it. If you don't persist installation_id when the webhook arrives, you can still recover it later via GET /repos/{owner}/{repo}/installation (authenticated with your JWT) — but your webhook handler shouldn't need a lookup on the hot path: every event payload from an installed repo carries installation.id.
A customer installs your app on 3 of their 40 repos. What can your bot reach?
4
Webhooks: Hearing the @Mention
issue_comment events, HMAC signature verification, finding the trigger

Your app subscribes to events at registration time. For an @mention bot you want issue_comment (fires for comments on both issues and PRs — PRs are issues underneath) and pull_request. When someone comments, GitHub POSTs a JSON payload to your webhook URL.

Two things matter in the handler:

1. Verify the signature. GitHub sends X-Hub-Signature-256: sha256=…, an HMAC-SHA256 of the raw body using your webhook secret. Compute it yourself and compare with a constant-time check — otherwise anyone who finds your URL can puppet your bot.

2. Respond fast, work async. Reply 2xx within ~10 seconds and push the real work (cloning, reviewing, PR creation) onto a queue.

Demo — post a comment, inspect what hits your server
Click a comment to deliver its issue_comment webhook.
How does your server know a webhook really came from GitHub and not an attacker?
5
Acting: Create a PR, Review a PR
From webhook to bot-authored pull request or review

Once the @mention is detected and verified, the worker mints a ghs_ token for payload.installation.id and drives the REST API. Everything it does appears as prbot[bot].

Demo — play the create-PR pipeline, click any step to inspect
Click a step to inspect it.

For "review their PRs": pull the diff, generate feedback, then submit one review with inline comments.

// 1. what changed?
GET /repos/acme-corp/web-app/pulls/88/files

// 2. submit the review — body + inline comments in one shot
POST /repos/acme-corp/web-app/pulls/88/reviews
{ "event": "COMMENT",  // or REQUEST_CHANGES / APPROVE
  "body": "Overall solid — two issues below.",
  "comments": [{ "path": "src/auth.ts", "line": 42,
                  "body": "This token is logged in plaintext." }] }
GitHub blocks APPROVE/REQUEST_CHANGES on a PR authored by the same identity — your bot can't formally approve its own PRs, and many orgs restrict bot approvals. COMMENT reviews always work.
The webhook arrived 50 minutes ago and sat in a slow queue. The worker's earlier ghs_ token now fails with 401. Why?
6
The Full Architecture of Your Service
User sign-in + installation + @mention → PR, end to end

Putting it together, your service uses two of the three tokens for two different jobs: the ghu_ user token to know who is signing in on your site (and gate who may trigger the bot), and ghs_ installation tokens for everything the bot does. The JWT exists only to mint the latter.

  1. Onboard — user signs in via the app's OAuth flow (ghu_), then installs the app on their repos (you store installation_id).
  2. Trigger — user comments @prbot review; GitHub delivers an issue_comment webhook.
  3. Authorize — verify the HMAC signature, then check your rules: is comment.user a registered customer / repo collaborator?
  4. Act — worker mints a ghs_ token from installation.id, creates the PR or posts the review, and replies on the thread.
Hover the boxes below for each component's responsibilities, then hit Play full flow.
Demo — your service, end to end
🧑‍💻
User
signs in · installs · @mentions
Authenticates on your site via the app's OAuth web flow, installs the app on their repos, and triggers work by commenting @prbot … on an issue or PR.
🐙
GitHub
events · identity
Hosts the repos, runs the OAuth flow, enforces installation permissions, and pushes signed webhook events (issue_comment, pull_request) to your server.
🖥️
Your Server
webhook + auth + queue
Verifies X-Hub-Signature-256, parses the mention, checks the commenter against your user DB, responds 200 immediately, and enqueues a job. Holds the app's private key + webhook secret.
⚙️
Worker
mints ghs_ · calls API
Pops the job, signs a JWT, exchanges it for an installation token (ghs_, 1h), then creates branches/PRs or posts reviews as prbot[bot].
Hover a component, or play the flow.
Don't skip the authorize step. Anyone who can comment on a public repo's issues can type @prbot create a PR. The signature check proves the event came from GitHub — it says nothing about whether the commenter should command your bot. Gate on your own user records (that's what the sign-in on your server is for) and/or check their repo permission via GET /repos/{o}/{r}/collaborators/{user}/permission.
In this architecture, what is the user's ghu_ OAuth token actually used for?

🤖 You can build prbot now

Register the app → users sign in (ghu_) and install it (installation_id) → verify webhooks → mint ghs_ → ship PRs and reviews as your bot.

Learning Reference · GitHub Apps Documentation · Authenticating with a GitHub App