Skip to content

Deploying Daily Stars Explorer

Daily Stars Explorer is a web application that helps developers and project maintainers track GitHub repository star growth over time. By visualizing star trends with beautiful charts and historical data, it provides insights into project popularity, community engagement, and growth patterns. The tool fetches data directly from the GitHub API, stores historical metrics, and presents them through an intuitive dashboard with interactive graphs and statistics.

What makes Daily Stars Explorer particularly useful is its ability to track multiple repositories simultaneously, compare growth patterns between projects, and identify trending periods. The application automatically updates star counts on a schedule, maintains historical data for trend analysis, and provides exportable reports. Whether you’re monitoring your own projects, analyzing competitors, or researching popular open-source tools, Daily Stars Explorer turns raw GitHub data into actionable insights.

Why Deploy Daily Stars Explorer on Klutch.sh?

Klutch.sh provides an excellent platform for hosting Daily Stars Explorer with several key advantages:

  • Simple Docker Deployment: Deploy your Dockerfile and Klutch.sh automatically handles containerization and orchestration
  • Persistent Storage: Attach volumes for historical star data and application state with guaranteed durability
  • Automatic HTTPS: All deployments come with automatic SSL certificates for secure access
  • Resource Scalability: Scale CPU and memory resources based on the number of tracked repositories
  • Database Integration: Run PostgreSQL or SQLite alongside the application with persistent storage
  • Cost-Effective: Pay only for resources used, scale based on tracking needs
  • Always-On Monitoring: Keep the application running 24/7 for continuous star tracking

Prerequisites

Before deploying Daily Stars Explorer, ensure you have:

  • A Klutch.sh account (sign up at klutch.sh)
  • Git installed locally
  • A GitHub account and Personal Access Token
  • Basic understanding of GitHub API and rate limits
  • Familiarity with Docker and container concepts
  • Knowledge of Node.js and npm (for local development)
  • Understanding of data visualization concepts

Understanding Daily Stars Explorer’s Architecture

Daily Stars Explorer uses a straightforward architecture designed for reliability and scalability:

Core Components

Frontend (React/Vue.js): The user interface that provides:

  • Dashboard for viewing tracked repositories
  • Interactive charts for star growth visualization
  • Repository search and addition interface
  • Historical data browsing
  • Filtering and comparison tools
  • Export functionality for reports
  • Responsive design for mobile access

Backend (Node.js/Express): Server-side logic handling:

  • GitHub API integration and data fetching
  • Rate limit management
  • Data storage and retrieval
  • Scheduled updates for star counts
  • Repository management endpoints
  • Data aggregation and calculations
  • Authentication and authorization

Database (PostgreSQL/SQLite): Data persistence layer storing:

  • Repository information (name, owner, URL)
  • Historical star counts with timestamps
  • User preferences and settings
  • API token management
  • Update schedules and logs
  • Aggregated statistics

Scheduler: Background job processor for:

  • Periodic star count updates
  • GitHub API polling
  • Data cleanup and archival
  • Report generation
  • Notification triggers

Data Flow

  1. User adds repository to track via web interface
  2. Backend validates repository exists on GitHub
  3. Initial star count fetched from GitHub API
  4. Repository saved to database
  5. Scheduler periodically fetches updated star counts
  6. New data points stored with timestamps
  7. Frontend queries historical data
  8. Charts generated from time-series data
  9. Users view trends and statistics
  10. Export features generate downloadable reports

GitHub API Integration

Authentication: Uses Personal Access Token for:

  • Higher rate limits (5,000 requests/hour)
  • Access to private repositories (optional)
  • Better reliability and performance

Rate Limit Handling:

  • Tracks remaining API calls
  • Implements exponential backoff
  • Queues requests when limit approached
  • Displays rate limit status to users

Data Endpoints Used:

  • /repos/{owner}/{repo} - Repository information
  • /repos/{owner}/{repo}/stargazers - Star count
  • /users/{username}/repos - User repositories
  • Rate limit endpoint for monitoring

Storage Strategy

Time-Series Data: Historical star counts stored with:

  • Repository identifier
  • Star count value
  • Timestamp of measurement
  • Growth rate calculation
  • Delta from previous measurement

Aggregation: Pre-calculated metrics:

  • Daily star growth averages
  • Weekly trend summaries
  • Monthly statistics
  • All-time growth rate
  • Peak growth periods

Retention Policy:

  • Raw data kept indefinitely (or configured period)
  • Hourly data for last 7 days
  • Daily data for last year
  • Weekly aggregates for older data

Installation and Setup

Step 1: Create the Dockerfile

Create a Dockerfile in your project root:

