Local GitHub organization activity workspace built with Next.js, PostgreSQL, and a GitHub App.
The app keeps PostgreSQL as the canonical local source of truth, syncs GitHub activity on demand, and exposes organization, repository, and user views without querying GitHub for every screen render.
Try the live demo on Vercel — pre-loaded with synthetic data, no setup required.
- organization overview focused on visible work
- people workspace with user detail pages
- repositories workspace with repository activity logs
- local historical storage in PostgreSQL
- gap-based backfill for missing time windows
- 90-day bootstrap flow for first-time setup
- rebuildable daily aggregates for faster dashboard queries
- export endpoints for user activity datasets
Current sync coverage includes:
- commits
- pull requests
- pull request reviews
- pull request review comments
- pull request timeline events
- issues
- issue comments
- workflow runs
- check runs
- deployments and deployment statuses
Canonical entities are stored in normalized tables with unique external IDs. Daily aggregates are stored separately and can always be rebuilt from canonical event data.
Next.jsfor UI and API routesPostgreSQLas the canonical local databaseGitHub Appfor authenticated API accesscoverage_windowsto track local temporal coveragesync_jobsfor audit, operational logs, and warningsuser_activity_dailyfor rebuildable daily aggregates
If you want the shortest path from clone to a working local dashboard:
- Create a GitHub App and install it on your target organization.
- Copy
.env.exampleto.env.localand fill in the required values. - Start PostgreSQL.
- Install dependencies and apply migrations.
- Start the Next.js app.
- Run the 90-day bootstrap.
- Open the dashboard and switch the range filter to
90d.
Commands:
cp .env.example .env.local
npm install
npm run db:up
npm run db:migrate
npm run dev
APP_BASE_URL=http://localhost:3000 npm run sync:bootstrap -- --repository all --lookbackDays 90Then open http://localhost:3000.
If you want a public demo without connecting a real GitHub App, the repo now includes mock-data scripts that populate PostgreSQL directly.
Seed a fresh demo dataset:
npm run demo:seedClear the demo dataset:
npm run demo:clearOptional flags:
--org demo-orgto choose the organization login stored in the database--days 90to control the generated time window
The demo seed:
- creates a synthetic organization, repositories, members, branches, and recent activity
- populates canonical event tables and daily aggregates
- inserts coverage windows and completed sync jobs so the dashboard feels alive
- loads
.env.localautomatically to findDATABASE_URL
For a demo deployment, use a dedicated database and set GITHUB_ORG to the same demo org login you seeded, for example demo-org.
The repo is usable from scratch, but you do need your own GitHub App installation.
Start here:
- full setup guide:
docs/github-app-setup.md
Short version:
- Go to GitHub developer settings and create a new GitHub App.
- Install it on the organization you want to analyze.
- Generate a private key and copy the App ID and Installation ID.
- Grant the repository permissions listed in the setup guide.
- Add the resulting values to
.env.local.
Recommended repository permissions for the current implementation:
Metadata: Read-onlyContents: Read-onlyPull requests: Read-onlyIssues: Read-onlyActions: Read-onlyChecks: Read-onlyDeployments: Read-only
Recommended organization permissions:
Members: Read-only
Notes:
Members: Read-onlyis helpful, but member discovery can also be improved withGITHUB_ORG_MEMBERS_TOKEN.GITHUB_APP_WEBHOOK_SECRETis optional for the current local workflow. It is reserved for webhook ingestion and can stay empty if you are only using discovery, sync, bootstrap, and dashboard views.- If a permission is missing for timeline events, checks, or deployments, bootstrap still completes and reports those families as warnings.
- For a mock-only demo deployment, you can skip GitHub App setup entirely and use
npm run demo:seedinstead.
- Node.js 20+
- PostgreSQL 14+
- a GitHub App installed on the target organization
Copy the example file and fill in your values:
cp .env.example .env.localEnvironment variables:
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL |
yes | PostgreSQL connection string used by the app and all CLI wrappers |
GITHUB_ORG |
yes | organization login to analyze |
GITHUB_APP_ID |
yes | numeric GitHub App ID |
GITHUB_APP_PRIVATE_KEY |
yes | private key generated from the GitHub App settings page |
GITHUB_APP_INSTALLATION_ID |
yes | installation ID for the app on the target organization |
DASHBOARD_REPOSITORY_BLACKLIST |
no | comma-separated repositories hidden from workspace rankings and filters |
GITHUB_APP_WEBHOOK_SECRET |
no | webhook secret reserved for webhook ingestion |
GITHUB_ORG_MEMBERS_TOKEN |
no | personal token used to improve org member discovery when membership is private |
Notes:
GITHUB_ORGhas no hardcoded fallback. Set it explicitly.GITHUB_APP_PRIVATE_KEYaccepts either real newlines or escaped\n.GITHUB_ORG_MEMBERS_TOKENis optional, but it is often useful when people are active in the org and their GitHub organization membership is still private.
Example:
DATABASE_URL=postgres://postgres:postgres@localhost:5432/watch_org
GITHUB_ORG=your-org
GITHUB_APP_ID=1234567
GITHUB_APP_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
GITHUB_APP_INSTALLATION_ID=98765432
DASHBOARD_REPOSITORY_BLACKLIST=legacy-repo-a,legacy-repo-b
GITHUB_APP_WEBHOOK_SECRET=
GITHUB_ORG_MEMBERS_TOKEN=npm installIf you want to use the included Docker setup:
npm run db:upFor a first local run, applying migrations is enough:
npm run db:migrateIf you are changing the schema locally, generate new migration files with:
npm run db:generatenpm run devThe API-side CLI wrappers automatically try local Next servers on common ports. If your server runs on a custom port, or you want deterministic behavior, set:
APP_BASE_URL=http://localhost:3000The recommended first run is the bootstrap flow. It performs:
- organization discovery
- 90-day extended sync
- aggregate rebuild
CLI:
APP_BASE_URL=http://localhost:3000 npm run sync:bootstrap -- --repository all --lookbackDays 90The same flow is also available from the System drawer in the UI.
For an unattended nightly run, use the dedicated command below. It syncs the previous UTC day by default, refreshes discovery first, and rebuilds aggregates for the same date window.
APP_BASE_URL=http://localhost:3000 npm run sync:nightlyUseful flags:
--date 2026-04-10to sync a specific UTC day--daysAgo 2to sync two days ago instead of yesterday--repository your-repoto limit the run to one repository--user octocatto scope to one user--branch mainto scope to one branch
Example cron entry at 01:30 every night with a persistent log file:
h30 1 * * * cd <project-path> && APP_BASE_URL=http://localhost:3000 npm run sync:nightly >> /var/log/watch-org-nightly.log 2>&1Each repository sync already writes an audit row to sync_jobs with status completed or failed, so you have both cron logs and database traceability for the execution outcome.
Fetch repositories and members from the configured organization:
APP_BASE_URL=http://localhost:3000 npm run sync:discoveryHTTP:
curl -X POST http://localhost:3000/api/discoverySync all supported activity families for a recent rolling window:
APP_BASE_URL=http://localhost:3000 npm run sync:all -- --repository all --lookbackDays 30Sync a precise day-based window:
APP_BASE_URL=http://localhost:3000 npm run sync:all -- --repository all --from 2026-03-01 --to 2026-03-31Single-family commands:
APP_BASE_URL=http://localhost:3000 npm run sync:commits -- --repository all --lookbackDays 30
APP_BASE_URL=http://localhost:3000 npm run sync:prs -- --repository all --lookbackDays 30
APP_BASE_URL=http://localhost:3000 npm run sync:reviews -- --repository all --lookbackDays 30
APP_BASE_URL=http://localhost:3000 npm run sync:review-comments -- --repository all --lookbackDays 30
APP_BASE_URL=http://localhost:3000 npm run sync:pr-timeline -- --repository all --lookbackDays 30
APP_BASE_URL=http://localhost:3000 npm run sync:issues -- --repository all --lookbackDays 30
APP_BASE_URL=http://localhost:3000 npm run sync:issue-comments -- --repository all --lookbackDays 30
APP_BASE_URL=http://localhost:3000 npm run sync:workflows -- --repository all --lookbackDays 30
APP_BASE_URL=http://localhost:3000 npm run sync:checks -- --repository all --lookbackDays 30
APP_BASE_URL=http://localhost:3000 npm run sync:deployments -- --repository all --lookbackDays 30Supported sync flags:
--repository <name>orall--user <login>orall--branch <name>orall--lookbackDays <n>--from <YYYY-MM-DD>--to <YYYY-MM-DD>
Date windows are interpreted as:
from=>YYYY-MM-DDT00:00:00.000Zto=>YYYY-MM-DDT23:59:59.999Z
You can inspect missing windows before launching a sync:
curl -X POST http://localhost:3000/api/backfill \
-H 'content-type: application/json' \
-d '{
"org": "your-org",
"repository": "all",
"from": "2026-03-01",
"to": "2026-03-31",
"activityType": "all",
"user": "all",
"branch": "all"
}'The planner returns only uncovered windows thanks to gap-based coverage evaluation. Empty windows are persisted as coverage too, so they are not re-fetched unnecessarily.
In the dashboard, commit coverage treats both complete and empty windows as covered. An empty window means the selected range was checked successfully and no commits were found for that repository.
user_activity_daily stores rebuildable daily aggregates per organization, repository, and user.
Rebuild recent aggregates:
APP_BASE_URL=http://localhost:3000 npm run aggregates:rebuild -- --repository all --lookbackDays 90Rebuild a precise range:
APP_BASE_URL=http://localhost:3000 npm run aggregates:rebuild -- --repository all --from 2026-03-01 --to 2026-03-31After bootstrap completes:
- open http://localhost:3000
- switch the top range control to
90d - open
PeopleandRepositoriesto confirm that members and repositories were discovered - open the
Systemdrawer and inspectStatus,Coverage,Discovery, andService Log
Useful verification routes:
/overview/peoplepeople workspace/repositoriesrepositories workspace/api/dashboard?preset=90draw overview payload
The off-page System drawer groups operational and technical information away from the main work surfaces.
Tabs currently include:
StatusCoverageDiscoveryService LogSync
The Sync tab exposes the 90-day bootstrap action and reports processed repositories, attempted activity families, and warnings.
Missing permissions for timeline events, checks, or deployments do not fail the whole bootstrap. They are reported as warnings and the affected coverage windows are marked as partial.
The UI uses next-intl with an English-first setup.
- active locale:
en - no locale prefix in routes
- messages centralized under
src/i18n/messages - structure is ready for additional locales later
Workspace rankings and repository-focused views intentionally hide archived or blacklisted repositories by default.
- archived repositories are excluded from workspace ranking surfaces
- repositories listed in
DASHBOARD_REPOSITORY_BLACKLISTare hidden from standard workspace filters and leaderboards - canonical data is still kept in PostgreSQL
- long-running sync jobs can take several minutes on large organizations
- the CLI now prints heartbeats while it waits
- prefer setting
APP_BASE_URL=http://localhost:3000explicitly so the wrapper does not guess the wrong port
- the default range preset is
30d - after importing 90 days, switch the UI range to
90d - if raw tables are fresh but charts still look stale, run
npm run aggregates:rebuild
- check whether the user has public organization visibility enabled on GitHub
- if not, add
GITHUB_ORG_MEMBERS_TOKENto improve org member discovery - then run
npm run sync:discoveryagain
issuesordeploymentsmay legitimately be empty for the selected repositories- timeline events, checks, and deployments may also be skipped when the GitHub App lacks the required permission
- bootstrap reports those cases as warnings instead of failing completely
- this usually means the Next.js server crashed while rendering the API route
- restart
npm run devand retry the same command
For a public demo on Vercel, keep it simple:
- create a dedicated Postgres database only for demo data
- run
npm run demo:seedagainst that database once - deploy the app to Vercel with
DATABASE_URLpointing to the demo database - set
GITHUB_ORGto the demo org login you seeded, for exampledemo-org
Recommended database options:
Vercel Postgresif you want the easiest Vercel-native setupNeonif you want a simple serverless Postgres with good free-tier ergonomics
The application uses a standard PostgreSQL connection string, so either option works without code changes.
/overview/peoplepeople workspace/people/[login]user detail/repositoriesrepositories workspace/repositories/[name]repository detail/api/dashboardoverview payload API/api/bootstrap90-day bootstrap API/api/syncsync API/api/backfillcoverage planning API/api/discoverydiscovery API/api/aggregates/rebuildaggregate rebuild API/api/export/useruser export API
Typical first-time setup:
- configure the GitHub App and environment variables
- start PostgreSQL
- install dependencies
- run migrations
- start the app
- run the 90-day bootstrap
- switch the range to
90dand inspect the workspace views
Typical ongoing usage:
- open a dashboard view
- inspect current local data
- run targeted sync or bootstrap when coverage is missing or stale
- rebuild aggregates when needed
Recommended checks during development:
npm run typecheck
npm run lintsrc/app Next.js routes and API handlers
src/components/dashboard workspace UI surfaces
src/features/dashboard dashboard data shaping and workspace models
src/lib/github GitHub App auth and remote fetchers
src/lib/db schema, persistence and aggregate rebuilds
src/lib/backfill coverage planning and gap detection
src/i18n next-intl setup and message catalogs
scripts CLI wrappers for sync/bootstrap/rebuild flows
drizzle SQL migrations and schema snapshots
docs project and setup documentation