Automate Contributor License Agreement workflows for your GitHub repos.
CLA Bot is a self-hostable GitHub App that manages Contributor License Agreements for organizations and personal accounts. It enforces CLA compliance on pull requests, lets contributors sign and re-sign agreements, and gives admins full control over CLA text, versioning, and bypass lists.
- Automated PR enforcement — GitHub checks and comments block merging until the CLA is signed
- CLA versioning — text changes are tracked by SHA-256 hash; contributors are prompted to re-sign when the CLA is updated
- Admin dashboard — manage CLA text with live markdown preview, view signing history, configure bypass lists
- Contributor dashboard — view, sign, and download every CLA version
- Org-scoped bypass lists — exempt specific GitHub users, bots, and apps from CLA requirements
- Async PR sync — after signing, open PRs are automatically updated to passing
- GitHub-native auth — OAuth login, no separate account system
- Stateless sessions — JWT-based with HTTP-only cookies
Contributor opens PR
│
▼
GitHub webhook fires
│
▼
CLA Bot checks signature status
│
┌────┴────┐
│ │
Signed Not signed
│ │
▼ ▼
✅ Pass ❌ Fail + comment with signing link
│
▼
Contributor signs CLA
│
▼
✅ PR checks updated to pass
- Node.js >= 20
- pnpm
- PostgreSQL database
pnpm installCreate a .env.local file:
# Required
DATABASE_URL=postgres://user:pass@localhost:5432/clabot
SESSION_SECRET=your-secret-key
ENCRYPTION_KEY=your-encryption-key
# GitHub App — user authorization (sign-in) credentials.
# Found under the App's "Client ID" + "Client secrets".
# The App must have "Expire user authorization tokens" enabled.
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...
# GitHub App — installation/identification credentials.
GITHUB_APP_SLUG=...
GITHUB_APP_ID=...
GITHUB_PRIVATE_KEY=...
GITHUB_WEBHOOK_SECRET=...pnpm db:migratepnpm devThe app will be available at http://localhost:3000.
| Command | Description |
|---|---|
pnpm dev |
Start development server |
pnpm build |
Production build |
pnpm start |
Run production server |
pnpm lint |
Lint and format check (Biome); warnings fail, same as CI |
pnpm lint:fix |
Apply Biome's safe fixes |
pnpm dead-code |
Unused files, exports, types and unlisted dependencies (knip); runs in CI |
pnpm test |
Unit + integration tests |
pnpm test:all |
Unit + integration + E2E tests |
pnpm test:unit |
Unit tests (Vitest) |
pnpm test:integration |
Integration tests (Vitest + PostgreSQL) |
pnpm test:e2e |
Browser tests (Playwright) |
pnpm webhook -- [options] |
Fire a synthetic GitHub webhook at a local server and time it |
pnpm db:generate |
Generate Drizzle migrations |
pnpm db:migrate |
Apply migrations |
pnpm db:studio |
Open Drizzle Studio |
Everything below runs with no real GitHub App and no remote database. External services are replaced by mocks that you can inspect and control.
pnpm dev:localThis starts three things and wires them together:
- a persistent embedded PostgreSQL in
tmp/embedded-pg-dev(port 5489, databaseclabot_dev), migrated withpnpm db:migrateand seeded (SEED_DATABASE=true); - a mock GitHub server (
scripts/mock-github-server.ts, default port 3998) that serves both the github.com OAuth endpoints and the api.github.com endpoints the app calls with a user token; next devwithGITHUB_OAUTH_BASE_URL/GITHUB_API_BASE_URLpointing at the mock,USE_REAL_GITHUB_APP=false(in-memory mock App client) and mock OAuth credentials. These overrides take precedence over anything in your.env.local.
Open http://localhost:3000/auth/signin and click "Continue with GitHub": instead of
GitHub you get a "Mock GitHub — choose a user" page. Pick orgadmin (admin of the
fiveonefour and moose-stack orgs) to use the admin pages, or any other user
(contributor1, dev-sarah, new-contributor, random-dev, external-contributor)
to act as a contributor. Append &login=<user> to the authorize URL to skip the picker.
Set PORT / MOCK_GITHUB_PORT to change ports. Ctrl-C stops everything; the database
survives between runs (delete tmp/embedded-pg-dev to start fresh). pnpm mock:github
runs just the mock server. Next.js allows one next dev per checkout, so stop any other
dev server first.
To exercise the webhook path while pnpm dev:local is running, use pnpm webhook
(below) and inspect the result at /api/test-support.
Integration and E2E runs always start an embedded PostgreSQL (tmp/embedded-pg,
port 5488) and run migrations against it. .env.local is deliberately ignored by the
test tooling because it usually holds a vercel env pull snapshot pointing at a real
Neon database, and the suites TRUNCATE tables on every reset.
- To run the suites against another database, set
TEST_DATABASE_URLexplicitly. - Non-local hosts are refused unless you also set
ALLOW_REMOTE_TEST_DATABASE=true. - The embedded Postgres binaries need their
postinstallscript (it hydrates library symlinks). pnpm 10 only runs it for packages listed underpnpm.onlyBuiltDependenciesinpackage.json, which is already configured. Ifinitdbaborts with aLibrary not loadederror, runpnpm installagain. pnpm test:e2eneeds a browser once:pnpm exec playwright install chromium.- Exit codes are trustworthy: a failing integration or e2e run exits non-zero. (The
embedded-postgrespackage installs a process exit hook that used to force exit code 0;tests/setup/embedded-postgres-loader.tsremoves it.)
When NODE_ENV !== production and USE_REAL_GITHUB_APP is not "true", the app talks
to an in-memory GitHub App client (lib/github/mock-github-client.ts) instead of
Octokit. It ships with a pool of GitHub users (orgadmin, contributor1, dev-sarah,
new-contributor, random-dev, external-contributor), org memberships, check runs
and PR comments. The mock is instrumented:
- every call is recorded (method, args, duration, error) in an ordered call log
MOCK_GITHUB_LATENCY_MS=200adds artificial latency to each call so serial chains show up in wall-clock time- failures can be injected per method (e.g. make
checkOrgMembershipreturn a 403 once)
The test runner and the Next.js dev server are separate processes, so server-side
state is inspected and reset over HTTP. The endpoint returns 404 unless the server was
started with ENABLE_TEST_SUPPORT=true (the integration runner and pnpm dev:local
set this; a plain pnpm dev does not), and it is always disabled in production builds.
The reset-db action additionally refuses to run against a non-local database host.
# Snapshot: mock GitHub call log, check runs, comments, and the DB statement counter
curl -s localhost:3000/api/test-support | jq
# Reset in-memory state (mock GitHub + counters)
curl -s -X POST localhost:3000/api/test-support -H 'content-type: application/json' \
-d '{"action":"reset"}'
# Truncate + re-seed the local database
curl -s -X POST localhost:3000/api/test-support -H 'content-type: application/json' \
-d '{"action":"reset-db"}'
# Inject latency and a one-shot failure into the mock GitHub client
curl -s -X POST localhost:3000/api/test-support -H 'content-type: application/json' \
-d '{"action":"configure-github","latencyMs":150,"failures":{"checkOrgMembership":{"status":403,"times":1}}}'The DB statement counter comes from a Drizzle logger that is attached outside
production, so db.count after a request is the number of SQL round trips it cost.
Integration tests use this to pin a budget on the PR-check path (see the
"TEST HARNESS" section of tests/integration/api-suite.test.ts).
# Unsigned external contributor opens a PR against fiveonefour/sdk
pnpm webhook -- --author external-contributor --org fiveonefour --repo sdk --pr 42
# Org member, synchronize event, 10 deliveries with p50/p95 timing
pnpm webhook -- --author orgadmin --action synchronize --repeat 10 --quiet
# /recheck comment
pnpm webhook -- --event issue_comment --author contributor1 --pr 42The CLI signs the payload with GITHUB_WEBHOOK_SECRET when set and always sends a
fresh x-github-delivery id, so it also works against a server that enforces
signatures and delivery dedup.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript 7 (strict) |
| Database | PostgreSQL + Drizzle ORM |
| UI | Tailwind CSS, Radix UI, shadcn/ui |
| Auth | GitHub OAuth, JWT (jose) |
| GitHub API | Octokit |
| Testing | Vitest, Playwright |
| Linting | Biome |
| Deployment | Vercel |
app/
├── api/ # API routes (webhooks, auth, signing)
├── admin/ # Admin dashboard pages
├── contributor/ # Contributor dashboard pages
├── sign/ # CLA signing flow
└── auth/ # Authentication pages
lib/
├── db/ # Schema, queries, migrations
├── github/ # GitHub API client and webhook handling
├── cla/ # CLA signing and recheck workflows
├── security/ # Security utilities
└── auth.ts # Session management
components/
├── admin/ # Admin UI components
├── sign/ # Signing flow components
└── ui/ # Design system (shadcn/ui)
tests/
├── unit/ # Vitest unit tests
├── integration/ # API integration tests
└── e2e/ # Playwright browser tests
| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
SESSION_SECRET |
JWT signing key |
ENCRYPTION_KEY |
OAuth token encryption key |
| Variable | Description |
|---|---|
GITHUB_CLIENT_ID |
GitHub App user-authorization client ID (the App's own client_id) |
GITHUB_CLIENT_SECRET |
GitHub App user-authorization client secret |
GITHUB_APP_SLUG |
GitHub App slug. Also identifies the bot's comment author (<slug>[bot]); without it the bot never updates or deletes existing PR comments and posts fresh ones instead |
GITHUB_APP_ID |
GitHub App ID |
GITHUB_PRIVATE_KEY |
GitHub App private key |
GITHUB_WEBHOOK_SECRET |
Webhook signature verification secret |
| Variable | Default | Description |
|---|---|---|
NEXT_PUBLIC_APP_URL |
Auto-detected | Optional override for the app base URL. On Vercel the production domain (VERCEL_PROJECT_PRODUCTION_URL) and preview branch URL (VERCEL_BRANCH_URL) are used automatically; elsewhere the request host is used. |
SEED_DATABASE |
false |
Auto-seed test data on startup |
DRIZZLE_MIGRATIONS_SCHEMA |
drizzle |
Migrations schema name |
DRIZZLE_MIGRATIONS_TABLE |
__drizzle_migrations |
Migrations table name |
For CLA enforcement to block merging, add CLA Bot / Contributor License Agreement as a required status check in your GitHub branch protection rules or rulesets.
Contributions are welcome! Please open an issue or pull request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/my-feature) - Run tests before submitting (
pnpm test && pnpm build) - Open a pull request