Open-source CLA platform: GitHub App PR checks, PDF templates, and e-sign for contributor agreements.
apps/api: Fastify API, GitHub App webhooks, OAuth, signing pages, Drizzle schema, and migrations.apps/web: Next.js admin app using Shadcn-style components.packages/shared: shared Zod schemas and TypeScript API contracts.
- Verifies GitHub webhook signatures.
- Handles
pull_requestevents and publishes a GitHub Check Run namedContributor License Agreement. - Lets contributors sign with GitHub OAuth.
- Lets repository admins select a managed CLA template, upload immutable template versions, and view signatures/checks.
- Supports corporate CLA agreements that cover active members of a GitHub organization.
- Uses Drizzle ORM with any Postgres-compatible database, including Neon.
-
Install dependencies.
pnpm install
-
Copy the API environment template.
cp apps/api/.env.example .env
The API loads
.envfrom the repository root orapps/api/.env. -
Create a Postgres database and set connection strings.
DATABASE_URL=postgresql://... DATABASE_MIGRATION_URL=postgresql://...
-
Start an ngrok tunnel for local GitHub webhooks.
ngrok http 3000
Use the tunnel URL as
PUBLIC_APP_URL. The admin app runs locally on port3001.PUBLIC_APP_URL=https://your-ngrok-url.ngrok-free.app ADMIN_WEB_URL=http://localhost:3001
-
Create a GitHub App.
Homepage URL: https://your-ngrok-url.ngrok-free.app Webhook URL: https://your-ngrok-url.ngrok-free.app/webhooks/githubSubscribe to
pull_request,installation, andinstallation_repositoriesevents, then configure repository permissions:Checks: read/write Contents: read Issues: read/write Pull requests: read/write Metadata: readConfigure organization permission:
Members: read -
Create a separate GitHub OAuth App for signer/admin login.
Homepage URL: http://localhost:3001 Authorization callback URL: https://your-ngrok-url.ngrok-free.app/auth/github/callbackThe OAuth App requests
read:orgby default (GITHUB_OAUTH_SCOPES) so org owners can see private org repos in the admin UI. The backend still verifies admin rights per repository. -
Set GitHub and session environment values.
GITHUB_APP_ID=... GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----" GITHUB_WEBHOOK_SECRET=... GITHUB_CLIENT_ID=... GITHUB_CLIENT_SECRET=... GITHUB_OAUTH_CLIENT_ID=... GITHUB_OAUTH_CLIENT_SECRET=... SESSION_SECRET=replace-with-at-least-32-characters
-
Run migrations.
pnpm run db:migrate
-
Start both apps in separate terminals.
pnpm run dev:api pnpm run dev:web
-
Visit the admin UI.
http://localhost:3001
GitHub OAuth establishes the user identity and stores an HTTP-only signed session cookie. Admin privileges are not global. Every admin API call includes a repository context, and apps/api verifies the logged-in GitHub user has admin permission on that installed repository before allowing template or signature access.
This keeps multi-account behavior explicit: one GitHub user can manage multiple orgs, personal accounts, and installations, but the backend authorizes each requested repository independently.
pnpm run dev:api: build shared contracts and start the Fastify API on port3000.pnpm run dev:web: build shared contracts and start the Next.js admin app on port3001.pnpm build: build shared contracts, API, and web app.pnpm typecheck: typecheck all workspaces.pnpm test: build shared contracts and run API tests.pnpm run db:generate: generate Drizzle migrations forapps/api.pnpm run db:migrate: apply Drizzle migrations forapps/api.
The API remains the GitHub webhook and signing trust boundary. Build the API container from the repository root:
docker build -f apps/api/Dockerfile .Deploy apps/web as a normal Next.js app with:
API_BASE_URL=https://your-api.example.com
NEXT_PUBLIC_API_BASE_URL=https://your-api.example.com
NEXT_PUBLIC_WEB_URL=https://your-admin.example.comSet ADMIN_WEB_URL on the API to the deployed admin origin so OAuth callbacks can safely return to the UI.
- Install the GitHub App on a test repository.
- Open or update a pull request.
- Confirm the API receives
POST /webhooks/github. - Confirm the PR gets a
Contributor License Agreementcheck. - If a contributor has not signed, confirm the check fails and links to the signing flow.
- Sign in with GitHub, sign personally or corporately, and confirm the check updates to success.
For local retesting on an existing PR, push an empty commit:
git commit --allow-empty -m "Retest CLA check"
git push