FROM node:18-alpine AS builder
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy application code
COPY . .
# Build frontend (if applicable)
RUN npm run build || echo "No build step"
# Production stage
FROM node:18-alpine
# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init
# Create app user
RUN addgroup -g 1001 appuser && \
adduser -D -u 1001 -G appuser appuser
# Set working directory
WORKDIR /app
# Copy dependencies from builder
COPY --from=builder --chown=appuser:appuser /app/node_modules ./node_modules
# Copy application files
COPY --chown=appuser:appuser . .
# Create data directory
RUN mkdir -p /app/data && \
chown appuser:appuser /app/data
# Switch to app user
USER appuser
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" || exit 1
# Start application
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "server.js"]

Step 2: Create Application Server

Create server.js:

const express = require('express');
const path = require('path');
const cors = require('cors');
const schedule = require('node-schedule');
const { Octokit } = require('@octokit/rest');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
// GitHub API client
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
userAgent: 'Daily-Stars-Explorer/1.0'
});
// Database setup (using SQLite for simplicity)
const Database = require('better-sqlite3');
const db = new Database(process.env.DB_PATH || './data/stars.db');
// Initialize database
db.exec(`
CREATE TABLE IF NOT EXISTS repositories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner TEXT NOT NULL,
name TEXT NOT NULL,
full_name TEXT NOT NULL UNIQUE,
description TEXT,
url TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_updated DATETIME
);
CREATE TABLE IF NOT EXISTS star_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
repository_id INTEGER NOT NULL,
star_count INTEGER NOT NULL,
recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (repository_id) REFERENCES repositories(id)
);
CREATE INDEX IF NOT EXISTS idx_star_history_repo
ON star_history(repository_id, recorded_at);
`);
// API Routes
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Get all tracked repositories
app.get('/api/repositories', (req, res) => {
try {
const repos = db.prepare(`
SELECT r.*,
(SELECT star_count FROM star_history
WHERE repository_id = r.id
ORDER BY recorded_at DESC LIMIT 1) as current_stars,
(SELECT COUNT(*) FROM star_history
WHERE repository_id = r.id) as data_points
FROM repositories r
ORDER BY r.created_at DESC
`).all();
res.json(repos);
} catch (error) {
console.error('Error fetching repositories:', error);
res.status(500).json({ error: 'Failed to fetch repositories' });
}
});
// Add new repository to track
app.post('/api/repositories', async (req, res) => {
const { owner, name } = req.body;
if (!owner || !name) {
return res.status(400).json({ error: 'Owner and name are required' });
}
try {
// Fetch repo info from GitHub
const { data: repoData } = await octokit.repos.get({ owner, repo: name });
// Insert into database
const stmt = db.prepare(`
INSERT INTO repositories (owner, name, full_name, description, url, last_updated)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(full_name) DO UPDATE SET last_updated = ?
`);
const now = new Date().toISOString();
const result = stmt.run(
repoData.owner.login,
repoData.name,
repoData.full_name,
repoData.description,
repoData.html_url,
now,
now
);
// Record initial star count
const repoId = result.lastInsertRowid;
db.prepare(`
INSERT INTO star_history (repository_id, star_count)
VALUES (?, ?)
`).run(repoId, repoData.stargazers_count);
res.json({
id: repoId,
full_name: repoData.full_name,
stars: repoData.stargazers_count
});
} catch (error) {
console.error('Error adding repository:', error);
if (error.status === 404) {
res.status(404).json({ error: 'Repository not found on GitHub' });
} else if (error.status === 403) {
res.status(403).json({ error: 'Rate limit exceeded or token invalid' });
} else {
res.status(500).json({ error: 'Failed to add repository' });
}
}
});
// Delete repository
app.delete('/api/repositories/:id', (req, res) => {
const { id } = req.params;
try {
db.prepare('DELETE FROM star_history WHERE repository_id = ?').run(id);
db.prepare('DELETE FROM repositories WHERE id = ?').run(id);
res.json({ success: true });
} catch (error) {
console.error('Error deleting repository:', error);
res.status(500).json({ error: 'Failed to delete repository' });
}
});
// Get star history for a repository
app.get('/api/repositories/:id/history', (req, res) => {
const { id } = req.params;
const { days = 30 } = req.query;
try {
const history = db.prepare(`
SELECT star_count, recorded_at
FROM star_history
WHERE repository_id = ?
AND recorded_at >= datetime('now', '-' || ? || ' days')
ORDER BY recorded_at ASC
`).all(id, days);
res.json(history);
} catch (error) {
console.error('Error fetching history:', error);
res.status(500).json({ error: 'Failed to fetch history' });
}
});
// Get statistics for a repository
app.get('/api/repositories/:id/stats', (req, res) => {
const { id } = req.params;
try {
const stats = db.prepare(`
SELECT
MIN(star_count) as min_stars,
MAX(star_count) as max_stars,
MAX(star_count) - MIN(star_count) as total_growth,
COUNT(*) as measurements
FROM star_history
WHERE repository_id = ?
`).get(id);
res.json(stats);
} catch (error) {
console.error('Error fetching stats:', error);
res.status(500).json({ error: 'Failed to fetch statistics' });
}
});
// Check GitHub API rate limit
app.get('/api/rate-limit', async (req, res) => {
try {
const { data } = await octokit.rateLimit.get();
res.json({
limit: data.rate.limit,
remaining: data.rate.remaining,
reset: new Date(data.rate.reset * 1000).toISOString()
});
} catch (error) {
console.error('Error checking rate limit:', error);
res.status(500).json({ error: 'Failed to check rate limit' });
}
});
// Update star counts for all repositories
async function updateStarCounts() {
console.log('Updating star counts...');
const repos = db.prepare('SELECT * FROM repositories').all();
for (const repo of repos) {
try {
const { data } = await octokit.repos.get({
owner: repo.owner,
repo: repo.name
});
db.prepare(`
INSERT INTO star_history (repository_id, star_count)
VALUES (?, ?)
`).run(repo.id, data.stargazers_count);
db.prepare(`
UPDATE repositories
SET last_updated = ?
WHERE id = ?
`).run(new Date().toISOString(), repo.id);
console.log(`Updated ${repo.full_name}: ${data.stargazers_count} stars`);
// Delay between requests to respect rate limits
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
console.error(`Error updating ${repo.full_name}:`, error.message);
}
}
console.log('Update complete');
}
// Schedule updates every hour
schedule.scheduleJob('0 * * * *', updateStarCounts);
// Manual update endpoint
app.post('/api/update', async (req, res) => {
res.json({ message: 'Update started' });
updateStarCounts().catch(console.error);
});
// Serve frontend
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// Start server
app.listen(PORT, '0.0.0.0', () => {
console.log(`Daily Stars Explorer running on port ${PORT}`);
console.log(`GitHub token configured: ${!!process.env.GITHUB_TOKEN}`);
// Run initial update
if (process.env.GITHUB_TOKEN) {
setTimeout(updateStarCounts, 5000);
} else {
console.warn('Warning: GITHUB_TOKEN not set. GitHub API access will be limited.');
}
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, closing database...');
db.close();
process.exit(0);
});

