Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ DiffKit is a **pnpm monorepo** managed with **Turborepo**:
- **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/` and `apps/dashboard/drizzle/`
- **Better Auth** — Authentication with GitHub OAuth
- **Better Auth** — Authentication with a GitHub App
- **Cloudflare D1** — SQLite database at the edge

### Adding a New Route
Expand Down
70 changes: 62 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ A fast, design-first GitHub dashboard for developers who want to stay on top of
| Routing | TanStack Router (file-based) |
| Data | TanStack Query + Octokit |
| Database | Cloudflare D1 (SQLite) via Drizzle ORM |
| Auth | Better Auth with GitHub OAuth |
| Auth | Better Auth with GitHub App |
| Styling | Tailwind CSS 4 + Radix UI |
| Icons | Lucide React |
| Build | Vite 7 + Turborepo |
Expand All @@ -32,7 +32,7 @@ A fast, design-first GitHub dashboard for developers who want to stay on top of

- [Node.js](https://nodejs.org/) (v20+)
- [pnpm](https://pnpm.io/) (v10+)
- A [GitHub OAuth App](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
- A [GitHub App](https://github.com/settings/apps)

### Setup

Expand All @@ -54,21 +54,75 @@ A fast, design-first GitHub dashboard for developers who want to stay on top of
Create a `.dev.vars` file in `apps/dashboard/`:

```
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
GITHUB_APP_CLIENT_ID=your_github_app_client_id
GITHUB_APP_CLIENT_SECRET=your_github_app_client_secret
GITHUB_WEBHOOK_SECRET=your_github_webhook_secret
BETTER_AUTH_SECRET=a_random_32_character_string
BETTER_AUTH_URL=http://localhost:3000
```

> To get GitHub OAuth credentials, create a new OAuth App in [GitHub Developer Settings](https://github.com/settings/developers) with the callback URL set to `http://localhost:3000/api/auth/callback/github`.
> DiffKit also accepts the legacy `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` names during migration, but new setups should use the `GITHUB_APP_*` names above.

4. **Run database migrations**
4. **Create and install the GitHub App**

