Ram2005 commited on
Commit
4bf8d80
·
verified ·
1 Parent(s): 72c8847

v2.3: Data accuracy + security + UI cleanup

Browse files

- Add 6 new unicorns (Neysa, Ather Energy, Captain Fresh, Kuku FM, Euler Motors, Kissht)
- Update Perfios valuation to $1B
- Replace all hardcoded stats with live DB queries in directory pages
- Add slowapi rate limiting (60/min general, input length validation)
- Sanitize LIKE queries (escape % and _ wildcards)
- Strip glassmorphism/blur, gradient-text, shimmer animations
- Add data disclaimer banner
- Health endpoint returns live counts from DB
- README updated with accurate numbers and data disclaimer

README.md CHANGED
@@ -8,119 +8,67 @@ app_port: 7860
8
  suggested_hardware: cpu-basic
9
  ---
10
 
11
- # Bharat Tech Atlas v2.1
12
 
13
- **Comprehensive mapping platform for India's startup ecosystem**5,770+ entities including Startups, SMEs, College E-Cells, Incubators, and Accelerators.
14
 
15
- ## What's New in v2.1
16
 
17
- ### Map Clustering
18
- - **Server-side grid clustering** at low zoom → numbered bubbles with count, color-coded by density
19
- - **Individual points** at high zoom (≥12) with entity labels, unicorn gold strokes
20
- - **Smooth zoom transitions**: click a cluster → zooms to expansion level
21
- - **City labels** under cluster bubbles with glow ring effects
22
- - **Adaptive grid cells**: cell size scales with zoom level for natural clustering
23
 
24
- ### UX Improvements
25
- - **Collapsible filter sections** with active-filter badges on collapsed headers
26
- - **In-filter search** in Sectors, DPIIT Categories, and Location sections
27
- - **Quick-filter pill bar** (always visible): 🦄 Unicorns, 👩 Women-led, 🏛️ DPIIT, 🏆 NSA
28
- - **Awards & Recognition** section merges Unicorn Status + Special Awards — less scrolling
29
- - **Entity Detail panel** (new): full profile with metrics, investors, links, nearby entities
30
- - **Analytics Panel** (new): Overview / Sectors / Geography tabs with charts
31
 
32
- ### Metrics Bar Redesign
33
- - **Floating widget cards** with breathing room (replaced single-bar strip)
34
- - Each stat is an independent card with glow hover effects and icon drop shadows
35
- - Collapsible with ▼ Hide / ▲ Show toggle
36
- - Unicorn count gets pulsing indicator when > 100
37
-
38
- ### Backend Performance
39
- - **Bounding-box viewport queries** via R-Tree spatial index → O(log n) lookups
40
- - **Server-side clustering endpoint** (`/api/entities/clusters`): returns pre-aggregated clusters at low zoom, individual GeoJSON at high zoom — avoids sending full payload
41
- - **Request abort on pan/zoom**: cancels stale in-flight fetches
42
- - **Optimized SQLite PRAGMAs**: WAL mode, 8MB cache, 64MB mmap, MEMORY temp store
43
- - **Composite indexes** for common filter combos (type+state, type+funding)
44
- - **Viewport summary** endpoint for lightweight count-only queries during pan/zoom
45
-
46
- ### SEO Directory Pages
47
- - `/api/entities/directory/startups` — Top 100 startups
48
- - `/api/entities/directory/startups/{state}` — By state
49
- - `/api/entities/directory/startups/{state}/{sector}` — By state + sector
50
- - `/api/entities/directory/unicorns` — All unicorns
51
- - `/api/entities/directory/unicorns/{sector}` — By sector
52
- - Schema.org JSON-LD, OG tags, canonical URLs, breadcrumbs
53
-
54
- ## Interactive Map Features
55
 
56
  ### Map Modes
57
  - **Clusters** (default): Server-side numbered bubble clusters at low zoom, individual points at high zoom
58
- - **Points**: Client-side clustering with all entities as colored dots
59
  - **Heatmap**: Funding-weighted density visualization
60
 
61
- ### Filtering System
62
- - **Entity Types**: Startups, SMEs, E-Cells, Incubators, Accelerators
63
- - **Top Sectors**: FinTech, SaaS/AI, E-Commerce, Healthcare, Manufacturing
64
- - **DPIIT Categories**: 18+ categories with in-filter search
65
- - **Business Models**: Lifestyle, Scalable, Social Enterprise, Large Company
66
- - **Special Awards**: Women-led (630), Rural Impact (431), Campus (276), NSA 5.0 (128)
67
- - **Unicorn Status**: 119 Unicorns
68
- - **Startup Stage**: Ideation → Validation → Early Traction → Scaling → Mature
69
- - **Location**: State filter with search
70
 
71
  ### Entity Profiles
72
- - Key metrics: Founded year, Team size, Funding amount
73
- - Sectors, Stage, Business Model, DPIIT Category
74
- - Investor list with names
75
- - Social links: Website, LinkedIn, Twitter, Instagram
76
- - Nearby entities with distance
77
- - Badges: Unicorn, Women-led, Rural Impact, Campus, NSA, DPIIT
78
 
79
  ## Tech Stack
80
 
81
  | Layer | Technology |
82
  |-------|-----------|
83
- | **Frontend** | React 18 + Vite + MapLibre GL JS + Tailwind CSS |
84
- | **Backend** | FastAPI (Python) with spatial query engine |
85
- | **Database** | SQLite with R-Tree spatial index + composite indexes |
86
- | **Map Tiles** | CARTO Dark Matter (free, no API key) |
87
- | **Deployment** | Docker on Hugging Face Spaces |
88
 
89
  ## API Endpoints
90
 
91
- - `GET /api/entities/clusters` — **Server-side clustered GeoJSON** (zoom-adaptive)
92
- - `GET /api/entities/geojson` — Viewport-optimized raw GeoJSON (max 3,000 features)
93
- - `GET /api/entities/viewport/summary` — Lightweight viewport counts
94
- - `GET /api/entities/heatmap` — Heatmap-optimized GeoJSON with weights
95
- - `GET /api/entities/nearby` — Proximity search with haversine distance
96
- - `GET /api/entities/search` — Full-text search with autocomplete
97
- - `GET /api/entities/detail/{slug}` — Full entity profile with nearby
98
- - `GET /api/entities/facets` — Filter sidebar counts
99
- - `GET /api/entities/analytics/overview` — Ecosystem statistics
100
  - `GET /api/entities/analytics/sectors` — Sector breakdown
101
- - `GET /api/entities/directory/*` — SEO directory pages (HTML)
 
102
 
103
  ## Data
104
 
105
- 5,770+ entities across India:
106
- - **119 Unicorns**: Flipkart, PhonePe, Zerodha, Zomato, Swiggy, CRED, Zepto + 112 more
107
- - **630 Women-led Startups**: Nykaa, Mamaearth, Sugar Cosmetics, OPEN Financial
108
- - **431 Rural Impact**: DeHaat, CropIn, Stellapps, Aye Finance
109
- - **276 Campus Startups**: Ather Energy, Pixxel, TartanSense, Zepto
110
- - **128 NSA Winners**: Tagged with award categories
111
- - **3,426 DPIIT Recognized**: Government-recognized startups
112
-
113
- ## Geographic Distribution (Top 10 States)
114
-
115
- | State | Count |
116
- |-------|-------|
117
- | Maharashtra | 1,157 |
118
- | Karnataka | 1,123 |
119
- | Haryana | 527 |
120
- | Delhi | 471 |
121
- | Tamil Nadu | 452 |
122
- | Telangana | 330 |
123
- | Gujarat | 226 |
124
- | Uttar Pradesh | 216 |
125
- | Kerala | 187 |
126
- | West Bengal | 146 |
 
8
  suggested_hardware: cpu-basic
9
  ---
10
 
11
+ # Bharat Tech Atlas v2.3
12
 
13
+ Mapping platform for India's startup ecosystem — curated dataset of startups, SMEs, college E-Cells, incubators, and accelerators.
14
 
15
+ > **Data disclaimer**: This is a curated subset. India has 223,000+ DPIIT-registered startups (as of April 2026). Stats shown here reflect only what's in our database. Sources: DPIIT, Tracxn, Crunchbase, LinkedIn.
16
 
17
+ ## What's New in v2.3
 
 
 
 
 
18
 