Step 3: Create Frontend Interface

Create public/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Daily Stars Explorer</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
h1 {
color: #333;
margin-bottom: 10px;
}
.subtitle {
color: #666;
}
.add-repo {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
input {
flex: 1;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 5px;
font-size: 16px;
}
button {
padding: 12px 24px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: background 0.3s;
}
button:hover {
background: #5568d3;
}
.rate-limit {
background: #fff3cd;
padding: 10px;
border-radius: 5px;
font-size: 14px;
color: #856404;
}
.repo-list {
display: grid;
gap: 20px;
}
.repo-card {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.repo-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 15px;
}
.repo-title {
color: #667eea;
font-size: 20px;
font-weight: 600;
text-decoration: none;
}
.repo-title:hover {
text-decoration: underline;
}
.repo-stats {
display: flex;
gap: 20px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.stat {
display: flex;
flex-direction: column;
gap: 5px;
}
.stat-label {
font-size: 12px;
color: #999;
text-transform: uppercase;
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: #333;
}
.chart-container {
height: 200px;
margin-top: 20px;
}
.delete-btn {
background: #dc3545;
padding: 8px 16px;
font-size: 14px;
}
.delete-btn:hover {
background: #c82333;
}
.loading {
text-align: center;
padding: 40px;
color: white;
font-size: 18px;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.empty-state {
background: white;
padding: 60px;
text-align: center;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.empty-state h2 {
color: #333;
margin-bottom: 10px;
}
.empty-state p {
color: #666;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>⭐ Daily Stars Explorer</h1>
<p class="subtitle">Track GitHub repository star growth over time</p>
</header>
<div class="add-repo">
<h2 style="margin-bottom: 15px;">Add Repository</h2>
<div class="input-group">
<input type="text" id="owner" placeholder="Repository owner (e.g., facebook)">
<input type="text" id="name" placeholder="Repository name (e.g., react)">
<button onclick="addRepository()">Add Repository</button>
</div>
<div id="rate-limit" class="rate-limit" style="display: none;"></div>
<div id="error" class="error" style="display: none;"></div>
</div>
<div id="loading" class="loading">Loading repositories...</div>
<div id="repo-list" class="repo-list"></div>
</div>
<script>
let repositories = [];
const charts = new Map();
async function fetchRateLimit() {
try {
const response = await fetch('/api/rate-limit');
const data = await response.json();
const rateLimitEl = document.getElementById('rate-limit');
rateLimitEl.style.display = 'block';
rateLimitEl.textContent = `API Rate Limit: ${data.remaining}/${data.limit} remaining (resets at ${new Date(data.reset).toLocaleTimeString()})`;
} catch (error) {
console.error('Error fetching rate limit:', error);
}
}
async function loadRepositories() {
try {
const response = await fetch('/api/repositories');
repositories = await response.json();
renderRepositories();
document.getElementById('loading').style.display = 'none';
} catch (error) {
console.error('Error loading repositories:', error);
document.getElementById('loading').textContent = 'Error loading repositories';
}
}
async function addRepository() {
const owner = document.getElementById('owner').value.trim();
const name = document.getElementById('name').value.trim();
const errorEl = document.getElementById('error');
if (!owner || !name) {
errorEl.textContent = 'Please enter both owner and name';
errorEl.style.display = 'block';
return;
}
errorEl.style.display = 'none';
try {
const response = await fetch('/api/repositories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ owner, name })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to add repository');
}
document.getElementById('owner').value = '';
document.getElementById('name').value = '';
await loadRepositories();
await fetchRateLimit();
} catch (error) {
errorEl.textContent = error.message;
errorEl.style.display = 'block';
}
}
async function deleteRepository(id) {
if (!confirm('Are you sure you want to stop tracking this repository?')) {
return;
}
try {
await fetch(`/api/repositories/${id}`, { method: 'DELETE' });
await loadRepositories();
} catch (error) {
console.error('Error deleting repository:', error);
}
}
async function renderRepositories() {
const listEl = document.getElementById('repo-list');
if (repositories.length === 0) {
listEl.innerHTML = `
<div class="empty-state">
<h2>No repositories tracked yet</h2>
<p>Add a repository above to start tracking its star growth</p>
</div>
`;
return;
}
listEl.innerHTML = '';
for (const repo of repositories) {
const card = document.createElement('div');
card.className = 'repo-card';
card.innerHTML = `
<div class="repo-header">
<a href="${repo.url}" target="_blank" class="repo-title">${repo.full_name}</a>
<button class="delete-btn" onclick="deleteRepository(${repo.id})">Delete</button>
</div>
<p style="color: #666; margin-bottom: 15px;">${repo.description || 'No description'}</p>
<div class="repo-stats">
<div class="stat">
<span class="stat-label">Current Stars</span>
<span class="stat-value">${repo.current_stars?.toLocaleString() || 0}</span>
</div>
<div class="stat">
<span class="stat-label">Data Points</span>
<span class="stat-value">${repo.data_points || 0}</span>
</div>
<div class="stat">
<span class="stat-label">Last Updated</span>
<span class="stat-value" style="font-size: 14px;">${new Date(repo.last_updated).toLocaleString()}</span>
</div>
</div>
<div class="chart-container">
<canvas id="chart-${repo.id}"></canvas>
</div>
`;
listEl.appendChild(card);
// Load and render chart
await loadChart(repo.id);
}
}
async function loadChart(repoId) {
try {
const response = await fetch(`/api/repositories/${repoId}/history?days=30`);
const history = await response.json();
if (history.length === 0) return;
const ctx = document.getElementById(`chart-${repoId}`);
if (charts.has(repoId)) {
charts.get(repoId).destroy();
}
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: history.map(h => new Date(h.recorded_at).toLocaleDateString()),
datasets: [{
label: 'Stars',
data: history.map(h => h.star_count),
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
y: {
beginAtZero: false,
ticks: {
callback: value => value.toLocaleString()
}
}
}
}
});
charts.set(repoId, chart);
} catch (error) {
console.error('Error loading chart:', error);
}
}
// Initialize
loadRepositories();
fetchRateLimit();
// Refresh every 5 minutes
setInterval(loadRepositories, 5 * 60 * 1000);
</script>
</body>
</html>

Step 4: Create Package Configuration

Create package.json:

{
"name": "daily-stars-explorer",
"version": "1.0.0",
"description": "Track GitHub repository star growth over time",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"keywords": ["github", "stars", "tracking", "analytics"],
"author": "Daily Stars Explorer",
"license": "MIT",
"dependencies": {
"@octokit/rest": "^20.0.2",
"better-sqlite3": "^9.2.2",
"cors": "^2.8.5",
"express": "^4.18.2",
"node-schedule": "^2.1.1"
},
"devDependencies": {
"nodemon": "^3.0.2"
},
"engines": {
"node": ">=18.0.0"
}
}

Step 5: Create Environment Configuration

Create .env.example:

Terminal window
# GitHub API Configuration
GITHUB_TOKEN=your_github_personal_access_token_here
# Server Configuration
PORT=3000
NODE_ENV=production
# Database Configuration
DB_PATH=./data/stars.db
# Update Schedule (cron format)
UPDATE_SCHEDULE=0 * * * *
# Data Retention (days)
RETENTION_DAYS=365

Step 6: Create Docker Compose for Local Development

Create docker-compose.yml:

version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- GITHUB_TOKEN=${GITHUB_TOKEN}
- PORT=3000
- NODE_ENV=development
- DB_PATH=/app/data/stars.db
volumes:
- ./data:/app/data
- ./public:/app/public
restart: unless-stopped
volumes:
data:

Step 7: Create Documentation

Create README.md:

# Daily Stars Explorer
Track GitHub repository star growth over time with beautiful visualizations.
## Features
- Track multiple repositories simultaneously
- Historical star count data
- Interactive charts and graphs
- GitHub API integration
- Automatic updates every hour
- Export data for reports
## Setup
1. Get a GitHub Personal Access Token from https://github.com/settings/tokens
2. Set the GITHUB_TOKEN environment variable
3. Run `npm install`
4. Run `npm start`
5. Open http://localhost:3000
## Usage
1. Enter repository owner and name
2. Click "Add Repository"
3. View real-time star tracking charts
4. Historical data accumulates over time
## API Endpoints
- GET /api/repositories - List all tracked repos
- POST /api/repositories - Add new repository
- DELETE /api/repositories/:id - Remove repository
- GET /api/repositories/:id/history - Get star history
- GET /api/repositories/:id/stats - Get statistics
- GET /api/rate-limit - Check GitHub API rate limit
- POST /api/update - Trigger manual update
## Environment Variables
- GITHUB_TOKEN - GitHub Personal Access Token (required)
- PORT - Server port (default: 3000)
- DB_PATH - SQLite database path (default: ./data/stars.db)

Step 8: Initialize Git Repository

Terminal window
git init
git add Dockerfile package.json server.js public/ .env.example docker-compose.yml README.md
git commit -m "Initial Daily Stars Explorer deployment configuration"

Step 9: Test Locally

Before deploying to Klutch.sh, test locally:

Terminal window
# Install dependencies
npm install
# Create data directory
mkdir -p data
# Set your GitHub token
export GITHUB_TOKEN=your_token_here
# Start the application
npm start
# Access at http://localhost:3000
# Test with Docker Compose
docker-compose up -d
docker-compose logs -f

Deploying to Klutch.sh

Step 1: Create GitHub Personal Access Token

  1. Navigate to GitHub Settings > Tokens
  2. Click "Generate new token (classic)"
  3. Give it a descriptive name like "Daily Stars Explorer"
  4. Select scopes: - `public_repo` (for public repositories) - `repo` (if tracking private repositories)
  5. Click "Generate token"
  6. Copy the token immediately (you won't see it again)

Step 2: Push Repository to GitHub

Create a new repository and push:

Terminal window
git remote add origin https://github.com/yourusername/daily-stars-explorer.git
git branch -M master
git push -u origin master

Step 3: Deploy to Klutch.sh

  1. Navigate to klutch.sh/app
  2. Click "New Project" and select "Import from GitHub"
  3. Authorize Klutch.sh to access your GitHub repositories
  4. Select your Daily Stars Explorer repository
  5. Klutch.sh will automatically detect the Dockerfile

Step 4: Configure Traffic Settings

  1. In the project settings, select **HTTP** as the traffic type
  2. Set the internal port to **3000**
  3. Klutch.sh will automatically provision an HTTPS endpoint

Step 5: Add Persistent Storage

Daily Stars Explorer requires persistent storage for the database:

  1. In your project settings, navigate to the "Storage" section
  2. Add a volume with mount path: `/app/data` and size: `5GB`

Storage recommendations:

  • Light usage (< 10 repos): 2GB
  • Medium usage (10-50 repos): 5GB
  • Heavy usage (50+ repos): 10GB+

Step 6: Configure Environment Variables

Add the following environment variables in Klutch.sh dashboard:

  • GITHUB_TOKEN: Your GitHub Personal Access Token (required)
  • PORT: 3000
  • NODE_ENV: production
  • DB_PATH: /app/data/stars.db

Important: Keep your GitHub token secure and never commit it to the repository.

Step 7: Deploy the Application

  1. Review your configuration settings in Klutch.sh
  2. Click "Deploy" to start the deployment
  3. Monitor build logs for any errors
  4. Wait for initialization (typically 2-3 minutes)
  5. Once deployed, Daily Stars Explorer will be available at `your-app.klutch.sh`

Step 8: Verify Deployment

After deployment:

  1. Access your deployment at `https://your-app.klutch.sh`
  2. Check the health endpoint: `https://your-app.klutch.sh/health`
  3. Verify GitHub API connection by checking rate limit display
  4. Add a test repository to ensure tracking works

Getting Started with Daily Stars Explorer

Adding Your First Repository

Track any public GitHub repository:

  1. Navigate to your Daily Stars Explorer deployment
  2. In the "Add Repository" section, enter: - **Owner**: Repository owner (e.g., `facebook`) - **Name**: Repository name (e.g., `react`)
  3. Click "Add Repository"
  4. The repository appears in the list with initial star count
  5. Chart begins building as data accumulates

Popular repositories to track:

  • microsoft/vscode
  • facebook/react
  • vuejs/vue
  • angular/angular
  • nodejs/node
  • golang/go
  • rust-lang/rust

Understanding the Dashboard

Repository Cards display:

  • Repository name and description
  • Current star count
  • Number of data points collected
  • Last update timestamp
  • Star growth chart (30-day view)

Charts show:

  • X-axis: Timeline (dates)
  • Y-axis: Star count
  • Trend line showing growth patterns
  • Filled area for visual clarity

Rate Limit Indicator shows:

  • Remaining API calls
  • Total limit (5,000 per hour with token)
  • Reset time for quota renewal

Tracking Multiple Repositories

Build a comprehensive tracking dashboard:

  1. Add related repositories for comparison
  2. Track competitors in your space
  3. Monitor your own projects
  4. Observe trending open-source tools

Example tracking sets:

  • Frontend Frameworks: React, Vue, Angular, Svelte
  • Backend Frameworks: Express, FastAPI, Django, Rails
  • Databases: PostgreSQL, MongoDB, Redis, SQLite
  • DevOps Tools: Docker, Kubernetes, Terraform, Ansible

Interpreting Star Growth Patterns

Steady Linear Growth: Consistent community interest

  • Indicates stable, ongoing adoption
  • Typical for mature projects
  • Example: Established frameworks

Exponential Growth: Viral interest or trending

  • Product Hunt or Hacker News features
  • Major version releases
  • Media coverage or influencer mentions

Plateaus: Maturity or saturation

  • Market saturation reached
  • Competing alternatives emerged
  • Development slowed or stopped

Spikes: Temporary attention bursts

  • Conference presentations
  • Social media mentions
  • Breaking news or controversies

Managing Tracked Repositories

Remove repositories you no longer need:

  1. Click “Delete” button on repository card
  2. Confirm deletion
  3. Historical data is removed

Update frequency:

  • Automatic updates every hour
  • Manual updates via API endpoint
  • View last update timestamp on each card

Production Best Practices

GitHub Token Security

  1. Use Environment Variables: Never hardcode tokens in code
// Good
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
// Bad
const octokit = new Octokit({ auth: 'ghp_hardcoded_token' });
  1. Token Permissions: Use minimal required scopes

    • Only public_repo for public repositories
    • Avoid unnecessary permissions
  2. Token Rotation: Regularly rotate tokens

    • Generate new token every 90 days
    • Update environment variable in Klutch.sh
    • Revoke old tokens
  3. Monitor Token Usage: Check rate limit regularly

Terminal window
curl https://your-app.klutch.sh/api/rate-limit

Rate Limit Management

  1. Respect GitHub Limits:

    • 5,000 requests/hour with authentication
    • 60 requests/hour without authentication
    • Rate limit resets hourly
  2. Implement Delays:

// Add delay between requests
async function updateStarCounts() {
for (const repo of repos) {
await updateRepo(repo);
await new Promise(resolve => setTimeout(resolve, 1000)); // 1 second delay
}
}
  1. Handle Rate Limit Errors:
try {
const data = await octokit.repos.get({ owner, repo });
} catch (error) {
if (error.status === 403 && error.response.headers['x-ratelimit-remaining'] === '0') {
const resetTime = new Date(error.response.headers['x-ratelimit-reset'] * 1000);
console.log(`Rate limit exceeded. Resets at ${resetTime}`);
// Wait until reset
}
}
  1. Monitor Remaining Quota:
const { data } = await octokit.rateLimit.get();
if (data.rate.remaining < 100) {
console.warn('Rate limit running low:', data.rate.remaining);
}

Performance Optimization

  1. Database Indexing:
CREATE INDEX idx_star_history_repo ON star_history(repository_id, recorded_at);
CREATE INDEX idx_repositories_fullname ON repositories(full_name);
  1. Caching Strategy:
// Cache repository data for 5 minutes
const cache = new Map();
const CACHE_TTL = 5 * 60 * 1000;
app.get('/api/repositories', (req, res) => {
const cached = cache.get('repositories');
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return res.json(cached.data);
}
const repos = db.prepare('SELECT * FROM repositories').all();
cache.set('repositories', { data: repos, timestamp: Date.now() });
res.json(repos);
});
  1. Optimize Chart Data:
// Return aggregated data for large datasets
app.get('/api/repositories/:id/history', (req, res) => {
const { days = 30 } = req.query;
let query;
if (days > 90) {
// Aggregate by day for long periods
query = `
SELECT
DATE(recorded_at) as date,
AVG(star_count) as star_count
FROM star_history
WHERE repository_id = ? AND recorded_at >= datetime('now', '-' || ? || ' days')
GROUP BY DATE(recorded_at)
ORDER BY date ASC
`;
} else {
// Return all data points for shorter periods
query = `
SELECT star_count, recorded_at
FROM star_history
WHERE repository_id = ? AND recorded_at >= datetime('now', '-' || ? || ' days')
ORDER BY recorded_at ASC
`;
}
const history = db.prepare(query).all(id, days);
res.json(history);
});

Data Management

  1. Backup Strategy:
backup-database.sh
#!/bin/bash
BACKUP_DIR="/backups/stars_$(date +%Y%m%d_%H%M%S)"
mkdir -p $BACKUP_DIR
# Backup SQLite database
sqlite3 /app/data/stars.db ".backup '${BACKUP_DIR}/stars.db'"
# Compress backup
gzip ${BACKUP_DIR}/stars.db
echo "Backup complete: ${BACKUP_DIR}/stars.db.gz"
  1. Data Retention Policy:
// Clean up old data beyond retention period
function cleanupOldData() {
const retentionDays = process.env.RETENTION_DAYS || 365;
db.prepare(`
DELETE FROM star_history
WHERE recorded_at < datetime('now', '-' || ? || ' days')
`).run(retentionDays);
console.log(`Cleaned up data older than ${retentionDays} days`);
}
// Run cleanup weekly
schedule.scheduleJob('0 0 * * 0', cleanupOldData);
  1. Database Maintenance:
// Optimize database periodically
function optimizeDatabase() {
db.exec('VACUUM');
db.exec('ANALYZE');
console.log('Database optimized');
}
schedule.scheduleJob('0 3 * * 0', optimizeDatabase);

Monitoring and Logging

  1. Structured Logging:
const logger = {
info: (message, meta = {}) => {
console.log(JSON.stringify({ level: 'info', message, ...meta, timestamp: new Date().toISOString() }));
},
error: (message, error, meta = {}) => {
console.error(JSON.stringify({ level: 'error', message, error: error.message, ...meta, timestamp: new Date().toISOString() }));
}
};
// Usage
logger.info('Repository added', { repo: 'facebook/react', stars: 200000 });
logger.error('Update failed', error, { repo: 'owner/name' });
  1. Health Checks:
app.get('/health', (req, res) => {
try {
// Check database connection
db.prepare('SELECT 1').get();
// Check GitHub API
const hasToken = !!process.env.GITHUB_TOKEN;
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
database: 'connected',
github_token: hasToken ? 'configured' : 'missing',
uptime: process.uptime()
});
} catch (error) {
res.status(500).json({
status: 'error',
error: error.message
});
}
});
  1. Update Monitoring:
