Thanks for your interest in contributing to DiffKit! This guide will help you get started.
Follow the Getting Started section in the README to set up your local environment. You'll need both a GitHub OAuth App and a GitHub App configured.
For webhook testing during local development, use an ngrok tunnel (or similar) and set DEV_TUNNEL_URL in your .dev.vars.
DiffKit is a pnpm monorepo managed with Turborepo:
diffkit/
├── apps/
│ └── dashboard/ # Main web app (TanStack Start + Cloudflare Workers)
│ ├── src/
│ │ ├── components/ # App-specific React components
│ │ ├── db/ # Drizzle ORM schema and database access
│ │ ├── lib/ # Auth, GitHub API clients, caching, utilities
│ │ └── routes/ # File-based routes (TanStack Router)
│ │ ├── api/ # API routes (auth callbacks, webhooks)
│ │ └── _protected/ # Auth-gated routes
│ └── drizzle/ # SQL migration files
├── extensions/
│ └── diffkit-redirect/ # Browser extension (GitHub → DiffKit redirects)
├── packages/
│ ├── ui/ # Shared UI components (Radix UI + Tailwind CSS)
│ ├── icons/ # Icon wrapper package
│ └── typescript-config/ # Shared TypeScript configurations
├── scripts/ # Migration runner and dev utilities
└── docs/ # Architecture documentation
- TanStack Start — Full-stack React 19 framework on Cloudflare Workers
- TanStack Router — File-based routing in
apps/dashboard/src/routes/ - TanStack Query — Server state management and caching
- Drizzle ORM — Database schema and migrations in
apps/dashboard/src/db/andapps/dashboard/drizzle/ - Better Auth — Authentication with a GitHub OAuth App, plus GitHub App user and installation tokens for installed repos
- Cloudflare D1 — SQLite database at the edge (auth data, cache control state)
- Cloudflare KV — Hot payload cache for GitHub API responses (
GITHUB_CACHE_KVbinding) - Cloudflare Durable Objects —
SignalRelayfor real-time webhook-to-client revalidation over WebSocket - Vite — Build tooling via
@cloudflare/vite-plugin - Vitest — Test framework (
pnpm --filter dashboard test) - Biome — Linting and formatting
DiffKit uses a hybrid GitHub auth model:
- The GitHub OAuth App signs users in and powers broad user-context reads, including public or external repositories where the GitHub App is not installed.
- The GitHub App user token (
ghu_prefix) powers installation discovery viaGET /user/installations. - The GitHub App installation token is preferred for repo-scoped reads and writes when the app is installed for that owner. Tokens are cached in KV (with in-memory fallback) and reused until five minutes before expiry.
Auth callbacks:
- OAuth App:
/api/auth/callback/github - GitHub App user authorization:
/api/github/app/callback - GitHub App setup URL:
/setup(with Redirect on update enabled)
Environment variables are documented in apps/dashboard/.dev.vars.example. Do not commit real .dev.vars values or private keys. If a private key is exposed, revoke it in GitHub App settings and generate a replacement.
DiffKit uses a split-cache architecture to minimize GitHub API calls while keeping data fresh. For a deep dive, see docs/github-cache-architecture.md. Here's the overview:
Split KV/D1 cache — GitHub API responses are cached in Cloudflare KV for fast reads, with D1 as the authoritative control plane for invalidation state. Key files:
apps/dashboard/src/lib/github-cache.ts— Core cache read/write logicapps/dashboard/src/lib/github-revalidation.ts— Signal key definitions and webhook-to-signal mappingapps/dashboard/src/lib/github.functions.ts— GitHub API operations with cache mode opt-ins
Invalidation flow — When a GitHub webhook arrives or a mutation runs:
- Affected signal keys are resolved (e.g.
pull:{owner}/{repo}#{number},pulls.mine) - D1 revalidation signal timestamps and namespace versions are bumped
- Future cache reads build a different KV key from the new namespace version, naturally bypassing stale entries
- Connected clients are notified in real time via the
SignalRelayDurable Object
Real-time revalidation — The SignalRelay Durable Object (apps/dashboard/src/lib/signal-relay.server.ts) maintains WebSocket connections per user. When the webhook handler broadcasts signal keys, subscribed clients receive them instantly. Detail routes use a one-shot signal check (apps/dashboard/src/lib/use-github-signal-stream.ts) to invalidate only the active query when a newer server-side signal exists.
Rate-limit resilience — The cache layer extends freshness when GitHub quota is low (≤100 remaining: 2-min floor; ≤25 remaining: 5-min floor or until reset). If GitHub returns a rate-limit error and a cached payload exists, the stale payload is served instead of failing.
The webhook endpoint at apps/dashboard/src/routes/api/webhooks/github.ts verifies the HMAC-SHA256 signature, maps events to cache signal keys, writes invalidation state to D1, and broadcasts to connected WebSocket clients. Supported events include pull_request, issues, issue_comment, check_run, check_suite, pull_request_review, and more.
Routes live in apps/dashboard/src/routes/. TanStack Router uses file-based routing — create a new file and the route is automatically registered.
Protected routes go under _protected/ which enforces authentication.
Shared components go in packages/ui/src/components/. App-specific components go in apps/dashboard/src/components/.
Migration files live in apps/dashboard/drizzle/. To run migrations:
pnpm --filter dashboard migrate # Local D1
pnpm --filter dashboard migrate:remote # Remote D1If you add a new table or column, create a new numbered SQL file in apps/dashboard/drizzle/.
- Fork the repo and create your branch from
main - Make your changes — keep commits focused and atomic
- Run checks before pushing:
pnpm check-types # Type checking pnpm lint # Linting pnpm format # Formatting
- Run tests if you touched caching or GitHub integration:
pnpm --filter dashboard test - Open a pull request against
main
This project uses Biome for linting and formatting. Run pnpm format to auto-format your code. Pre-commit hooks via Husky and lint-staged will also catch issues before they're committed.
A few conventions:
- Prefer editing existing files over creating new ones
- Keep components in the appropriate package (
apps/dashboardfor app-specific,packages/uifor shared) - Use
cn()for composing class names (not template literals) - Use
flex+gapfor spacing (notspace-x/space-y)
- Keep PRs focused — one feature or fix per PR
- Write a clear title and description explaining what changed and why
- Include screenshots for UI changes
- Make sure all checks pass before requesting review
Open an issue with:
- A clear description of the bug
- Steps to reproduce
- Expected vs actual behavior
- Screenshots if applicable
Open an issue with the enhancement label describing:
- The problem you're trying to solve
- Your proposed solution
- Any alternatives you've considered
By contributing, you agree that your contributions will be licensed under the MIT License.