A web application to view and export GitHub issues and pull requests, deployed on Cloudflare Workers.
- 🔐 Secure GitHub OAuth authentication
- 🔒 HTTP-only encrypted cookies with HMAC signatures
- 💾 Server-side token storage in Cloudflare KV
- 🌐 Web Crypto API for session management
- 🔑 Support for both public and private repositories
- 📊 View issues and pull requests in separate, sortable tables
- 🔍 Search within tables and filter by state (open, closed, all)
- 📄 Pagination with configurable rows per page (25, 50, 100)
- 🏷️ Type column showing "Issue" or "Pull Request"
- 📝 Description column (truncated to 25 chars, full text on hover and in CSV)
- 🎯 Click any row to open issue/PR on GitHub in new tab
- 📥 Export all pages to CSV (not just current page)
- 💾 Automatic background fetching of all data
- 📊 All columns included in export
- 💿 State persistence (selected org/repo saved to localStorage)
- 👤 Personal GitHub account shown first
- 📋 Organizations sorted alphabetically
- 🕒 Repositories sorted by last modified (most recent first)
- 🔄 Auto-rebuild on file changes during development
- 🎨 Clean, minimal UI with Pico CSS
- Node.js 18+ and npm/pnpm/yarn
- A Cloudflare account
- A GitHub account
- Go to GitHub Developer Settings
- Click "New OAuth App"
- Fill in the details:
- Application name: GitHub Issues Viewer (or your preferred name)
- Homepage URL:
http://localhost:8787(for development) - Authorization callback URL:
http://localhost:8787/auth/github/callback
- Click "Register application"
- Note your Client ID
- Generate a new Client Secret and note it
For production, you'll need to update these URLs to your Cloudflare Worker domain.
npm installGet your Cloudflare Account ID and API Token:
- Log in to Cloudflare Dashboard
- Copy your Account ID from the right sidebar (Workers & Pages overview)
- Create an API Token:
- Go to API Tokens
- Click "Create Token"
- Use "Edit Cloudflare Workers" template
- Select your account and click "Continue to summary"
- Click "Create Token" and copy it
Add these to your .dev.vars file along with the other credentials.
Create a KV namespace for session storage:
# Create production namespace
npm run kv:create
# Create preview namespace for development
npm run kv:create:previewUpdate wrangler.jsonc with the IDs returned from these commands:
Create a .dev.vars file in the root directory (copy from .dev.vars.example):
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
SESSION_SECRET=your_random_secret_key_at_least_32_chars
CLOUDFLARE_ACCOUNT_ID=your_cloudflare_account_id
CLOUDFLARE_API_TOKEN=your_cloudflare_api_tokenFor production deployment, set these as Wrangler secrets (GITHUB vars only, not Cloudflare vars):
npm run secret:put GITHUB_CLIENT_ID
npm run secret:put GITHUB_CLIENT_SECRET
npm run secret:put SESSION_SECRETNote: Generate a strong random string for SESSION_SECRET. You can use:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Or use the link in .dev.vars.example to generate one online.
Start the development server:
npm run devThis single command will:
- Build the application
- Start the Wrangler development server
- Watch for file changes and auto-rebuild
- Load your
.dev.varsenvironment variables
The application will be available at http://localhost:8787
When you edit files (.js, .jsx, .css), the app auto-rebuilds. Just refresh your browser to see changes!
Set your GitHub credentials and session secret as Cloudflare secrets (these are separate from .dev.vars):
# Set GitHub OAuth credentials (from production OAuth app)
npm run secret:put GITHUB_CLIENT_ID
# Enter your GitHub Client ID when prompted
npm run secret:put GITHUB_CLIENT_SECRET
# Enter your GitHub Client Secret when prompted
# Generate and set SESSION_SECRET (REQUIRED!)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Copy the output, then:
npm run secret:put SESSION_SECRET
# Paste the generated secret when promptedSESSION_SECRET must be set in production or authentication will fail with an HMAC error. Generate a strong random secret using the command above.
Create a new GitHub OAuth App for production (or update your existing one):
- Homepage URL:
https://github-issues-viewer.your-subdomain.workers.dev - Authorization callback URL:
https://github-issues-viewer.your-subdomain.workers.dev/auth/github/callback
Use the Client ID and Secret from this production OAuth app when setting the secrets above.
npm run deployThe deploy script will automatically build and deploy your application using the Cloudflare credentials from your .dev.vars file.
To view real-time logs from your deployed worker:
npm run api:tailThis connects to your production worker and streams logs, useful for debugging authentication issues or API errors.
github-issues-viewer/
├── src/
│ ├── client/ # React frontend
│ │ ├── components/ # React components
│ │ │ ├── LoginPage.jsx # GitHub login page
│ │ │ ├── OrgSelector.jsx # Organization selector (sorted)
│ │ │ ├── RepoSelector.jsx # Repository selector (by last modified)
│ │ │ ├── IssuesView.jsx # Issues table with export
│ │ │ ├── PullRequestsView.jsx # PRs table with export
│ │ │ └── DataTable.jsx # Reusable sortable table
│ │ ├── utils/
│ │ │ └── csv.js # CSV export utility
│ │ ├── styles.css # All CSS (auto-injected at build time)
│ │ ├── App.jsx # Main app with localStorage persistence
│ │ └── index.jsx # React entry point
│ └── worker/ # Cloudflare Worker
│ ├── routes/
│ │ ├── auth.js # GitHub OAuth with Web Crypto API
│ │ └── api.js # GitHub API proxy with User-Agent
│ ├── utils/
│ │ ├── html.js # HTML template helper
│ │ └── session.js # Session management (Web Crypto)
│ └── index.js # Worker entry point
├── scripts/
│ ├── wrangler.js # Wrangler wrapper with env loading
│ └── watch.js # File watcher for auto-rebuild
├── build.js # Build script (bundles JS + CSS)
├── package.json
├── wrangler.jsonc # Cloudflare Worker config
├── README.md # This file
├── QUICKSTART.md # 5-minute setup guide
└── DEV-GUIDE.md # Development workflow guide
- User clicks "Sign in with GitHub"
- App redirects to GitHub OAuth
- User authorizes the application
- GitHub redirects back with an authorization code
- Worker exchanges code for access token
- Worker creates encrypted session using Web Crypto API
- Session stored in Cloudflare KV with HMAC-signed cookie
- HTTP-only secure cookie set
- User is redirected to the application
- Frontend makes authenticated requests to
/api/*endpoints - Worker verifies HMAC-signed session cookie
- Worker retrieves access token from Cloudflare KV
- Worker proxies requests to GitHub API with User-Agent header
- GitHub returns paginated data
- Worker returns data to frontend
- Frontend renders in sortable, filterable tables
- User can:
- Search within tables
- Sort by clicking column headers
- Filter by state (open/closed/all)
- Paginate with configurable page size
- Export all pages to CSV
- Click rows to open on GitHub
- React app bundled to
dist/app.js - CSS read from
src/client/styles.css, minified - Worker bundled to
dist/worker.jswith embedded JS & CSS - Watch mode monitors files and auto-rebuilds on changes
- Worker serves HTML with inline CSS and JS
- Web Crypto API: Modern cryptographic operations
- HTTP-only cookies: Prevents XSS attacks from stealing tokens
- Secure flag: Cookies only sent over HTTPS in production
- HMAC signatures: Session cookies cryptographically signed with SESSION_SECRET
- Server-side token storage: Access tokens never exposed to client
- Cloudflare KV: Distributed, secure session storage
- 30-day session expiration: Automatic cleanup of old sessions
- User-Agent required: All GitHub API requests include proper headers
- Navigate to
http://localhost:8787 - Click "Sign in with GitHub"
- Authorize the application
- Verify you're redirected back and see your username
- Refresh the page - verify you stay logged in
- After authentication, verify your personal account appears first
- Verify other organizations are sorted alphabetically
- Try selecting your personal account
- Try selecting an organization you're a member of
- Try entering a custom organization name
- Refresh the page - verify your selection persists
- Select an organization
- Verify repositories are sorted by last modified (newest first)
- Use the search to filter repositories
- Select a repository with issues and pull requests
- Refresh the page - verify your selection persists
- Verify issues are displayed in a table
- Verify Type column shows "Issue"
- Verify Description column shows truncated text (25 chars)
- Hover over description - verify full text appears
- Test the state filter (open/closed/all)
- Test rows per page selector (25, 50, 100)
- Test sorting by clicking column headers
- Test searching within the table
- Click on an issue row - verify it opens GitHub in new tab
- Test pagination if the repo has many issues
- Switch to the "Pull Requests" tab
- Verify Type column shows "Pull Request"
- Repeat all tests from Issues View
- Verify pull requests (not issues) are displayed
- Select a repo with many issues/PRs (more than one page)
- Click "Export All to CSV"
- Verify button shows "Exporting all pages..." during export
- Open the downloaded CSV file
- Verify all columns are present: Number, Type, Title, Description, Author, Labels, Created Date, Status, Assignees, URL
- Verify Description shows full text (not truncated)
- Verify all pages of data are included (not just current page)
- Verify filename includes count (e.g.,
owner-repo-issues-open-142-items.csv) - Verify data is properly escaped (commas, quotes, newlines)
- Select an organization and repository
- Navigate through issues/PRs
- Refresh the browser
- Verify you're still on the same org/repo (not back to org selection)
- Click "Change Repository" - verify it clears the selection
- Logout - verify selection is cleared
- Run
npm run dev - Edit
src/client/styles.css(change a color) - Verify terminal shows "🔨 Building..." then "✅ Build complete"
- Refresh browser - verify changes appear
- Edit a
.jsxfile - Verify auto-rebuild happens
- Refresh browser - verify changes appear
Development:
- Check that your GitHub OAuth credentials are correct in
.dev.vars - Verify the callback URL exactly matches in GitHub settings:
http://localhost:8787/auth/github/callback - Check that
SESSION_SECRETis set in.dev.vars - Clear your browser cookies and try again
- Check terminal for detailed error messages
Production:
- Verify
SESSION_SECRETis set as a Wrangler secret:npm run secret:put SESSION_SECRET - Check that production GitHub OAuth credentials are set correctly
- Verify callback URL in GitHub OAuth app matches your production domain
- Use
npm run api:tailto view real-time production logs
DataError: Imported HMAC key length (0) must be a non-zero value...
This means SESSION_SECRET is not set in production. Fix:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
npm run secret:put SESSION_SECRET
# Paste the generated secret- This was a missing
User-Agentheader issue - should be fixed in current version - If you see this, verify you're running the latest build
- Verify you have access to repositories in the selected organization
- Check browser console (F12) for API errors
- Check terminal for server-side errors
- Try selecting your personal account instead
- Ensure you're using Node.js 18+
- Delete
node_modules,dist, and.wranglerfolders - Run
npm installagain - Check that all dependencies installed successfully
- Already handled via npm overrides in
package.json - Sharp is replaced with
noop-packagesince we don't need image processing - If you still see errors, check
package.jsonhas:"overrides": { "sharp": "npm:noop-package@^1.0.0" }
- Verify KV namespace IDs in
wrangler.jsoncare correct (not "placeholder") - Make sure you've created both production and preview namespaces
- Check that SESSIONS binding is correctly configured
- Run
npm run kv:createandnpm run kv:create:previewif needed
- Check that
scripts/watch.jsexists and is executable - Verify you're editing files in
src/client/orsrc/worker/ - Check terminal for build errors
- Try stopping (
Ctrl+C) and restartingnpm run dev
- Check browser console → Application tab → LocalStorage
- Look for
selectedOrgandselectedRepoentries - If missing, check for browser privacy settings blocking localStorage
- Try in a different browser
- Check that
src/client/styles.cssexists - Run
npm run buildmanually and checkdist/worker.jscontains CSS - View page source in browser - look for
<style>tag after Pico CSS link - Clear browser cache and hard reload (Ctrl+Shift+R / Cmd+Shift+R)
GET /auth/github- Start GitHub OAuth flowGET /auth/github/callback- OAuth callback (exchanges code for token)GET /auth/user- Get current user (returns user info or 401)POST /auth/logout- Logout (clears session cookie and KV)
GET /api/orgs- Get user's organizations (personal account + orgs)GET /api/orgs/:org/repos- Get repositories for an organization (max 100)GET /api/repos/:owner/:repo/issues?state=open&page=1&per_page=30- Get issues- Query params:
state(open/closed/all),page,per_page - Returns:
{ data: [...], pagination: { prev, next, first, last } } - Filters out pull requests from response
- Query params:
GET /api/repos/:owner/:repo/pulls?state=open&page=1&per_page=30- Get pull requests- Query params:
state(open/closed/all),page,per_page - Returns:
{ data: [...], pagination: { prev, next, first, last } }
- Query params:
Note: All API endpoints include User-Agent: GitHub-Issues-Viewer header as required by GitHub API.
MIT
Contributions are welcome! Please feel free to submit a Pull Request.