async function updateStarCounts() {
const startTime = Date.now();
let successCount = 0;
let errorCount = 0;
const repos = db.prepare('SELECT * FROM repositories').all();
for (const repo of repos) {
try {
await updateRepo(repo);
successCount++;
} catch (error) {
errorCount++;
logger.error('Update failed', error, { repo: repo.full_name });
}
}
const duration = Date.now() - startTime;
logger.info('Update complete', {
total: repos.length,
success: successCount,
errors: errorCount,
duration: `${duration}ms`
});
}

Error Handling

  1. API Error Handling:
async function fetchRepoData(owner, name) {
try {
const { data } = await octokit.repos.get({ owner, repo: name });
return data;
} catch (error) {
if (error.status === 404) {
throw new Error('Repository not found');
} else if (error.status === 403) {
throw new Error('Rate limit exceeded or access denied');
} else if (error.status === 401) {
throw new Error('Invalid GitHub token');
} else {
throw new Error('GitHub API error: ' + error.message);
}
}
}
  1. Database Error Handling:
app.post('/api/repositories', async (req, res) => {
try {
// ... repository addition logic
} catch (error) {
if (error.message.includes('UNIQUE constraint')) {
res.status(409).json({ error: 'Repository already tracked' });
} else {
logger.error('Failed to add repository', error);
res.status(500).json({ error: 'Internal server error' });
}
}
});

