Architecture Overview
This document describes the high-level architecture of Sightline.
For detailed information about specific subsystems, see Data Flow, Adapters, and Type System.
Design Principles
- Modularity - Components are self-contained and composable
- Data Agnosticism - The UI doesn't care where data comes from
- Configuration over Code - Deploy customization without code changes
- Progressive Enhancement - Works with minimal config, scales with complexity
System Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ Next.js Application │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ App Shell (page.tsx) │ │
│ │ - Composes all components │ │
│ │ - Manages UI state (sidebar open, timeline open, etc.) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ DataProvider (Context) │ │
│ │ - Fetches from adapters │ │
│ │ - Manages filter state │ │
│ │ - Provides data to all components │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ │
│ │ Map │ │ Controls │ │
│ │ Engine │ │ │ │
│ └───────────┘ └───────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Data Adapters │
├─────────────────┬─────────────────┬─────────────────────────────────────────┤
│ Supabase │ Static JSON │ REST API │
│ Adapter │ Adapter │ Adapter │
└────────┬────────┴────────┬────────┴──────────────────┬──────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Supabase │ │ JSON Files │ │ External │
│ Database │ │ /data/*.json│ │ APIs │
└─────────────┘ └─────────────┘ └─────────────┘Directory Structure
src/
├── core/ # Core platform code
│ ├── types/ # Unified type system (20+ modules)
│ │ ├── index.ts # Re-exports all types
│ │ ├── incident.ts # Core Incident interface
│ │ ├── temporal.ts # Time-related types
│ │ ├── locations.ts # Location and geography
│ │ ├── classification.ts # Hynek, Vallee classifications
│ │ ├── evidence.ts # Sensor and physical evidence
│ │ ├── objects.ts # Object characteristics
│ │ ├── movement.ts # Movement patterns
│ │ └── ... # + 12 more domain modules
│ ├── schemas/ # Zod validation schemas
│ ├── map-engine/ # Mapbox GL map component
│ ├── config.ts # Runtime configuration system
│ ├── config-context.tsx # Config provider
│ ├── data-context.tsx # React context for data/state
│ ├── theme-context.tsx # Theme provider and hooks
│ └── index.ts # Public exports
│
├── adapters/ # Data source adapters
│ ├── types.ts # DataAdapter interface
│ ├── static-adapter.ts # JSON file adapter
│ ├── supabase-adapter.ts # Supabase database adapter
│ ├── api-adapter.ts # REST API adapter
│ ├── security.ts # SSRF protection, validation
│ └── index.ts # Adapter factory
│
├── controls/ # UI control modules
│ ├── filters/ # Filter sidebar
│ ├── timeline/ # Timeline playback
│ ├── stats/ # Stats panel
│ ├── source-toggle/ # Data source toggle
│ ├── theme-toggle/ # Light/dark mode toggle
│ ├── incident-feed/ # Incident list sidebar
│ └── index.ts
│
├── examples/
│ └── renderers/ # Example custom renderers
│ ├── popups/ # Popup templates
│ ├── feed-items/ # Feed item templates
│ ├── markers/ # Marker templates
│ └── clusters/ # Cluster templates
│
├── hooks/ # Custom React hooks
├── lib/ # Utility functions
├── ui/ # Shared UI components
│
└── app/ # Next.js app router
├── layout.tsx
├── page.tsx
├── globals.css
├── docs/ # Documentation (Fumadocs)
└── api/ # API routesCore Modules
Unified Type System (core/types/)
All types are organized by domain in the types/ directory:
incident.ts- Core Incident interface and status typestemporal.ts- Date, time, duration, certainty levelslocations.ts- Coordinates, site types, location sensitivityclassification.ts- Hynek/Vallée classifications, incident typesobjects.ts- Shape, size, color, luminosity enumsmovement.ts- Speed, altitude, maneuver patternswitnesses.ts- Witness categories and credibilityevidence.ts- Sensor data and detection methodsinvestigation.ts- Investigation status and bodiesresponse.ts- Response actions and impactsources.ts- Source documentationweather.ts- Environmental conditionsaviation.ts- Aviation-specific fieldsconfig.ts- Configuration typesfilters.ts- Filter state typesrenderer.ts- Renderer prop interfaces
Configuration (core/config.ts)
Runtime configuration system:
interface MapConfig {
branding: BrandingConfig; // Name, logo, colors
dataSources: DataSourceConfig[]; // Data adapters
controls: ControlsConfig; // Which controls to show
map: MapViewConfig; // Initial map state, clustering
theme: ThemeConfig; // Light/dark mode settings
}Configuration is loaded from map.config.ts at the project root.
Data Context (core/data-context.tsx)
React context that:
- Initializes data adapters based on config
- Fetches incidents from all enabled sources
- Manages filter state
- Provides data and actions to all components
const {
incidents, // All incidents
filteredIncidents, // After filters applied
filters, // Current filter state
updateFilter, // Update a filter
dataSources, // Available sources
toggleSource, // Enable/disable a source
} = useData();Theme Context (core/theme-context.tsx)
Theme management with:
- Light/dark/system mode support
- LocalStorage persistence
- System preference detection
- Map style switching
const {
theme, // Current theme setting
resolvedTheme, // Actual theme (light or dark)
setTheme, // Set theme
toggleTheme, // Toggle between light/dark
} = useTheme();Map Engine (core/map-engine.tsx)
Mapbox GL wrapper that handles:
- Marker rendering with clustering
- Custom renderer support via render props
- Popup display
- Fly-to animations
- Pulse animations for new incidents
- Bounds fitting
- Theme-aware map styles
Data Flow
1. App loads
│
▼
2. DataProvider initializes
│
├─► Creates adapters from config
│
└─► Fetches incidents from each enabled source
│
▼
3. Incidents stored in context
│
├─► Raw incidents (all data)
│
└─► Filtered incidents (after applying filters)
│
▼
4. Components consume data
│
├─► MapEngine renders markers
│
├─► FilterSidebar shows filter options
│
├─► TimelineControl shows histogram
│
├─► IncidentFeed shows list
│
└─► StatsPanel shows countsAdapter Pattern
All data adapters implement the DataAdapter interface:
interface DataAdapter {
readonly id: string;
readonly name: string;
getIncidents(filters?: IncidentFilters): Promise<Incident[]>;
getIncidentById(id: string): Promise<Incident | null>;
getFilterOptions(): Promise<FilterOptions>;
}Creating a Custom Adapter
// src/adapters/my-adapter.ts
import type { DataAdapter, Incident, DataSourceConfig } from './types';
export class MyAdapter implements DataAdapter {
readonly id: string;
readonly name: string;
constructor(config: DataSourceConfig) {
this.id = config.id;
this.name = config.name;
}
async getIncidents(): Promise<Incident[]> {
// Fetch and transform your data
const response = await fetch('...');
const data = await response.json();
return data.map(transformToIncident);
}
// ... implement other methods
}Register in adapters/index.ts:
export function createAdapter(config: DataSourceConfig): DataAdapter {
switch (config.adapter) {
case 'my-adapter':
return new MyAdapter(config);
// ...
}
}Render Props Pattern
The MapEngine supports custom renderers via render props:
<MapEngine
incidents={incidents}
renderPopup={(props) => <CustomPopup {...props} />}
renderFeedItem={(props) => <CustomFeedItem {...props} />}
renderMarker={(props) => <CustomMarker {...props} />}
renderCluster={(props) => <CustomCluster {...props} />}
/>Clustering Modes
Three clustering modes are available:
- Mapbox Native (
mapbox) - Best performance, limited customization - React Clustering (
react) - Full customization withrenderCluster - No Clustering (
none) - Show all points individually
State Management
The application uses React Context + local state:
| State | Location | Scope |
|---|---|---|
| Incidents | DataContext | Global |
| Filters | DataContext | Global |
| Current Date | DataContext | Global |
| Selected Incident | DataContext | Global |
| Theme | ThemeContext | Global |
| Sidebar Open | page.tsx | Local |
| Timeline Open | page.tsx | Local |
| Playing | page.tsx | Local |
We intentionally avoid external state libraries to minimize dependencies.
Styling
- Tailwind CSS v4 - Utility-first styling
- CSS Variables - Theme colors defined in
globals.css - Light/Dark Theme - Full theme support
- Glassmorphism -
backdrop-blur-mdon floating panels
Security Architecture
Sightline includes comprehensive security features built into its core architecture:
Data Adapter Security
All data adapters include built-in security measures:
| Feature | Description |
|---|---|
| HTTPS Enforcement | Production requests must use HTTPS |
| SSRF Protection | Blocks requests to private IPs, localhost, and cloud metadata endpoints |
| Rate Limiting | 60 requests/minute per adapter, configurable |
| Response Size Limits | 10MB default limit prevents memory exhaustion |
| Path Traversal Prevention | Validates paths to prevent directory traversal attacks |
| Error Sanitization | Removes sensitive data (API keys, tokens, connection strings) from error messages |
HTTP Security Headers
The application configures comprehensive security headers:
// Configured in next.config.ts
{
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'Content-Security-Policy': '...',
'X-Frame-Options': 'DENY',
'X-Content-Type-Options': 'nosniff',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
'X-DNS-Prefetch-Control': 'off',
}Input Validation
- Zod Runtime Validation - Optional schema validation for incident data
- Filter Sanitization - URL parameters are sanitized and length-limited
- ID Validation - Alphanumeric validation prevents injection attacks
Error Handling
- Sanitized User Messages - Technical errors are mapped to user-friendly messages
- No Information Leakage - Stack traces and sensitive data are not exposed to users
- Structured Logging - Errors are logged with sanitized messages for debugging
Performance Considerations
- Marker Clustering - Group nearby incidents to reduce DOM nodes
- Memoization -
useMemofor filtered incidents calculation - Callback Stability -
useCallbackfor event handlers - Lazy Loading - Adapters fetch on mount, not at import
- Bounds Fitting - Debounced to avoid excessive map updates
- Adapter Caching - API and Supabase adapters cache responses (5 min default TTL)
- Context Memoization - Context values are memoized to prevent unnecessary re-renders
Extension Points
Adding a New Control
- Create component in
controls/my-control/index.tsx - Accept props from DataContext hooks
- Export from
controls/index.ts - Add to page.tsx layout
- Add config option in
core/config.ts
Adding a New Popup/Renderer
- Create in
examples/renderers/popups/my-popup.tsx - Implement component accepting
PopupRendererProps - Export from
examples/renderers/index.ts - Use via the
renderPopupprop on MapEngine
Deployment
The application is a standard Next.js app deployable to:
- Vercel (recommended) - Zero config deployment
- Netlify - With Next.js adapter
- Docker - Self-hosted option
- Node.js server -
pnpm build && pnpm start
Environment Variables Required
NEXT_PUBLIC_MAPBOX_TOKEN= # Required
NEXT_PUBLIC_SUPABASE_URL= # If using Supabase
NEXT_PUBLIC_SUPABASE_ANON_KEY= # If using SupabaseSee Environment Variables Reference for the complete list.
Related
- Data Flow — How data flows through the system
- Adapters — Data source adapter architecture
- Type System — TypeScript type definitions
- Configuration Reference — All config options
- Performance Guide — Optimization strategies
- Testing Guide — Testing architecture components