GitHub Issues Integration
Building a GitHub Issues Integration from Scratch: HagiCode’s Frontend Direct Connection Practice
Section titled “Building a GitHub Issues Integration from Scratch: HagiCode’s Frontend Direct Connection Practice”This article documents the full process of integrating GitHub Issues into the HagiCode platform. We will explore how to use a “frontend direct connection + minimal backend” architecture to achieve secure OAuth authentication and efficient issue synchronization while keeping the backend lightweight.
Background: Why Integrate GitHub?
Section titled “Background: Why Integrate GitHub?”As an AI-assisted development platform, HagiCode’s core value lies in connecting ideas with implementation. But in actual use, we found that after users complete a Proposal in HagiCode, they often need to manually copy the content into GitHub Issues for project tracking.
This creates several obvious pain points:
- Fragmented workflow: Users need to switch back and forth between two systems. The experience is not smooth, and key information can easily be lost during copy and paste.
- Inconvenient collaboration: Other team members are used to checking tasks on GitHub and cannot directly see proposal progress inside HagiCode.
- Repeated manual work: Every time a proposal is updated, someone has to manually update the corresponding issue on GitHub, adding unnecessary maintenance cost.
To solve this problem, we decided to introduce the GitHub Issues Integration feature, connecting HagiCode sessions with GitHub repositories to enable “one-click sync.”
About HagiCode
Section titled “About HagiCode”Hey, let us introduce what we are building
We are building HagiCode — an AI-powered coding assistant that makes development smarter, easier, and more enjoyable.
Smarter — AI assists throughout the entire journey, from idea to code, multiplying development efficiency. Easier — Multi-threaded concurrent operations make full use of resources and keep the development workflow smooth. More enjoyable — Gamification and an achievement system make coding less tedious and more rewarding.
The project is iterating quickly. If you are interested in technical writing, knowledge management, or AI-assisted development, welcome to check us out on GitHub~
Technical Choice: Frontend Direct Connection vs Backend Proxy
Section titled “Technical Choice: Frontend Direct Connection vs Backend Proxy”When designing the integration approach, we had two options in front of us: the traditional “backend proxy model” and the more aggressive “frontend direct connection model.”
Solution Comparison
Section titled “Solution Comparison”In the traditional backend proxy model, every request from the frontend must first go through our backend, which then calls the GitHub API. This centralizes the logic, but it also puts a significant burden on the backend:
- Bloated backend: We would need to write a dedicated GitHub API client wrapper and also handle the complex OAuth state machine.
- Token risk: The user’s GitHub token would have to be stored in the backend database. Even with encryption, this still increases the security surface.
- Development cost: We would need database migrations to store tokens and an additional synchronization service to maintain.
The frontend direct connection model is much lighter. In this approach, we use the backend only for the most sensitive “secret exchange” step (the OAuth callback). After obtaining the token, we store it directly in the browser’s localStorage. Later operations such as creating issues and updating comments are sent directly from the frontend to GitHub over HTTP.
| Comparison Dimension | Backend Proxy Model | Frontend Direct Connection Model |
|---|---|---|
| Backend complexity | Requires a full OAuth service and GitHub API client | Only needs an OAuth callback endpoint |
| Token management | Must be encrypted and stored in the database, with leakage risk | Stored in the browser and visible only to the user |
| Implementation cost | Requires database migrations and multi-service development | Primarily frontend work |
| User experience | Centralized logic, but server latency may be slightly higher | Extremely fast response with direct GitHub interaction |
Because we wanted rapid integration and minimal backend changes, we ultimately chose the “frontend direct connection model”. It is like giving the browser a “temporary pass.” Once it gets the pass, the browser can go handle things on GitHub by itself without asking the backend administrator for approval every time.
Core Design: Data Flow and Security
Section titled “Core Design: Data Flow and Security”After settling on the architecture, we needed to design the specific data flow. The core of the synchronization process is how to obtain the token securely and use it efficiently.
Overall Architecture Diagram
Section titled “Overall Architecture Diagram”The whole system can be abstracted into three roles: the browser (frontend), the HagiCode backend, and GitHub.
+--------------+ +--------------+ +--------------+| Frontend | | Backend | | GitHub || React | | ASP.NET | | REST API || | | | | || +--------+ | | | | || | OAuth |--+--------> /callback | | || | Flow | | | | | || +--------+ | | | | || | | | | || +--------+ | | +--------+ | | +--------+ || | GitHub | +------------> Session | +----------> Issues | || | API | | | |Metadata| | | | | || | Direct | | | +--------+ | | +--------+ || +--------+ | | | | |+--------------+ +--------------+ +--------------+The key point is: only one small step in OAuth (exchanging the code for a token) needs to go through the backend. After that, the heavy lifting (creating issues) is handled directly between the frontend and GitHub.
Synchronization Data Flow in Detail
Section titled “Synchronization Data Flow in Detail”When the user clicks the “Sync to GitHub” button in the HagiCode UI, a series of complex actions takes place:
User clicks "Sync to GitHub" │ ▼1. Frontend checks localStorage for the GitHub token │ ▼2. Format issue content (convert the Proposal into Markdown) │ ▼3. Frontend directly calls the GitHub API to create/update the issue │ ▼4. Call the HagiCode backend API to update Session.metadata (store issue URL and other info) │ ▼5. Backend broadcasts the SessionUpdated event via SignalR │ ▼6. Frontend receives the event and updates the UI to show the "Synced" stateSecurity Design
Section titled “Security Design”Security is always the top priority when integrating third-party services. We made the following considerations:
- Defend against CSRF attacks: Generate a random
stateparameter during the OAuth redirect and store it insessionStorage. Strictly validate the state in the callback to prevent forged requests. - Isolated token storage: The token is stored only in the browser’s
localStorage. Using the Same-Origin Policy, only HagiCode scripts can read it, avoiding the risk of a server-side database leak affecting users. - Error boundaries: We designed dedicated handling for common GitHub API errors (such as 401 expired token, 422 validation failure, and 429 rate limiting), so users receive friendly feedback.
In Practice: Implementation Details in Code
Section titled “In Practice: Implementation Details in Code”Theory only goes so far. Let us look at how the code actually works.
1. Minimal Backend Changes
Section titled “1. Minimal Backend Changes”The backend only needs to do two things: store synchronization information and handle the OAuth callback.
Database changes
We only need to add a Metadata column to the Sessions table to store extension data in JSON format.
-- Add metadata column to Sessions tableALTER TABLE "Sessions" ADD COLUMN "Metadata" text NULL;Entity and DTO definitions
public class Session : AuditedAggregateRoot<SessionId>{ // ... other properties ...
/// <summary> /// JSON metadata for storing extension data like GitHub integration /// </summary> public string? Metadata { get; set; }}
// DTO definition for easier frontend serializationpublic class GitHubIssueMetadata{ public required string Owner { get; set; } public required string Repo { get; set; } public int IssueNumber { get; set; } public required string IssueUrl { get; set; } public DateTime SyncedAt { get; set; } public string LastSyncStatus { get; set; } = "success";}
public class SessionMetadata{ public GitHubIssueMetadata? GitHubIssue { get; set; }}2. Frontend OAuth Flow
Section titled “2. Frontend OAuth Flow”This is the entry point of the connection. We use the standard Authorization Code Flow.
// Generate the authorization URL and redirectexport async function generateAuthUrl(): Promise<string> { const state = generateRandomString(); // Generate a random string for CSRF protection sessionStorage.setItem('hagicode_github_state', state);
const params = new URLSearchParams({ client_id: clientId, redirect_uri: window.location.origin + '/settings?tab=github&oauth=callback', scope: ['repo', 'public_repo'].join(' '), state: state, });
return `https://github.com/login/oauth/authorize?${params.toString()}`;}
// Handle the code-to-token exchange on the callback pageexport async function exchangeCodeForToken(code: string, state: string): Promise<GitHubToken> { // 1. Validate state to prevent CSRF const savedState = sessionStorage.getItem('hagicode_github_state'); if (state !== savedState) throw new Error('Invalid state parameter');
// 2. Call the backend API to exchange the token // Note: this must go through the backend because ClientSecret cannot be exposed to the frontend const response = await fetch('/api/GitHubOAuth/callback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code, state, redirectUri: window.location.origin + '/settings?tab=github&oauth=callback' }), });
if (!response.ok) throw new Error('Failed to exchange token');
const token = await response.json();
// 3. Save into LocalStorage saveToken(token); return token;}3. GitHub API Client Wrapper
Section titled “3. GitHub API Client Wrapper”Once we have the token, we need a solid tool for calling the GitHub API.
const GITHUB_API_BASE = 'https://api.github.com';
// Core request wrapperasync function githubApi<T>(endpoint: string, options: RequestInit = {}): Promise<T> { const token = localStorage.getItem('gh_token'); if (!token) throw new Error('Not connected to GitHub');
const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, { ...options, headers: { ...options.headers, Authorization: `Bearer ${token}`, Accept: 'application/vnd.github.v3+json', // Specify the API version }, });
// Error handling logic if (!response.ok) { if (response.status === 401) throw new Error('GitHub token expired, please reconnect'); if (response.status === 403) throw new Error('No permission to access this repository or rate limit exceeded'); if (response.status === 422) throw new Error('Issue validation failed, the title may be duplicated'); throw new Error(`GitHub API Error: ${response.statusText}`); }
return response.json();}
// Create issueexport async function createIssue(owner: string, repo: string, data: { title: string, body: string, labels: string[] }) { return githubApi(`/repos/${owner}/${repo}/issues`, { method: 'POST', body: JSON.stringify(data), });}4. Content Formatting and Synchronization
Section titled “4. Content Formatting and Synchronization”The final step is to convert HagiCode session data into the format of a GitHub issue. It is a bit like translation work.
// Convert a Session object into a Markdown stringfunction formatIssueForSession(session: Session): string { let content = `# ${session.title}\n\n`; content += `**> HagiCode Session:** #${session.code}\n`; content += `**> Status:** ${session.status}\n\n`; content += `## Description\n\n${session.description || 'No description provided.'}\n\n`;
// If this is a Proposal session, add extra fields if (session.type === 'proposal') { content += `## Chief Complaint\n\n${session.chiefComplaint || ''}\n\n`; // Add a deep link so users can jump back from GitHub to HagiCode content += `---\n\n**[View in HagiCode](hagicode://sessions/${session.id})**\n`; }
return content;}
// Main logic when clicking the sync buttonconst handleSync = async (session: Session) => { try { const repoInfo = parseRepositoryFromUrl(session.repoUrl); // Parse the repository URL if (!repoInfo) throw new Error('Invalid repository URL');
toast.loading('Syncing to GitHub...');
// 1. Format content const issueBody = formatIssueForSession(session);
// 2. Call API const issue = await githubApiClient.createIssue(repoInfo.owner, repoInfo.repo, { title: `[HagiCode] ${session.title}`, body: issueBody, labels: ['hagicode', 'proposal', `status:${session.status}`], });
// 3. Update Session Metadata (save the issue link) await SessionsService.patchApiSessionsSessionId(session.id, { metadata: { githubIssue: { owner: repoInfo.owner, repo: repoInfo.repo, issueNumber: issue.number, issueUrl: issue.html_url, syncedAt: new Date().toISOString(), } } });
toast.success('Synced successfully!'); } catch (err) { console.error(err); toast.error('Sync failed, please check your token or network'); }};Summary and Outlook
Section titled “Summary and Outlook”With this “frontend direct connection” approach, we achieved seamless GitHub Issues integration with the least possible backend code.
Key gains
Section titled “Key gains”- High development efficiency: Backend changes are minimal, mainly one extra database field and a simple OAuth callback endpoint. Most logic is completed on the frontend.
- Strong security: The token does not pass through the server database, reducing leakage risk.
- Great user experience: Requests are initiated directly from the frontend, so response speed is fast and there is no need for backend forwarding.
Things to watch out for
Section titled “Things to watch out for”There are a few pitfalls to keep in mind during real deployment:
- OAuth App settings: Remember to enter the correct
Authorization callback URLin your GitHub OAuth App settings (usuallyhttp://localhost:3000/settings?tab=github&oauth=callback). - Rate limits: GitHub API limits unauthenticated requests quite strictly, but with a token the quota is usually sufficient (5000 requests/hour).
- URL parsing: Users enter all kinds of repository URLs, so make sure your regex can match
.gitsuffixes, SSH formats, and similar cases.
Future enhancements
Section titled “Future enhancements”The current feature is still one-way synchronization (HagiCode -> GitHub). In the future, we plan to implement two-way synchronization through GitHub Webhooks. For example, if an issue is closed on GitHub, the session state on the HagiCode side could also update automatically. That will require us to expose a webhook endpoint on the backend, which will be an interesting next step.
We hope this article gives you a bit of inspiration for your own third-party integration development. If you have questions, feel free to open an issue for discussion on HagiCode GitHub.