GitHub Release Notification API – subscribe to email alerts when your favorite repositories ship new versions.
🔗 Live demo: tag-whisperer.onrender.com 📘 API docs: tag-whisperer.onrender.com/api-docs
Tag Whisperer monitors GitHub repositories for new releases and sends email notifications to subscribers. The entire flow – subscribing, confirming via email, scanning for releases, and notifying – runs as a single monolithic service.
User flow:
- Enter a repository (
owner/repo) and your email on the web form - Receive a confirmation email with a one-click link
- Once confirmed, the scanner checks for new releases every 10 minutes
- When a new release appears, you get an email with a direct link to the GitHub release page
- Every notification includes an unsubscribe link
Built to match the Swagger specification. Interactive documentation available at /api-docs.
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST |
/api/subscribe |
Subscribe to release notifications | API Key |
GET |
/api/confirm/{token} |
Confirm subscription via email link | – |
GET |
/api/unsubscribe/{token} |
Unsubscribe via email link | – |
GET |
/api/subscriptions?email={email} |
List active subscriptions | API Key |
GET |
/health |
Health check | – |
GET |
/metrics |
Prometheus metrics | – |
GET |
/api-docs |
Swagger UI documentation | – |
Subscribe (200 / 400 / 404 / 409):
curl -X POST https://tag-whisperer.onrender.com/api/subscribe \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{"email": "user@example.com", "repo": "facebook/react"}'List subscriptions:
curl -H "X-API-Key: your-key" \
"https://tag-whisperer.onrender.com/api/subscriptions?email=user@example.com"┌──────────────────────────────────────────────────────┐
│ Node.js Server │
│ │
│ ┌───────────┐ ┌───────────┐ ┌──────────────────┐ │
│ │ REST API │ │ gRPC API │ │ Scanner │ │
│ │ (Express) │ │ (:50051) │ │ (10 min cron) │ │
│ └─────┬──────┘ └─────┬─────┘ └───────┬─────────┘ │
│ │ │ │ │
│ ┌─────┴───────────────┴────────────────┴──────────┐ │
│ │ Service Layer │ │
│ │ subscriptionService · githubService │ │
│ │ emailService (Resend API) │ │
│ └─────┬───────────────┬────────────────┬──────────┘ │
│ │ │ │ │
│ ┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴──────┐ │
│ │ PostgreSQL │ │ Redis │ │ GitHub API │ │
│ │ (Neon) │ │ (Upstash) │ │ │ │
│ └───────────┘ └───────────┘ └────────────┘ │
└──────────────────────────────────────────────────────┘
Key design decisions:
- Repositories table separated from subscriptions – one repo is scanned once for all its subscribers, avoiding redundant GitHub API calls
- Redis caching with 10-minute TTL on all GitHub API responses reduces rate limit consumption
- Graceful degradation – if Redis is unavailable, the service continues without caching
- Scanner stops on 429 – when rate-limited, the entire scan halts instead of burning remaining quota
- Transactions in subscription creation – repo lookup + subscription insert are atomic
- Dual API interface – REST (Express) and gRPC run side by side, sharing the same service layer
| Component | Technology |
|---|---|
| Runtime | Node.js 22 |
| Framework | Express |
| Database | PostgreSQL (Neon) |
| Cache | Redis (Upstash) |
| Resend (HTTP API) | |
| API Docs | Swagger UI |
| RPC | gRPC (port 50051) |
| Tests | Vitest (19 tests) |
| Lint | ESLint |
| CI/CD | GitHub Actions |
| Hosting | Render |
| Monitoring | UptimeRobot |
CREATE TABLE repositories (
id SERIAL PRIMARY KEY,
owner VARCHAR(255) NOT NULL,
repo VARCHAR(255) NOT NULL,
last_seen_tag VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(owner, repo)
);
CREATE TABLE subscriptions (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
repository_id INTEGER REFERENCES repositories(id),
confirmed BOOLEAN DEFAULT FALSE,
confirm_token VARCHAR(255) NOT NULL,
unsubscribe_token VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(email, repository_id)
);Migrations run automatically on service startup (CREATE TABLE IF NOT EXISTS).
- Node.js 22+
- Docker & Docker Compose (for containerized setup)
git clone https://github.com/AM1007/tag-whisperer.git
cd tag-whisperer/backend
cp .env.example .env # fill in your credentials
npm install
npm run dev # starts with hot reload# From project root
docker-compose up --buildThis spins up three containers: the app, PostgreSQL, and Redis. The API is available at http://localhost:3000.
PORT=3000
DATABASE_URL=postgresql://user:pass@host/db?sslmode=require
REDIS_URL=rediss://default:pass@host:6379
RESEND_API_KEY=re_xxxxxxxxxxxx # email delivery via Resend HTTP API
GITHUB_TOKEN= # optional, raises rate limit to 5000/hr
API_KEY= # optional, leave empty for public access
BASE_URL=https://your-domain.com
GRPC_PORT=50051 # optional, default 50051Real emails delivered via Resend HTTP API. Confirmation emails include a one-click confirm link. Release notifications include a GitHub release link and an unsubscribe link.
Browser users see styled HTML pages instead of raw JSON. API consumers (curl, Postman) still receive JSON responses – the server checks the Accept header to decide.
| Confirmed | Unsubscribed | Error |
|---|---|---|
![]() |
![]() |
![]() |
All GitHub API responses are cached with a 10-minute TTL, reducing rate limit usage and improving response times. If Redis is unavailable, the service continues without caching.
Protected endpoints (/api/subscribe, /api/subscriptions) require an X-API-Key header. Authentication is optional – when API_KEY is not set, all endpoints are publicly accessible. Confirm and unsubscribe endpoints are always open since users access them via email links.
GET /metrics exposes:
http_requests_total– counter by method, route, statushttp_request_duration_seconds– histogram by method, routeactive_subscriptions_total– gauge of monitored repositoriesscanner_runs_total– counter by status (started/completed)- Default Node.js metrics (CPU, memory, event loop, GC)
Runs every 10 minutes via setInterval. Checks only repositories with confirmed subscriptions. Compares tag_name from GitHub's latest release API against last_seen_tag in the database. On mismatch – notifies all subscribers and updates the tag.
Rate limit handling: on HTTP 429, the scanner stops immediately and resumes at the next interval.
Available on port 50051 as an alternative to REST API. Both interfaces share the same service layer and database.
Supported methods:
| gRPC Method | REST Equivalent |
|---|---|
Subscribe |
POST /api/subscribe |
Confirm |
GET /api/confirm/{token} |
Unsubscribe |
GET /api/unsubscribe/{token} |
GetSubscriptions |
GET /api/subscriptions?email= |
Proto file: backend/src/grpc/subscription.proto
- Without token: 60 requests/hour
- With
GITHUB_TOKEN: 5,000 requests/hour - HTTP 403/429 responses trigger an error with
retryAfterfrom response headers - Scanner halts on rate limit; individual subscription failures don't block others
cd backend
npm test19 unit tests covering validation logic, token generation, and scanner behavior.
Lint and tests run on every push to main via GitHub Actions.
tag-whisperer/
├── docker-compose.yml
├── .github/workflows/ci.yml
├── backend/
│ ├── Dockerfile
│ ├── src/
│ │ ├── index.js entry point (Express + gRPC)
│ │ ├── config/ db, redis, metrics
│ │ ├── controllers/ REST request handlers
│ │ ├── db/migrations/ SQL schema
│ │ ├── grpc/ proto file + gRPC server
│ │ ├── middleware/ apiKey, metricsMiddleware
│ │ ├── routes/ Express routers
│ │ ├── scanner/ release polling logic
│ │ ├── services/ business logic layer
│ │ ├── utils/ validation, token generation
│ │ └── public/ HTML pages (form, results)
│ └── tests/ unit tests + gRPC test client
└── docs/screenshots/ README screenshots
Hosted on Render (free tier) with:
- Database: Neon – serverless PostgreSQL, fast cold start
- Cache: Upstash – serverless Redis with TLS
- Email: Resend – HTTP-based email delivery (no SMTP needed)
- Monitoring: UptimeRobot – pings
/healthevery 5 minutes to prevent cold starts
- API matches Swagger specification (4 endpoints, correct status codes)
- Monolith architecture (API + Scanner + Notifier)
- PostgreSQL with migrations on startup
- Dockerfile + docker-compose.yml
- Release scanner with
last_seen_tagtracking - GitHub repo validation via API (404/400 handling)
- Rate limit handling (429 with retry logic)
- Express framework (thin, allowed)
- Unit tests (19 tests, Vitest)
- README with architecture documentation
- Deployed to Render + HTML subscription page
- gRPC interface (port 50051)
- Redis caching (TTL 10 min)
- API key authentication
- Prometheus metrics (
/metrics) - GitHub Actions CI (lint + tests on push)
MIT













