How A Simple Assignment Became A Full-Stack Automated System (And Why Vercel's Timeout Message Haunts Me)



TL;DR: Built a MGNREGA dashboard for one state. Turned into automated system for all of India. Fought timeouts, laggy maps, and CSV hell. Won using GitHub Actions, CSS Grid, and zero paid features.
Day 0: The Assignment (Not the official Days, I didn't count them then)
Bharat Digital Fellowship 2026 Assignment:
- Choose a state (Maharashtra, Bihar, etc.)
- Build a dashboard with MGNREGA data
- Add district comparison tool
- Deadline: November 1st, 2025
My reaction: "Easy. I'll pick Maharashtra, throw up a React dashboard, add some charts. Done in 1 week."
Narrator:It was not done in around 5 days.*
Day 1: The Data Nightmare
I logged into data.gov.in to fetch MGNREGA data.
My plan:
- Call API
- Get all data
- Display in dashboard
- Submit assignment
Reality:
1fetch('https://api.data.gov.in/resource/...')
2 .then(res => res.json())
3 .then(data => {
4 // Still fetching...
5 // Still fetching...
6 // Still fetching...
7 // *30 minutes later*
8 // Still fetching...
9 });The API wasn't slow. The data was massive*data was massive*.
The Numbers That Broke My Fetch Call
- 28,988 total metrics*28,988 total metrics* across all states
- 14,028 unique metrics*14,028 unique metrics* after deduplication
- 740 districts*740 districts* across 36 states/UTs
- 35+ data points*35+ data points* per district per month
- Updates:*Updates:* Daily
Problem:*Problem:* Fetching all this via API = infinite loop of doom.
Day 1.5: The CSV Pivot
New plan:
- Screw the API (for now)
- Download CSV files manually from data.gov.in
- Import to database
- Build dashboard from local data
Downloaded:
MAHARASHTRA_2024-2025.csv(19 months of data)MAHARASHTRA_2025-2026.csv(latest October data)
Result:
- 646 monthly records
- 34 districts
- Database imported successfully ✅
- Dashboard built ✅
- Submission on Nov 1st ✅
Assignment complete. Time to relax, right?
Me: "...but what if I did this for ALL of India?"
Day 2: The "Just For Fun" Spiral
Date: November 2nd, 2025
Time: 3 AM (all bad decisions happen at 3 AM)
Thought: "I can add filters to the API for all states and inject that data."
The All-India Challenge
Instead of just Maharashtra, let's get:
- All 36 states and UTs*36 states and UTs*
- Both 2024-25 and 2025-26*2024-25 and 2025-26* financial years
- All 740 districts*740 districts*
- All 14,028 unique metrics*14,028 unique metrics*
Added API filters:
1
2for (const state of states) {
3 for (const year of financialYears) {
4 await fetchAndInjectData(state, year);
5 }
6}Result: Data injected successfully into Supabase! 🎉
Then reality hit: This data updates daily
Manual updates? Hell no.
Day 2.5: The Cron Job That Never Was
The Plan: Add a cron job to sync data daily at 1 AM IST.
The Problem: Deployed on Vercel. Cron jobs = Paid feature. I'm broke.
New Plan: Use Vercel's Edge Functions with external cron service (cron-job.org) → Free!
1// /api/cron/daily-sync
2export async function GET(request: Request) {
3 // Verify secret
4 // Fetch latest data
5 // Update database
6 return Response.json({ success: true });
7}GitHub Actions workflow:
1schedule:
2 - cron: '30 19 * * *' # 1:00 AM IST
3
4jobs:
5 sync:
6 runs-on: ubuntu-latest
7 steps:
8 - name: Call sync endpoint
9 run: |
10 curl -X GET "https://your-app.vercel.app/api/cron/daily-sync?secret=${{ secrets.CRON_SECRET }}"Deployed. Triggered. Watched logs.
1✅ GitHub Action started
2✅ API endpoint called
3⏰ Syncing data...
4⏰ Still syncing...
5⏰ Still syncing...
6❌ Error: Function execution timeout (300 seconds)Me: "WHAT."
Day 3: The Vercel Wall
The Problem:
- Vercel's max function timeout: 300 seconds*max function timeout: 300 seconds* (5 minutes)
- My data sync: 15-60 minutes*15-60 minutes* depending on API rate limits
- Math: 15 minutes > 5 minutes → ❌ Timeout
Options:
- Pay for Vercel Pro (timeout up to 900s / 15 min) — $$$
- Split sync into smaller chunks — Complex, fragile
- Run sync directly on GitHub Actions — Free, reliable, no timeout*Free, reliable, no timeout*
Guess which one I picked.*Guess which one I picked.*
The Real Solution: Direct Database Sync
New architecture:
1GitHub Actions Runner
2 ↓
3 Install Node.js, pnpm
4 ↓
5 pnpm install (frozen lockfile)
6 ↓
7 Generate Prisma Client
8 ↓
9 Run scripts/daily-sync.ts
10 ↓
11 Connect directly to Supabase PostgreSQL
12 ↓
13 Sync data (15-60 min, no timeout)
14 ↓
15 Upload logs as artifactsKey changes:
1# .github/workflows/daily-sync.yml
2jobs:
3 sync-data:
4 runs-on: ubuntu-latest
5 timeout-minutes: 180 # 3 hours (GitHub's max)
6
7 steps:
8 - name: Enable pnpm
9 run: corepack enable && corepack prepare pnpm@latest --activate
10
11 - name: Install dependencies
12 run: pnpm install --frozen-lockfile
13
14 - name: Generate Prisma Client
15 run: pnpm prisma generate
16
17 - name: Run sync
18 env:
19 DATABASE_URL: ${{ secrets.DATABASE_URL }}
20 DATA_GOV_API_KEY: ${{ secrets.DATA_GOV_API_KEY }}
21 run: pnpm run sync:cliResult:
- ✅ Runs daily at 1:00 AM IST
- ✅ No timeouts (3-hour limit)
- ✅ Full logs uploaded as artifacts
- ✅ Resume support (if sync fails midway)
- ✅ Completely free (GitHub Free Tier)
Lesson learned: Sometimes the "hacky" solution is the right solution.
Day 3.5: The Map Saga (Or: When Simple Beats Complex)
Goal: Interactive India map where users can click states and navigate to state pages.
Attempt 1: The SVG Dream
Found: India GeoJSON file with precise state boundaries
Size: 210KB
Coordinates: Thousands of polygon points
Used: react-simple-maps library
1<ComposableMap>
2 <Geographies geography={indiaGeoJSON}>
3 {({ geographies }) =>
4 geographies.map((geo) => (
5 <Geography
6 key={geo.rsmKey}
7 geography={geo}
8 onClick={() => navigate(`/state/${geo.id}`)}
9 />
10 ))
11 }
12 </Geographies>
13</ComposableMap>Result:
- ✅ Looks beautiful
- ✅ Precise boundaries
- ❌ Laggy as hell*Laggy as hell* on mobile
- ❌ 19MB file size
- ❌ Re-renders on every hover
- ❌ Unusable on ₹3,000 phones
Me: "There has to be a better way."
Attempt 2: CSS Grid Simplicity
Idea: What if... I just use CSS Grid?
Implementation:
- 27 rows × 32 columns grid
- Each state = series of grid cells
- Cells merge using
grid-columnandgrid-row - Click handlers on each state div
- Tooltip on hover
1// Simplified example
2<div className="grid grid-cols-32 grid-rows-27">
3 {/* Kashmir */}
4 <div
5 className="col-start-13 col-span-7 row-start-1 row-span-5"
6 onClick={() => navigate('/state/jammu-kashmir')}
7 >
8 Jammu & Kashmir
9 </div>
10 {/* Repeat for all 36 states */}
11</div>Result:
- ✅ Instant load
- ✅ Zero lag on mobile
- ✅ 5KB CSS (vs 210KB GeoJSON)
- ✅ Works on ancient phones
- ✅ Easy to maintain
Trade-off: Not geographically precise, but functional and fast.
Lesson: Fancy isn't always better. Sometimes CSS Grid > Complex SVG.
Day 4: Search Bar With Voice (Because Why Not)
Goal: Search districts by name with voice input.
Why voice? Not everyone types well. Especially older users or regional language speakers.
Tech Stack:
- Fuse.js for fuzzy search (handles typos)
- Web Speech API (browser-native, zero dependencies)
- Keyboard navigation (↑↓ arrows, Enter, Escape)
Implementation:
1// Voice input
2const recognition = new webkitSpeechRecognition();
3recognition.lang = 'en-IN'; // or hi-IN, mr-IN, etc.
4recognition.onresult = (event) => {
5 const transcript = event.results[0][0].transcript;
6 setSearchQuery(transcript); // Auto-searches
7};
8
9// Fuzzy search
10const fuse = new Fuse(districts, {
11 keys: ['name', 'stateName'],
12 threshold: 0.3, // 30% tolerance for typos
13});
14
15const results = fuse.search(searchQuery);Features added:
- 🎤 Voice input button (click to speak)
- ⌨️ Keyboard navigation (arrows to move, Enter to select)
- 🔍 Fuzzy matching (handles typos like "Mumbia" → "Mumbai")
- 📱 Mobile-optimized (large touch targets)
Result: Search works in 9 languages with voice support. Zero external paid APIs.
Day 4.5: The i18n Rabbit Hole
Realization: 1.4 billion Indians don't all speak English.
Solution: Multi-language support.
Languages Added:
- English (en)
- Hindi (hi)
- Marathi (mr)
- Tamil (ta)
- Telugu (te)
- Malayalam (ml)
- Kannada (kn)
- Bengali (bn)
- Gujarati (gu)
Tool: next-intl for routing + i18n
Translation process:
- Extract all UI strings (25+ keys)
- Use AI (ChatGPT/Claude) for initial translations
- Manual review by native speakers (where possible)
- Store in JSON files (
messages/en.json,messages/hi.json, etc.)
Did I use AI? Yes. Obviously.
Why? Because manually translating 25 keys × 9 languages = 225 strings. Ain't nobody got time for that.
But: AI translations need review. Some phrases don't translate well. Cultural context matters.
Example:
- English: "Explore Districts"
- Hindi (AI): "जिलों का अन्वेषण करें" (technically correct)
- Hindi (Better): "जिले देखें" (more natural)
Lesson: Use AI as a starting point, not the finish line.
Next Chapter: The Next Evolution - Golang Backend (The 10x Future)
Current status: Working system. Data syncs daily. APIs respond fast. Users happy.
But here's the thing: I'm already hitting limits.
The Current Bottlenecks
1. Sync Time
- Current: 15-60 minutes for 740 districts
- GitHub Actions timeout: 180 minutes max
- Buffer: Only 2-3x (uncomfortable)
2. Memory Usage
- Current: 500MB during sync
- GitHub runner limit: 7GB (but shared)
- Vercel function limit: 1GB (serverless constraint)
3. Concurrency
- TypeScript Promise.all: 3-5 concurrent operations max
- Reason: Event loop saturation, memory pressure
- Result: Slow throughput
4. Cost
- Vercel Pro: $20/month (for cron jobs)
- Supabase Pro: $25/month (approaching limits)
- Total: $540/year (for a side project!)
The Golang Solution (The 10x Rewrite)
I'm planning a backend migration to Golang while keeping the TypeScript frontend. Here's why this isn't just "rewrite for fun":
1. Concurrency Model (The Game Changer)
Current TypeScript limitation:
1// Max 3-5 concurrent operations before event loop saturates
2await Promise.all(states.slice(0, 3).map(s => syncState(s)));Golang goroutines:
1// 50-100 concurrent operations easily
2var wg sync.WaitGroup
3for _, state := range states {
4 wg.Add(1)
5 go func(s State) {
6 defer wg.Done()
7 syncState(s) // Each runs in parallel
8 }(state)
9}
10wg.Wait()Real impact:
- Sync time: 15-60 min → 5-15 min (3-4x faster)
- Throughput: 3 req/s → 50 req/s (16x)
- GitHub Actions: More buffer for future data growth
2. Memory Efficiency (Critical at Scale)
Current issue:
- Loading 28,988 metrics → 500MB RAM
- Node.js garbage collection pauses
- Memory spikes during sync
Golang stream processing:
1rows, _ := db.Query("SELECT * FROM metrics WHERE finYear = ?", year)
2defer rows.Close()
3
4for rows.Next() { // One row at a time
5 var metric Metric
6 rows.Scan(&metric.ID, &metric.Value, ...)
7 processAndInsert(metric) // Immediate processing
8 // Memory freed after each iteration
9}Result:
- Memory usage: Constant 100-150MB (3-4x less)
- No GC pauses:Golang GC is microsecond-level
- Predictable performance
3. Deployment Simplicity
Current (Next.js + TypeScript):
1FROM node:20-alpine # 150MB
2COPY package.json .
3RUN npm install # +200MB node_modulesCOPY . .
4RUN npm run build # +50MB build artifacts# Final image: 400MB+Golang (Single Binary):
1FROM scratch # 0MB base
2COPY main /main # 15MB binary (all dependencies included)CMD ["/main"]
3# Final image: 15MBBenefits:
- Deploy: Just copy one file to server
- Cold start: < 100ms (vs 2-3s for Node.js)
- No dependency hell: Everything compiled in
4. Cost Optimization
Current annual costs:
| **Service** | **Current** | **With Golang** | **Savings** |
|---|---|---|---|
| Compute | Vercel Pro $240 | VPS $60 | $180 |
| Database | Supabase $300 | Supabase Free $0 | $300 |
| GitHub Actions | $0 (barely) | $0 (comfortable) | Buffer |
| **Total** | **$540/year** | **$60/year** | **$480** |
Why savings?
- Golang uses 5x less resources = Supabase stays in free tier
- Self-host on $5/month VPS instead of Vercel Pro
- Faster sync = more GitHub Actions free tier headroom
5. Real-World Validation
Companies that migrated Node.js → Golang:
PayPal:
- 35% faster response time
- 50% fewer servers needed
- Handled 10x more requests
Uber:
- Geofence service: 10x concurrency
- 80% memory reduction
- No more OOM crashes
Medium:
- API response: 500ms → 50ms
- User-facing pages 10x faster
- Happier users
The pattern: Heavy data systems benefit massively from Golang.
6. The Proposed Architecture
1Frontend (Keep TypeScript/Next.js)
2 ├── UI components
3 ├── i18n (9 languages)
4 ├── Voice search
5 ├── Interactive map
6 └── Deployed on Vercel (static/SSR)
7
8Backend (Migrate to Golang)
9 ├── REST API server (Fiber/Gin)
10 │ ├── /api/districts
11 │ ├── /api/compare
12 │ └── /api/states
13 │
14 ├── Daily sync service
15 │ ├── 50 concurrent workers
16 │ ├── Batch inserts (1000 rows)
17 │ └── Auto-retry logic
18 │
19 └── Deployed on $5 VPS or Fly.io
20
21Database (Same)
22 └── PostgreSQL (Supabase or self-hosted)
23
24Cache (Same)
25 └── Redis (Upstash or self-hosted)Key insight: Keep TypeScript where it shines (frontend), use Golang where performance matters (backend).
Implementation Roadmap
Month 1: Golang API Server
- Replace Next.js API routes with Golang
- Deploy both (Next.js frontend + Golang API)
- Zero downtime migration
Month 2: Golang Sync Service
- Replace TypeScript daily sync script
- Deploy as separate microservice
- 10x faster sync times
Month 3: Optimization
- Horizontal scaling (run multiple instances)
- Advanced caching strategies
- Monitoring (Prometheus + Grafana)
Month 4: Cost Optimization
- Move to self-hosted VPS
- Annual cost: $540 → $60
Why This Matters
This isn't just about performance.
This is about sustainability for a side project that could grow into something real.
The questions I ask myself:
- What if 10,000 users start using this daily?
- What if government wants to partner and needs SLAs?
- What if data grows to 100,000+ districts (all of rural India)?
- What if I want to add real-time updates?
With TypeScript alone: Expensive, slow, hitting limits.
With Golang backend: Cheap, fast, scales horizontally.
Learning Plan (Public Commitment)
I'm learning Golang by building in public:
Week 1-2: Golang basics
- Tour of Go (official tutorial)
- Concurrency patterns (goroutines, channels)
- Standard library exploration
Week 3-4: Build simple API
- Fiber/Gin framework (Express-like)
- Database connections (pgx, GORM)
- API endpoints (REST)
Week 5-6: Build sync service
- Worker pool pattern
- Batch processing
- Error handling & retries
Week 7-8: Deploy to production
- Dockerize
- Deploy to Fly.io (or VPS)
- Monitor performance
Timeline: 2 months to production Golang backend.
The End Goal
A hybrid system:
- TypeScript frontend (React ecosystem, rapid iteration)
- Golang backend (performance, cost-efficiency, scalability)
- Best of both worlds
This is how side projects evolve into production-grade systems.
This is how you build for scale without venture capital.
This is the future of this project.
Stay tuned for the Golang migration series. I'll document everything: wins, losses, and "why did I think this was a good idea" moments. 😅
The Tech Stack (Final)
1Frontend:
2 - Next.js 16 (App Router)
3 - React 19.2
4 - TypeScript 5
5 - TailwindCSS 4
6 - Framer Motion (animations)
7 - Fuse.js (search)
8 - Web Speech API (voice)
9 - next-intl (i18n)
10
11Backend:
12 - Next.js API Routes
13 - PostgreSQL (Supabase)
14 - Prisma ORM 6
15
16DevOps:
17 - GitHub Actions (automated sync)
18 - Vercel (hosting)
19 - pnpm (package manager)
20
21Data Source:
22 - data.gov.in API
23 - Manual CSV importsTotal Cost: $0/month (Free tiers everywhere)
Unknown Chapter: The Map That Nobody Asked For (But I Built Anyway)
Remember that 19MB SVG map that was mocking me?
It's getting a proper funeral. And a rebirth. As an interactive Leaflet map.
The Unnecessary Journey Begins
Rational Brain: "The CSS Grid map works fine. Users can click states. It's functional. Move on."
Developer Brain: "But what if... zoom controls? Tooltips? Choropleth visualization? Fullscreen mode?"
Rational Brain: "That's overkill for employment data."
Developer Brain: "CHALLENGE ACCEPTED."
Why Leaflet? (The Technical Justification)
Current CSS Grid Map:*Current CSS Grid Map:*
- ✅ Works
- ✅ Fast
- ✅ Simple
- ❌ No zoom/pan
- ❌ No hover tooltips
- ❌ Not "industry standard"
Leaflet Interactive Map:*Leaflet Interactive Map:*
- ✅ Zoom/pan controls
- ✅ Rich hover tooltips
- ✅ Choropleth color scales
- ✅ Fullscreen mode
- ✅ Mobile gestures (pinch-to-zoom)
- ✅ Export as image
- ⚠️ Slightly more complex
The Trade-off:
- Complexity: +30%
- User Experience: +200%
- Coolness Factor: +1000%
Decision: Worth it.
The Open Source Moment
I needed GeoJSON files for all 36 Indian states with district boundaries.
Option 1: Process raw shapefiles myself
- Download from government sources
- Convert SHP → GeoJSON
- Simplify coordinates
- Test each file
- Time: 2-3 weeks
Option 2: Search GitHub
- Type: "india geojson districts"
- Find:
udit-001/india-maps-data - Files ready: All 36 states ✅
- Time: 5 minutes
This. This is why I love open source.
Discovering india-maps-data
Repository: https://github.com/udit-001/india-maps-data
What I found:
1geojson/
2├── india.geojson # All India with state boundaries
3└── states/
4 ├── maharashtra.geojson # Districts of Maharashtra
5 ├── tamil-nadu.geojson # Districts of Tamil Nadu
6 ├── uttar-pradesh.geojson # Districts of UP
7 └── ... (all 36 states!)CDN URLs (no download needed):
1https://cdn.jsdelivr.net/gh/udit-001/india-maps-data@latest/geojson/india.geojson
2https://cdn.jsdelivr.net/gh/udit-001/india-maps-data@latest/geojson/states/maharashtra.geojsonMy reaction: 🤯
Someone already did the hard work. Clean files. CDN hosted. Free. MIT licensed.
What I saved:
- 2 weeks of map data processing
- Countless hours debugging coordinate systems
- Sanity dealing with QGIS/MapShaper
What I learned: Before building, search GitHub. Seriously. Just search.
The UI Inspiration
While researching Leaflet implementations, I found another gem:
Repository: https://github.com/udit-001/india-maps
An interactive India map built with:
- React + Vite
- Leaflet + react-leaflet
- GeoJSON rendering
- Color customization
- Export functionality
I didn't copy the code.*I didn't copy the code.* But I studied:
- How they handled GeoJSON loading
- Tooltip implementations
- Color scale logic
- Mobile responsiveness
- State name normalization
Open source isn't just about code. It's about learning patterns, seeing solutions, understanding trade-offs.
The Implementation Plan
Architecture:
1/map → All India state-level map
2/map/[stateCode] → Individual state district mapData Flow:
1User clicks state on home page
2 ↓
3/map (All India Leaflet map)
4 ↓ (clicks Maharashtra)
5/map/maharashtra (District-level map)
6 ↓ (clicks district card)
7/state/maharashtra (Existing state page)Tech Stack:
1// Dependencies
2import { MapContainer, GeoJSON, ZoomControl } from 'react-leaflet';
3import 'leaflet/dist/leaflet.css';
4
5// Data
6const geoData = await fetch(
7 'https://cdn.jsdelivr.net/gh/udit-001/india-maps-data@latest/geojson/india.geojson'
8).then(r => r.json());
9
10// Visualization
11const getColor = (expenditure) => {
12 if (exp > 1000000000) return '#0f766e'; // High
13 if (exp > 500000000) return '#14b8a6'; // Medium
14 if (exp > 100000000) return '#5eead4'; // Low
15 return '#ccfbf1'; // Very low
16};Features:
- Choropleth Visualization
- Color states by expenditure
- Dark teal = High, Light teal = Low
- Gray = No data
- Interactive Tooltips
1 Maharashtra
2 ━━━━━━━━━━━━━━━━
3 Districts: 36
4 Expenditure: ₹2,450.56 Cr
5 Households: 1,23,456
6 Click to drill down →- Controls
- Zoom in/out
- Reset view
- Fullscreen toggle
- Metric switcher (expenditure/households/person days)
- Mobile Gestures
- Pinch to zoom
- Two-finger pan
- Tap for details
The "Is This Overkill?" Moment
The friend in my brain: "So... you already have a working map."
Me: "Yes."
Friend: "And users can click states and navigate."
Me:"Yes."
Friend: "Then why are you rebuilding it with Leaflet?"
Me: "..."
Me: "Because I can?"
Friend:
The Truth:
- Do I need zoom controls for state boundaries? No.
- Do I need choropleth visualization? Not really.
- Do I need hover tooltips? Tables work fine.
But here's the thing:
Side projects aren't about "need." They're about:
- Learning new libraries (Leaflet)
- Solving interesting problems (GeoJSON rendering)
- Building portfolio pieces (impressive demos)
- Having fun (the real reason)
If I only built what was "necessary," I'd never learn anything new.
The Implementation (Week-by-Week)
Hour 1: Basic Setup
- Install
leaflet,react-leaflet - Create
/maproute - Fetch India GeoJSON
- Render basic map
- Result:Blue static map, no interactions
Hour 2: Data Integration
- Connect to PostgreSQL
- Aggregate state-level metrics
- Implement color scale
- Add click handlers
- Result: Colored map with navigation
Hour 3: District Maps
- Create
/map/[stateCode]routes - Fetch state GeoJSON files
- Connect district data
- Add district cards below map
- Result: Full drill-down experience
Hour 4: Polish
- Add tooltips
- Implement legend
- Fullscreen mode
- Mobile optimization
- Result: Production-ready map
Next: Performance
- GeoJSON caching
- Dynamic imports (avoid SSR issues)
- Loading skeletons
- Error boundaries
- Result: Fast, robust, user-friendly
The Challenges (Because Nothing Ever Works First Try)
Challenge 1: "window is not defined"
Error:
1ReferenceError: window is not definedCause: Leaflet uses browser APIs. Next.js SSR doesn't have window.
Solution:
1// Dynamic import with ssr: false
2const Map = dynamic(() => import('./map'), {
3 ssr: false,
4 loading: () => <MapSkeleton />
5});Challenge 2: GeoJSON State Names Mismatch
GeoJSON: "Andhra Pradesh"
Database: "ANDHRA PRADESH"
Result: States not matching, no data displayed
Solution:
1function normalizeStateName(name: string): string {
2 return name.toUpperCase().trim()
3 .replace(/&/g, 'AND')
4 .replace(/\s+/g, ' ');
5}Challenge 3: Mobile Tooltips Not Working
Problem: Hover doesn't work on touch devices
Solution: Detect touch device, use tap instead of hover
1const isTouchDevice = 'ontouchstart' in window;
2const eventType = isTouchDevice ? 'click' : 'mouseover';The Performance Numbers
GeoJSON File Sizes:
- All India: 450KB (simplified)
- Maharashtra: 120KB
- Uttar Pradesh: 180KB (most districts)
Load Times:
- Initial map load: ~1.5s (including GeoJSON fetch)
- State map load: ~800ms
- Interactions: 60fps (smooth)
Optimization Techniques:
- GeoJSON Caching: Keep in memory after first load
- Dynamic Imports: Only load Leaflet on map pages
- Coordinate Simplification: Use simplified GeoJSON (10% detail)
- React Query: Cache DB queries for 5 minutes
The Open Source Lesson
Before this project, I thought:
- "I need to build everything from scratch to learn"
- "Using others' work is 'cheating'"
- "Real developers don't use libraries"
Now I know:
- Standing on shoulders of giants is smart, not lazy
- Open source accelerates learning exponentially
- Community contributions make ambitious projects possible
What @udit-001's repositories taught me:
- Someone has solved your problem. Just search.
- Clean data is worth gold. GeoJSON files saved me weeks.
- Code examples teach patterns. I learned Leaflet best practices.
- Give credit. Always acknowledge sources.
- Contribute back. I'll add MGNREGA-specific examples to my repo.
The GitHub Philosophy:
1See a problem → Search GitHub → Find solution
2↓
3Learn from it → Build on it → Share your version
4↓
5Others find YOUR work → Cycle repeatsThis is how the web gets built. One open-source project at a time.
The Unnecessary Features I'm Adding Next
Because why stop now?*Because why stop now?*
- Metric Toggle
- Switch between: Expenditure | Households | Person Days | Works Completed
- Real-time color scale updates
- Time Slider
- Slide through months: April 2024 → October 2025
- Animate employment trends
- Comparison Mode
- Select 2 states
- Side-by-side maps
- Highlight differences
- Export Options
- Download map as PNG
- Export data as CSV
- Generate PDF report
Are these necessary? Not at all.
Will they be cool?Absolutely.
Will I build them?You know it.
The Takeaway
Building this map taught me:
- Overkill is underrated
- Open source is a superpower
- Learning happens when you go beyond "good enough"
- Side projects are for experimentation, not efficiency
If I only built what was "necessary":
- I'd never learn Leaflet
- I'd never discover india-maps-data
- I'd never push my skills further
- This blog post wouldn't exist
The "unnecessary" features are where the real learning happens.
Shoutout & Gratitude
Huge thanks to @udit-001:
- india-maps-data: Clean GeoJSON files for all Indian states
- india-maps: UI reference and implementation patterns
- Saving me from weeks of map data processing hell
Check out his work:
This is the power of open source. One person's weekend project becomes the foundation for someone else's production app.
The Reality Check
Did I Use LLMs?
Yes. A lot.
- Initial code scaffolding
- Prisma schema design help
- Translation assistance
- Debugging weird TypeScript errors
- CSS Grid layout calculations
Did I "Vibe-Code" Everything?
Hell no.
Here's the truth:
- LLMs are tools not magic wands
- You need to understand your stack
- You need to review all generated code
- You need to test everything
- You need to debug when things break (and they will)
What LLMs CAN'T do:
- Understand your project's architecture
- Make design decisions for you
- Debug production issues (without context)
- Optimize performance
- Handle edge cases
What I DID:
- Designed database schema
- Architected sync system
- Chose tech stack
- Made UX decisions
- Debugged all issues
- Wrote documentation
- Tested on real devices
LLMs helped me code faster. They didn't code FOR me.
The Numbers (Final)
Data Coverage
- States: 36 states and UTs
- Districts: 740 (Maharashtra: 34 complete)
- Records: 646+ monthly metrics (Maharashtra)
- Time Range: April 2024 - October 2025
- Data Points: 35+ metrics per record
Technical Metrics
- Lines of Code: ~15,000 (excluding dependencies)
- Components: 40+ Shadcn UI components
- API Endpoints: 6 REST APIs
- Languages: 9 (3 production, 6 beta)
- Translation Keys: 225+ total
- Bundle Size: 150KB gzipped (first load)
- Build Time: ~90 seconds
- TypeScript: 100% coverage
Performance
- Lighthouse Score: 90+ (target)
- First Contentful Paint: < 1.5s
- Time to Interactive: < 3s
- API Response: < 500ms (uncached)
- Daily Sync: 15-60 minutes (automated)
What I Learned
1. Assignments Can Evolve
Don't limit yourself to the brief. If you see potential, explore it.
2. Free > Paid (When Possible)
GitHub Actions > Vercel Cron Jobs (for my use case)
3. Simple > Complex (When It Works)
CSS Grid > 210KB SVG Map (for performance)
4. Automation > Manual Labor
15-60 min automated sync > Manual CSV updates forever
5. LLMs Are Tools, Not Gods
Use them to speed up, not replace thinking.
6. Real Users Have Real Needs
Voice input, multi-language support, mobile optimization — these aren't "nice-to-haves".
What's Next?
Short-term
- ✅ Complete Maharashtra (Done)
- ⏳ Optimize SVG map (still in repo, waiting for redemption)
- ⏳ Add 5 more states (UP, Bihar, Karnataka, TN, Rajasthan)
- ⏳ District detail pages with charts
- ⏳ PWA support (offline mode)
Mid-term
- ⏳ All 36 states/UTs
- ⏳ ML-based insights
- ⏳ Export data (CSV, PDF)
- ⏳ Mobile app (React Native)
Long-term
- ⏳ Real-time updates
- ⏳ AI chatbot
- ⏳ Predictive analytics
- ⏳ Government partnership (fingers crossed)
Try It Yourself
Live Demo: MGNREGA INDIA
GitHub: github.com/certainlyMohneeesh/mnrega-mah
Want to contribute?
- Optimize the SVG map (please, I'm begging)
- Add more language translations
- Report bugs
- Suggest features
Final Thoughts
This started as a 2-week assignment. It's been 3+ months.
I fought:
- Infinite fetch loops
- Vercel timeouts
- Laggy SVG maps
- CSV hell
- Daily data sync nightmares
I learned:
- GitHub Actions are underrated
- CSS Grid is underrated
- Free tiers are your friend
- LLMs are powerful tools (not magic)
- Real users need accessible UIs
Would I do it again? Yes. In a heartbeat.
Because assignments that turn into real projects? Those are the ones that matter.
Let's Connect
GitHub: @certainlyMohneeesh
LinkedIn: linkedin.com/in/mohneeesh
Email: certainlymohneesh@gmail.com
P.S. If you know how to make a 19MB GeoJSON SVG map NOT lag on mobile, seriously, DM me. That code is still sitting in interactive-india-map-v3.tsx, mocking me. 😅
⭐ Star on GitHub • 🐛 Report Bug • 💡 Request Feature
content = """# How A Simple Assignment Became A Full-Stack Automated System (And Why Vercel's Timeout Message Haunts Me)
TL;DR:*TL;DR:* Built a dashboard for one state. Accidentally scaled it to all of India. Fought timeouts, laggy maps, and CSV hell. Won using GitHub Actions, CSS Grid, and zero paid features.
---
The "5-Minute" Assignment
The Task:*The Task:* Build a dashboard for MGNREGA data (Maharashtra state).
My Brain:*My Brain:* "Easy. Fetch API. Render Chart. Submit. Done by lunch."
The Narrator:*The Narrator: It was, in fact, not done by lunch.*
The Data Nightmare
I logged into data.gov.in. I wrote a simple fetch() call.
I waited.
And waited.
And waited.
The Reality:*The Reality:*
- 28,988*28,988* total metrics.
- 740*740* districts.
- Infinite*Infinite* loading state.
The API wasn't broken; the data was just massive. My browser froze, and my console looked like a crime scene.
The Vercel Heartbreak 💔
I decided to be smart. "I'll just run a cron job on Vercel to sync the data to my database at night!"
The Plan:*The Plan:*
- Vercel Cron triggers function.
- Function fetches data.
- Function updates DB.
The Result:*The Result:*
Error: Function execution timeout (10s)
I dug into the docs. Vercel Serverless Functions on the free tier have a hard limit. Even Edge Functions (the "fast" ones) cap out at 300 seconds. My sync took 15 minutes.
I had two choices:
- Pay $20/mo for Vercel Pro.
- Rewrite everything.
The "Hacky" Solution (GitHub Actions)
I chose option 3: Abuse GitHub Actions.*Abuse GitHub Actions.*
GitHub Actions runners are designed for CI/CD, but they are basically just free computers that you can borrow for up to 6 hours*6 hours*.
The New Architecture:*The New Architecture:*
- Cron Trigger:*Cron Trigger:* GitHub Action scheduled for 1 AM.
- The Script:*The Script:* A simple Node.js script that connects to Supabase, fetches the massive JSON, and pushes it to the DB.
- Cost:*Cost:* $0.
1# .github/workflows/sync.yml
2on:
3 schedule:
4 - cron: '0 19 * * *' # 1 AM IST
5jobs:
6 sync:
7 timeout-minutes: 360 # 6 hours of freedom
8 steps:
9 - run: npm run sync-dbcontent = """# How A Simple Assignment Became A Full-Stack Automated System (And Why Vercel's Timeout Message Haunts Me)
TL;DR: Built a dashboard for one state. Accidentally scaled it to all of India. Fought timeouts, laggy maps, and CSV hell. Won using GitHub Actions, CSS Grid, and zero paid features.
---
The "5-Minute" Assignment
The Task: Build a dashboard for MGNREGA data (Maharashtra state).
My Brain: "Easy. Fetch API. Render Chart. Submit. Done by lunch."
The Narrator: *It was, in fact, not done by lunch.*
The Data Nightmare
I logged into data.gov.in. I wrote a simple fetch() call.
I waited.
And waited.
And waited.
The Reality:
- 28,988 total metrics.
- 740 districts.
- Infinite loading state.
The API wasn't broken; the data was just massive. My browser froze, and my console looked like a crime scene.
The Vercel Heartbreak 💔
I decided to be smart. "I'll just run a cron job on Vercel to sync the data to my database at night!"
The Plan:
- Vercel Cron triggers function.
- Function fetches data.
- Function updates DB.
The Result:
Error: Function execution timeout (10s)
I dug into the docs. Vercel Serverless Functions on the free tier have a hard limit. Even Edge Functions (the "fast" ones) cap out at 300 seconds. My sync took 15 minutes.
I had two choices:
- Pay $20/mo for Vercel Pro.
- Rewrite everything.
The "Hacky" Solution (GitHub Actions)
I chose option 3: Abuse GitHub Actions.
GitHub Actions runners are designed for CI/CD, but they are basically just free computers that you can borrow for up to 6 hours.
The New Architecture:
- Cron Trigger: GitHub Action scheduled for 1 AM.
- The Script: A simple Node.js script that connects to Supabase, fetches the massive JSON, and pushes it to the DB.
- Cost: $0.
1# .github/workflows/sync.yml
2on:
3 schedule:
4 - cron: '0 19 * * *' # 1 AM IST
5jobs:
6 sync:
7 timeout-minutes: 360 # 6 hours of freedom
8 steps:
9 - run: npm run sync-dbcontent = """# How A Simple Assignment Became A Full-Stack Automated System (And Why Vercel's Timeout Message Haunts Me)
TL;DR: Built a dashboard for one state. Accidentally scaled it to all of India. Fought timeouts, laggy maps, and CSV hell. Won using GitHub Actions, CSS Grid, and zero paid features.
---
The "5-Minute" Assignment
The Task: Build a dashboard for MGNREGA data (Maharashtra state).
My Brain: "Easy. Fetch API. Render Chart. Submit. Done by lunch."
The Narrator: *It was, in fact, not done by lunch.*
The Data Nightmare
I logged into data.gov.in. I wrote a simple fetch() call.
I waited.
And waited.
And waited.
The Reality:
- 28,988 total metrics.
- 740 districts.
- Infinite loading state.
The API wasn't broken; the data was just massive. My browser froze, and my console looked like a crime scene.
The Vercel Heartbreak 💔
I decided to be smart. "I'll just run a cron job on Vercel to sync the data to my database at night!"
The Plan:
- Vercel Cron triggers function.
- Function fetches data.
- Function updates DB.
The Result:
Error: Function execution timeout (10s)
I dug into the docs. Vercel Serverless Functions on the free tier have a hard limit. Even Edge Functions (the "fast" ones) cap out at 300 seconds. My sync took 15 minutes.
I had two choices:
- Pay $20/mo for Vercel Pro.
- Rewrite everything.
The "Hacky" Solution (GitHub Actions)
I chose option 3: Abuse GitHub Actions.
GitHub Actions runners are designed for CI/CD, but they are basically just free computers that you can borrow for up to 6 hours.
The New Architecture:
- Cron Trigger: GitHub Action scheduled for 1 AM.
- The Script: A simple Node.js script that connects to Supabase, fetches the massive JSON, and pushes it to the DB.
- Cost: $0.
1# .github/workflows/sync.yml
2on:
3 schedule:
4 - cron: '0 19 * * *' # 1 AM IST
5jobs:
6 sync:
7 timeout-minutes: 360 # 6 hours of freedom
8 steps:
9 - run: npm run sync-dbcontent = """# How A Simple Assignment Became A Full-Stack Automated System (And Why Vercel's Timeout Message Haunts Me)
TL;DR: Built a dashboard for one state. Accidentally scaled it to all of India. Fought timeouts, laggy maps, and CSV hell. Won using GitHub Actions, CSS Grid, and zero paid features.
---
The "5-Minute" Assignment
The Task: Build a dashboard for MGNREGA data (Maharashtra state).
My Brain: "Easy. Fetch API. Render Chart. Submit. Done by lunch."
The Narrator: *It was, in fact, not done by lunch.*
The Data Nightmare
I logged into data.gov.in. I wrote a simple fetch() call.
I waited.
And waited.
And waited.
The Reality:
- 28,988 total metrics.
- 740 districts.
- Infinite loading state.
The API wasn't broken; the data was just massive. My browser froze, and my console looked like a crime scene.
The Vercel Heartbreak 💔
I decided to be smart. "I'll just run a cron job on Vercel to sync the data to my database at night!"
The Plan:
- Vercel Cron triggers function.
- Function fetches data.
- Function updates DB.
The Result:
Error: Function execution timeout (10s)
I dug into the docs. Vercel Serverless Functions on the free tier have a hard limit. Even Edge Functions (the "fast" ones) cap out at 300 seconds. My sync took 15 minutes.
I had two choices:
- Pay $20/mo for Vercel Pro.
- Rewrite everything.
The "Hacky" Solution (GitHub Actions)
I chose option 3: Abuse GitHub Actions.
GitHub Actions runners are designed for CI/CD, but they are basically just free computers that you can borrow for up to 6 hours.
The New Architecture:
- Cron Trigger: GitHub Action scheduled for 1 AM.
- The Script: A simple Node.js script that connects to Supabase, fetches the massive JSON, and pushes it to the DB.
- Cost: $0.
1# .github/workflows/sync.yml
2on:
3 schedule:
4 - cron: '0 19 * * *' # 1 AM IST
5jobs:
6 sync:
7 timeout-minutes: 360 # 6 hours of freedom
8 steps:
9 - run: npm run sync-dbIt runs every night. It never times out. It prints lovely green checkmarks.
The Map That Nobody Asked For
I wanted a map of India where you could click a state. Attempt 1: A 19MB GeoJSON file. Result: My laptop fan sounded like a jet engine. Mobile users probably just saw a white screen.
Attempt 2: CSS Grid. I realized I didn't need precise geographical borders. I just needed squares that looked like India.
- Grid: 27 rows x 32 columns.
- Size: 5KB.
- Performance: Instant.
Sometimes, dumb technology is the best technology.
The Future: Golang?
I'm currently hitting the limits of Node.js concurrency. The plan is to rewrite the backend in Golang.
- Why? Goroutines.
- Goal: Sync all 740 districts in under 5 minutes.
- Status: Learning syntax and regretting nothing.
Final Stack
- Frontend: Next.js (App Router)
- DB: Supabase (PostgreSQL)
- Sync: GitHub Actions (The real MVP)
- Cost: $0.00
