| title | Technology Stack |
|---|---|
| description | Cost-optimized, performant open-source technology stack for the Evoteli |
This document defines the production technology stack for the Evoteli MVP, optimized for:
- Cost efficiency: 97%+ savings vs. commercial alternatives
- Performance: Sub-10s latency, 10k+ events/sec throughput
- Scalability: Start small, scale to enterprise
- Open-source first: Minimize vendor lock-in
Total MVP Cost (3 months): ~$6,000 (vs. $220,000+ commercial)
Why MapLibre:
- ✅ Free & Open Source (BSD 3-Clause)
- ✅ Google Maps alternative with identical API
- ✅ Vector tiles support (smooth zoom, client-side styling)
- ✅ 3D terrain & buildings support
- ✅ Mobile-friendly (React Native support)
Installation:
npm install maplibre-glBasic Setup:
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json', // Free tiles
center: [-84.3880, 33.7490], // Atlanta
zoom: 12
});Cost: $0/month (self-hosted tiles)
Option 1: Protomaps (Recommended)
- Pre-built vector tiles from OSM data
- Self-hosted (S3/GCS bucket)
- Global coverage in single ~100GB file
- Cost: $0.23/GB storage + $0.09/GB transfer = ~$50/month
Option 2: OpenMapTiles
- Vector tiles from OSM
- Self-hosted or cloud-hosted
- Cost: $0 (self-hosted) or $49/month (cloud)
Option 3: Maptiler (Commercial fallback)
- 100,000 free map loads/month
- $99/month for 500k loads
- Use case: MVP testing before self-hosting
Parcel Boundaries: PostGIS + GeoJSON
map.addSource('parcels', {
type: 'geojson',
data: '/api/parcels?bbox=-84.4,-84.3,33.7,33.8'
});
map.addLayer({
id: 'parcels-fill',
type: 'fill',
source: 'parcels',
paint: {
'fill-color': '#088',
'fill-opacity': 0.4
}
});Heat Maps: Deck.gl
import { HeatmapLayer } from '@deck.gl/aggregation-layers';
const layer = new HeatmapLayer({
id: 'heatmap',
data: occupancyData,
getPosition: d => [d.lon, d.lat],
getWeight: d => d.occupancy,
radiusPixels: 30
});Draw & Edit Polygons: Mapbox GL Draw (Open Source)
npm install @mapbox/mapbox-gl-drawimport MapboxDraw from '@mapbox/mapbox-gl-draw';
const draw = new MapboxDraw({
displayControlsDefault: false,
controls: {
polygon: true,
trash: true
}
});
map.addControl(draw);
// Save drawn territories
map.on('draw.create', (e) => {
const territory = e.features[0];
fetch('/api/territories', {
method: 'POST',
body: JSON.stringify(territory)
});
});Cost: $0 (MIT license)
Why React:
- ✅ Largest ecosystem for map/geo libraries
- ✅ Component reusability
- ✅ MapLibre GL has official React wrapper
- ✅ Deck.gl has React bindings
Alternative: Svelte (lighter, faster, but smaller ecosystem)
Setup:
npm create vite@latest geointel-ui -- --template react-ts
cd geointel-ui
npm install maplibre-gl @mapbox/mapbox-gl-draw deck.glWhy shadcn/ui:
- ✅ Free (copy-paste components, no bundle bloat)
- ✅ Radix UI primitives (accessible, unstyled)
- ✅ Tailwind CSS styling
- ✅ Customizable (own your code)
Components Needed:
Command- Advanced search (Cmd+K style)Dialog- Property details modalDropdownMenu- Filters and actionsTable- Results listTabs- Product switcher (LotWatch/HomeScope)
Cost: $0
npm install zustandimport create from 'zustand';
interface MapState {
selectedParcels: string[];
filters: Record<string, any>;
addParcel: (id: string) => void;
}
const useMapStore = create<MapState>((set) => ({
selectedParcels: [],
filters: {},
addParcel: (id) => set((state) => ({
selectedParcels: [...state.selectedParcels, id]
}))
}));Alternative: Jotai (atom-based, more granular)
Why FastAPI:
- ✅ Fastest Python framework (async/await native)
- ✅ Auto-generated OpenAPI docs
- ✅ Type safety with Pydantic
- ✅ Easy ML model integration (PyTorch, ONNX)
Setup:
pip install fastapi[all] uvicorn[standard]Example Endpoint:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class SignalQuery(BaseModel):
site_id: str
metrics: list[str]
time_from: str
time_to: str
@app.post("/v1/signals:query")
async def query_signals(query: SignalQuery):
# Query ClickHouse
results = await db.query(
"SELECT * FROM signals WHERE site_id = ?",
query.site_id
)
return {"rows": results}Performance: 10k+ req/sec (with Uvicorn)
Alternative: Go with Gin framework (faster, but less ML integration)
Why ClickHouse:
- ✅ 100x faster than PostgreSQL for time-series
- ✅ Column-oriented (compress to 10% of row-store)
- ✅ Real-time aggregations (sub-second on billions of rows)
- ✅ Free (Apache 2.0)
Deployment:
docker run -d \
--name clickhouse \
-p 8123:8123 -p 9000:9000 \
-v clickhouse-data:/var/lib/clickhouse \
clickhouse/clickhouse-serverSchema:
CREATE TABLE signals (
site_id String,
ts DateTime64(3),
metric LowCardinality(String),
value Float64,
quality_score Float32
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(ts)
ORDER BY (site_id, metric, ts);Cost (Cloud):
- ClickHouse Cloud: $0.36/hour (~$260/month for 1TB RAM node)
- Self-hosted (AWS): r6i.2xlarge = $0.504/hour (~$365/month)
Alternative: TimescaleDB (PostgreSQL extension, easier but slower)
Why PostGIS:
- ✅ Industry standard for geo queries
- ✅ Spatial indexes (GiST, BRIN)
- ✅ Geometry operations (intersect, buffer, union)
- ✅ Free (PostgreSQL license)
Deployment:
docker run -d \
--name postgis \
-e POSTGRES_PASSWORD=mysecret \
-p 5432:5432 \
postgis/postgis:15-3.3Example Query:
-- Find parcels within 5 miles of site
SELECT p.parcel_id, p.address
FROM parcels p
WHERE ST_DWithin(
p.geometry,
ST_SetSRID(ST_MakePoint(-84.3880, 33.7490), 4326)::geography,
8046.72 -- 5 miles in meters
);Cost (Cloud):
- AWS RDS: db.t3.medium = $0.068/hour (~$50/month)
- Supabase (managed): Free tier (500MB) or $25/month (8GB)
Why Redis:
- ✅ Sub-millisecond latency
- ✅ TTL support (auto-expire cached queries)
- ✅ Pub/Sub for real-time updates
- ✅ Free (BSD license)
Deployment:
docker run -d \
--name redis \
-p 6379:6379 \
redis:7-alpineUsage:
import redis
r = redis.Redis(host='localhost', port=6379)
# Cache signal query (5-minute TTL)
cache_key = f"signals:{site_id}:{metric}:{time_range}"
cached = r.get(cache_key)
if cached:
return json.loads(cached)
else:
result = query_database()
r.setex(cache_key, 300, json.dumps(result))
return resultCost (Cloud):
- Upstash: Free tier (10k requests/day) or $0.2/100k requests
- AWS ElastiCache: cache.t3.micro = $0.017/hour (~$12/month)
Why MinIO:
- ✅ S3-compatible API (drop-in replacement)
- ✅ Self-hosted (no egress fees)
- ✅ Free (AGPL license, Apache for enterprise)
Deployment:
docker run -d \
--name minio \
-p 9000:9000 -p 9001:9001 \
-v minio-data:/data \
minio/minio server /data --console-address ":9001"Alternative: Use AWS S3 ($0.023/GB + $0.09/GB egress) or Cloudflare R2 ($0.015/GB, no egress)
Cost (Self-hosted): $0 + storage hardware
Why YOLO11:
- ✅ AGPL-3.0 license (free for open-source projects)
- ✅ State-of-art accuracy (90%+ mAP on COCO)
- ✅ Fast inference (50+ FPS on Jetson Orin)
- ✅ Pre-trained models (vehicle detection ready)
Installation:
pip install ultralyticsInference:
from ultralytics import YOLO
model = YOLO('yolo11n.pt') # Nano model
results = model('frame.jpg')
vehicles = [r for r in results[0].boxes if r.cls in [2, 3, 5, 7]] # car, motorcycle, bus, truck
print(f"Detected {len(vehicles)} vehicles")Alternative: YOLOv8 (slightly older, same license)
pip install bytetrackfrom bytetrack import BYTETracker
tracker = BYTETracker(track_thresh=0.5)
tracks = tracker.update(detections)Face/Plate Anonymization:
pip install deep-privacyfrom deep_privacy import DeepPrivacy
anonymizer = DeepPrivacy()
anonymized_frame = anonymizer.anonymize(frame)Alternative: Simple blur with OpenCV (faster, less accurate)
Specs:
- 6-core ARM CPU
- 1024 CUDA cores
- 8GB RAM
- 40 TOPS AI performance
Alternative: Google Coral TPU Dev Board ($150, but only 4 TOPS)
Software Stack:
# JetPack 6.0 (Ubuntu 22.04 + CUDA 12)
sudo apt install nvidia-jetpack
pip3 install ultralytics opencv-python paho-mqtt1. Sentinel-2 (ESA - Free)
- Resolution: 10-20m
- Cadence: 5-10 days
- Coverage: Global
- API: Copernicus Data Space (free, requires registration)
- Cost: $0
from sentinelsat import SentinelAPI
api = SentinelAPI('username', 'password')
products = api.query(
area='POLYGON((-84.4 33.7, ...))',
date=('20250101', '20250131'),
platformname='Sentinel-2',
cloudcoverpercentage=(0, 10)
)2. Google Earth Engine (Free for Research/Non-Commercial)
- Resolution: Varies (Landsat: 30m, Sentinel: 10m)
- Cadence: Daily composites available
- Cost: $0 (non-commercial) or $0.01-$0.10/image (commercial)
import ee
ee.Initialize()
# Get insolation data
insolation = ee.Image('NASA/NREL/NSRDB/v1').select('clearsky_ghi')
mean_insolation = insolation.reduceRegion(
reducer=ee.Reducer.mean(),
geometry=parcel_geometry,
scale=1000
).getInfo()3. NASA APIs (Free)
- Earth API: Landsat 8 imagery
- POWER API: Solar radiation data
- GIBS: Global Imagery Browse Services
import requests
# NASA Earth API
url = f"https://api.nasa.gov/planetary/earth/imagery"
params = {
'lon': -84.3880,
'lat': 33.7490,
'date': '2025-01-01',
'api_key': 'DEMO_KEY'
}
response = requests.get(url, params=params)Cost: $0 (rate limited to 1000 requests/hour)
Planet Labs:
- Resolution: 3-5m
- Cadence: Daily
- Cost: $10,000/year (education) or $30,000+/year (commercial)
Maxar/Nearmap:
- Resolution: 30-50cm
- Cost: $50,000+/year
Recommendation: Start with Sentinel-2 + Earth Engine (free), upgrade to Planet only if needed.
Why Airflow:
- ✅ Industry standard for data pipelines
- ✅ DAG-based (visualize dependencies)
- ✅ Extensive integrations (ClickHouse, PostGIS, APIs)
- ✅ Free (Apache 2.0)
Setup:
pip install apache-airflow
airflow standaloneExample DAG:
from airflow import DAG
from airflow.operators.python import PythonOperator
dag = DAG('homescope_weekly', schedule='@weekly')
def fetch_satellite():
# Download Sentinel-2 tiles
pass
def analyze_roofs():
# Run SAM segmentation
pass
fetch = PythonOperator(task_id='fetch', python_callable=fetch_satellite, dag=dag)
analyze = PythonOperator(task_id='analyze', python_callable=analyze_roofs, dag=dag)
fetch >> analyzeAlternative: Dagster (modern, better UI, but less mature)
Cost: $0 (self-hosted) or $100/month (Astronomer Cloud)
Why Kafka:
- ✅ High throughput (millions of events/sec)
- ✅ Durable (disk-backed, replicated)
- ✅ Industry standard
- ✅ Free (Apache 2.0)
Setup (Docker Compose):
services:
kafka:
image: confluentinc/cp-kafka:7.5.0
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181Alternative: RedPanda (Kafka-compatible, C++, 10x faster, easier to manage)
docker run -d --name redpanda \
-p 9092:9092 \
vectorized/redpanda:latest \
redpanda start --smp 1Cost (Cloud):
- Confluent Cloud: $1/hour (~$730/month)
- AWS MSK: $0.21/hour (~$150/month)
- Self-hosted: $0 + compute
Why Flink:
- ✅ True streaming (not micro-batch like Spark)
- ✅ Low latency (<100ms)
- ✅ Stateful processing (windows, joins)
- ✅ Free (Apache 2.0)
Alternative: Apache Beam (unified batch + stream, runs on Flink/Spark/Dataflow)
Recommendation: Use Beam with FlinkRunner (flexibility + performance)
import apache_beam as beam
with beam.Pipeline() as p:
(p
| beam.io.ReadFromKafka(...)
| beam.WindowInto(beam.window.FixedWindows(60)) # 1-min windows
| beam.CombinePerKey(beam.combiners.MeanCombineFn())
| beam.io.WriteToClickHouse(...))Cost: $0 (self-hosted)
Why ONNX:
- ✅ Framework agnostic (PyTorch, TF, etc. → ONNX)
- ✅ Optimized inference (2-5x faster than native)
- ✅ Cross-platform (CPU, GPU, edge)
- ✅ Free (MIT license)
Convert YOLO to ONNX:
from ultralytics import YOLO
model = YOLO('yolo11n.pt')
model.export(format='onnx', opset=14)Inference:
import onnxruntime as ort
session = ort.InferenceSession('yolo11n.onnx')
outputs = session.run(None, {'images': input_tensor})Alternative: NVIDIA Triton Inference Server (more features, overkill for MVP)
Why Keycloak:
- ✅ OAuth 2.0 / OIDC standard
- ✅ SSO support (Google, GitHub, SAML)
- ✅ Multi-tenant
- ✅ Free (Apache 2.0)
Setup:
docker run -d \
--name keycloak \
-p 8080:8080 \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:23.0 start-devAlternative: Auth0 (free tier: 7k MAUs) or Supabase Auth (free tier: 50k MAUs)
Cost: $0 (self-hosted) or $0 (Auth0/Supabase free tier)
Prometheus:
docker run -d \
--name prometheus \
-p 9090:9090 \
-v prometheus.yml:/etc/prometheus/prometheus.yml \
prom/prometheusGrafana:
docker run -d \
--name grafana \
-p 3000:3000 \
grafana/grafanaCost: $0 (self-hosted) or $50/month (Grafana Cloud)
docker run -d \
--name jaeger \
-p 16686:16686 \
jaegertracing/all-in-oneLoki is like Prometheus but for logs (from Grafana Labs).
Cost: $0
import requests
url = "https://api.census.gov/data/2021/acs/acs5"
params = {
'get': 'B01001_001E,B19013_001E', # Population, Median Income
'for': 'tract:*',
'in': 'state:13 county:121' # Fulton County, GA
}
response = requests.get(url, params=params).json()Data Available:
- Population, age, race
- Income, poverty
- Housing units, occupancy
- Education, employment
Cost: $0, unlimited
import requests
query = """
[out:json];
area["name"="Atlanta"]->.a;
(
node(area.a)["amenity"="restaurant"];
way(area.a)["amenity"="restaurant"];
);
out center;
"""
url = "https://overpass-api.de/api/interpreter"
response = requests.post(url, data={'data': query})
restaurants = response.json()Data: POIs (restaurants, schools, parks, etc.)
import requests
url = "https://api.open-meteo.com/v1/forecast"
params = {
'latitude': 33.7490,
'longitude': -84.3880,
'hourly': 'temperature_2m,precipitation',
'timezone': 'America/New_York'
}
response = requests.get(url, params=params).json()Cost: $0, 10k requests/day (upgrade to $50/month for 5M)
| Component | Instance Type | Monthly Cost |
|---|---|---|
| ClickHouse | c6i.2xlarge (8 vCPU, 16GB) | $244 |
| PostGIS | db.t3.medium (2 vCPU, 4GB) | $50 |
| FastAPI (2 nodes) | t3.medium (2 vCPU, 4GB) × 2 | $120 |
| Kafka/RedPanda | c6i.large (2 vCPU, 4GB) | $61 |
| Flink | c6i.xlarge (4 vCPU, 8GB) | $122 |
| Redis | cache.t3.micro | $12 |
| Airflow | t3.medium | $60 |
| Total Compute | $669/month |
| Component | Usage | Monthly Cost |
|---|---|---|
| ClickHouse data (compressed) | 500GB | $11.50 (S3) |
| PostGIS backup | 50GB | $1.15 (S3) |
| Imagery tiles (Protomaps) | 100GB | $2.30 (S3) |
| Object storage (MinIO/S3) | 200GB | $4.60 (S3) |
| Total Storage | $19.55/month |
| API | Usage | Cost |
|---|---|---|
| Sentinel-2 (ESA) | Unlimited | $0 |
| Earth Engine | 1000 images/month | $0 |
| Census.gov | Unlimited | $0 |
| Open-Meteo | 10k requests/day | $0 |
| OpenStreetMap | Unlimited | $0 |
| Geocode.xyz | Unlimited (throttled) | $0 |
| Total APIs | $0/month |
| Device | Quantity | Total |
|---|---|---|
| Jetson Orin Nano | 10 | $4,990 |
| Cameras (IP cameras) | 10 | $1,000 |
| Mounts/enclosures | 10 | $500 |
| Total Hardware | $6,490 (one-time) |
- Monthly recurring: $688.55/month × 3 = $2,065
- One-time hardware: $6,490
- Grand total: $8,555 (3 months)
vs. Commercial Stack: $220,000+ (Planet + Google Maps + Snowflake)
Savings: 97%
┌─────────────────────────────────────────────────────────────────┐
│ FRONTEND (React) │
│ MapLibre GL + Deck.gl + Mapbox Draw + shadcn/ui │
│ Hosted: Vercel/Netlify (Free tier) or Cloudflare Pages │
└────────────────────────────┬────────────────────────────────────┘
│ HTTPS
┌────────────────────────────▼────────────────────────────────────┐
│ API GATEWAY (FastAPI) │
│ OAuth 2.0 (Keycloak) + Rate Limiting + Caching (Redis) │
│ Hosted: AWS ECS/Fargate or Fly.io │
└──────┬──────────────────────┬──────────────────────┬───────────┘
│ │ │
▼ ▼ ▼
┌────────────┐ ┌──────────────────┐ ┌──────────────┐
│ ClickHouse │ │ PostGIS │ │ Redis │
│ (Signals) │ │ (Parcels) │ │ (Cache) │
└────────────┘ └──────────────────┘ └──────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ EDGE LAYER (Jetson) │
│ YOLO11 + ByteTrack + DeepPrivacy → MQTT → Kafka │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌────────────────┐
│ Kafka/RedPanda │
└────────┬───────┘
│
▼
┌────────────────┐
│ Flink/Beam │
│ (Aggregations) │
└────────┬───────┘
│
▼
┌────────────────┐
│ ClickHouse │
└────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ BATCH LAYER (Airflow) │
│ Sentinel-2 → SAM Segmentation → PostGIS │
│ Earth Engine → Insolation/NDVI → ClickHouse │
└─────────────────────────────────────────────────────────────────┘
- ✅ Deploy ClickHouse (Docker)
- ✅ Deploy PostGIS (Docker)
- ✅ Deploy Redis (Docker)
- ✅ Set up Keycloak (OAuth)
- ✅ FastAPI skeleton + OpenAPI docs
- ✅ MapLibre GL + Protomaps tiles
- ✅ Mapbox Draw (territory mapping)
- ✅ Deck.gl heat maps
- ✅ shadcn/ui filters & modals
- ✅ Flash Jetson with YOLO11
- ✅ ByteTrack integration
- ✅ DeepPrivacy redaction
- ✅ MQTT → Kafka pipeline
- ✅ Sentinel-2 API integration
- ✅ SAM roof segmentation
- ✅ Earth Engine insolation
- ✅ Airflow DAGs
- ✅ Census.gov demographics
- ✅ Open-Meteo weather
- ✅ OSM Overpass POI data
- ✅ Geocode.xyz reverse geocoding
- ✅ Grafana dashboards
- ✅ Quality assurance
- ✅ Load testing
- ✅ Documentation
Detailed map UI/UX specifications Step-by-step integration for 65+ public APIs Deploy the full stack on AWS/GCP/Hetzner Reduce monthly costs by 50%+ with caching