Troubleshooting

Repository Not Found Error

Symptoms: “Repository not found on GitHub” when adding repository

Solutions:

  1. Verify Repository Exists:

    • Check repository URL on GitHub
    • Ensure correct owner and name
    • Repository may have been renamed or deleted
  2. Check Token Permissions:

    • Private repositories require repo scope
    • Verify token is active on GitHub
  3. Test API Access:

Terminal window
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://api.github.com/repos/owner/name

Rate Limit Exceeded

Symptoms: Cannot add new repositories or updates fail

Solutions:

  1. Check Rate Limit Status:
Terminal window
curl https://your-app.klutch.sh/api/rate-limit
  1. Wait for Reset:

    • Rate limits reset every hour
    • Check reset time in rate limit response
  2. Verify Token is Set:

Terminal window
# Check environment variable in Klutch.sh dashboard
echo $GITHUB_TOKEN
  1. Reduce Update Frequency:
    • Modify schedule in server.js
    • Increase delay between requests

Charts Not Displaying

Symptoms: Repository cards show but charts are empty

Solutions:

  1. Check Data Collection:
Terminal window
# Verify data exists
curl https://your-app.klutch.sh/api/repositories/1/history
  1. Verify Chart.js Load:

    • Check browser console for errors
    • Ensure Chart.js CDN is accessible
  2. Wait for Data Accumulation:

    • Charts need at least 2 data points
    • Initial chart appears after first update cycle

