# GigLez Architecture Decisions (Based on Wigle Analysis) ## Overview This document captures key architectural decisions for GigLez, informed by analysis of the Wigle Android wardriving client. **Date**: 2026-01-12 **Status**: Proposed **Source**: Wigle Analysis (/docs/wigle_analysis.md) --- ## Decision 1: Database Schema - PostgreSQL with PostGIS **Context**: Need to store millions of RF captures with GPS coordinates for geospatial queries. **Decision**: Use PostgreSQL + PostGIS with a 3-table structure: - `captures` - Primary RF file data (like Wigle's `network` table) - `capture_matches` - Device identification results (like Wigle's `location` table) - `devices` - Known device signature database (reference data) **Rationale**: - Wigle's 3-table design (network/location/route) has scaled to **349M WiFi networks** and **billions of observations** - PostGIS provides efficient geospatial indexing for radius/bounding box queries - Separate matches table allows many-to-many relationships (one capture → multiple device matches) **Alternatives Considered**: - MongoDB: No ACID guarantees, weaker geospatial query performance - Single table: Would duplicate device info, slower queries **Implementation**: ```sql CREATE TABLE captures ( file_hash text primary key, latitude double precision not null, longitude double precision not null, geom geometry(Point, 4326), -- PostGIS spatial index timestamp bigint not null, frequency integer not null, protocol text, session_id text ); CREATE INDEX idx_captures_geom ON captures USING GIST(geom); ``` --- ## Decision 2: File Deduplication - SHA256 Hash as Primary Key **Context**: Prevent duplicate .sub file submissions (same file uploaded multiple times). **Decision**: Use SHA256 hash of file contents as primary key in `captures` table. **Rationale**: - Wigle uses BSSID (MAC address) as primary key for natural deduplication - SHA256 provides **cryptographically strong** uniqueness guarantee - Automatic deduplication at database level (INSERT fails on duplicate) - No need for separate deduplication logic **Alternatives Considered**: - Auto-increment ID + separate hash check: More complex, requires application-level logic - GPS + timestamp composite key: Could have legitimate duplicates **Implementation**: ```python import hashlib def compute_file_hash(file_path: str) -> str: sha256 = hashlib.sha256() with open(file_path, 'rb') as f: for chunk in iter(lambda: f.read(8192), b''): sha256.update(chunk) return sha256.hexdigest() ``` --- ## Decision 3: Upload Format - JSON Manifest + Binary Files **Context**: Need to upload .sub files with GPS metadata to server. **Decision**: Use multipart/form-data with: - JSON manifest (GPS coordinates, metadata) - Binary .sub files (unmodified) **Rationale**: - Wigle uses CSV for structured data export - works for text but not binary files - JSON provides structured metadata (per-file GPS, session info) - Binary files preserve exact capture data (no encoding issues) - Supports batch uploads naturally (multiple files in one request) **Alternatives Considered**: - CSV with base64-encoded files: Bloated (33% overhead), parsing complexity - Separate API calls per file: Network inefficiency, no atomic batch **Implementation**: ```json { "version": "GigLez-1.0", "session_id": "uuid", "captures": [ { "file": "capture_001.sub", "sha256": "abc123...", "gps": { "latitude": 40.7128, "longitude": -74.0060, "accuracy": 5.0, "timestamp": "2025-01-11T20:30:00Z" } } ] } ``` --- ## Decision 4: Background Processing - Async Task Queue **Context**: .sub file parsing and device matching are CPU-intensive operations. **Decision**: Use async background task queue (FastAPI BackgroundTasks or Celery/RQ) for: - File parsing - Device signature matching - Database writes **Rationale**: - Wigle uses **background thread** for all database writes (THREAD_PRIORITY_BACKGROUND) - Prevents API timeouts on large uploads - Enables **immediate response** to user (202 Accepted) - Allows **horizontal scaling** of workers **Alternatives Considered**: - Synchronous processing: Slow, API timeouts on large files - Lambda functions: Cold start overhead, cost at scale **Implementation**: ```python from fastapi import BackgroundTasks @app.post("/api/submit") async def submit_capture(file: UploadFile, background_tasks: BackgroundTasks): file_path = await save_upload(file) task_id = uuid4() background_tasks.add_task( process_capture_pipeline, file_path=file_path, task_id=task_id ) return {"status": "accepted", "task_id": str(task_id)} ``` --- ## Decision 5: GPS Validation - Multi-Level Checks **Context**: GPS coordinates can be invalid (null island, extreme accuracy, out of bounds). **Decision**: Implement Wigle's multi-level GPS validation: 1. Bounds check (-90≤lat≤90, -180≤lon≤180) 2. Accuracy threshold (reject if >100m) 3. Null island check (reject lat=0, lon=0) 4. Timestamp validation (reject if 0) **Rationale**: - Wigle's horribleGps() function filters out **GPS noise** from hardware bugs - Prevents database pollution with junk coordinates - 100m threshold balances accuracy vs. rejection rate **Alternatives Considered**: - Accept all GPS: Database pollution, poor map quality - Strict threshold (<10m): Too restrictive, rejects valid mobile captures **Implementation**: ```python def validate_gps(lat, lon, accuracy=None, timestamp=None): if not (-90 <= lat <= 90) or not (-180 <= lon <= 180): raise ValueError("GPS out of bounds") if accuracy and accuracy > 100: raise ValueError(f"GPS accuracy too low: {accuracy}m") if lat == 0.0 and lon == 0.0: raise ValueError("GPS at null island") if timestamp == 0: raise ValueError("Invalid timestamp") ``` --- ## Decision 6: Upload Marker System - Incremental Sync **Context**: Users may upload same database multiple times (after adding new captures). **Decision**: Track last uploaded capture ID per user (like Wigle's PREF_DB_MARKER). **Rationale**: - Wigle's marker system enables **incremental sync** (only new data) - Prevents re-uploading entire database on each sync - Enables **resume after failure** (upload only remaining captures) **Alternatives Considered**: - Always upload all captures: Wasteful bandwidth, slow - Client-side tracking only: Lost on app reinstall **Implementation**: ```python class UploadMarker(Base): __tablename__ = 'upload_markers' user_id = Column(String, primary_key=True) last_uploaded_id = Column(Integer, default=0) last_upload_time = Column(BigInteger) def get_unuploaded_captures(user_id): marker = db.query(UploadMarker).filter_by(user_id=user_id).first() last_id = marker.last_uploaded_id if marker else 0 return db.query(Capture).filter( Capture.id > last_id, Capture.uploaded == False ).all() ``` --- ## Decision 7: Session Management - Run ID System **Context**: Group captures by wardriving session for batch operations and statistics. **Decision**: Add `session_id` to captures table (like Wigle's `run_id`). **Rationale**: - Wigle's run_id groups **route points** by wardriving session - Enables batch operations (upload session, view session stats) - Useful for statistics (captures per session, coverage area) - Supports partial uploads (session-by-session) **Alternatives Considered**: - No grouping: Hard to track wardriving runs, no batch operations - Timestamp-based grouping: Unreliable (gaps in time) **Implementation**: ```sql CREATE TABLE sessions ( session_id text primary key, user_id text not null, start_time bigint not null, end_time bigint, capture_count integer default 0, uploaded boolean default false ); ALTER TABLE captures ADD COLUMN session_id text REFERENCES sessions(session_id); CREATE INDEX idx_captures_session ON captures(session_id); ``` --- ## Decision 8: Performance - Transaction Batching **Context**: Writing captures one-at-a-time is slow (disk I/O overhead). **Decision**: Batch database writes in transactions (up to 512 operations). **Rationale**: - Wigle batches up to **512 DB updates** per transaction - Reduces disk I/O by 100x+ (one fsync per batch vs. per operation) - Critical for write-heavy workloads (wardriving generates 1000s of observations) **Alternatives Considered**: - Individual transactions: 100x slower, disk thrashing - Larger batches (>1000): Increased memory, longer transaction locks **Implementation**: ```python from sqlalchemy.orm import Session def batch_insert_captures(captures: List[Capture], batch_size=512): session = Session() for i in range(0, len(captures), batch_size): batch = captures[i:i+batch_size] session.bulk_insert_mappings(Capture, batch) session.commit() # One commit per batch ``` --- ## Decision 9: Caching - LRU Cache for Device Lookups **Context**: Device signature matching queries database frequently for same devices. **Decision**: Add LRU cache (64-256 entries) for device lookups. **Rationale**: - Wigle uses **64-entry LRU cache** for network lookups - Reduces database queries by 90%+ (most captures match known devices) - Small memory footprint (few KB per entry) **Alternatives Considered**: - No cache: Database bottleneck, slow matching - Redis cache: Overkill for simple lookups, adds network latency **Implementation**: ```python from functools import lru_cache @lru_cache(maxsize=256) def get_device_by_id(device_id: int) -> Device: return db.query(Device).filter_by(device_id=device_id).first() ``` --- ## Decision 10: API Authentication - JWT + API Keys **Context**: Need to authenticate users for uploads and API access. **Decision**: Support both JWT tokens (web) and API keys (scripts/tools). **Rationale**: - Wigle uses **basic auth with API tokens** (retrieved via username/password) - JWT for web sessions (short-lived, stateless) - API keys for CLI tools (long-lived, revocable) - Flexible for different use cases **Alternatives Considered**: - Username/password only: Insecure for API, no token revocation - OAuth only: Overkill for simple use cases **Implementation**: ```python from fastapi import Depends, HTTPException from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials security = HTTPBearer() def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)): token = credentials.credentials # Try JWT first try: payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) return payload["user_id"] except: pass # Try API key user = db.query(User).filter_by(api_key=token).first() if user: return user.user_id raise HTTPException(status_code=401, detail="Invalid authentication") ``` --- ## Summary Table | Decision | Wigle Pattern | GigLez Adaptation | Rationale | |----------|---------------|-------------------|-----------| | Database | SQLite 3-table | PostgreSQL + PostGIS 3-table | Geospatial queries, ACID | | Deduplication | BSSID primary key | SHA256 hash primary key | File-based, not MAC-based | | Upload Format | CSV | JSON + binary | Binary file support | | Processing | Background thread | Async task queue | Horizontal scaling | | GPS Validation | Multi-level checks | Same checks | Proven filtering | | Upload Marker | PREF_DB_MARKER | user_id → last_uploaded_id | Incremental sync | | Session Tracking | run_id | session_id | Batch operations | | Performance | Transaction batching | Same (512 ops) | Write optimization | | Caching | 64-entry LRU | 256-entry LRU | Larger device DB | | Auth | API tokens | JWT + API keys | Flexibility | --- ## Next Steps 1. **Implement PostgreSQL schema** (captures, devices, capture_matches, sessions) 2. **Build .sub parser** with GPS validation 3. **Create upload API** with background processing 4. **Add device matching engine** (signature database) 5. **Implement upload markers** for incremental sync 6. **Add caching layer** (LRU for devices, Redis for API responses) 7. **Build web interface** (map visualization, search, upload form) --- **Document Version**: 1.0 **Last Updated**: 2026-01-12 **References**: /docs/wigle_analysis.md