Skip to content

ADR-20: GitHub App Installation Schema

🇰🇷 한국어 버전

DateAuthorRepos
2026-01-19@specvitalinfra, web

Context

Problem

OAuth App authentication requires organization admin approval, preventing regular users from accessing organization repositories. Additionally:

IssueImpact
Organization admin approval requirementRegular users cannot access org repositories without admin intervention
Long-lived OAuth token storageSecurity risk from persistent credentials in database
Shared rate limitsSingle 5000/hr pool per user exhausted quickly with multiple repos
Background processing dependencyWorkers require stored user tokens for private repo access

Requirements

  1. Organization repository access without user membership requirement
  2. Independent rate limits per installation (5000/hr each)
  3. Secure token handling - minimize credential storage
  4. Background worker support without long-lived stored tokens
  5. Webhook-based installation lifecycle tracking

Relation to ADR-09

This schema implements the "Installation Store" component from ADR-09: GitHub App Integration Strategy. ADR-09 established the integration strategy; this ADR defines the specific schema design that realizes that strategy.

Decision

Store installation metadata only (installation_id, account_type, account_id) without access tokens. Generate short-lived installation tokens on-demand via JWT authentication with the GitHub App's private key.

Schema Design

sql
CREATE TYPE "public"."github_account_type" AS ENUM ('organization', 'user');

CREATE TABLE "public"."github_app_installations" (
  "id" uuid NOT NULL DEFAULT gen_random_uuid(),
  "installation_id" bigint NOT NULL,
  "account_type" "public"."github_account_type" NOT NULL,
  "account_id" bigint NOT NULL,
  "account_login" character varying(255) NOT NULL,
  "account_avatar_url" text NULL,
  "installer_user_id" uuid NULL,
  "suspended_at" timestamptz NULL,
  "created_at" timestamptz NOT NULL DEFAULT now(),
  "updated_at" timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY ("id"),
  CONSTRAINT "uq_github_app_installations_account" UNIQUE ("account_type", "account_id"),
  CONSTRAINT "uq_github_app_installations_installation_id" UNIQUE ("installation_id"),
  CONSTRAINT "fk_github_app_installations_installer" FOREIGN KEY ("installer_user_id")
    REFERENCES "public"."users" ("id") ON DELETE SET NULL
);

CREATE INDEX "idx_github_app_installations_installer"
  ON "public"."github_app_installations" ("installer_user_id")
  WHERE (installer_user_id IS NOT NULL);

Column Specification

ColumnTypePurpose
idUUIDInternal primary key
installation_idBIGINT UNIQUEGitHub's installation identifier
account_typeENUM'organization' or 'user'
account_idBIGINTGitHub account ID
account_loginVARCHAR(255)GitHub username/org name (display)
account_avatar_urlTEXTAvatar URL (nullable)
installer_user_idUUID FK (nullable)User who initiated installation
suspended_atTIMESTAMPTZSuspension timestamp (null = active)

Key Design Decisions

  1. No token storage: Tokens generated on-demand via JWT, never persisted
  2. PostgreSQL enum for account_type: Type safety over VARCHAR
  3. Dual unique constraints: Both installation_id and (account_type, account_id)
  4. Partial index on installer: Optimize "my installations" queries without indexing nulls
  5. Soft suspension: suspended_at timestamp vs boolean for audit trail

Token Generation Flow

installation_id → JWT signed with private key → GitHub API → 1-hour access token

Options Considered

Option A: Store installation_id Only (Selected)

Generate access tokens on-demand using JWT + private key.

ProsCons
Zero credential storage - eliminates token exfiltration riskToken generation latency (~50-200ms per request)
Always-fresh tokens (no staleness/refresh logic)Private key management complexity
Aligns with GitHub security best practicesRequires in-memory caching for performance

Option B: Store Full access_token

Store generated tokens with expiry, refresh before use.

ProsCons
No generation latency for cached tokensStored credentials vulnerable to database breach
Simpler code path for token retrievalToken refresh logic and expiry monitoring required
Stale token handling edge cases

Rejected: Security risk from stored credentials outweighs latency benefits. GitHub explicitly recommends against storing installation access tokens.

Option C: Extend Users Table

Add installation columns directly to the users table.

ProsCons
No new table, simpler schemaCannot support organization installations (no user row)
Natural user-installation relationshipOne installation per user limitation
Conflates user identity with installation identity

Rejected: Organization installations have no corresponding user row; separate entity required.

Consequences

Positive

AreaBenefit
SecurityNo long-lived credentials stored; short-lived tokens limit breach impact
ScalabilityIndependent 5000/hr rate limit per installation
Organization supportFirst-class support without user membership requirement
Webhook alignmentSchema maps directly to GitHub lifecycle events (install/delete/suspend)

Negative

AreaTrade-offMitigation
LatencyToken generation adds ~50-200msIn-memory cache with ~55-minute TTL
OperationalPrivate key becomes critical secretKey vault storage, rotation procedures
UXAdditional installation step post-authClear onboarding flow, installation prompt

Technical Implications

  • Database Design: PostgreSQL enum for type safety; partial index optimizes common query pattern
  • Cross-service Coordination: Web service tracks installations, Worker requests tokens via installation_id
  • Webhook Processing: Events (installation.created, installation.deleted, installation.suspend) map to table operations

Implementation Files

specvital/infra

FilePurpose
db/schema/migrations/20251226154124_github_app_installations.sqlTable creation migration

specvital/web

FilePurpose
internal/client/github_app.goGitHubAppClient with JWT token generation
modules/github-app/domain/entity/installation.goInstallation domain entity
modules/github-app/domain/port/repository.goInstallationRepository port
modules/github-app/adapter/repository_postgres.goPostgreSQL repository adapter
modules/github-app/handler/http.goWebhook handler
modules/github-app/usecase/get_installation_token.goToken generation use case

References

Open-source test coverage insights