← Back to articles

Building with NextJS Pt.3 (Syncing GitHub Issues because Jira is Terrible)

NextJSGoGitHub APIFrontend

The Inevitable Frontend Collision

In Part 2, I waxed poetic about the boring, beautiful stability of a Go standard library backend. Now, it’s time to pay the Piper. We are back in the land of JavaScript, state management, and the ever-shifting sands of Next.js App Router.

My goal for this phase was simple: build a dashboard to manage Projects and Tasks, and then execute the real reason I’m building this app—syncing GitHub issues directly into my workflow so I don’t have to use another bloated enterprise project management tool.

Shadcn to the Rescue

I stand by my original statement: I am not spending 50% of my time tweaking button padding. Enter shadcn/ui.

If you haven’t used it, shadcn/ui isn’t a component library you install via npm; it’s a collection of beautifully designed React components that you copy and paste (via a CLI) directly into your project. You own the code.

npx shadcn@latest add dialog form input select table card

In ten seconds, I had accessible, nicely styled components. The Next.js pages themselves are standard React. I created a main /dashboard to list Projects using cards, and a dynamic /projects/[id] route to show the tasks for a specific project in a clean table.

I hooked these up using the plain fetchClient wrapper I showed off in Part 2. No heavy data-fetching libraries. Just useEffect, useState, and fetch. Is it the most perfectly optimized caching strategy in the world? No. Does it work flawlessly for a single-user task manager? Absolutely.

The Real Work: The GitHub API

Now for the fun part. The premise of this tool is that I manage multiple repositories and want my GitHub issues to sync down as tasks in my app.

First, I had to admit I missed something in my initial “perfect” database schema (I literally predicted this in Part 2). My projects table needed a github_repo column, and my users table needed a place to store a github_token (Personal Access Token) so I don’t get immediately rate-limited by GitHub’s API.

Back to Go for a quick migration:

ALTER TABLE projects ADD COLUMN github_repo TEXT;
ALTER TABLE users ADD COLUMN github_token TEXT;

With the schema updated, I wrote a new handler in Go: POST /projects/{id}/sync-github.

The logic is straightforward but satisfying:

  1. Grab the user’s github_token from the database.
  2. Fetch the github_repo string (e.g., mohammedeabdelaziz/go-next) from the project.
  3. Make a standard http.NewRequest to https://api.github.com/repos/{repo}/issues.
  4. Parse the JSON response.

The Upsert Dilemma

The tricky part of syncing external data is avoiding duplicates without accidentally destroying local changes. If an issue exists in GitHub, I want to pull it down. But if I’ve already pulled it down and modified its priority locally, I don’t want the sync to overwrite my local priority.

I handled this by looping through the fetched GitHub issues and checking them against the tasks already in the database for that project (matching on github_issue_id).

if existingTask, ok := issueMap[issue.ID]; ok {
    // Update existing task (Title, Body, Status)
    // ... but leave my custom local Priority alone!
    db.UpdateTask(existingTask)
} else {
    // Insert brand new task
    db.CreateTask(newTask)
}

Hooking it up

Back on the frontend, I added a “Settings” modal to the dashboard where I can paste my GitHub Personal Access Token. Then, on the Project Details page, if a project has a linked repository, a shiny new “Sync GitHub Issues” button appears.

You click it. The Next.js client fires a request to the Go backend. Go talks to GitHub, does the deduplication math, updates SQLite, and returns the fresh tasks. React state updates, the table re-renders.

It’s fast. It’s entirely decoupled. And best of all, I now have my GitHub issues sitting right next to my personal to-dos in a UI I actually control.

Next Steps

The app is functionally complete for basic use. I can create projects, manage tasks, and sync my code issues. But remember Rule #1 from Part 1? I am definitely building a mobile app.

In the next part, we’ll see if this “decoupled API” architecture pays off when we try to wrap this exact same backend with a Tauri or Capacitor mobile shell.

Stay tuned.

© 2026 Mohammed Essam. All rights reserved.