Home

madison / open-git

public
Overview Code History Branches Pull requestsIssuesInsights Campfire Docs
main
HomeOverview Code PRsIssues

Docs

Markdown docs from docs.

DocsAPI
Pages
Bot marketplace (moved)Dashboard list "table" patternGitHub two-way syncHow install worksUninstall, unpublish, and billingSign in with open-gitCreate a botSDK and a working exampleReceive webhooksOrganizations and Repository RBACUI Component Audit
How install works

How install works

A user grants your bot some repositories and the permissions you requested. That grant is the install. It is not a collaborator seat and not a personal access token.

What the user does

  1. Opens /integrations or your listing URL (/integrations/{your-org}/{your-bot}).
  2. Clicks Install, picks one account, then repositories they administer on that account. Personal and org grants are separate installs. Mixed accounts in one submit are rejected.
  3. If you set a valid Setup URL, the browser is sent there with ?installation_id=…. Otherwise they go to Settings → Connections.

If One install + Sign in screen is on (and the bot has a linked OAuth app), that install page also lists Sign in with open-git identity scopes. Continuing records OAuth consent at the same time. It does not log them into your product — your Setup URL still starts the authorize request so you can store PKCE and receive tokens. The second consent page is skipped because consent already exists. Turn the toggle off if the bot does not need a customer identity.

If this account already installed you, we add the newly selected repos instead of creating a second install. You get installation.repos_changed, not installation.created. The original installer is not overwritten. If that install is already connected (external_customer_id), we skip Setup URL and send them to Connections instead — identity is already linked.

They can change the repo set later, or uninstall, from:

  • Settings → Connections (/settings/account/connections)
  • /integrations/installations/{id}
  • Organization settings → Integrations (org-owned repos)
  • /{owner}/{repo}/settings/integrations (Remove from this repo — if other repos remain, the installation id stays the same and you get installation.repos_changed with the remaining ids. Removing the last repo uninstalls and sends installation.deleted.)

A repo admin can remove your bot from their repo even if they cannot manage the whole install. Removing the last repo uninstalls. Uninstall from Connections or Manage also revokes every remaining repo at once.

Connect (the button on Connections and the manage page) is shown when the install is active, the bot is still published, and the Setup URL is a valid absolute URL. “Connected” means you wrote an external_customer_id via link.

What you should do

When the webhook arrives, or when the browser hits Setup URL (or when you GET the installation):

  1. If you do not know this person, send them through Sign in with open-git.
  2. Save open_git_user_id (sub) and installation_id on your customer.
  3. Optionally tell open-git they are connected (below).
  4. Start handling pull-request events for that install.

If they never finish connect or never pay, the install still exists. You choose not to run. Do not expect open-git to disable it for you.

type StoredInstall = {
  id: string
  repositoryIds: string[]
  openGitUserId: string | null
  entitled: boolean // your flag — open-git does not store this
}

// From the webhook (you may not know the user yet)
async function upsertInstallFromWebhook(payload: {
  installation_id?: string
  repositories?: unknown
}) {
  if (typeof payload.installation_id !== "string") {
    return
  }
  // Install events send repository UUIDs, not owner/name.
  const repositoryIds = Array.isArray(payload.repositories)
    ? payload.repositories.filter((id): id is string => typeof id === "string")
    : []
  await db.installs.upsert({
    id: payload.installation_id,
    repositoryIds,
  })
}

// After Sign in with open-git succeeds
async function attachCustomerToInstall(options: {
  installationId: string | null
  openGitUserId: string
}) {
  if (!options.installationId) {
    return
  }
  await db.installs.update(options.installationId, {
    openGitUserId: options.openGitUserId,
  })
  await db.customers.upsert({
    openGitUserId: options.openGitUserId,
    installationId: options.installationId,
  })
}

db.installs / db.customers are your store.

Show Connected on open-git

After you know the customer, you can write an id back so Connections shows “connected”:

import { createBotAppClient } from "@opengit/sdk"

async function botApp(installationId: string) {
  return createBotAppClient({
    baseUrl: process.env.OPEN_GIT_URL,
    botId: process.env.OPEN_GIT_BOT_ID!,
    installationId,
    privateKey: process.env.OPEN_GIT_BOT_PRIVATE_KEY!.replace(/\\n/g, "\n"),
  })
}

async function markConnected(installationId: string, customerId: string) {
  const app = await botApp(installationId)
  await app.linkInstallation(installationId, customerId)
}

async function markDisconnected(installationId: string) {
  const app = await botApp(installationId)
  await app.linkInstallation(installationId, null)
}

Same request by hand:

POST /api/v1/app/installations/{id}/link
Authorization: Bearer <bot JWT or ogi_ token for this install>
Content-Type: application/json

{ "external_customer_id": "cus_123" }

external_customer_id must be a string or null. Send null to clear it. This is a label in our UI, not a billing switch. See Calling the API.

Read the current grant without waiting on a webhook:

GET /api/v1/app/installations/{id}
Authorization: Bearer <bot JWT or ogi_ token for this install>

app.getInstallation(id) returns repositories as { id, owner, name }.

Connect is just your Setup URL plus installation_id again — the same OAuth hop as after a fresh install.

app.get("/connect", async (req, res) => {
  const installationId =
    typeof req.query.installation_id === "string"
      ? req.query.installation_id
      : null
  res.redirect(await beginSignIn({ installationId }))
})

Forks and deleted repos

A fork is a new repository. Installing on the source repo does not grant the fork. The user must install you there if they want you on the fork.

If they delete a granted repository, we notify you with installation.repos_changed and the remaining repository ids. Drop the deleted one from your store.

if (event === "installation.repos_changed") {
  await db.installs.update(payload.installation_id, {
    repositoryIds: payload.repositories,
  })
}

if (event === "installation.deleted") {
  await db.installs.markDeleted(payload.installation_id)
}

installation.deleted includes installation_id only (plus action).

After they uninstall

You receive installation.deleted. Stop calling the API for that id. History (old comments and checks) stays on the pull request.