| title | Development Plan |
|---|---|
| description | Comprehensive 90-day development, architecture, and implementation plan for MVP |
This document outlines the complete development plan for the Evoteli MVP, covering methodology, architecture, implementation phases, and success criteria for a 90-day delivery.
Target: Production-ready MVP with 10 pilot sites and 1,000+ analyzed parcels Team: 4.5 FTE (Platform, ML, Backend, Frontend, DevOps) Budget: $8,555 (3 months including hardware) Methodology: Agile with 2-week sprints
Sprint Duration: 2 weeks Total Sprints: 6 sprints (12 weeks) Sprint Structure:
- Day 1: Sprint planning (4 hours)
- Days 2-9: Development with daily standups (15 min)
- Day 10: Sprint review & retrospective (2 hours)
Ceremonies:
- Daily Standup: 9:00 AM, 15 minutes (What did you do? What will you do? Blockers?)
- Sprint Planning: Define sprint goals, break down user stories, estimate effort
- Sprint Review: Demo working software to stakeholders
- Retrospective: What went well? What to improve? Action items
A task is "Done" when:
- ✅ Code is written and reviewed
- ✅ Unit tests pass (>80% coverage)
- ✅ Integration tests pass
- ✅ Documentation is updated
- ✅ Deployed to staging environment
- ✅ Product owner approves
Why: Enable independent development and deployment of components
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Frontend │────▶│ API Gateway│────▶│ Services │
│ (React) │ │ (FastAPI) │ │ (Micro) │
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ Databases │
│ Queues │
└─────────────┘
Implementation:
- Each service has its own database (no shared DB)
- Communication via REST APIs or message queues
- Services can be deployed independently
Why: Enable multiple clients (web, mobile, partners) and parallel development
Process:
- Define OpenAPI spec first
- Generate client SDKs
- Frontend and backend teams work in parallel
- Mock servers for frontend development
Why: Optimize for read-heavy workloads (1000:1 read:write ratio)
Strategy:
- Write path: Edge → Kafka → Flink → ClickHouse (optimized for ingestion)
- Read path: API → Redis cache → ClickHouse (optimized for queries)
- Cache first: 95% cache hit rate target
Why: Legal compliance and user trust
Implementation:
- Redaction at edge (before network transmission)
- Aggregation by default (no individual tracking)
- Retention policies enforced at database level (TTL)
- Opt-out registry with immediate effect
Why: Catch issues before users do
Stack:
- Metrics: Prometheus (every component exports metrics)
- Logs: OpenSearch (structured JSON logs)
- Traces: Jaeger (distributed tracing across services)
- Dashboards: Superset (business metrics)
┌──────────────────────────────────────────────────────────────────┐
│ USER LAYER │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Web App │ │ Mobile App │ │
│ │ (React) │ │ (Future) │ │
│ └──────┬───────┘ └──────┬───────┘ │
└─────────┼──────────────────────────────────────────────┼──────────┘
│ │
│ HTTPS (TLS 1.3) │
▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ API GATEWAY LAYER │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ NGINX (Load Balancer) │ │
│ │ - Rate limiting │ │
│ │ - TLS termination │ │
│ │ - Request routing │ │
│ └────────────────────┬───────────────────────────────────────┘ │
└───────────────────────┼──────────────────────────────────────────┘
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ FastAPI │ │ FastAPI │ │ FastAPI │
│ Instance 1 │ │ Instance 2 │ │ Instance 3 │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└───────────────┼───────────────┘
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Valkey │ │ ClickHouse │ │ PostGIS │
│ (Cache) │ │(Time-Series)│ │ (Spatial) │
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ EDGE DEVICE (Jetson Orin) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Camera │───────▶│ PP-YOLOE │───────▶│ ByteTrack │ │
│ │ (RTSP) │ │ Detector │ │ Tracker │ │
│ └─────────────┘ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ DeepPrivacy │ │
│ │ Redaction │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ MQTT │ │
│ │ Publisher │ │
│ └──────┬──────┘ │
└────────────────────────────────────────────────────────┼────────┘
│
│ WiFi/LTE
▼
┌─────────────────────────────────────────────────────────────────┐
│ CLOUD INGESTION │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ MQTT │───────▶│ Kafka │───────▶│ Flink │ │
│ │ Broker │ │ Topics │ │ Processing │ │
│ └─────────────┘ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ ClickHouse │ │
│ │ Insertion │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ BATCH ORCHESTRATION (Airflow) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ DAG: weekly_imagery │ │
│ │ │ │
│ │ 1. fetch_sentinel2 ──▶ 2. segment_roofs ──▶ │ │
│ │ │ │
│ │ 3. analyze_solar ────▶ 4. compute_metrics ──▶ │ │
│ │ │ │
│ │ 5. update_postgis ───▶ 6. update_clickhouse │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Sentinel2 │ │ PP-YOLOE │ │ Earth │ │ PostGIS │
│ API │ │ Segmentation│ │ Engine │ │ Update │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
| Use Case | Database | Why |
|---|---|---|
| Time-series signals | ClickHouse | 100x faster than Postgres for time-series, column-oriented compression |
| Geospatial data | PostGIS | Industry standard, spatial indexes, geometry operations |
| Cache | Valkey | Sub-millisecond latency, Redis-compatible |
| Object storage | SeaweedFS | S3-compatible, 10x faster for small files |
| Search/logs | OpenSearch | Full-text search, 75% faster than Loki |
-- Primary time-series table
CREATE TABLE signals_timeseries (
site_id LowCardinality(String),
polygon_id Nullable(String),
ts DateTime64(3, 'UTC'),
metric LowCardinality(String),
value Float64,
unit LowCardinality(String),
method Enum8(
'edge_cv' = 1,
'sat_change' = 2,
'aerial_oblique' = 3,
'earth_engine' = 4,
'fused' = 5
),
quality_score Float32,
provenance String -- JSON
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(ts)
ORDER BY (site_id, metric, ts)
SETTINGS index_granularity = 8192;
-- 15-minute rollup (materialized view)
CREATE MATERIALIZED VIEW signals_15m_mv
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(ts)
ORDER BY (site_id, metric, ts)
AS SELECT
site_id,
polygon_id,
toStartOfFifteenMinutes(ts) AS ts,
metric,
avgState(value) AS value_avg,
medianState(value) AS value_median,
sumState(value) AS value_sum,
maxState(value) AS value_max,
avgState(quality_score) AS quality_avg,
count() AS sample_count
FROM signals_timeseries
GROUP BY site_id, polygon_id, ts, metric;
-- Query 15m rollup
SELECT
ts,
metric,
avgMerge(value_avg) AS avg_value,
medianMerge(value_median) AS median_value
FROM signals_15m_mv
WHERE site_id = 'ATL-CTF-001'
AND metric = 'occupancy'
AND ts >= now() - INTERVAL 24 HOUR
GROUP BY ts, metric
ORDER BY ts;-- Sites table
CREATE TABLE sites (
site_id VARCHAR(50) PRIMARY KEY,
name VARCHAR(200) NOT NULL,
brand VARCHAR(100),
geometry GEOMETRY(Polygon, 4326) NOT NULL,
address JSONB,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX sites_geom_idx ON sites USING GIST (geometry);
CREATE INDEX sites_brand_idx ON sites (brand);
-- Parcels table
CREATE TABLE parcels (
parcel_id VARCHAR(50) PRIMARY KEY,
geometry GEOMETRY(Polygon, 4326) NOT NULL,
area_sqft FLOAT,
property_type VARCHAR(50),
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX parcels_geom_idx ON parcels USING GIST (geometry);
CREATE INDEX parcels_type_idx ON parcels (property_type);
-- Model inferences (roof segmentation, etc.)
CREATE TABLE model_inferences (
inference_id SERIAL PRIMARY KEY,
parcel_id VARCHAR(50) REFERENCES parcels(parcel_id),
model_name VARCHAR(100) NOT NULL,
model_version VARCHAR(50) NOT NULL,
inference_date TIMESTAMPTZ NOT NULL,
result JSONB NOT NULL, -- Segmentation masks, bboxes, scores
artifact_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX model_inferences_parcel_idx
ON model_inferences (parcel_id, inference_date DESC);
-- Territories (drawn by users)
CREATE TABLE territories (
territory_id SERIAL PRIMARY KEY,
user_id VARCHAR(50) NOT NULL,
name VARCHAR(200) NOT NULL,
geometry GEOMETRY(Polygon, 4326) NOT NULL,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX territories_geom_idx ON territories USING GIST (geometry);
CREATE INDEX territories_user_idx ON territories (user_id);# Signals API
POST /v1/signals:query # Query time-series metrics
GET /v1/signals/metrics # List available metrics
GET /v1/signals/quality # Quality audit report
# Sites API
GET /v1/sites # List sites
POST /v1/sites # Create site
GET /v1/sites/{site_id} # Get site details
PATCH /v1/sites/{site_id} # Update site
DELETE /v1/sites/{site_id} # Delete site
# Parcels API
GET /v1/parcels # List parcels (with bbox filter)
POST /v1/parcels # Create parcel
GET /v1/parcels/{parcel_id} # Get parcel details
GET /v1/parcels/{parcel_id}/analysis # RoofIQ, SolarFit, etc.
# Alerts API
POST /v1/alerts # Create alert
GET /v1/alerts # List alerts
GET /v1/alerts/{alert_id} # Get alert
PATCH /v1/alerts/{alert_id} # Update alert
DELETE /v1/alerts/{alert_id} # Delete alert
# Audiences API
POST /v1/audiences/export # Create export job
GET /v1/audiences/export/{job_id} # Check export status
# Earth Engine API
POST /v1/earthengine/task # Trigger analysis
GET /v1/earthengine/task/{job_id} # Check task status
# Territories API
POST /v1/territories # Create territory
GET /v1/territories # List territories
DELETE /v1/territories/{id} # Delete territory
# Provenance API
GET /v1/provenance/{metric_id} # Get metric provenance
POST /v1/provenance:batch # Batch provenance lookupopenapi: 3.0.3
info:
title: Evoteli API
version: 1.0.0
description: |
Decision-grade intelligence from computer vision, satellite imagery,
and location data.
servers:
- url: https://api.evoteli.com/v1
description: Production
- url: https://staging-api.evoteli.com/v1
description: Staging
security:
- OAuth2: [signals:read, signals:write]
components:
securitySchemes:
OAuth2:
type: oauth2
flows:
clientCredentials:
tokenUrl: https://auth.evoteli.com/oauth/token
scopes:
signals:read: Read time-series signals
signals:write: Write custom signals
alerts:read: Read alerts
alerts:write: Create and manage alerts
audiences:write: Export audiences
schemas:
Signal:
type: object
required: [ts, metric, value, unit, method, quality_score]
properties:
ts:
type: string
format: date-time
example: "2025-11-06T14:30:00Z"
metric:
type: string
example: "occupancy"
value:
type: number
format: double
example: 18.4
unit:
type: string
example: "count"
method:
type: string
enum: [edge_cv, sat_change, aerial_oblique, earth_engine, fused]
quality_score:
type: number
format: float
minimum: 0
maximum: 1
example: 0.82
provenance:
$ref: '#/components/schemas/Provenance'
paths:
/signals:query:
post:
summary: Query time-series signals
operationId: querySignals
security:
- OAuth2: [signals:read]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SignalQuery'
responses:
'200':
description: Successful query
content:
application/json:
schema:
$ref: '#/components/schemas/SignalQueryResponse'Goal: Infrastructure + basic API
Tasks:
-
Infrastructure Setup (Week 1)
- [ ] Provision cloud resources (AWS/GCP)
- [ ] Deploy ClickHouse cluster (3 nodes)
- [ ] Deploy PostGIS database
- [ ] Deploy Valkey cache
- [ ] Set up VPC and security groups
-
API Scaffolding (Week 1-2)
- [ ] Create FastAPI project structure
- [ ] Implement OAuth 2.0 with Keycloak
- [ ] Create OpenAPI spec
- [ ] Set up CI/CD pipeline (GitHub Actions)
-
Database Schemas (Week 2)
- [ ] Create ClickHouse tables and views
- [ ] Create PostGIS tables
- [ ] Write migration scripts
- [ ] Seed test data
-
Basic Endpoints (Week 3-4)
- [ ] Implement
/v1/sitesCRUD - [ ] Implement
/v1/signals:query - [ ] Add Valkey caching layer
- [ ] Write integration tests
- [ ] Implement
Deliverables:
- ✅ Working API with 2 endpoints
- ✅ Database schemas deployed
- ✅ CI/CD pipeline running
- ✅ Postman collection for testing
Goal: Real-time camera ingestion
Tasks:
-
Edge Device Setup (Week 5)
- [ ] Flash Jetson Orin with Ubuntu 22.04
- [ ] Install PP-YOLOE model
- [ ] Implement ByteTrack tracking
- [ ] Test on sample video
-
Privacy Redaction (Week 5)
- [ ] Integrate DeepPrivacy2
- [ ] Benchmark redaction speed
- [ ] Validate blur accuracy (>95%)
-
MQTT Pipeline (Week 6)
- [ ] Deploy Eclipse Mosquitto broker
- [ ] Implement MQTT publisher on Jetson
- [ ] Create MQTT → Kafka bridge
- [ ] Test end-to-end latency (<10s)
-
Stream Processing (Week 7-8)
- [ ] Deploy Kafka cluster (3 brokers)
- [ ] Implement Flink job for aggregation
- [ ] Create 5m/15m/1h rollup logic
- [ ] Insert into ClickHouse
Deliverables:
- ✅ 1 Jetson device ingesting live camera
- ✅ Occupancy and queue_len metrics in ClickHouse
- ✅ End-to-end latency <10 seconds
- ✅ Redaction working (no faces/plates)
Goal: Satellite analysis + map interface
Tasks:
-
Satellite Imagery (Week 9)
- [ ] Integrate Sentinel-2 API
- [ ] Download sample tiles (100 parcels)
- [ ] Implement roof segmentation (SAM or U-Net)
- [ ] Store results in PostGIS
-
Solar Analysis (Week 9-10)
- [ ] Integrate Google Earth Engine
- [ ] Compute insolation for parcels
- [ ] Calculate solar_score
- [ ] Store in ClickHouse
-
Map Interface (Week 10-11)
- [ ] Create React app with Vite
- [ ] Integrate MapLibre GL
- [ ] Add Protomaps tiles
- [ ] Implement parcel layer
-
Advanced Features (Week 11-12)
- [ ] Add Mapbox GL Draw (territories)
- [ ] Implement advanced search (Cmd+K)
- [ ] Create property details modal
- [ ] Add demographics overlay (Census API)
Deliverables:
- ✅ 1,000 parcels analyzed (RoofIQ + SolarFit)
- ✅ Interactive map with territory drawing
- ✅ Advanced search working
- ✅ Property details modal
Coverage Target: 80%
Framework: pytest (Python), Jest (TypeScript)
# Example: test_signals_query.py
import pytest
from app.services.signals import SignalService
def test_query_signals_with_rollup():
service = SignalService()
result = service.query(
site_id="ATL-CTF-001",
metrics=["occupancy"],
time_from="2025-11-06T00:00:00Z",
time_to="2025-11-06T01:00:00Z",
rollup="15m"
)
assert len(result.rows) == 4 # 4 x 15-min windows in 1 hour
assert result.rows[0].metric == "occupancy"
assert 0 <= result.rows[0].quality_score <= 1Framework: pytest + Docker Compose
# Example: test_edge_to_clickhouse.py
import pytest
from mqtt_publisher import publish_detection
from clickhouse_client import query_signals
@pytest.mark.integration
def test_edge_to_clickhouse_pipeline():
# Publish detection event
publish_detection(
site_id="TEST-001",
detections=[{"class": "car", "confidence": 0.9}]
)
# Wait for pipeline (max 15 seconds)
import time
time.sleep(15)
# Query ClickHouse
result = query_signals(
site_id="TEST-001",
metric="occupancy",
time_from="now() - 1 minute"
)
assert len(result) > 0
assert result[0]["value"] == 1 # 1 car detectedFramework: Playwright (frontend), pytest (API)
// Example: e2e/test_map_interface.spec.ts
import { test, expect } from '@playwright/test';
test('user can draw territory and save', async ({ page }) => {
// Navigate to map
await page.goto('http://localhost:3000');
// Wait for map to load
await page.waitForSelector('.maplibregl-canvas');
// Click polygon draw button
await page.click('[data-testid="draw-polygon-btn"]');
// Draw polygon (click 4 points)
const map = page.locator('.maplibregl-canvas');
await map.click({ position: { x: 100, y: 100 } });
await map.click({ position: { x: 200, y: 100 } });
await map.click({ position: { x: 200, y: 200 } });
await map.click({ position: { x: 100, y: 200 } });
await map.click({ position: { x: 100, y: 100 } }); // Close
// Save territory
await page.fill('[data-testid="territory-name"]', 'Atlanta North');
await page.click('[data-testid="save-territory-btn"]');
// Verify saved
await expect(page.locator('.toast-success')).toBeVisible();
});| Metric | Target | Measurement |
|---|---|---|
| Query latency (cached) | p95 < 800ms | Prometheus histogram |
| Query latency (cold) | p95 < 2.5s | Prometheus histogram |
| Throughput | 1,000 req/sec | Load test with k6 |
| Error rate | < 0.1% | Prometheus counter |
| Metric | Target | Measurement |
|---|---|---|
| Inference FPS | > 20 FPS | Jetson logs |
| Detection accuracy | > 85% AP | Manual validation |
| End-to-end latency | < 10 seconds | Kafka lag monitor |
| Metric | Target | Measurement |
|---|---|---|
| Average quality_score | ≥ 0.75 | ClickHouse query |
| Uptime | > 99% | Prometheus uptime |
| Data completeness | > 95% | Great Expectations |
- ✅ 10 pilot sites ingesting camera streams
- ✅ Occupancy accuracy within ±8% vs. manual counts
- ✅ RoofIQ geometry error <5% (n≥50 parcels)
- ✅ API p95 latency <800ms (cached)
- ✅ Average quality_score ≥0.7
- ✅ 99% uptime over last 2 weeks
- ✅ Zero critical security findings
- ✅ 2 pilot customers trained and using API
- ✅ 1 documented case study (queue management or solar leads)
- ✅ Pricing model validated ($2k-$5k/mo Starter tier)
- ✅ 100% permissive licenses (no AGPL/GPL)
- ✅ Privacy redaction >95% accuracy
- ✅ Provenance on 100% of metrics
- ✅ DPIA template completed
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Jetson supply delays | Medium | High | Order devices Week 0; fallback to CPU |
| Model accuracy below target | Low | High | Use pre-trained COCO weights; fine-tune if needed |
| ClickHouse performance issues | Low | Medium | Start with 3-node cluster; use rollups |
| Satellite imagery license delays | Medium | Medium | Start with free Sentinel-2; upgrade to commercial later |
| Privacy redaction failures | Low | High | Test with 1000+ frames; achieve >95% accuracy |
Review the full system architecture Detailed task breakdown with dependencies OpenAPI spec and endpoint documentation 100% permissively-licensed tech stack