Database Connection Errors

Symptoms: “Cannot open database” or “SQLITE_CANTOPEN” errors

Solutions:

  1. Verify Data Directory:
Terminal window
ls -la /app/data
  1. Check Permissions:
Terminal window
chown -R appuser:appuser /app/data
chmod 755 /app/data
  1. Verify Persistent Volume:

    • Ensure volume is mounted in Klutch.sh
    • Check volume size hasn’t exceeded limit
  2. Database Path:

Terminal window
# Verify DB_PATH environment variable
echo $DB_PATH

Updates Not Running

Symptoms: Star counts not updating automatically

Solutions:

  1. Check Scheduler:
// Verify schedule in server.js
schedule.scheduleJob('0 * * * *', updateStarCounts); // Every hour
  1. Manual Update:
Terminal window
curl -X POST https://your-app.klutch.sh/api/update
  1. Check Logs:

    • View Klutch.sh container logs
    • Look for update messages
  2. Verify GitHub Token:

    • Token must be valid
    • Check rate limit not exceeded

High Memory Usage

Symptoms: Application crashes or becomes slow

Solutions:

  1. Limit Tracked Repositories:

    • Remove unused repositories
    • Track only essential projects
  2. Implement Data Aggregation:

    • Reduce granularity for old data
    • Archive detailed historical data
  3. Add Memory Limits:

// In server.js
if (process.memoryUsage().heapUsed > 500 * 1024 * 1024) {
console.warn('High memory usage detected');
// Trigger cleanup
}
  1. Increase Resources:
    • Scale up in Klutch.sh dashboard
    • Add more RAM allocation

Advanced Configuration

Custom Update Schedules

Modify update frequency based on needs:

// Update every 30 minutes
schedule.scheduleJob('*/30 * * * *', updateStarCounts);
// Update daily at 2 AM
schedule.scheduleJob('0 2 * * *', updateStarCounts);
// Update every 6 hours
schedule.scheduleJob('0 */6 * * *', updateStarCounts);
// Multiple schedules
schedule.scheduleJob('0 * * * *', updateStarCounts); // Hourly
schedule.scheduleJob('0 0 * * 0', cleanupOldData); // Weekly cleanup

Data Export Functionality

Add CSV export for data analysis:

app.get('/api/repositories/:id/export', (req, res) => {
const { id } = req.params;
const history = db.prepare(`
SELECT r.full_name, sh.star_count, sh.recorded_at
FROM star_history sh
JOIN repositories r ON r.id = sh.repository_id
WHERE sh.repository_id = ?
ORDER BY sh.recorded_at ASC
`).all(id);
let csv = 'Repository,Stars,Date\n';
history.forEach(row => {
csv += `${row.full_name},${row.star_count},${row.recorded_at}\n`;
});
res.header('Content-Type', 'text/csv');
res.header('Content-Disposition', `attachment; filename="stars-${id}.csv"`);
res.send(csv);
});