In [GitHub App settings](https://github.com/settings/apps):

- Set the callback URL to `http://localhost:3000/api/auth/callback/github`
- Grant the account permission `Email addresses: Read-only`
- Install the app on the repositories or organizations you want DiffKit to access

Recommended GitHub App permissions derived from the current roadmap:

| Roadmap area | Roadmap items | GitHub App permission | Level | Notes |
| --- | --- | --- | --- | --- |
| Auth | Sign in and identify the user | User `Email addresses` | Read-only | Required for Better Auth to resolve the user's email address. |
| Core dashboard | Overview, repo list, repo search, private repo access | Repository `Metadata` | Read-only | Required baseline permission for repository-aware reads. |
| Pull requests | View/edit PRs, update branch, request reviewers, review diffs, merge, close/reopen, link issues | Repository `Pull requests` | Read & write | Required now for existing PR mutations and future PR management. |
| Issues | View/create/edit/close issues, labels, milestones, comments | Repository `Issues` | Read & write | Required now for current label mutations and future issue workflows. |
| CI and review status | PR checks, CI-aware review flows, CI notification filtering | Repository `Checks` | Read-only | Required now for pull request status surfaces. |
| GitHub Actions | Workflow run history, job logs, artifacts, rerun/cancel/retry flows, Actions-focused UI | Repository `Actions` | Read & write | `Read` is enough for viewing workflow runs and logs. Use `Read & write` if the product should also rerun, cancel, delete, or otherwise manage workflow runs. |
| Collaborators and teams | Reviewer pickers, org team reviewer flows | Organization `Members` | Read-only | Required for org installs if reviewer assignment should include teams. |
| Repository content | File browser, README preview, branch/tag management, create PR from branches | Repository `Contents` | Read & write | Inference from the roadmap. Likely needed once repository browsing and branch operations ship. |
| Workflow files and policy | Editing `.github/workflows/*`, enabling/disabling workflows, workflow policy/config management | Repository `Workflows` | Read & write | Separate from `Actions`. Needed when the app modifies workflow definitions or workflow configuration, not just when it reads logs or manages runs. |
| Search | Global search across PRs, issues, and repos | No extra permission beyond the resources being searched | N/A | Search inherits access from `Metadata`, `Pull requests`, `Issues`, and likely `Contents`. |
| Notifications | Notification inbox, mark read/unread, filter by type | No matching GitHub App permission | N/A | GitHub's notifications REST endpoints do not support GitHub App user or installation tokens, so this roadmap area needs a different implementation strategy. |

If we add new permissions after users have already installed the app, GitHub will require those installations to approve the expanded permission set.

Recommended webhook events in GitHub App setup:

| GitHub UI label | Enable now | Why |
| --- | --- | --- |
| `Check run` | Yes | Keeps PR status and check-derived cache fresh. |
| `Check suite` | Yes | Captures suite-level CI state changes for PR refreshes. |
| `Issue comment` | Yes | Refreshes issue and PR comment-related views. |
| `Issues` | Yes | Refreshes issue and PR metadata when titles, bodies, labels, or state change. |
| `Pull request` | Yes | Core PR invalidation event. |
| `Pull request review` | Yes | Refreshes review state and PR detail data. |
| `Pull request review comment` | Yes | Refreshes diff discussion and review comment data. |
| `Pull request review thread` | Yes | Refreshes review thread state changes. |
| `Workflow run` | Later | Recommended once the Actions dashboard ships. Useful for workflow-run level updates, logs, reruns, and run state transitions. |
| `Workflow job` | Later | Recommended once the Actions dashboard ships. Useful for job-level logs, timing, and per-job status updates. |
| `Push` | Later | Not used by the current invalidation code, but likely useful once branch-aware repo/activity features expand. |
| `Repository` | Later | Useful for repo settings and metadata changes if repository management surfaces expand. |
| `Create` | Later | Useful for branch/tag creation flows if repo management features ship. |
| `Delete` | Later | Useful for branch/tag deletion flows if repo management features ship. |

`Workflow run` and `Workflow job` require at least repository `Actions: Read-only`.

The current webhook invalidation route is wired for the first 8 events above. If you enable the `Later` events now, they are harmless, but the app will ignore them until we add handlers.

Set the webhook URL to `/api/webhooks/github` on your deployed app. For local webhook testing, use a tunnel that forwards to `http://localhost:3000/api/webhooks/github`.

For local Vite development, set `DEV_TUNNEL_URL` in `apps/dashboard/.dev.vars` to the full public tunnel URL, for example `https://your-subdomain.ngrok-free.app`. The dev server will use it to allow the tunnel host and configure HMR correctly.

5. **Run database migrations**

```bash
pnpm --filter dashboard migrate
```

5. **Start the dev server**
6. **Start the dev server**

```bash
pnpm dev
Expand Down Expand Up @@ -156,7 +210,7 @@ A fast, design-first GitHub dashboard for developers who want to stay on top of

### General

- [x] GitHub OAuth authentication
- [x] GitHub App authentication
- [x] Dark mode with system preference
- [x] Response caching with ETags
- [ ] Keyboard shortcuts
Expand Down
23 changes: 18 additions & 5 deletions apps/dashboard/.dev.vars.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
# GitHub OAuth App credentials
# 1. Go to https://github.com/settings/developers
# 2. Click "New OAuth App"
# 3. Set "Authorization callback URL" to http://localhost:3000/api/auth/callback/github
# 4. Copy the Client ID and generate a Client Secret
# GitHub App credentials
# 1. Go to https://github.com/settings/apps
# 2. Create a new GitHub App
# 3. Set the callback URL to http://localhost:3000/api/auth/callback/github
# 4. Under Permissions & events, grant Email addresses account permission: Read-only
# 5. Install the app on the repositories or organizations you want DiffKit to access
GITHUB_APP_CLIENT_ID=
GITHUB_APP_CLIENT_SECRET=

# GitHub webhook secret used to verify deliveries to /api/webhooks/github
# For local development, point your GitHub App webhook URL at a tunnel that forwards here.
GITHUB_WEBHOOK_SECRET=

# Optional: full tunnel URL used by Vite dev server to allow webhook traffic and HMR
# Example: https://your-subdomain.ngrok-free.app
DEV_TUNNEL_URL=

# Legacy OAuth-style names are still supported as a fallback during migration.
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

Expand Down
4 changes: 4 additions & 0 deletions apps/dashboard/drizzle/0002_github_revalidation_signal.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
CREATE TABLE `github_revalidation_signal` (
`signal_key` text PRIMARY KEY NOT NULL,
`updated_at` integer NOT NULL
);
2 changes: 2 additions & 0 deletions apps/dashboard/src/components/layouts/dashboard-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
githubMyIssuesQueryOptions,
githubMyPullsQueryOptions,
} from "#/lib/github.query";
import { useGitHubRevalidation } from "#/lib/use-github-revalidation";
import { useHasMounted } from "#/lib/use-has-mounted";
import { DashboardTopbar } from "./dashboard-topbar";

Expand All @@ -14,6 +15,7 @@ export function DashboardLayout() {
const { user } = routeApi.useRouteContext();
const scope = { userId: user.id };
const hasMounted = useHasMounted();
useGitHubRevalidation(user.id);

const pullsQuery = useQuery({
...githubMyPullsQueryOptions(scope),
Expand Down
8 changes: 8 additions & 0 deletions apps/dashboard/src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,11 @@ export const githubResponseCache = sqliteTable(
),
}),
);

export const githubRevalidationSignal = sqliteTable(
"github_revalidation_signal",
{
signalKey: text("signal_key").primaryKey(),
updatedAt: integer("updated_at").notNull(),
},
);
36 changes: 9 additions & 27 deletions apps/dashboard/src/lib/auth-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ import { getRequest } from "@tanstack/react-start/server";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { tanstackStartCookies } from "better-auth/tanstack-start";
import { and, eq } from "drizzle-orm";
import { drizzle } from "drizzle-orm/d1";
import type { Octokit as OctokitType } from "octokit";
import { Octokit } from "octokit";
import { getDb } from "../db";
import * as schema from "../db/schema";
import { account } from "../db/schema";
import {
getGitHubAccessTokenByUserId,
getGitHubAppAuthConfig,
} from "./github-app.server";

const authDb = drizzle(env.DB, { schema });

function createAuth() {
const github = getGitHubAppAuthConfig();

return betterAuth({
baseURL: env.BETTER_AUTH_URL,
secret: env.BETTER_AUTH_SECRET,
Expand All @@ -22,18 +25,8 @@ function createAuth() {
}),
socialProviders: {
github: {
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
scope: [
"read:user",
"user:email",
"repo",
"notifications",
"workflow",
"read:project",
"security_events",
"admin:repo_hook",
],
clientId: github.clientId,
clientSecret: github.clientSecret,
},
},
plugins: [tanstackStartCookies()],
Expand All @@ -57,19 +50,8 @@ export async function getRequestSession() {
export async function getGitHubClientByUserId(
userId: string,
): Promise<OctokitType> {
const db = getDb();
const githubAccount = await db
.select()
.from(account)
.where(and(eq(account.userId, userId), eq(account.providerId, "github")))
.get();

if (!githubAccount?.accessToken) {
throw new Error("No GitHub account linked");
}

return new Octokit({
auth: githubAccount.accessToken,
auth: await getGitHubAccessTokenByUserId(userId),
retry: { enabled: false },
throttle: { enabled: false },
});
Expand Down
16 changes: 4 additions & 12 deletions apps/dashboard/src/lib/auth.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { tanstackStartCookies } from "better-auth/tanstack-start";
import { drizzle } from "drizzle-orm/d1";
import * as schema from "../db/schema";
import { getGitHubAppAuthConfig } from "./github-app.server";

export function getAuth() {
const db = drizzle(env.DB, { schema });
const github = getGitHubAppAuthConfig();

return betterAuth({
baseURL: env.BETTER_AUTH_URL,
Expand All @@ -17,18 +19,8 @@ export function getAuth() {
}),
socialProviders: {
github: {
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
scope: [
"read:user",
"user:email",
"repo",
"notifications",
"workflow",
"read:project",
"security_events",
"admin:repo_hook",
],
clientId: github.clientId,
clientSecret: github.clientSecret,
},
},
plugins: [tanstackStartCookies()],
Expand Down
18 changes: 18 additions & 0 deletions apps/dashboard/src/lib/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
type DebugDetails = Record<string, unknown> | undefined;

function isDevEnvironment() {
return import.meta.env.DEV;
}

export function debug(scope: string, message: string, details?: DebugDetails) {
if (!isDevEnvironment()) {
return;
}

if (typeof details === "undefined") {
console.log(`[debug:${scope}] ${message}`);
return;
}

console.log(`[debug:${scope}] ${message}`, details);
}
Loading
Loading