19
+ - **Data accuracy**: 125 unicorns (added Neysa, Ather Energy, Captain Fresh, Kuku FM, Euler Motors, Kissht; updated Perfios)
20
+ - **Security**: Rate limiting (60 req/min general, 10 req/min search), input sanitization, LIKE wildcard escaping
21
+ - **Dynamic stats**: All directory page counts pulled from DB — no hardcoded numbers
22
+ - **UI cleanup**: Removed glassmorphism/blur, gradient text, shimmer animations. Flat dark + single accent (#F97316)
 
 
 
23
 
24
+ ## Map Features
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  ### Map Modes
27
  - **Clusters** (default): Server-side numbered bubble clusters at low zoom, individual points at high zoom
28
+ - **Points**: All entities as colored dots
29
  - **Heatmap**: Funding-weighted density visualization
30
 
31
+ ### Filtering
32
+ - Entity types, sectors, DPIIT categories, business models, stage, location
33
+ - Special filters: unicorns, women-led, rural impact, campus startups, NSA winners
34
+ - State filter with fly-to
 
 
 
 
 
35
 
36
  ### Entity Profiles
37
+ - Metrics: founded year, team size, funding
38
+ - Investors, social links, nearby entities
39
+ - Badges: unicorn, women-led, rural impact, campus, NSA, DPIIT
 
 
 
40
 
41
  ## Tech Stack
42
 
43
  | Layer | Technology |
44
  |-------|-----------|
45
+ | Frontend | React 18 + Vite + MapLibre GL JS + Tailwind CSS |
46
+ | Backend | FastAPI + slowapi rate limiting |
47
+ | Database | SQLite with R-Tree spatial index |
48
+ | Map Tiles | CARTO Dark Matter |
49
+ | Deployment | Docker on Hugging Face Spaces |
50
 
51
  ## API Endpoints
52
 
53
+ - `GET /api/entities/clusters` — Server-side clustered GeoJSON
54
+ - `GET /api/entities/geojson` — Viewport-optimized raw GeoJSON
55
+ - `GET /api/entities/viewport/summary` — Viewport counts
56
+ - `GET /api/entities/heatmap` — Heatmap GeoJSON with weights
57
+ - `GET /api/entities/nearby` — Proximity search
58
+ - `GET /api/entities/search` — Full-text search (sanitized input)
59
+ - `GET /api/entities/detail/{slug}` — Entity profile with nearby
60
+ - `GET /api/entities/facets` — Filter counts
61
+ - `GET /api/entities/analytics/overview` — Ecosystem stats
62
  - `GET /api/entities/analytics/sectors` — Sector breakdown
63
+ - `GET /directory/*` — SEO directory pages (HTML)
64
+ - `GET /api/health` — Health check with live entity/unicorn counts
65
 
66
  ## Data
67
 
68
+ Curated dataset across India (counts are dynamic from DB):
69
+ - **125 Unicorns**: Flipkart, PhonePe, Zerodha, Zomato, Swiggy, CRED, Zepto, Neysa, Ather Energy + more
70
+ - **Women-led startups**: Nykaa, Mamaearth, Sugar Cosmetics, OPEN Financial + more
71
+ - **Rural impact**: DeHaat, CropIn, Stellapps, Aye Finance + more
72
+ - **Campus startups**: Pixxel, TartanSense, Zepto + more
73
+ - **NSA winners**: Tagged with award categories
74
+ - **DPIIT recognized**: Government-recognized startups
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/directory.py CHANGED
@@ -59,8 +59,8 @@ DIRECTORY_HTML_TEMPLATE = """<!DOCTYPE html>
59
  :root{{--bg:#0F172A;--surface:#1E293B;--border:#334155;--text:#F8FAFC;--muted:#94A3B8;--brand:#F97316;}}
60
  *{{margin:0;padding:0;box-sizing:border-box}}
61
  body{{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}}
62
- .header{{background:linear-gradient(135deg,#1E293B,#0F172A);border-bottom:1px solid var(--border);padding:1rem 2rem;display:flex;align-items:center;justify-content:space-between;}}
63
- .header h1{{font-size:1.1rem;font-weight:800;background:linear-gradient(135deg,#F97316,#FBBF24);-webkit-background-clip:text;-webkit-text-fill-color:transparent}}
64
  .header a{{color:var(--muted);text-decoration:none;font-size:0.85rem}}
65
  .header a:hover{{color:var(--brand)}}
66
  .hero{{padding:3rem 2rem;text-align:center;border-bottom:1px solid var(--border)}}
@@ -85,7 +85,7 @@ DIRECTORY_HTML_TEMPLATE = """<!DOCTYPE html>
85
  .card p.desc{{font-size:0.8rem;color:var(--muted);margin-bottom:0.75rem;line-height:1.4}}
86
  .card .meta{{font-size:0.75rem;color:var(--muted);display:flex;gap:1rem;flex-wrap:wrap}}
87
  .badge{{font-size:0.7rem;padding:0.2rem 0.5rem;border-radius:4px;font-weight:500}}
88
- .unicorn-badge{{background:linear-gradient(90deg,#A855F7,#EC4899,#F97316,#A855F7);background-size:200% auto;color:transparent;background-clip:text;-webkit-background-clip:text;-webkit-text-fill-color:transparent}}
89
  .women-badge{{background:rgba(236,72,153,0.15);color:#F472B6}}
90
  .rural-badge{{background:rgba(34,197,94,0.15);color:#4ADE80}}
91
  .dpiit-badge{{background:rgba(59,130,246,0.15);color:#60A5FA}}
@@ -132,12 +132,12 @@ DIRECTORY_HTML_TEMPLATE = """<!DOCTYPE html>
132
  {cards_html}
133
  </div>
134
  <div class="cta">
135
- <a href="/">🗺️ Explore the full interactive map with 5,000+ entities</a>
136
  </div>
137
  </div>
138
  <footer>
139
- Bharat Tech Atlas — Comprehensive mapping platform for India's startup ecosystem.
140
- Data: 5,000+ entities, 119 unicorns, across 45+ Indian cities.
141
  </footer>
142
  </body>
143
  </html>"""
@@ -225,6 +225,13 @@ router = APIRouter()
225
  @router.get("/startups", response_class=HTMLResponse)
226
  async def directory_startups_root():
227
  conn = get_db()
 
 
 
 
 
 
 
228
  rows = conn.execute("""
229
  SELECT * FROM entities
230
  WHERE entity_type = 'startup' AND is_active = 1
@@ -235,11 +242,11 @@ async def directory_startups_root():
235
 
236
  items = [row_to_dict(r) for r in rows]
237
 
238
- stats_html = """
239
- <div class="stat"><div class="num">5,000+</div><div class="label">Startups</div></div>
240
- <div class="stat"><div class="num">119</div><div class="label">Unicorns</div></div>
241
- <div class="stat"><div class="num">45+</div><div class="label">Cities</div></div>
242
- <div class="stat"><div class="num">647</div><div class="label">Women-led</div></div>
243
  """
244
 
245
  filter_nav = "".join([
@@ -251,7 +258,7 @@ async def directory_startups_root():
251
 
252
  html = _build_directory_page(
253
  title="Indian Startups Directory",
254
- description="Comprehensive directory of 5,000+ Indian startups — unicorns, funded companies, and emerging ventures across fintech, SaaS, AI, edtech, healthtech and more.",
255
  keywords="Indian startups, India startup directory, unicorn startups India, funded startups, DPIIT startups, tech startups Bangalore, Mumbai startups",
256
  canonical="https://huggingface.co/spaces/Ram2005/StartupMap-India/directory/startups",
257
  breadcrumb_last="Startups",
@@ -357,15 +364,21 @@ async def directory_unicorns():
357
  ORDER BY funding_inr DESC
358
  LIMIT 150
359
  """).fetchall()
 
 
 
 
 
 
360
  conn.close()
361
 
362
  items = [row_to_dict(r) for r in rows]
363
 
364
  stats_html = f"""
365
  <div class="stat"><div class="num">{len(items)}</div><div class="label">Unicorns</div></div>
366
- <div class="stat"><div class="num">45+</div><div class="label">Cities</div></div>
367
- <div class="stat"><div class="num">35+</div><div class="label">Sectors</div></div>
368
- <div class="stat"><div class="num">$120B+</div><div class="label">Combined Valuation</div></div>
369
  """
370
 
371
  filter_nav = "".join([
@@ -377,7 +390,7 @@ async def directory_unicorns():
377
 
378
  html = _build_directory_page(
379
  title="Indian Unicorns",
380
- description="Complete directory of 119+ Indian unicorn startups — from Flipkart and PhonePe to newer entrants like Zepto and Physics Wallah. Valuations, funding, investors, and locations.",
381
  keywords="Indian unicorns, Indian unicorn startups, India unicorn list, Flipkart, PhonePe, Zepto, Zomato, Swiggy, unicorn valuation",
382
  canonical="https://huggingface.co/spaces/Ram2005/StartupMap-India/directory/unicorns",
383
  breadcrumb_last="Unicorns",
 
59
  :root{{--bg:#0F172A;--surface:#1E293B;--border:#334155;--text:#F8FAFC;--muted:#94A3B8;--brand:#F97316;}}
60
  *{{margin:0;padding:0;box-sizing:border-box}}
61
  body{{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}}
62
+ .header{{background:var(--bg);border-bottom:1px solid var(--border);padding:1rem 2rem;display:flex;align-items:center;justify-content:space-between;}}
63
+ .header h1{{font-size:1.1rem;font-weight:800;color:var(--brand)}}
64
  .header a{{color:var(--muted);text-decoration:none;font-size:0.85rem}}
65
  .header a:hover{{color:var(--brand)}}
66
  .hero{{padding:3rem 2rem;text-align:center;border-bottom:1px solid var(--border)}}
 
85
  .card p.desc{{font-size:0.8rem;color:var(--muted);margin-bottom:0.75rem;line-height:1.4}}
86
  .card .meta{{font-size:0.75rem;color:var(--muted);display:flex;gap:1rem;flex-wrap:wrap}}
87
  .badge{{font-size:0.7rem;padding:0.2rem 0.5rem;border-radius:4px;font-weight:500}}
88
+ .unicorn-badge{{color:var(--brand);font-weight:600}}
89
  .women-badge{{background:rgba(236,72,153,0.15);color:#F472B6}}
90
  .rural-badge{{background:rgba(34,197,94,0.15);color:#4ADE80}}
91
  .dpiit-badge{{background:rgba(59,130,246,0.15);color:#60A5FA}}
 
132
  {cards_html}
133
  </div>
134
  <div class="cta">
135
+ <a href="/">🗺️ Explore the full interactive map</a>
136
  </div>
137
  </div>
138
  <footer>
139
+ Bharat Tech Atlas — Curated dataset. India has 223,000+ DPIIT-registered startups.
140
+ Source: DPIIT, Tracxn, Crunchbase. Data may not reflect real-time changes.
141
  </footer>
142
  </body>
143
  </html>"""
 
225
  @router.get("/startups", response_class=HTMLResponse)
226
  async def directory_startups_root():
227
  conn = get_db()
228
+
229
+ # Live stats from DB
230
+ total_startups = conn.execute("SELECT COUNT(*) FROM entities WHERE entity_type='startup' AND is_active=1").fetchone()[0]
231
+ unicorn_count = conn.execute("SELECT COUNT(*) FROM entities WHERE unicorn_status='unicorn' AND is_active=1").fetchone()[0]
232
+ women_count = conn.execute("SELECT COUNT(*) FROM entities WHERE is_women_led=1 AND is_active=1").fetchone()[0]
233
+ city_count = conn.execute("SELECT COUNT(DISTINCT city) FROM entities WHERE is_active=1").fetchone()[0]
234
+
235
  rows = conn.execute("""
236
  SELECT * FROM entities
237
  WHERE entity_type = 'startup' AND is_active = 1
 
242
 
243
  items = [row_to_dict(r) for r in rows]
244
 
245
+ stats_html = f"""
246
+ <div class="stat"><div class="num">{total_startups:,}</div><div class="label">Startups</div></div>
247
+ <div class="stat"><div class="num">{unicorn_count}</div><div class="label">Unicorns</div></div>
248
+ <div class="stat"><div class="num">{city_count}+</div><div class="label">Cities</div></div>
249
+ <div class="stat"><div class="num">{women_count}</div><div class="label">Women-led</div></div>
250
  """
251
 
252
  filter_nav = "".join([
 
258
 
259
  html = _build_directory_page(
260
  title="Indian Startups Directory",
261
+ description=f"Curated directory of {total_startups:,}+ Indian startups — {unicorn_count} unicorns, funded companies, and emerging ventures across fintech, SaaS, AI, edtech, healthtech and more. India has 223,000+ DPIIT-registered startups.",
262
  keywords="Indian startups, India startup directory, unicorn startups India, funded startups, DPIIT startups, tech startups Bangalore, Mumbai startups",
263
  canonical="https://huggingface.co/spaces/Ram2005/StartupMap-India/directory/startups",
264
  breadcrumb_last="Startups",
 
364
  ORDER BY funding_inr DESC
365
  LIMIT 150
366
  """).fetchall()
367
+
368
+ # Live valuation from DB
369
+ val_row = conn.execute("SELECT SUM(valuation_usd) FROM entities WHERE unicorn_status='unicorn' AND is_active=1").fetchone()
370
+ total_valuation_b = round((val_row[0] or 0) / 1e9)
371
+ city_count = conn.execute("SELECT COUNT(DISTINCT city) FROM entities WHERE unicorn_status='unicorn' AND is_active=1").fetchone()[0]
372
+ sector_count = conn.execute("SELECT COUNT(DISTINCT dpiit_category) FROM entities WHERE unicorn_status='unicorn' AND is_active=1").fetchone()[0]
373
  conn.close()
374
 
375
  items = [row_to_dict(r) for r in rows]
376
 
377
  stats_html = f"""
378
  <div class="stat"><div class="num">{len(items)}</div><div class="label">Unicorns</div></div>
379
+ <div class="stat"><div class="num">{city_count}+</div><div class="label">Cities</div></div>
380
+ <div class="stat"><div class="num">{sector_count}+</div><div class="label">Sectors</div></div>
381
+ <div class="stat"><div class="num">${total_valuation_b}B+</div><div class="label">Combined Valuation</div></div>
382
  """
383
 
384
  filter_nav = "".join([
 
390
 
391
  html = _build_directory_page(
392
  title="Indian Unicorns",
393
+ description=f"Directory of {len(items)} Indian unicorn startups — from Flipkart and PhonePe to newer entrants like Zepto and Neysa. Valuations, funding, investors, and locations.",
394
  keywords="Indian unicorns, Indian unicorn startups, India unicorn list, Flipkart, PhonePe, Zepto, Zomato, Swiggy, unicorn valuation",
395
  canonical="https://huggingface.co/spaces/Ram2005/StartupMap-India/directory/unicorns",
396
  breadcrumb_last="Unicorns",
backend/main.py CHANGED
@@ -1,24 +1,33 @@
1
  """
2
- Bharat Tech Atlas v2.1 — FastAPI Application Entry Point
3
  - SPA frontend serving
4
  - SEO directory routes at /directory/*
 
 
5
  """
6
  import os
7
  from contextlib import asynccontextmanager
8
- from fastapi import FastAPI
9
  from fastapi.staticfiles import StaticFiles
10
  from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
11
  from fastapi.middleware.cors import CORSMiddleware
 
 
 
12
 
13
- from .database import init_db, DB_PATH
14
  from .seed import seed_database
15
  from .routes.entities import router as entities_router
16
  from .directory import router as directory_router
17
 
18
 
 
 
 
 
19
  @asynccontextmanager
20
  async def lifespan(app: FastAPI):
21
- print("🚀 Bharat Tech Atlas v2.1 — Initializing...")
22
  os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
23
  init_db()
24
  print("✅ Database schema initialized")
@@ -30,11 +39,15 @@ async def lifespan(app: FastAPI):
30
 
31
  app = FastAPI(
32
  title="Bharat Tech Atlas",
33
- description="Comprehensive mapping platform for India's startup ecosystem — 5,000+ entities",
34
- version="2.1.0",
35
  lifespan=lifespan,
36
  )
37
 
 
 
 
 
38
  app.add_middleware(
39
  CORSMiddleware,
40
  allow_origins=["*"],
@@ -43,10 +56,41 @@ app.add_middleware(
43
  allow_headers=["*"],
44
  )
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  # ─── Health endpoint ──────────────────────────────────────────────────────────
47
  @app.get("/api/health")
48
- async def health():
49
- return {"status": "ok", "service": "Bharat Tech Atlas", "version": "2.1.0", "entities": "5,000+"}
 
 
 
 
 
 
 
 
 
 
 
50
 
51
 
52
  # ─── API routes ───────────────────────────────────────────────────────────────
@@ -79,15 +123,15 @@ async def serve_spa(full_path: str):
79
  # Don't intercept API routes or directory routes
80
  if full_path.startswith("api/") or full_path.startswith("directory/"):
81
  return JSONResponse({"error": "Not found"}, status_code=404)
82
-
83
  # Try to serve static file first
84
  file_path = os.path.join(STATIC_DIR, full_path)
85
  if os.path.isfile(file_path):
86
  return FileResponse(file_path)
87
-
88
  # Fallback to index.html for SPA routing
89
  index_path = os.path.join(STATIC_DIR, "index.html")
90
  if os.path.exists(index_path):
91
  return FileResponse(index_path)
92
-
93
  return JSONResponse({"message": "Bharat Tech Atlas API running."})
 
1
  """
2
+ Bharat Tech Atlas v2.3 — FastAPI Application Entry Point
3
  - SPA frontend serving
4
  - SEO directory routes at /directory/*
5
+ - Rate limiting on API endpoints
6
+ - Input sanitization middleware
7
  """
8
  import os
9
  from contextlib import asynccontextmanager
10
+ from fastapi import FastAPI, Request
11
  from fastapi.staticfiles import StaticFiles
12
  from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
13
  from fastapi.middleware.cors import CORSMiddleware
14
+ from slowapi import Limiter, _rate_limit_exceeded_handler
15
+ from slowapi.util import get_remote_address
16
+ from slowapi.errors import RateLimitExceeded
17
 
18
+ from .database import init_db, DB_PATH, get_db
19
  from .seed import seed_database
20
  from .routes.entities import router as entities_router
21
  from .directory import router as directory_router
22
 
23
 
24
+ # ─── Rate limiter ────────────────────────────────────────────────────────────
25
+ limiter = Limiter(key_func=get_remote_address, default_limits=["60/minute"])
26
+
27
+
28
  @asynccontextmanager
29
  async def lifespan(app: FastAPI):
30
+ print("🚀 Bharat Tech Atlas v2.3 — Initializing...")
31
  os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
32
  init_db()
33
  print("✅ Database schema initialized")
 
39
 
40
  app = FastAPI(
41
  title="Bharat Tech Atlas",
42
+ description="Mapping platform for India's startup ecosystem",
43
+ version="2.3.0",
44
  lifespan=lifespan,
45
  )
46
 
47
+ # Attach rate limiter
48
+ app.state.limiter = limiter
49
+ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
50
+
51
  app.add_middleware(
52
  CORSMiddleware,
53
  allow_origins=["*"],
 
56
  allow_headers=["*"],
57
  )
58
 
59
+
60
+ # ─── Input sanitization middleware ───────────────────────────────────────────
61
+ @app.middleware("http")
62
+ async def sanitize_input(request: Request, call_next):
63
+ # Reject query strings longer than 2000 chars
64
+ if len(str(request.url.query)) > 2000:
65
+ return JSONResponse(
66
+ {"error": "Query string too long (max 2000 chars)"},
67
+ status_code=400,
68
+ )
69
+ # Reject any single query param longer than 500 chars
70
+ for key, value in request.query_params.items():
71
+ if len(value) > 500:
72
+ return JSONResponse(
73
+ {"error": f"Parameter '{key}' too long (max 500 chars)"},
74
+ status_code=400,
75
+ )
76
+ return await call_next(request)
77
+
78
+
79
  # ─── Health endpoint ──────────────────────────────────────────────────────────
80
  @app.get("/api/health")
81
+ @limiter.limit("60/minute")
82
+ async def health(request: Request):
83
+ conn = get_db()
84
+ total = conn.execute("SELECT COUNT(*) FROM entities WHERE is_active=1").fetchone()[0]
85
+ unicorns = conn.execute("SELECT COUNT(*) FROM entities WHERE unicorn_status='unicorn' AND is_active=1").fetchone()[0]
86
+ conn.close()
87
+ return {
88
+ "status": "ok",
89
+ "service": "Bharat Tech Atlas",
90
+ "version": "2.3.0",
91
+ "entities": total,
92
+ "unicorns": unicorns,
93
+ }
94
 
95
 
96
  # ─── API routes ───────────────────────────────────────────────────────────────
 
123
  # Don't intercept API routes or directory routes
124
  if full_path.startswith("api/") or full_path.startswith("directory/"):
125
  return JSONResponse({"error": "Not found"}, status_code=404)
126
+
127
  # Try to serve static file first
128
  file_path = os.path.join(STATIC_DIR, full_path)
129
  if os.path.isfile(file_path):
130
  return FileResponse(file_path)
131
+
132
  # Fallback to index.html for SPA routing
133
  index_path = os.path.join(STATIC_DIR, "index.html")
134
  if os.path.exists(index_path):
135
  return FileResponse(index_path)
136
+
137
  return JSONResponse({"message": "Bharat Tech Atlas API running."})
backend/routes/entities.py CHANGED
@@ -17,6 +17,11 @@ from ..database import get_db, haversine_distance
17
  router = APIRouter()
18
 
19
 
 
 
 
 
 
20
  def row_to_dict(row) -> dict:
21
  d = dict(row)
22
  for field in ["sectors", "data_sources", "investors", "funding_rounds"]:
@@ -111,8 +116,9 @@ def _apply_filters(query, params, filters):
111
  params.append(int(filters["founded_before"]))
112
 
113
  if filters.get("search"):
114
- query += " AND e.name LIKE ?"
115
- params.append(f"%{filters['search']}%")
 
116
 
117
  if filters.get("state"):
118
  query += " AND e.state = ?"
@@ -553,7 +559,7 @@ async def get_entity_detail(slug: str):
553
 
554
  @router.get("/search")
555
  async def search_entities(
556
- q: str = Query(..., min_length=1),
557
  entity_type: Optional[str] = Query(None),
558
  sector: Optional[str] = Query(None),
559
  state: Optional[str] = Query(None),
@@ -562,12 +568,13 @@ async def search_entities(
562
  offset: int = Query(0, ge=0),
563
  ):
564
  conn = get_db()
 
565
  query = """
566
  SELECT * FROM entities e
567
  WHERE e.is_active = 1
568
- AND (e.name LIKE ? OR e.city LIKE ? OR e.description LIKE ? OR e.college_name LIKE ?)
569
  """
570
- search_term = f"%{q}%"
571
  params = [search_term, search_term, search_term, search_term]
572
 
573
  filters = {"entity_type": entity_type, "sector": sector, "state": state, "city": city}
@@ -598,8 +605,9 @@ async def get_facets(
598
  base_where = "WHERE is_active = 1"
599
  params = []
600
  if search:
601
- base_where += " AND name LIKE ?"
602
- params.append(f"%{search}%")
 
603
 
604
  # Entity type counts
605
  tq = f"SELECT entity_type, COUNT(*) as count FROM entities {base_where}"
 
17
  router = APIRouter()
18
 
19
 
20
+ def _sanitize_like(value: str) -> str:
21
+ """Escape SQL LIKE wildcards in user input."""
22
+ return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
23
+
24
+
25
  def row_to_dict(row) -> dict:
26
  d = dict(row)
27
  for field in ["sectors", "data_sources", "investors", "funding_rounds"]:
 
116
  params.append(int(filters["founded_before"]))
117
 
118
  if filters.get("search"):
119
+ safe = _sanitize_like(filters['search'])
120
+ query += " AND e.name LIKE ? ESCAPE '\\'"
121
+ params.append(f"%{safe}%")
122
 
123
  if filters.get("state"):
124
  query += " AND e.state = ?"
 
559
 
560
  @router.get("/search")
561
  async def search_entities(
562
+ q: str = Query(..., min_length=1, max_length=200),
563
  entity_type: Optional[str] = Query(None),
564
  sector: Optional[str] = Query(None),
565
  state: Optional[str] = Query(None),
 
568
  offset: int = Query(0, ge=0),
569
  ):
570
  conn = get_db()
571
+ safe_q = _sanitize_like(q)
572
  query = """
573
  SELECT * FROM entities e
574
  WHERE e.is_active = 1
575
+ AND (e.name LIKE ? ESCAPE '\\' OR e.city LIKE ? ESCAPE '\\' OR e.description LIKE ? ESCAPE '\\' OR e.college_name LIKE ? ESCAPE '\\')
576
  """
577
+ search_term = f"%{safe_q}%"
578
  params = [search_term, search_term, search_term, search_term]
579
 
580
  filters = {"entity_type": entity_type, "sector": sector, "state": state, "city": city}
 
605
  base_where = "WHERE is_active = 1"
606
  params = []
607
  if search:
608
+ safe_search = _sanitize_like(search)
609
+ base_where += " AND name LIKE ? ESCAPE '\\'"
610
+ params.append(f"%{safe_search}%")
611
 
612
  # Entity type counts
613
  tq = f"SELECT entity_type, COUNT(*) as count FROM entities {base_where}"
backend/seed.py CHANGED
@@ -159,7 +159,7 @@ UNICORNS = [
159
  {"name": "Dunzo", "city": "Bangalore", "sectors": ["logistics", "d2c"], "founded": 2015, "employees": 600, "funding": 21000000000, "desc": "Hyperlocal delivery platform for essentials.", "investors": ["Reliance Industries", "Google", "Lightbox Ventures"], "funding_stage": "series_e", "linkedin_url": "https://linkedin.com/company/dunzo-digital", "website": "https://dunzo.com", "linkedin_team": 700, "linkedin_industry": "Delivery", "dpiit_cat": "logistics", "valuation_usd": 800000000},
160
  {"name": "Porter", "city": "Mumbai", "sectors": ["logistics"], "founded": 2014, "employees": 1000, "funding": 9500000000, "desc": "Intra-city logistics and mini-truck aggregator.", "investors": ["Tiger Global", "Kae Capital"], "funding_stage": "series_e", "linkedin_url": "https://linkedin.com/company/porter-delivery", "website": "https://porter.in", "linkedin_team": 1100, "linkedin_industry": "Logistics", "dpiit_cat": "logistics", "valuation_usd": 1000000000},
161
  {"name": "Tracxn", "city": "Bangalore", "sectors": ["fintech", "saas", "saas_ai"], "founded": 2013, "employees": 500, "funding": 5000000000, "desc": "Market intelligence platform for private companies. Public.", "investors": ["Accel", "SAIF Partners"], "funding_stage": "public", "linkedin_url": "https://linkedin.com/company/tracxn", "website": "https://tracxn.com", "linkedin_team": 550, "linkedin_industry": "FinTech", "dpiit_cat": "fintech", "valuation_usd": 600000000},
162
- {"name": "Perfios", "city": "Bangalore", "sectors": ["fintech", "saas", "saas_ai"], "founded": 2008, "employees": 1200, "funding": 6500000000, "desc": "Financial data analytics platform for lenders.", "investors": ["Warburg Pincus", "Bessemer Venture Partners"], "funding_stage": "series_c", "linkedin_url": "https://linkedin.com/company/perfios", "website": "https://perfios.com", "linkedin_team": 1400, "linkedin_industry": "FinTech", "dpiit_cat": "fintech", "valuation_usd": 900000000},
163
  {"name": "Khatabook", "city": "Bangalore", "sectors": ["fintech", "saas"], "founded": 2019, "employees": 300, "funding": 10000000000, "desc": "Digital ledger app for SME bookkeeping.", "investors": ["Sequoia Capital", "B Capital", "Tencent"], "funding_stage": "series_c", "linkedin_url": "https://linkedin.com/company/khatabook", "website": "https://khatabook.com", "linkedin_team": 350, "linkedin_industry": "FinTech", "dpiit_cat": "fintech", "valuation_usd": 600000000},
164
  {"name": "Rivigo", "city": "Gurgaon", "sectors": ["logistics", "deeptech"], "founded": 2014, "employees": 2000, "funding": 17000000000, "desc": "Technology-enabled logistics company using relay trucking.", "investors": ["Warburg Pincus", "SAIF Partners"], "funding_stage": "series_e", "linkedin_url": "https://linkedin.com/company/rivigo", "website": "https://rivigo.com", "linkedin_team": 2200, "linkedin_industry": "Logistics", "dpiit_cat": "logistics", "valuation_usd": 1000000000},
165
  {"name": "Zepto", "city": "Mumbai", "sectors": ["ecommerce", "foodtech", "logistics"], "founded": 2021, "employees": 3000, "funding": 85000000000, "desc": "10-minute grocery delivery startup founded by teenage entrepreneurs.", "investors": ["StepStone Group", "Glade Brook Capital", "Nexus Ventures", "Y Combinator"], "funding_stage": "series_e", "linkedin_url": "https://linkedin.com/company/zeptonow", "website": "https://zepto.co", "linkedin_team": 3200, "linkedin_industry": "E-Commerce", "dpiit_cat": "foodtech", "valuation_usd": 5000000000, "campus": True},
@@ -201,6 +201,12 @@ UNICORNS = [
201
  {"name": "INDwealth", "city": "Mumbai", "sectors": ["fintech", "wealthtech"], "founded": 2019, "employees": 200, "funding": 3000000000, "desc": "Wealth management and investment advisory platform.", "investors": ["Tiger Global", "Steadview Capital"], "funding_stage": "series_c", "linkedin_url": "https://linkedin.com/company/indwealth", "website": "https://indwealth.in", "linkedin_team": 220, "linkedin_industry": "FinTech", "dpiit_cat": "fintech", "valuation_usd": 600000000},
202
  {"name": "Simplilearn", "city": "Bangalore", "sectors": ["edtech"], "founded": 2010, "employees": 2000, "funding": 8000000000, "desc": "Online bootcamp and certification training platform.", "investors": ["Blackstone"], "funding_stage": "acquired", "linkedin_url": "https://linkedin.com/company/simplilearn", "website": "https://simplilearn.com", "linkedin_team": 2200, "linkedin_industry": "E-Learning", "dpiit_cat": "edtech", "valuation_usd": 1200000000},
203
  {"name": "Jiomeet (Reliance Jio)", "city": "Mumbai", "sectors": ["saas", "saas_ai"], "founded": 2020, "employees": 300, "funding": 0, "desc": "Enterprise video conferencing by Reliance Jio.", "investors": ["Reliance Industries"], "funding_stage": None, "linkedin_url": "https://linkedin.com/company/jiomeet", "website": "https://jiomeet.com", "linkedin_team": 350, "linkedin_industry": "Software", "dpiit_cat": "saas", "dpiit": False, "valuation_usd": None},
 
 
 
 
 
 
204
  ]
205
 
206
  # ─── Well-funded non-unicorn startups ─────────────────────────────────────────
@@ -210,7 +216,6 @@ FUNDED_STARTUPS = [
210
  {"name": "CropIn", "city": "Bangalore", "sectors": ["agritech", "ai_ml"], "founded": 2010, "employees": 400, "funding": 3800000000, "dpiit_cat": "agritech", "desc": "AI-powered agri-intelligence platform.", "investors": ["ABC World Asia", "Chiratae Ventures"], "funding_stage": "series_c", "rural_impact": True},
211
  {"name": "DeHaat", "city": "Patna", "sectors": ["agritech"], "founded": 2012, "employees": 1500, "funding": 16000000000, "dpiit_cat": "agritech", "desc": "Full-stack agricultural services platform.", "investors": ["Sofina", "Prosus", "RTP Global"], "funding_stage": "series_d", "rural_impact": True, "nsa": True, "nsa_cat": "Agriculture"},
212
  {"name": "Ninjacart", "city": "Bangalore", "sectors": ["agritech", "logistics"], "founded": 2015, "employees": 2000, "funding": 30000000000, "dpiit_cat": "agritech", "desc": "B2B fresh produce supply chain platform.", "investors": ["Tiger Global", "Flipkart", "Accel"], "funding_stage": "series_c", "rural_impact": True},
213
- {"name": "Ather Energy", "city": "Bangalore", "sectors": ["mobility", "cleantech", "ev"], "founded": 2013, "employees": 1500, "funding": 21000000000, "dpiit_cat": "ev", "desc": "Electric scooter manufacturer with charging infra.", "investors": ["Hero MotoCorp", "Tiger Global"], "funding_stage": "series_e", "campus": True},
214
  {"name": "Agnikul Cosmos", "city": "Chennai", "sectors": ["spacetech", "deeptech"], "founded": 2017, "employees": 200, "funding": 7500000000, "dpiit_cat": "spacetech", "desc": "Building 3D-printed rocket engines.", "investors": ["Anand Mahindra", "pi Ventures"], "funding_stage": "series_b", "campus": True},
215
  {"name": "Skyroot Aerospace", "city": "Hyderabad", "sectors": ["spacetech", "deeptech"], "founded": 2018, "employees": 250, "funding": 5200000000, "dpiit_cat": "spacetech", "desc": "India's first private space launch vehicle (Vikram series).", "investors": ["GIC", "Nexus Venture Partners"], "funding_stage": "series_b"},
216
  {"name": "Pixxel", "city": "Bangalore", "sectors": ["spacetech", "ai_ml"], "founded": 2019, "employees": 120, "funding": 4800000000, "dpiit_cat": "spacetech", "desc": "Hyperspectral earth-imaging satellite constellation.", "investors": ["Google", "Radical Ventures"], "funding_stage": "series_b", "campus": True},
 
159
  {"name": "Dunzo", "city": "Bangalore", "sectors": ["logistics", "d2c"], "founded": 2015, "employees": 600, "funding": 21000000000, "desc": "Hyperlocal delivery platform for essentials.", "investors": ["Reliance Industries", "Google", "Lightbox Ventures"], "funding_stage": "series_e", "linkedin_url": "https://linkedin.com/company/dunzo-digital", "website": "https://dunzo.com", "linkedin_team": 700, "linkedin_industry": "Delivery", "dpiit_cat": "logistics", "valuation_usd": 800000000},
160
  {"name": "Porter", "city": "Mumbai", "sectors": ["logistics"], "founded": 2014, "employees": 1000, "funding": 9500000000, "desc": "Intra-city logistics and mini-truck aggregator.", "investors": ["Tiger Global", "Kae Capital"], "funding_stage": "series_e", "linkedin_url": "https://linkedin.com/company/porter-delivery", "website": "https://porter.in", "linkedin_team": 1100, "linkedin_industry": "Logistics", "dpiit_cat": "logistics", "valuation_usd": 1000000000},
161
  {"name": "Tracxn", "city": "Bangalore", "sectors": ["fintech", "saas", "saas_ai"], "founded": 2013, "employees": 500, "funding": 5000000000, "desc": "Market intelligence platform for private companies. Public.", "investors": ["Accel", "SAIF Partners"], "funding_stage": "public", "linkedin_url": "https://linkedin.com/company/tracxn", "website": "https://tracxn.com", "linkedin_team": 550, "linkedin_industry": "FinTech", "dpiit_cat": "fintech", "valuation_usd": 600000000},
162
+ {"name": "Perfios", "city": "Bangalore", "sectors": ["fintech", "saas", "saas_ai"], "founded": 2008, "employees": 1500, "funding": 9000000000, "desc": "Financial data analytics and decisioning platform for lenders.", "investors": ["Warburg Pincus", "Bessemer Venture Partners"], "funding_stage": "series_d", "linkedin_url": "https://linkedin.com/company/perfios", "website": "https://perfios.com", "linkedin_team": 1600, "linkedin_industry": "FinTech", "dpiit_cat": "fintech", "valuation_usd": 1000000000},
163
  {"name": "Khatabook", "city": "Bangalore", "sectors": ["fintech", "saas"], "founded": 2019, "employees": 300, "funding": 10000000000, "desc": "Digital ledger app for SME bookkeeping.", "investors": ["Sequoia Capital", "B Capital", "Tencent"], "funding_stage": "series_c", "linkedin_url": "https://linkedin.com/company/khatabook", "website": "https://khatabook.com", "linkedin_team": 350, "linkedin_industry": "FinTech", "dpiit_cat": "fintech", "valuation_usd": 600000000},
164
  {"name": "Rivigo", "city": "Gurgaon", "sectors": ["logistics", "deeptech"], "founded": 2014, "employees": 2000, "funding": 17000000000, "desc": "Technology-enabled logistics company using relay trucking.", "investors": ["Warburg Pincus", "SAIF Partners"], "funding_stage": "series_e", "linkedin_url": "https://linkedin.com/company/rivigo", "website": "https://rivigo.com", "linkedin_team": 2200, "linkedin_industry": "Logistics", "dpiit_cat": "logistics", "valuation_usd": 1000000000},
165
  {"name": "Zepto", "city": "Mumbai", "sectors": ["ecommerce", "foodtech", "logistics"], "founded": 2021, "employees": 3000, "funding": 85000000000, "desc": "10-minute grocery delivery startup founded by teenage entrepreneurs.", "investors": ["StepStone Group", "Glade Brook Capital", "Nexus Ventures", "Y Combinator"], "funding_stage": "series_e", "linkedin_url": "https://linkedin.com/company/zeptonow", "website": "https://zepto.co", "linkedin_team": 3200, "linkedin_industry": "E-Commerce", "dpiit_cat": "foodtech", "valuation_usd": 5000000000, "campus": True},
 
201
  {"name": "INDwealth", "city": "Mumbai", "sectors": ["fintech", "wealthtech"], "founded": 2019, "employees": 200, "funding": 3000000000, "desc": "Wealth management and investment advisory platform.", "investors": ["Tiger Global", "Steadview Capital"], "funding_stage": "series_c", "linkedin_url": "https://linkedin.com/company/indwealth", "website": "https://indwealth.in", "linkedin_team": 220, "linkedin_industry": "FinTech", "dpiit_cat": "fintech", "valuation_usd": 600000000},
202
  {"name": "Simplilearn", "city": "Bangalore", "sectors": ["edtech"], "founded": 2010, "employees": 2000, "funding": 8000000000, "desc": "Online bootcamp and certification training platform.", "investors": ["Blackstone"], "funding_stage": "acquired", "linkedin_url": "https://linkedin.com/company/simplilearn", "website": "https://simplilearn.com", "linkedin_team": 2200, "linkedin_industry": "E-Learning", "dpiit_cat": "edtech", "valuation_usd": 1200000000},
203
  {"name": "Jiomeet (Reliance Jio)", "city": "Mumbai", "sectors": ["saas", "saas_ai"], "founded": 2020, "employees": 300, "funding": 0, "desc": "Enterprise video conferencing by Reliance Jio.", "investors": ["Reliance Industries"], "funding_stage": None, "linkedin_url": "https://linkedin.com/company/jiomeet", "website": "https://jiomeet.com", "linkedin_team": 350, "linkedin_industry": "Software", "dpiit_cat": "saas", "dpiit": False, "valuation_usd": None},
204
+ {"name": "Neysa", "city": "Bangalore", "sectors": ["ai_ml", "deeptech"], "founded": 2023, "employees": 80, "funding": 3000000000, "desc": "AI infrastructure company providing GPU cloud computing.", "investors": ["Matrix Partners", "Lightspeed"], "funding_stage": "series_a", "linkedin_url": "https://linkedin.com/company/neysaai", "website": "https://neysa.ai", "linkedin_team": 100, "linkedin_industry": "AI", "dpiit_cat": "ai_ml", "valuation_usd": 1000000000},
205
+ {"name": "Ather Energy", "city": "Bangalore", "sectors": ["ev", "mobility", "cleantech"], "founded": 2013, "employees": 3000, "funding": 21000000000, "desc": "Electric scooter manufacturer with charging network.", "investors": ["Hero MotoCorp", "Sachin Bansal", "Tiger Global"], "funding_stage": "series_e", "linkedin_url": "https://linkedin.com/company/atherenergy", "website": "https://atherenergy.com", "linkedin_team": 3200, "linkedin_industry": "EV", "dpiit_cat": "ev", "valuation_usd": 1300000000},
206
+ {"name": "Captain Fresh", "city": "Bangalore", "sectors": ["foodtech", "logistics"], "founded": 2019, "employees": 500, "funding": 3500000000, "desc": "B2B seafood supply chain platform.", "investors": ["Tiger Global", "Prosus", "Accel"], "funding_stage": "series_c", "linkedin_url": "https://linkedin.com/company/captainfresh", "website": "https://captainfresh.in", "linkedin_team": 550, "linkedin_industry": "FoodTech", "dpiit_cat": "foodtech", "valuation_usd": 1000000000},
207
+ {"name": "Kuku FM", "city": "Bangalore", "sectors": ["mediatech", "edtech"], "founded": 2018, "employees": 300, "funding": 4000000000, "desc": "Audio content platform for stories, courses, and audiobooks.", "investors": ["Google", "Vertex Ventures", "3one4 Capital"], "funding_stage": "series_c", "linkedin_url": "https://linkedin.com/company/kukufm", "website": "https://kukufm.com", "linkedin_team": 350, "linkedin_industry": "Media", "dpiit_cat": "mediatech", "valuation_usd": 1000000000},
208
+ {"name": "Euler Motors", "city": "Delhi", "sectors": ["ev", "mobility", "cleantech"], "founded": 2018, "employees": 600, "funding": 3000000000, "desc": "Commercial electric vehicle manufacturer for last-mile delivery.", "investors": ["GIC", "Blume Ventures", "Athera Venture Partners"], "funding_stage": "series_c", "linkedin_url": "https://linkedin.com/company/eulermotors", "website": "https://eulermotors.com", "linkedin_team": 650, "linkedin_industry": "EV", "dpiit_cat": "ev", "valuation_usd": 1000000000},
209
+ {"name": "Kissht", "city": "Mumbai", "sectors": ["fintech"], "founded": 2015, "employees": 500, "funding": 4000000000, "desc": "Digital lending platform for instant personal loans.", "investors": ["Vertex Ventures", "Endiya Partners", "BAce Capital"], "funding_stage": "series_d", "linkedin_url": "https://linkedin.com/company/kissht", "website": "https://kissht.com", "linkedin_team": 550, "linkedin_industry": "FinTech", "dpiit_cat": "fintech", "valuation_usd": 1000000000},
210
  ]
211
 
212
  # ─── Well-funded non-unicorn startups ─────────────────────────────────────────
 
216
  {"name": "CropIn", "city": "Bangalore", "sectors": ["agritech", "ai_ml"], "founded": 2010, "employees": 400, "funding": 3800000000, "dpiit_cat": "agritech", "desc": "AI-powered agri-intelligence platform.", "investors": ["ABC World Asia", "Chiratae Ventures"], "funding_stage": "series_c", "rural_impact": True},
217
  {"name": "DeHaat", "city": "Patna", "sectors": ["agritech"], "founded": 2012, "employees": 1500, "funding": 16000000000, "dpiit_cat": "agritech", "desc": "Full-stack agricultural services platform.", "investors": ["Sofina", "Prosus", "RTP Global"], "funding_stage": "series_d", "rural_impact": True, "nsa": True, "nsa_cat": "Agriculture"},
218
  {"name": "Ninjacart", "city": "Bangalore", "sectors": ["agritech", "logistics"], "founded": 2015, "employees": 2000, "funding": 30000000000, "dpiit_cat": "agritech", "desc": "B2B fresh produce supply chain platform.", "investors": ["Tiger Global", "Flipkart", "Accel"], "funding_stage": "series_c", "rural_impact": True},
 
219
  {"name": "Agnikul Cosmos", "city": "Chennai", "sectors": ["spacetech", "deeptech"], "founded": 2017, "employees": 200, "funding": 7500000000, "dpiit_cat": "spacetech", "desc": "Building 3D-printed rocket engines.", "investors": ["Anand Mahindra", "pi Ventures"], "funding_stage": "series_b", "campus": True},
220
  {"name": "Skyroot Aerospace", "city": "Hyderabad", "sectors": ["spacetech", "deeptech"], "founded": 2018, "employees": 250, "funding": 5200000000, "dpiit_cat": "spacetech", "desc": "India's first private space launch vehicle (Vikram series).", "investors": ["GIC", "Nexus Venture Partners"], "funding_stage": "series_b"},
221
  {"name": "Pixxel", "city": "Bangalore", "sectors": ["spacetech", "ai_ml"], "founded": 2019, "employees": 120, "funding": 4800000000, "dpiit_cat": "spacetech", "desc": "Hyperspectral earth-imaging satellite constellation.", "investors": ["Google", "Radical Ventures"], "funding_stage": "series_b", "campus": True},
frontend/index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <meta name="description" content="Comprehensive mapping platform for India's startup ecosystem — 5,770+ entities including Startups, SMEs, College E-Cells, Incubators, and Accelerators." />
7
  <meta name="keywords" content="India startups, startup map, Indian unicorns, DPIIT startups, tech ecosystem India" />
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
 
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="Curated mapping platform for India's startup ecosystem — startups, unicorns, SMEs, E-Cells, incubators. India has 223,000+ DPIIT-registered startups." />
7
  <meta name="keywords" content="India startups, startup map, Indian unicorns, DPIIT startups, tech ecosystem India" />
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
frontend/src/App.jsx CHANGED
@@ -249,7 +249,7 @@ export default function App() {
249
  ) : (
250
  <>
251
  <span className="text-xl">🗺️</span>
252
- <span className="font-bold gradient-text hidden sm:inline text-sm">Bharat Tech Atlas</span>
253
  </>
254
  )}
255
  </button>
@@ -295,6 +295,15 @@ export default function App() {
295
  </div>
296
  </div>
297
 
 
 
 
 
 
 
 
 
 
298
  {/* Stats Bar */}
299
  <StatsBar overview={overview} viewportSummary={viewportSummary} featureCount={displayCount} loading={loading} isMobile={isMobile} />
300
 
 
249
  ) : (
250
  <>
251
  <span className="text-xl">🗺️</span>
252
+ <span className="font-bold text-brand-500 hidden sm:inline text-sm">Bharat Tech Atlas</span>
253
  </>
254
  )}
255
  </button>
 
295
  </div>
296
  </div>
297
 
298
+ {/* Data Disclaimer */}
299
+ {overview && (
300
+ <div className="absolute bottom-[90px] sm:bottom-[100px] left-1/2 -translate-x-1/2 z-10 pointer-events-none">
301
+ <p className="text-[10px] text-atlas-muted/70 text-center whitespace-nowrap">
302
+ Curated dataset of {overview.total_entities?.toLocaleString()} entities · India has 223,000+ DPIIT-registered startups · Source: DPIIT, Tracxn, Crunchbase
303
+ </p>
304
+ </div>
305
+ )}
306
+
307
  {/* Stats Bar */}
308
  <StatsBar overview={overview} viewportSummary={viewportSummary} featureCount={displayCount} loading={loading} isMobile={isMobile} />
309
 
frontend/src/components/AnalyticsPanel.jsx CHANGED
@@ -34,7 +34,7 @@ export default function AnalyticsPanel({ onClose }) {
34
  }
35
 
36
  return (
37
- <div className="fixed inset-0 z-40 flex items-center justify-center bg-black/50 backdrop-blur-sm p-2 sm:p-4">
38
  <div className="glass rounded-2xl shadow-2xl w-full max-w-3xl max-h-[85vh] flex flex-col overflow-hidden">
39
  <div className="flex items-center justify-between px-4 sm:px-6 py-3 sm:py-4 border-b border-atlas-border flex-shrink-0">
40
  <div>
 
34
  }
35
 
36
  return (
37
+ <div className="fixed inset-0 z-40 flex items-center justify-center bg-black/50 p-2 sm:p-4">
38
  <div className="glass rounded-2xl shadow-2xl w-full max-w-3xl max-h-[85vh] flex flex-col overflow-hidden">
39
  <div className="flex items-center justify-between px-4 sm:px-6 py-3 sm:py-4 border-b border-atlas-border flex-shrink-0">
40
  <div>
frontend/src/components/EntityDetail.jsx CHANGED
@@ -37,7 +37,7 @@ export default function EntityDetail({ entity, onClose, onEntityClick, isMobile
37
  }>
38
  <div className={
39
  isMobile
40
- ? "bg-atlas-bg/95 backdrop-blur-xl border-t border-atlas-border rounded-t-2xl shadow-2xl flex flex-col overflow-hidden max-h-[80vh]"
41
  : "glass rounded-2xl shadow-2xl flex flex-col overflow-hidden pointer-events-auto max-h-full"
42
  }>
43
  {/* Header */}
 
37
  }>
38
  <div className={
39
  isMobile
40
+ ? "bg-atlas-bg/95 border-t border-atlas-border rounded-t-2xl shadow-2xl flex flex-col overflow-hidden max-h-[80vh]"
41
  : "glass rounded-2xl shadow-2xl flex flex-col overflow-hidden pointer-events-auto max-h-full"
42
  }>
43
  {/* Header */}
frontend/src/components/Sidebar.jsx CHANGED
@@ -149,7 +149,7 @@ export default function Sidebar({ filters, facets, onFilterChange, onReset, onCl
149
  }>
150
  <div className={
151
  isMobile
152
- ? "bg-atlas-bg/95 backdrop-blur-xl border-t border-atlas-border rounded-t-2xl shadow-2xl flex flex-col overflow-hidden max-h-[75vh]"
153
  : "glass rounded-2xl shadow-2xl flex flex-col overflow-hidden pointer-events-auto max-h-full"
154
  }>
155
  {/* Mobile drag handle */}
 
149
  }>
150
  <div className={
151
  isMobile
152
+ ? "bg-atlas-bg/95 border-t border-atlas-border rounded-t-2xl shadow-2xl flex flex-col overflow-hidden max-h-[75vh]"
153
  : "glass rounded-2xl shadow-2xl flex flex-col overflow-hidden pointer-events-auto max-h-full"
154
  }>
155
  {/* Mobile drag handle */}
frontend/src/components/StatsBar.jsx CHANGED
@@ -10,55 +10,36 @@ export default function StatsBar({ overview, viewportSummary, featureCount, load
10
  label: 'In View',
11
  value: viewportSummary?.count || featureCount,
12
  icon: '📍',
13
- gradient: 'from-blue-500/20 to-indigo-500/20',
14
- border: 'border-blue-500/20',
15
- glow: '#3B82F6',
16
  desc: 'Entities visible on map',
17
  },
18
  {
19
  label: 'Total',
20
  value: overview.total_entities,
21
  icon: '🏢',
22
- gradient: 'from-indigo-500/20 to-purple-500/20',
23
- border: 'border-indigo-500/20',
24
- glow: '#6366F1',
25
  desc: 'All ecosystem entities',
26
  },
27
  {
28
  label: 'Startups',
29
  value: overview.by_type?.startup || 0,
30
  icon: '🚀',
31
- gradient: 'from-emerald-500/20 to-teal-500/20',
32
- border: 'border-emerald-500/20',
33
- glow: '#10B981',
34
  desc: 'Active startups',
35
  },
36
  {
37
  label: 'Unicorns',
38
  value: overview.unicorn_count || 0,
39
  icon: '🦄',
40
- gradient: 'from-orange-500/20 to-amber-500/20',
41
- border: 'border-orange-500/20',
42
- glow: '#F97316',
43
  desc: 'Valued >$1B',
44
- highlight: true,
45
  },
46
  {
47
  label: 'Women-led',
48
  value: overview.women_led_count || 0,
49
  icon: '👩',
50
- gradient: 'from-pink-500/20 to-rose-500/20',
51
- border: 'border-pink-500/20',
52
- glow: '#EC4899',
53
  desc: 'Women founders',
54
  },
55
  {
56
  label: 'Funding',
57
  value: overview.total_funding_display,
58
  icon: '💰',
59
- gradient: 'from-amber-500/20 to-yellow-500/20',
60
- border: 'border-amber-500/20',
61
- glow: '#F59E0B',
62
  desc: 'Total raised',
63
  isText: true,
64
  },
@@ -81,35 +62,11 @@ export default function StatsBar({ overview, viewportSummary, featureCount, load
81
  {cards.map((card) => (
82
  <div
83
  key={card.label}
84
- className={`
85
- relative group rounded-2xl border ${card.border}
86
- bg-gradient-to-br ${card.gradient}
87
- backdrop-blur-xl shadow-lg
88
- px-3 sm:px-4 py-2.5 sm:py-3 min-w-[100px] sm:min-w-[120px]
89
- transition-all duration-200 ease-out
90
- hover:scale-105 hover:shadow-xl
91
- cursor-default
92
- `}
93
  title={card.desc}
94
- style={{
95
- background: `linear-gradient(135deg, rgba(30,41,59,0.9), rgba(15,23,42,0.95))`,
96
- }}
97
  >
98
- {/* Subtle glow on hover */}
99
- <div
100
- className="absolute inset-0 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"
101
- style={{
102
- boxShadow: `0 0 20px ${card.glow}25, inset 0 1px 0 ${card.glow}15`,
103
- }}
104
- />
105
-
106
- <div className="relative flex items-start gap-2.5">
107
- <span
108
- className="text-lg mt-0.5"
109
- style={{ filter: `drop-shadow(0 0 6px ${card.glow}50)` }}
110
- >
111
- {card.icon}
112
- </span>
113
  <div>
114
  <p className="text-base font-bold text-atlas-text leading-none tabular-nums">
115
  {typeof card.value === 'number' ? card.value.toLocaleString() : card.value}
@@ -119,14 +76,6 @@ export default function StatsBar({ overview, viewportSummary, featureCount, load
119
  </p>
120
  </div>
121
  </div>
122
-
123
- {/* Unicorn pulse indicator */}
124
- {card.highlight && card.value > 100 && (
125
- <div className="absolute -top-1 -right-1 w-2.5 h-2.5">
126
- <span className="absolute inline-flex h-full w-full rounded-full bg-orange-400 opacity-75 animate-ping" />
127
- <span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-orange-500" />
128
- </div>
129
- )}
130
  </div>
131
  ))}
132
  </div>
 
10
  label: 'In View',
11
  value: viewportSummary?.count || featureCount,
12
  icon: '📍',
 
 
 
13
  desc: 'Entities visible on map',
14
  },
15
  {
16
  label: 'Total',
17
  value: overview.total_entities,
18
  icon: '🏢',
 
 
 
19
  desc: 'All ecosystem entities',
20
  },
21
  {
22
  label: 'Startups',
23
  value: overview.by_type?.startup || 0,
24
  icon: '🚀',
 
 
 
25
  desc: 'Active startups',
26
  },
27
  {
28
  label: 'Unicorns',
29
  value: overview.unicorn_count || 0,
30
  icon: '🦄',
 
 
 
31
  desc: 'Valued >$1B',
 
32
  },
33
  {
34
  label: 'Women-led',
35
  value: overview.women_led_count || 0,
36
  icon: '👩',
 
 
 
37
  desc: 'Women founders',
38
  },
39
  {
40
  label: 'Funding',
41
  value: overview.total_funding_display,
42
  icon: '💰',
 
 
 
43
  desc: 'Total raised',
44
  isText: true,
45
  },
 
62
  {cards.map((card) => (
63
  <div
64
  key={card.label}
65
+ className="relative rounded-2xl border border-atlas-border bg-atlas-bg/90 shadow-lg px-3 sm:px-4 py-2.5 sm:py-3 min-w-[100px] sm:min-w-[120px] cursor-default"
 
 
 
 
 
 
 
 
66
  title={card.desc}
 
 
 
67
  >
68
+ <div className="flex items-start gap-2.5">
69
+ <span className="text-lg mt-0.5">{card.icon}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  <div>
71
  <p className="text-base font-bold text-atlas-text leading-none tabular-nums">
72
  {typeof card.value === 'number' ? card.value.toLocaleString() : card.value}
 
76
  </p>
77
  </div>
78
  </div>
 
 
 
 
 
 
 
 
79
  </div>
80
  ))}
81
  </div>
frontend/src/index.css CHANGED
@@ -24,17 +24,13 @@
24
 
25
  @layer components {
26
  .glass {
27
- @apply bg-atlas-bg/80 border border-atlas-border backdrop-blur-xl;
28
  }
29
  .gradient-text {
30
- @apply bg-gradient-to-r from-brand-500 to-amber-400 bg-clip-text text-transparent;
31
  }
32
  .unicorn-badge {
33
- background: linear-gradient(90deg, #A855F7, #EC4899, #F97316, #A855F7);
34
- background-size: 200% auto;
35
- -webkit-background-clip: text;
36
- -webkit-text-fill-color: transparent;
37
- animation: shimmer 3s linear infinite;
38
  }
39
  .scrollbar-thin::-webkit-scrollbar {
40
  width: 4px;
@@ -44,9 +40,6 @@
44
  }
45
  }
46
 
47
- @keyframes shimmer {
48
- to { background-position: 200% center; }
49
- }
50
  @keyframes slideUp {
51
  from { transform: translateY(100%); }
52
  to { transform: translateY(0); }
@@ -54,7 +47,7 @@
54
 
55
  /* MapLibre popup overrides */
56
  .maplibregl-popup-content {
57
- @apply !bg-atlas-bg/95 !border !border-atlas-border !rounded-2xl !p-0 !shadow-2xl backdrop-blur-xl;
58
  max-width: 340px !important;
59
  }
60
  .maplibregl-popup-tip {
 
24
 
25
  @layer components {
26
  .glass {
27
+ @apply bg-atlas-bg/80 border border-atlas-border;
28
  }
29
  .gradient-text {
30
+ @apply text-brand-500;
31
  }
32
  .unicorn-badge {
33
+ @apply text-brand-500 font-semibold;
 
 
 
 
34
  }
35
  .scrollbar-thin::-webkit-scrollbar {
36
  width: 4px;
 
40
  }
41
  }
42
 
 
 
 
43
  @keyframes slideUp {
44
  from { transform: translateY(100%); }
45
  to { transform: translateY(0); }
 
47
 
48
  /* MapLibre popup overrides */
49
  .maplibregl-popup-content {
50
+ @apply !bg-atlas-bg/95 !border !border-atlas-border !rounded-2xl !p-0 !shadow-2xl;
51
  max-width: 340px !important;
52
  }
53
  .maplibregl-popup-tip {
frontend/src/styles/index.css CHANGED
@@ -79,10 +79,6 @@ body {
79
  from { opacity: 0; transform: translateY(8px); }
80
  to { opacity: 1; transform: translateY(0); }
81
  }
82
- @keyframes pulseGlow {
83
- 0%, 100% { box-shadow: 0 0 0 0 rgba(249, 115, 22, 0.4); }
84
- 50% { box-shadow: 0 0 0 8px rgba(249, 115, 22, 0); }
85
- }
86
  @keyframes countUp {
87
  from { opacity: 0; transform: translateY(10px); }
88
  to { opacity: 1; transform: translateY(0); }
@@ -98,34 +94,21 @@ body {
98
  .animate-count-up { animation: countUp 0.5s ease-out; }
99
  .animate-slide-up { animation: slideUp 0.3s ease-out; }
100
 
101
- /* Glass morphismdark */
102
  .glass {
103
  background: rgba(30, 41, 59, 0.92);
104
- backdrop-filter: blur(16px);
105
- -webkit-backdrop-filter: blur(16px);
106
  border: 1px solid rgba(51, 65, 85, 0.6);
107
  }
108
 
109
- /* Gradient text */
110
  .gradient-text {
111
- background: linear-gradient(135deg, #F97316 0%, #FB923C 50%, #FBBF24 100%);
112
- -webkit-background-clip: text;
113
- -webkit-text-fill-color: transparent;
114
- background-clip: text;
115
- }
116
-
117
- /* Unicorn badge shimmer */
118
- @keyframes shimmer {
119
- 0% { background-position: -200% center; }
120
- 100% { background-position: 200% center; }
121
  }
122
 
 
123
  .unicorn-badge {
124
- background: linear-gradient(90deg, #A855F7, #EC4899, #F97316, #A855F7);
125
- background-size: 200% auto;
126
- animation: shimmer 3s linear infinite;
127
- -webkit-background-clip: text;
128
- -webkit-text-fill-color: transparent;
129
  }
130
 
131
  .line-clamp-2 {
 
79
  from { opacity: 0; transform: translateY(8px); }
80
  to { opacity: 1; transform: translateY(0); }
81
  }
 
 
 
 
82
  @keyframes countUp {
83
  from { opacity: 0; transform: translateY(10px); }
84
  to { opacity: 1; transform: translateY(0); }
 
94
  .animate-count-up { animation: countUp 0.5s ease-out; }
95
  .animate-slide-up { animation: slideUp 0.3s ease-out; }
96
 
97
+ /* Flat dark panel no blur */
98
  .glass {
99
  background: rgba(30, 41, 59, 0.92);
 
 
100
  border: 1px solid rgba(51, 65, 85, 0.6);
101
  }
102
 
103
+ /* Plain brand color text */
104
  .gradient-text {
105
+ color: #F97316;
 
 
 
 
 
 
 
 
 
106
  }
107
 
108
+ /* Unicorn badge — plain brand color */
109
  .unicorn-badge {
110
+ color: #F97316;
111
+ font-weight: 600;
 
 
 
112
  }
113
 
114
  .line-clamp-2 {
requirements.txt CHANGED
@@ -2,3 +2,4 @@ fastapi==0.115.0
2
  uvicorn[standard]==0.32.0
3
  pydantic==2.9.0
4
  aiofiles==24.1.0
 
 
2
  uvicorn[standard]==0.32.0
3
  pydantic==2.9.0
4
  aiofiles==24.1.0
5
+ slowapi==0.1.9