GitHub Issues as a Database: A Fun Go Project
Can you store app data in GitHub issues and comments? A playful deep dive into issues-as-a-datastore, four wild things you could build with it, and why it makes a great real-world Go project.
Here is a thought experiment that turned into a genuine weekend rabbit hole: can you use GitHub issues and comments as your primary database?
Every developer has had this thought at 1 AM.
“I have data. I need somewhere to put it. I already have GitHub. Issues support labels, comments, reactions, mentions, and state changes. That is… kind of a database, right?”
The responsible senior engineer answer is: “No. Do not do that.”
The fun engineer answer is: “Hold my chai.”
In this post, we will map SQL concepts onto GitHub primitives, look at where the abstraction falls apart, explore four genuinely wild architectures you could build with it, and see why building this in Go is a great portfolio project.
Key Takeaways
- An issue can act like a database row: the title is your primary field, the body holds structured JSON, labels act as indexes, and comments become an append-only event log.
- It is terrible for high-throughput production: API rate limits, eventual consistency through webhooks, and zero multi-row transactions make it unsuitable for transactional workloads.
- It is fantastic for human-scale apps: guestbooks, changelogs, public voting boards, and status pages.
- Building an issues-as-a-datastore driver in Go teaches real backend fundamentals: API client design, pagination, caching, HMAC webhooks, and concurrency.
- The wildest idea is not a database: it is using GitHub as a free, auditable, public message bus.
Mapping SQL concepts onto GitHub issues
Think of it like turning a notebook into a filing cabinet. The notebook was never meant to be one, but the compartments exist if you look closely.
| SQL Concept | GitHub Equivalent |
|---|---|
| Table | A GitHub repository |
| Row | An individual issue |
| Primary Key | Issue number (globally unique per repo) |
| Columns | Labels, or structured fields in the issue body |
| Index | Labels |
| UPDATE | Edit the issue body |
| Soft Delete | Close the issue |
| Event Log | Issue comments |
| Reactions | Aggregatable counters (votes!) |
| Foreign Key | Mention another issue with #123 |
The structured JSON trick is the core mechanism. You keep a clean JSON envelope in the issue body:
{
"type": "guestbook-entry",
"author": "priya",
"message": "cool site!",
"created_at": "2026-08-25T21:04:00Z"
}
Your app reads the issue, parses the fenced JSON block from the body, and there is your row.
Comments become the audit trail for free. If someone edits the body, GitHub keeps the full revision history automatically. You inherit versioning without writing a single line of database migration code.
Where it gloriously falls apart
Let us be honest about the failure modes, because they are where the real learning happens.
1. Rate limits. The GitHub REST API allows 5,000 requests per hour per authenticated user (GitHub REST API Rate Limits, retrieved 2026-08-25). That sounds generous until your homepage gets featured on Hacker News and receives 50 visits a second. Caching is not optional; it is survival.
2. Zero ACID transactions. Updating two issues requires two distinct API calls. If the second call fails, your “database” is now in an inconsistent state. You end up writing compensating logic, which is the exact distributed-systems headache you were trying to avoid.
3. Eventual consistency via webhooks. If you cache data locally and update it via webhook events, readers will see stale data between the write and the webhook delivery. That is fine for a personal guestbook, but disastrous for anything involving money.
Wild Architecture #1: GitHub as a public message bus
This is my favorite concept, because it stops being a joke halfway through.
A comment on an issue is an append-only, timestamped, publicly readable event. Webhooks push that event to any subscriber within moments. That is the exact definition of a pub/sub message broker.
Picture it:
- One repo called
events. Each microservice owns an issue titledservice:payments. - Services publish by posting a comment containing an event payload.
- Consumers subscribe via webhook or polling, tracking their offset using the last processed comment ID.
- Need replay? Read the issue history from comment #1.
You would never run real payment transactions on this. But think about what you learn building it: consumer offsets, at-least-once delivery, poison message handling, and backpressure. It is Kafka’s mental model with a free control plane.
Wild Architecture #2: Voting and reaction-based aggregation
Reactions are the sleeper feature.
Every issue and comment supports a fixed set of emoji reactions, and the GitHub API exposes counts for each. That gives you a free, spam-resistant voting system where GitHub handles identity and deduplication.
You can build a public feature-request board on top:
- Users open issues instead of filling a form.
- The 👍 reaction count becomes the vote tally.
- Labels like
status/plannedrepresent your roadmap states. - The GitHub Projects view becomes your Kanban board.
Why this is secretly a great Go portfolio project
Say you want a real-world Go project that is not another generic todo CLI.
Building an issues-as-a-datastore layer forces you to touch the parts of Go that matter in production engineering:
package store
import (
"context"
"sync"
"github.com/google/go-github/v60/github"
)
type Store struct {
client *github.Client
owner string
repo string
cache sync.Map // issue number -> parsed row
}
func (s *Store) Get(ctx context.Context, num int) (*Row, error) {
if v, ok := s.cache.Load(num); ok {
return v.(*Row), nil
}
issue, _, err := s.client.Issues.Get(ctx, s.owner, s.repo, num)
if err != nil {
return nil, err
}
row := parseRow(issue)
s.cache.Store(num, row)
return row, nil
}
In this small package, you practice:
- API client design: Wrapping
go-github, handling pagination, and retrying secondary rate limits with exponential backoff. - Concurrency: Fan-out reads across issues with a bounded worker pool, then merge results with channels.
- Caching discipline: In-memory maps plus webhook-driven refresh.
- Webhook verification: Verifying HMAC-SHA256 signatures, handling redeliveries, and deduplicating events.
Summary
For production data holding money, customer identity, or strict business promises: do not do this.
For everything else, apply this test: would a human benefit from seeing this data in a GitHub UI?
Guestbook entries, feature voting, public status logs, and deploy histories all pass that test.
And even if you never run it in production, building a toy version will teach you more practical Go than three generic tutorials. Pick an idea, make a repository, and give an issue tracker a career change it never asked for.