Webhook Notifications

Send notifications when star milestones reached:

async function checkMilestones(repoId, newCount, oldCount) {
const milestones = [1000, 5000, 10000, 50000, 100000];
for (const milestone of milestones) {
if (oldCount < milestone && newCount >= milestone) {
await sendWebhook({
event: 'milestone_reached',
repository_id: repoId,
milestone: milestone,
current_stars: newCount
});
}
}
}
async function sendWebhook(data) {
if (!process.env.WEBHOOK_URL) return;
try {
await fetch(process.env.WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
} catch (error) {
console.error('Webhook error:', error);
}
}

Multi-Repository Comparison

Add comparison endpoint:

app.get('/api/compare', (req, res) => {
const { repos } = req.query; // Comma-separated repo IDs
const repoIds = repos.split(',').map(Number);
const comparison = repoIds.map(id => {
const repo = db.prepare('SELECT * FROM repositories WHERE id = ?').get(id);
const history = db.prepare(`
SELECT star_count, recorded_at
FROM star_history
WHERE repository_id = ?
ORDER BY recorded_at ASC
`).all(id);
return {
id: repo.id,
name: repo.full_name,
history: history
};
});
res.json(comparison);
});

Authentication Layer

Add basic authentication for private deployments:

const basicAuth = require('express-basic-auth');
if (process.env.ADMIN_PASSWORD) {
app.use(basicAuth({
users: { 'admin': process.env.ADMIN_PASSWORD },
challenge: true,
realm: 'Daily Stars Explorer'
}));
}

Set ADMIN_PASSWORD environment variable in Klutch.sh.

Additional Resources

Conclusion

Daily Stars Explorer provides a simple yet powerful way to track GitHub repository star growth over time. By deploying on Klutch.sh, you benefit from automatic HTTPS, persistent storage, and simple Docker-based deployment while maintaining continuous monitoring of your projects’ popularity and community engagement.

The application’s straightforward architecture makes it easy to customize and extend with additional features. Whether you’re a project maintainer tracking your own repositories, a developer researching popular tools, or an organization monitoring competitive landscapes, Daily Stars Explorer transforms raw GitHub star counts into meaningful insights about project trajectory and community interest.

Start tracking your repositories today and gain visibility into growth patterns, identify trending periods, and understand what drives community engagement with your open-source projects.

Deploy Daily Stars Explorer now and turn GitHub stars into actionable data for your development strategy.