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
Sign in with open-git

Sign in with open-git

Put a “Sign in with open-git” button on your site. The user consents, you receive a stable user id (sub), and you create or log in your customer.

This does not let you read or write repositories. After they install your bot, use an installation token for that.

Register an OAuth app

In the organization that owns the bot: Organization settings → Developer → New OAuth app.

You need:

  • A name (shown on the consent screen)
  • At least one redirect URI, for example https://your-app.com/oauth/open-git/callback
  • Homepage URL (optional)

Creating a bot with a Setup URL or Redirect URI also creates a linked OAuth app, unless you attach one you already have. The registered callback is the Redirect URI if you set one, otherwise the Setup URL. Use that client for connect-after-install. Saving the bot again adds that callback to the client’s redirect list and leaves any extra callbacks in place. For a callback that is not the Setup URL, set Redirect URI on the bot — that is not the webhook URL.

Store client_id and client_secret. The secret is shown once; rotate it from the same page if you lose it. The create-bot dump names them OPEN_GIT_OAUTH_CLIENT_ID and OPEN_GIT_OAUTH_CLIENT_SECRET.

const OPEN_GIT_URL = process.env.OPEN_GIT_URL ?? "https://open-git.com"
const OPEN_GIT_OAUTH_CLIENT_ID = process.env.OPEN_GIT_OAUTH_CLIENT_ID!
const OPEN_GIT_OAUTH_CLIENT_SECRET = process.env.OPEN_GIT_OAUTH_CLIENT_SECRET!
const REDIRECT_URI = "https://your-app.com/oauth/open-git/callback"

The redirect URI you send in the authorize request must match one you registered, character for character. A mismatch returns you to open-git with invalid_redirect and a visible error — it is not the webhook URL.

Allowed identity scopes: openid, profile, email.

Start the sign-in (send them to open-git)

@opengit/sdk discovers the endpoints, builds PKCE (S256), and returns the authorize URL. You still persist state + code_verifier (and installation_id if they came from an install). That store is yours — a cookie, session, or short-lived row, one-time use.

Discovery reads /.well-known/openid-configuration. If those three endpoints are missing, the SDK falls back to /api/auth/oauth2/authorize, /api/auth/oauth2/token, and /api/auth/oauth2/userinfo. /.well-known/oauth-authorization-server is also published if you want to read it yourself.

import { startOpenGitOAuth } from "@opengit/sdk"

export async function beginSignIn(options: {
  installationId?: string | null
}) {
  const started = await startOpenGitOAuth({
    clientId: OPEN_GIT_OAUTH_CLIENT_ID,
    origin: OPEN_GIT_URL,
    redirectUri: REDIRECT_URI,
  })

  await saveOAuthPending(started.state, {
    codeVerifier: started.codeVerifier,
    installationId: options.installationId ?? null,
  })

  return started.url
}

Standalone button:

app.get("/sign-in/open-git", async (_req, res) => {
  res.redirect(await beginSignIn({}))
})

Setup URL after install (https://your-app.com/connect?installation_id=…):

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

The user signs in (or creates an account), then sees your app name and the identity scopes. Consent copy says this does not grant repository access. A disabled client, or a client linked to an unpublished bot, shows “This application is unavailable.”

Redirect URI (the callback)

open-git sends them back to your registered redirect:

https://your-app.com/oauth/open-git/callback?code=…&state=…

Verify state, then let the SDK exchange code and load userinfo. Create or look up your customer by sub. If you stored an installation_id with that state, attach it to the customer.

import { exchangeOpenGitCode } from "@opengit/sdk"

export async function handleOpenGitCallback(request: Request) {
  const url = new URL(request.url)
  const code = url.searchParams.get("code")
  const state = url.searchParams.get("state")
  const oauthError = url.searchParams.get("error")

  if (oauthError) {
    throw new Error(url.searchParams.get("error_description") ?? oauthError)
  }
  if (!code || !state) {
    throw new Error("Missing code or state.")
  }

  const pending = await takeOAuthPending(state)
  if (!pending) {
    throw new Error("OAuth state was missing or already used.")
  }

  const { user } = await exchangeOpenGitCode({
    clientId: OPEN_GIT_OAUTH_CLIENT_ID,
    clientSecret: OPEN_GIT_OAUTH_CLIENT_SECRET,
    code,
    codeVerifier: pending.codeVerifier,
    origin: OPEN_GIT_URL,
    redirectUri: REDIRECT_URI,
  })

  const customer = await upsertCustomer({
    openGitUserId: user.sub,
    email: user.email ?? null,
    username: user.username ?? user.preferred_username ?? null,
    installationId: pending.installationId,
  })

  return customer
}

A Next.js App Router route at app/oauth/open-git/callback/route.ts:

import { NextResponse } from "next/server"

export async function GET(request: Request) {
  try {
    const customer = await handleOpenGitCallback(request)
    return NextResponse.redirect(
      new URL(`/dashboard?customer=${customer.id}`, request.url)
    )
  } catch (error) {
    const message = error instanceof Error ? error.message : "Sign-in failed"
    return NextResponse.redirect(
      new URL(`/sign-in?error=${encodeURIComponent(message)}`, request.url)
    )
  }
}
Claim Meaning
sub Stable account id from userinfo. Store this. Do not key off username.
username / preferred_username Current handle, added when profile is granted. Can change.
email Requested with the email scope. Treat as optional; key the customer on sub.

saveOAuthPending / takeOAuthPending / upsertCustomer are yours — a cookie, Redis key, or database row keyed by state, deleted after one use.

Two ways people arrive

They start on your site. Sign in with open-git → you have a customer → later they install the bot → your webhook includes installation_id → you attach it to the customer you already know.

They start on the marketplace. They install first. If One install + Sign in screen is on, that page grants bot access and identity together. We send them to your Setup URL with ?installation_id=… unless this install is already connected. Start the same OAuth flow from your origin (PKCE has to live on your app). The second consent page is skipped. Save sub + installation_id. Optionally link so Connections can show “connected”.

If they install and never finish Sign in, the grant still exists. You simply do not have a customer yet — do not run paid work until you do.

What this token is not

The OAuth access token is identity only. It cannot list pull requests, post checks, or comment. Mint an installation token for that.

Users can revoke the consent under Settings → Connections → Authorized applications. Revoke access does not uninstall the bot. If the OAuth app is tied to an install they can manage, we clear Connected (external_customer_id) on those installs. Installation tokens and webhooks keep working until they uninstall. They can also choose Revoke and uninstall, which revokes the consent and uninstalls every install of that bot they can manage — you get installation.deleted for each, the same as a normal uninstall. If they revoke, stop treating them as signed in on your side.