A production-ready Go backend REST API that connects GitHub accounts, surfaces user profiles and repositories, manages repository subscriptions, and delivers pull-request notifications through a RabbitMQ-backed async pipeline.
- Prerequisites
- GitHub OAuth App Setup
- Configuration
- Running with Docker (recommended)
- Running Locally (without Docker)
- Using the Frontend
- API Reference
- Testing the OAuth Flow (API)
- Webhook Setup
- Makefile Reference
- Project Structure
| Requirement | Version | Notes |
|---|---|---|
| Docker Desktop | Latest | Required — all infrastructure (Postgres, Redis, RabbitMQ) runs in containers |
| Go | 1.26.3+ | Only needed if running outside Docker |
| GitHub account | — | Needed to create an OAuth App and test the auth flow |
Note: If you only plan to run the service through Docker Compose, you do not need Go installed locally.
-
Open https://github.com/settings/applications/new in your browser.
-
Fill in the form:
Field Value Application name github-integration-local(or anything you like)Homepage URL http://localhost:8080Authorization callback URL http://localhost:8080/auth/github/callback/index.htmlThe callback URL points to
index.htmlbecause the frontend handles the OAuth code exchange on page load. If you are using the API flow without the frontend, set it tohttp://localhost:8080/api/auth/github/callbackinstead. -
Click Register application.
-
On the next page:
- Copy the Client ID — you will need it for
github.client_idinconfig.yaml. - Click Generate a new client secret, then copy the generated value — you will need it for
github.client_secret.
- Copy the Client ID — you will need it for
⚠️ The client secret is shown once. Save it immediately.
All configuration lives in config.yaml at the project root. The file is mounted read-only into the Docker container; environment variables set in docker-compose.yml override infra connection values (host, port, credentials) automatically via Viper.
You must set the following fields before the service will start correctly:
| Key | How to generate / where to find |
|---|---|
github.client_id |
From your GitHub OAuth App page |
github.client_secret |
From your GitHub OAuth App page |
github.oauth_redirect_url |
http://localhost:8080/auth/github/callback (matches OAuth App) |
jwt.private_key |
openssl genrsa -out private.pem 2048 |
jwt.public_key |
penssl rsa -in private.pem -pubout -out public.pem |
encryption.token_key |
openssl rand -hex 32 (must be exactly 32 bytes / 64 hex chars) |
webhook.secret |
Set after creating the webhook in GitHub (see Webhook Setup) |
internal.service_key |
Any random string — used to authenticate POST /internal/notifications/pr |
Full config.yaml with placeholders:
server:
port: 8080
env: development
database:
host: localhost # overridden to "postgres" in Docker
port: 5432
name: github_int
user:
password:
sslmode: disable
max_open_conns: 25
max_idle_conns: 5
redis:
addr: localhost:6379 # overridden to "redis:6379" in Docker
password: ""
db: 0
rabbitmq:
url: amqp://guest:guest@localhost:5672/ # overridden in Docker
exchange: github.events
routing_key: pull_request.created
queue: email.pr.notifications
github:
client_id: "" # REQUIRED — from GitHub OAuth App
client_secret: "" # REQUIRED — from GitHub OAuth App
oauth_redirect_url: "http://localhost:8080/auth/github/callback"
jwt:
private_key: |
-----BEGIN RSA PRIVATE KEY-----
<paste private key content here>
-----END RSA PRIVATE KEY-----
public_key: |
-----BEGIN PUBLIC KEY-----
<paste public key content here>
-----END PUBLIC KEY-----
expiry_minutes: 15
encryption:
token_key: "" # REQUIRED — openssl rand -hex 32 (64 hex chars = 32 bytes)
webhook:
secret: "" # Set after creating the webhook in GitHub
app_url: "" # Publicly reachable URL, e.g. https://your-app.ngrok.io
internal:
service_key: "" # Any random string for POST /internal/notifications/prMake sure Docker Desktop is running, then:
# 1. Build the app Docker image
make build
# 2. Start PostgreSQL, Redis, RabbitMQ, and the app
make up
# 3. Apply the database migration (run once after first 'make up')
make migrate
# 4. Tail app logs
make logsThe app container waits for all three infrastructure services to pass their health checks before it starts. No manual ordering is required.
To stop everything:
make downData volumes (
pgdata,redisdata,rmqdata) are preserved acrossmake down/make upcycles. Usedocker compose down -vto wipe them.
Infrastructure management UIs (available while containers are running):
| Service | URL | Credentials |
|---|---|---|
| RabbitMQ Management | http://localhost:15672 | guest / guest |
Requires Go 1.26.3+ installed locally.
Either start the full stack with Docker Compose and stop only the app container, or start infra services individually:
docker compose up -d postgres redis rabbitmqpsql -h localhost -U postgres -d github_int -f migrations/001_initial_schema.sqlgo run ./cmd/main.goThe server starts on http://localhost:8080.
The service ships a minimal GitHub-themed single-page frontend served from the /frontend directory at http://localhost:8080.
| URL | Description |
|---|---|
/index.html |
Login page — initiates GitHub OAuth and handles the callback |
/landing.html |
Dashboard — profile summary + navigation cards |
/repos.html |
Browse your GitHub repositories; subscribe with one click |
/subscriptions.html |
View and manage active repo subscriptions |
/profile.html |
Full GitHub profile details (bio, stats, followers) |
Browser App (Go) GitHub
│ │ │
│ GET /index.html │ │
│────────────────────────────────>│ │
│<────────────────────────────────│ │
│ │ │
│ GET /api/auth/github/login │ │
│────────────────────────────────>│ │
│ { authorizeUrl } │ │
│<────────────────────────────────│ │
│ │ │
│ window.location = authorizeUrl │ │
│────────────────────────────────────────────────────────────>│
│ │ │
│ Redirect to /index.html │ │
│ ?code=...&state=... │ │
│<────────────────────────────────────────────────────────────│
│ │ │
│ GET /api/auth/github/callback │ │
│ ?code=...&state=... │ │
│────────────────────────────────>│ │
│ │ Exchange code for token │
│ │───────────────────────────>│
│ │ { access_token } │
│ │<───────────────────────────│
│ { accessToken, refreshToken, │ │
│ expiresAt, user } │ │
│<────────────────────────────────│ │
│ │ │
│ Store tokens in localStorage │ │
│ Redirect to /landing.html │ │
- Tokens are stored in
localStorage(accessToken,refreshToken,expiresAt). - A
scheduleRefresh()timer fires automatically 60 seconds beforeexpiresAt, callingPOST /api/auth/refreshand rotating both tokens. - On
401or410responses from any API call, the frontend clears localStorage and redirects to/index.html. - The back-button bfcache is handled via a
pageshowlistener — navigating back to a protected page after logout redirects to login.
- Open
http://localhost:8080(orhttp://localhost:8080/index.html). - Click Sign in with GitHub — you will be redirected to GitHub for authorization.
- After authorizing, you land on the Dashboard (
/landing.html). - Use the navigation cards to browse Repositories or manage Subscriptions.
- On the Repositories page, click Subscribe on any repo to receive PR notifications.
- Click Sign out (top-right) to log out of the current session, or use the API to log out all sessions.
All authenticated endpoints require an Authorization: Bearer <accessToken> header.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/auth/github/login |
None | Returns { authorizeUrl } — the GitHub OAuth authorization URL. Does not redirect. |
GET |
/api/auth/github/callback |
None | Exchanges the OAuth code for a GitHub token, upserts the user, and returns { accessToken, refreshToken, expiresAt, user }. |
POST |
/api/auth/refresh |
Bearer JWT | Validates the current JWT and refresh token, then issues a new access token and rotates the refresh token. Returns 401 for expired JWT, 410 for a blocked session. |
POST |
/api/auth/logout |
Bearer JWT | Blocks the current session, deletes the stored GitHub token, and invalidates the user's Redis cache. |
POST |
/api/auth/logout-all |
Bearer JWT | Blocks all active sessions for the user, deletes all stored GitHub tokens, and invalidates the Redis cache. |
GET |
/api/profile |
Bearer JWT | Returns the authenticated user's GitHub profile. Cache-first (profile:{user_id}, 15 min TTL); falls back to GET /user on the GitHub API. |
GET |
/api/repos |
Bearer JWT | Returns the user's GitHub repositories. Supports ?page=, ?per_page=, and ?visibility= query params. Cache-first (90 min TTL); falls back to GET /user/repos. |
POST |
/api/subscriptions |
Bearer JWT | Subscribe to a repository. Validates repo ownership via cache or GitHub API. Returns 409 on duplicate. |
GET |
/api/subscriptions |
Bearer JWT | Returns a paginated list of the authenticated user's subscriptions. Supports ?page= and ?per_page=. |
GET |
/api/subscriptions/{subId} |
Bearer JWT | Returns a single subscription. Returns 403 if the subscription belongs to a different user. |
DELETE |
/api/subscriptions/{subId} |
Bearer JWT | Soft-deletes a subscription (sets is_active = false). Returns 403 on ownership mismatch. |
POST |
/api/webhooks/github |
HMAC signature | Receives GitHub webhook events. Validates X-Hub-Signature-256, publishes pull_request events to RabbitMQ, and returns 202 Accepted. |
POST |
/api/internal/notifications/pr |
Service key | Triggers fan-out email notification delivery for a PR event (max 10 concurrent goroutines). Logs email payload; does not send real email. |
All errors return JSON:
{ "error": "human-readable message" }| Status | Meaning |
|---|---|
400 |
Validation failed or missing required field |
401 |
Missing, invalid, or expired JWT |
403 |
Authenticated user does not own the requested resource |
404 |
Resource not found |
409 |
Conflict (e.g. duplicate subscription) |
410 |
Session has been blocked or explicitly logged out |
422 |
Repository inaccessible or not in the user's repo list |
429 |
GitHub API rate limit exceeded |
500 |
Internal server error (details logged server-side only) |
Follow this happy-path sequence with any HTTP client (curl, Postman, Insomnia, etc.) to test the API directly without the frontend:
curl http://localhost:8080/auth/github/loginResponse:
{
"authorizeUrl": "https://github.com/login/oauth/authorize?client_id=...&state=...&scope=..."
}Open the authorizeUrl value in a browser. Sign in to GitHub if prompted, then click Authorize.
GitHub redirects to http://localhost:8080/auth/github/callback?code=...&state=....
The callback response body contains:
{
"accessToken": "eyJ...",
"refreshToken": "...",
"expiresAt": "2026-06-11T21:03:08Z",
"user": {
"id": "...",
"username": "...",
"email": "...",
"avatarUrl": "..."
}
}Copy the accessToken value.
Pass the token as a Bearer header on all subsequent requests:
curl -H "Authorization: Bearer <accessToken>" http://localhost:8080/profilecurl -H "Authorization: Bearer <accessToken>" \
"http://localhost:8080/repos?visibility=public&page=1&per_page=10"curl -X POST http://localhost:8080/subscriptions \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{"repoFullName": "owner/repo-name"}'Full step-by-step instructions — including ngrok configuration, GitHub webhook registration, and local testing — are in docs/webhook-setup.md.
Quick summary:
- Expose the local service with a public URL (e.g.
ngrok http 8080). - Set
webhook.app_urlandwebhook.secretinconfig.yaml, then restart the service. - Register the webhook in the target GitHub repository under Settings → Webhooks.
- Set the Payload URL to
{app_url}/webhooks/github, content typeapplication/json, and enable Pull requests events.
The service does not auto-register webhooks. Each repository must be configured manually.
| Target | Command | Description |
|---|---|---|
build |
make build |
(Re)build the app Docker image via docker compose build |
up |
make up |
Start all services in detached mode (docker compose up -d) |
down |
make down |
Stop and remove all containers (named volumes are preserved) |
migrate |
make migrate |
Apply migrations/001_initial_schema.sql inside the running Postgres container |
logs |
make logs |
Tail the app container logs (docker compose logs -f app) |
github-integration/
├── cmd/
│ └── main.go # Server startup: DB/Redis/RMQ init, route registration, worker startup
├── config/
│ └── config.go # Config struct + Viper loader
├── config.yaml # All infra credentials and app config (edit before running)
├── frontend/
│ ├── index.html # Login page — OAuth initiation + callback handling
│ ├── landing.html # Dashboard — profile summary + navigation cards
│ ├── repos.html # Repository browser with subscribe/unsubscribe
│ ├── subscriptions.html # Active subscriptions list + management
│ └── profile.html # Full GitHub profile details
├── internal/
│ ├── cache/
│ │ └── cache.go # Redis helper: get / set / delete with TTL
│ ├── github/
│ │ └── client.go # GitHub REST API client (profile, repos, token exchange)
│ ├── handler/
│ │ ├── auth.go # Auth handlers: login, callback, refresh, logout, logout-all
│ │ ├── internal.go # Internal handler: POST /internal/notifications/pr
│ │ ├── profile.go # Profile handler: GET /profile
│ │ ├── subscription.go # Subscription CRUD handlers
│ │ ├── webhook.go # Webhook handler: POST /webhooks/github
│ │ └── util.go # Shared handler helpers (JSON write, error response)
│ ├── middleware/
│ │ ├── auth.go # JWT Bearer auth middleware (validates + injects user/session IDs)
│ │ ├── context.go # Context key definitions
│ │ ├── service_key.go # Service-key middleware for internal endpoints
│ │ ├── util.go # Middleware utilities
│ │ └── webhook.go # HMAC-SHA256 signature validation middleware
│ ├── models/
│ │ └── models.go # GORM model structs (User, Session, GithubToken, Subscription, …)
│ ├── notification/
│ │ └── notification.go # Email template builder + slog logger (no real SMTP)
│ ├── pubsub/
│ │ └── pubsub.go # RabbitMQ publisher + consumer (topic exchange, durable queue)
│ ├── repository/
│ │ └── repository.go # All DB queries (no business logic; accepts context.Context)
│ └── service/
│ ├── auth.go # Auth business logic: OAuth flow, JWT creation, session management
│ ├── errors.go # Typed service errors mapped to HTTP status codes
│ ├── notification.go # Notification fan-out: semaphore, retry, delivered_at tracking
│ ├── profile.go # Profile service: cache-first GitHub API fetch
│ ├── security.go # AES-GCM token encryption / decryption helpers
│ ├── subscription.go # Subscription service: repo validation, upsert, soft delete
│ └── webhook.go # Webhook service: event parsing + RabbitMQ publish
├── migrations/
│ └── 001_initial_schema.sql # All CREATE TABLE, indexes, and constraints
├── docs/
│ └── webhook-setup.md # Manual webhook registration guide (ngrok + GitHub UI)
├── Dockerfile # Multi-stage Go build → minimal runtime image
├── docker-compose.yml # Postgres 16, Redis 7, RabbitMQ 3, app (with health checks)
├── Makefile # build / up / down / migrate / logs targets
├── go.mod
└── go.sum