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
- User adds repository to track via web interface
- Backend validates repository exists on GitHub
- Initial star count fetched from GitHub API
- Repository saved to database
- Scheduler periodically fetches updated star counts
- New data points stored with timestamps
- Frontend queries historical data
- Charts generated from time-series data
- Users view trends and statistics
- 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 directoryWORKDIR /app
# Copy package filesCOPY package*.json ./
# Install dependenciesRUN npm ci --only=production
# Copy application codeCOPY . .
# Build frontend (if applicable)RUN npm run build || echo "No build step"
# Production stageFROM node:18-alpine
# Install dumb-init for proper signal handlingRUN apk add --no-cache dumb-init
# Create app userRUN addgroup -g 1001 appuser && \ adduser -D -u 1001 -G appuser appuser
# Set working directoryWORKDIR /app
# Copy dependencies from builderCOPY --from=builder --chown=appuser:appuser /app/node_modules ./node_modules
# Copy application filesCOPY --chown=appuser:appuser . .
# Create data directoryRUN mkdir -p /app/data && \ chown appuser:appuser /app/data
# Switch to app userUSER appuser
# Expose portEXPOSE 3000
# Health checkHEALTHCHECK --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 applicationENTRYPOINT ["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;
// Middlewareapp.use(cors());app.use(express.json());app.use(express.static(path.join(__dirname, 'public')));
// GitHub API clientconst 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 databasedb.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 checkapp.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() });});
// Get all tracked repositoriesapp.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 trackapp.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 repositoryapp.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 repositoryapp.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 repositoryapp.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 limitapp.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 repositoriesasync 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 hourschedule.scheduleJob('0 * * * *', updateStarCounts);
// Manual update endpointapp.post('/api/update', async (req, res) => { res.json({ message: 'Update started' }); updateStarCounts().catch(console.error);});
// Serve frontendapp.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html'));});
// Start serverapp.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 shutdownprocess.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:
# GitHub API ConfigurationGITHUB_TOKEN=your_github_personal_access_token_here
# Server ConfigurationPORT=3000NODE_ENV=production
# Database ConfigurationDB_PATH=./data/stars.db
# Update Schedule (cron format)UPDATE_SCHEDULE=0 * * * *
# Data Retention (days)RETENTION_DAYS=365Step 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/tokens2. Set the GITHUB_TOKEN environment variable3. Run `npm install`4. Run `npm start`5. Open http://localhost:3000
## Usage
1. Enter repository owner and name2. Click "Add Repository"3. View real-time star tracking charts4. 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
git initgit add Dockerfile package.json server.js public/ .env.example docker-compose.yml README.mdgit commit -m "Initial Daily Stars Explorer deployment configuration"Step 9: Test Locally
Before deploying to Klutch.sh, test locally:
# Install dependenciesnpm install
# Create data directorymkdir -p data
# Set your GitHub tokenexport GITHUB_TOKEN=your_token_here
# Start the applicationnpm start
# Access at http://localhost:3000
# Test with Docker Composedocker-compose up -ddocker-compose logs -fDeploying to Klutch.sh
Step 1: Create GitHub Personal Access Token
- Navigate to GitHub Settings > Tokens
- Click "Generate new token (classic)"
- Give it a descriptive name like "Daily Stars Explorer"
- Select scopes: - `public_repo` (for public repositories) - `repo` (if tracking private repositories)
- Click "Generate token"
- Copy the token immediately (you won't see it again)
Step 2: Push Repository to GitHub
Create a new repository and push:
git remote add origin https://github.com/yourusername/daily-stars-explorer.gitgit branch -M mastergit push -u origin masterStep 3: Deploy to Klutch.sh
- Navigate to klutch.sh/app
- Click "New Project" and select "Import from GitHub"
- Authorize Klutch.sh to access your GitHub repositories
- Select your Daily Stars Explorer repository
- Klutch.sh will automatically detect the Dockerfile
Step 4: Configure Traffic Settings
- In the project settings, select **HTTP** as the traffic type
- Set the internal port to **3000**
- Klutch.sh will automatically provision an HTTPS endpoint
Step 5: Add Persistent Storage
Daily Stars Explorer requires persistent storage for the database:
- In your project settings, navigate to the "Storage" section
- 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:3000NODE_ENV:productionDB_PATH:/app/data/stars.db
Important: Keep your GitHub token secure and never commit it to the repository.
Step 7: Deploy the Application
- Review your configuration settings in Klutch.sh
- Click "Deploy" to start the deployment
- Monitor build logs for any errors
- Wait for initialization (typically 2-3 minutes)
- Once deployed, Daily Stars Explorer will be available at `your-app.klutch.sh`
Step 8: Verify Deployment
After deployment:
- Access your deployment at `https://your-app.klutch.sh`
- Check the health endpoint: `https://your-app.klutch.sh/health`
- Verify GitHub API connection by checking rate limit display
- Add a test repository to ensure tracking works
Getting Started with Daily Stars Explorer
Adding Your First Repository
Track any public GitHub repository:
- Navigate to your Daily Stars Explorer deployment
- In the "Add Repository" section, enter: - **Owner**: Repository owner (e.g., `facebook`) - **Name**: Repository name (e.g., `react`)
- Click "Add Repository"
- The repository appears in the list with initial star count
- Chart begins building as data accumulates
Popular repositories to track:
microsoft/vscodefacebook/reactvuejs/vueangular/angularnodejs/nodegolang/gorust-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:
- Add related repositories for comparison
- Track competitors in your space
- Monitor your own projects
- 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:
- Click “Delete” button on repository card
- Confirm deletion
- 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
- Use Environment Variables: Never hardcode tokens in code
// Goodconst octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
// Badconst octokit = new Octokit({ auth: 'ghp_hardcoded_token' });-
Token Permissions: Use minimal required scopes
- Only
public_repofor public repositories - Avoid unnecessary permissions
- Only
-
Token Rotation: Regularly rotate tokens
- Generate new token every 90 days
- Update environment variable in Klutch.sh
- Revoke old tokens
-
Monitor Token Usage: Check rate limit regularly
curl https://your-app.klutch.sh/api/rate-limitRate Limit Management
-
Respect GitHub Limits:
- 5,000 requests/hour with authentication
- 60 requests/hour without authentication
- Rate limit resets hourly
-
Implement Delays:
// Add delay between requestsasync function updateStarCounts() { for (const repo of repos) { await updateRepo(repo); await new Promise(resolve => setTimeout(resolve, 1000)); // 1 second delay }}- 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 }}- 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
- Database Indexing:
CREATE INDEX idx_star_history_repo ON star_history(repository_id, recorded_at);CREATE INDEX idx_repositories_fullname ON repositories(full_name);- Caching Strategy:
// Cache repository data for 5 minutesconst 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);});- Optimize Chart Data:
// Return aggregated data for large datasetsapp.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
- Backup Strategy:
#!/bin/bashBACKUP_DIR="/backups/stars_$(date +%Y%m%d_%H%M%S)"mkdir -p $BACKUP_DIR
# Backup SQLite databasesqlite3 /app/data/stars.db ".backup '${BACKUP_DIR}/stars.db'"
# Compress backupgzip ${BACKUP_DIR}/stars.db
echo "Backup complete: ${BACKUP_DIR}/stars.db.gz"- Data Retention Policy:
// Clean up old data beyond retention periodfunction 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 weeklyschedule.scheduleJob('0 0 * * 0', cleanupOldData);- Database Maintenance:
// Optimize database periodicallyfunction optimizeDatabase() { db.exec('VACUUM'); db.exec('ANALYZE'); console.log('Database optimized');}
schedule.scheduleJob('0 3 * * 0', optimizeDatabase);Monitoring and Logging
- 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() })); }};
// Usagelogger.info('Repository added', { repo: 'facebook/react', stars: 200000 });logger.error('Update failed', error, { repo: 'owner/name' });- 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 }); }});- 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
- 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); } }}- 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:
-
Verify Repository Exists:
- Check repository URL on GitHub
- Ensure correct owner and name
- Repository may have been renamed or deleted
-
Check Token Permissions:
- Private repositories require
reposcope - Verify token is active on GitHub
- Private repositories require
-
Test API Access:
curl -H "Authorization: Bearer YOUR_TOKEN" \ https://api.github.com/repos/owner/nameRate Limit Exceeded
Symptoms: Cannot add new repositories or updates fail
Solutions:
- Check Rate Limit Status:
curl https://your-app.klutch.sh/api/rate-limit-
Wait for Reset:
- Rate limits reset every hour
- Check reset time in rate limit response
-
Verify Token is Set:
# Check environment variable in Klutch.sh dashboardecho $GITHUB_TOKEN- Reduce Update Frequency:
- Modify schedule in
server.js - Increase delay between requests
- Modify schedule in
Charts Not Displaying
Symptoms: Repository cards show but charts are empty
Solutions:
- Check Data Collection:
# Verify data existscurl https://your-app.klutch.sh/api/repositories/1/history-
Verify Chart.js Load:
- Check browser console for errors
- Ensure Chart.js CDN is accessible
-
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:
- Verify Data Directory:
ls -la /app/data- Check Permissions:
chown -R appuser:appuser /app/datachmod 755 /app/data-
Verify Persistent Volume:
- Ensure volume is mounted in Klutch.sh
- Check volume size hasn’t exceeded limit
-
Database Path:
# Verify DB_PATH environment variableecho $DB_PATHUpdates Not Running
Symptoms: Star counts not updating automatically
Solutions:
- Check Scheduler:
// Verify schedule in server.jsschedule.scheduleJob('0 * * * *', updateStarCounts); // Every hour- Manual Update:
curl -X POST https://your-app.klutch.sh/api/update-
Check Logs:
- View Klutch.sh container logs
- Look for update messages
-
Verify GitHub Token:
- Token must be valid
- Check rate limit not exceeded
High Memory Usage
Symptoms: Application crashes or becomes slow
Solutions:
-
Limit Tracked Repositories:
- Remove unused repositories
- Track only essential projects
-
Implement Data Aggregation:
- Reduce granularity for old data
- Archive detailed historical data
-
Add Memory Limits:
// In server.jsif (process.memoryUsage().heapUsed > 500 * 1024 * 1024) { console.warn('High memory usage detected'); // Trigger cleanup}- 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 minutesschedule.scheduleJob('*/30 * * * *', updateStarCounts);
// Update daily at 2 AMschedule.scheduleJob('0 2 * * *', updateStarCounts);
// Update every 6 hoursschedule.scheduleJob('0 */6 * * *', updateStarCounts);
// Multiple schedulesschedule.scheduleJob('0 * * * *', updateStarCounts); // Hourlyschedule.scheduleJob('0 0 * * 0', cleanupOldData); // Weekly cleanupData 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
- GitHub REST API Documentation
- Creating GitHub Personal Access Tokens
- Octokit.js Documentation
- Chart.js Documentation
- better-sqlite3 Documentation
- Klutch.sh Documentation
- Persistent Storage Guide
- Networking Configuration
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.