Database Architecture¶
Estate Sentry uses a multi-database architecture optimized for different workloads:
- PostgreSQL - Relational data and time-series (via TimescaleDB)
- Neo4j - Graph relationships for threat intelligence
- ChromaDB - Vector embeddings for AI analysis
- MinIO - Object storage for media files
PostgreSQL (Relational Database)¶
Overview¶
PostgreSQL handles all relational data including users, sensors, readings, and alerts.
Production Setup: - PostgreSQL 16 (Docker) - Automatic backups - Connection pooling
Development Setup: - SQLite (default) - Automatic migrations - Easy to reset
Configuration¶
Set via environment variable:
# SQLite (local development)
DATABASE_ENGINE=sqlite
# PostgreSQL (Docker/production)
DATABASE_ENGINE=postgresql
POSTGRES_DB=estate_sentry
POSTGRES_USER=estate_sentry
POSTGRES_PASSWORD=your-password
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
Data Models¶
User Model¶
Extends Django's AbstractUser:
class User(AbstractUser):
auth_method = CharField(max_length=20) # password/pin/username
pin = CharField(max_length=4, null=True)
# Inherits: username, email, password, etc.
Sensor Model¶
class Sensor(Model):
owner = ForeignKey(User)
name = CharField(max_length=255)
sensor_type = CharField(max_length=50) # CAMERA, DOOR_CONTACT, etc.
location = CharField(max_length=255)
status = CharField(max_length=20) # ACTIVE, INACTIVE, ERROR, MAINTENANCE
handler_class = CharField(max_length=100)
connection_config = JSONField()
metadata = JSONField()
created_at = DateTimeField(auto_now_add=True)
updated_at = DateTimeField(auto_now=True)
Sensor Types:
- CAMERA
- DOOR_CONTACT
- WINDOW_CONTACT
- GLASS_BREAK
- MOTION
- SMOKE
- CO
- WATER_LEAK
- CUSTOM
SensorReading Model¶
Time-series data storage:
class SensorReading(Model):
sensor = ForeignKey(Sensor)
timestamp = DateTimeField(auto_now_add=True)
value = JSONField() # Flexible sensor data
reading_type = CharField(max_length=50)
processed = BooleanField(default=False)
Example Values:
// Door contact
{"state": "open"}
// Camera
{"image_url": "/media/cameras/front.jpg", "motion_detected": true}
// Temperature
{"temperature": 72.5, "unit": "F"}
Alert Model¶
class Alert(Model):
owner = ForeignKey(User)
alert_type = CharField(max_length=50)
severity = CharField(max_length=20) # INFO, LOW, MEDIUM, HIGH, CRITICAL
sensor = ForeignKey(Sensor)
timestamp = DateTimeField(auto_now_add=True)
description = TextField()
acknowledged = BooleanField(default=False)
acknowledged_at = DateTimeField(null=True)
metadata = JSONField()
Alert Types:
- INTRUSION - Unauthorized entry
- ENVIRONMENTAL - Smoke, CO, water
- MALFUNCTION - Sensor errors
- SYSTEM - System issues
- CUSTOM - User-defined
Indexing Strategy¶
Key indexes for performance:
# Sensors
indexes = [
Index(fields=['owner', 'status']),
Index(fields=['sensor_type']),
]
# SensorReadings
indexes = [
Index(fields=['sensor', '-timestamp']),
Index(fields=['processed']),
]
# Alerts
indexes = [
Index(fields=['owner', '-timestamp']),
Index(fields=['acknowledged', 'severity']),
]
Migrations¶
Django handles schema migrations:
# Create migrations
python manage.py makemigrations
# Apply migrations
python manage.py migrate
# Show migration status
python manage.py showmigrations
Backup & Restore¶
SQLite (Development):
task db:backup # Backup to timestamped file
task db:restore # Restore from latest backup
PostgreSQL (Docker):
task db:postgres:backup # pg_dump to data/backups/
task db:postgres:restore # Restore from latest
task db:reset:docker # Reset database (⚠️)
Neo4j (Graph Database)¶
Overview¶
Neo4j stores relationship data and powers advanced threat intelligence.
Features:
- APOC plugins
- Graph Data Science
- Real-time sync to ./data/neo4j/
Access: - Browser: http://localhost:7474 - Bolt: bolt://localhost:7687 - User: neo4j / changeme
Configuration¶
NEO4J_URI=bolt://localhost:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=changeme
Data Model¶
Neo4j stores relationships:
Nodes:
- (:User) - User accounts
- (:Sensor) - Sensors
- (:Alert) - Security alerts
- (:Location) - Physical locations
- (:ThreatPattern) - Known threat patterns
Relationships:
(:User)-[:OWNS]->(:Sensor)
(:Sensor)-[:LOCATED_IN]->(:Location)
(:Sensor)-[:GENERATED]->(:Alert)
(:Alert)-[:SIMILAR_TO]->(:ThreatPattern)
(:Sensor)-[:CONNECTED_TO]->(:Sensor)
Example Queries¶
Find all sensors in a location:
MATCH (u:User {username: 'john'})-[:OWNS]->(s:Sensor)-[:LOCATED_IN]->(l:Location {name: 'Living Room'})
RETURN s
Find related alerts:
MATCH (a:Alert)-[:SIMILAR_TO]->(pattern:ThreatPattern)
WHERE a.timestamp > datetime() - duration('P7D')
RETURN pattern.name, count(a) as occurrences
ORDER BY occurrences DESC
Analyze sensor network:
MATCH path = (s1:Sensor)-[:CONNECTED_TO*]-(s2:Sensor)
WHERE s1.status = 'ACTIVE' AND s2.status = 'ACTIVE'
RETURN path
Data Synchronization¶
Neo4j data is automatically synchronized:
data/neo4j/
├── data/ # Database files (auto-generated)
├── logs/ # Server logs
├── import/ # Files for LOAD CSV
├── export/ # Exported backups
└── plugins/ # APOC, GDS plugins
Commands:
task db:neo4j:sync # Show sync status
task db:neo4j:export # Export to Cypher script
task db:neo4j:import # Import from latest export
task db:neo4j:clear # Clear all data (⚠️)
Backup & Restore¶
# Export database
task db:neo4j:export
# Files saved to: data/neo4j/export/backup_TIMESTAMP.cypher
# Import from latest
task db:neo4j:import
Graph Data Science¶
Neo4j includes Graph Data Science (GDS) for analytics:
Community Detection:
// Find sensor clusters
CALL gds.louvain.stream('sensor-graph')
YIELD nodeId, communityId
RETURN gds.util.asNode(nodeId).name, communityId
Pathfinding:
// Find shortest path between sensors
MATCH (s1:Sensor {name: 'Front Door'}), (s2:Sensor {name: 'Back Door'})
CALL gds.shortestPath.dijkstra.stream('sensor-graph', {
sourceNode: s1,
targetNode: s2
})
YIELD path
RETURN path
Centrality Analysis:
// Find most connected sensors
CALL gds.degree.stream('sensor-graph')
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).name, score
ORDER BY score DESC
LIMIT 10
Database Strategy¶
When to Use PostgreSQL¶
- User authentication
- Sensor configurations
- Time-series sensor readings
- Alert records
- Any tabular data
When to Use Neo4j¶
- Sensor relationships
- Location hierarchies
- Threat pattern matching
- Behavioral analysis
- Network topology
Data Consistency¶
Both databases stay synchronized:
- Django ORM updates PostgreSQL
- Signals trigger Neo4j updates
- Neo4j stores relationships only
- PostgreSQL is source of truth
Performance Optimization¶
PostgreSQL¶
- Indexes on foreign keys
- Indexes on timestamp fields
- Connection pooling (production)
- Query optimization with
select_related()
Neo4j¶
- Property indexes on IDs
- Composite indexes for common queries
- Memory configuration (1GB page cache, 2GB heap)
- APOC for batch operations
Monitoring¶
PostgreSQL¶
# Docker logs
task docker:logs:postgres
# Database size
docker-compose exec postgres psql -U estate_sentry -d estate_sentry -c "\l+"
# Table sizes
docker-compose exec postgres psql -U estate_sentry -d estate_sentry -c "\dt+"
Neo4j¶
# Docker logs
task docker:logs:neo4j
# Database info
task docker:shell:neo4j
CALL dbms.components() YIELD name, versions, edition
TimescaleDB (Time-Series Extension)¶
TimescaleDB extends PostgreSQL for efficient time-series data storage.
Configuration¶
# docker-compose.yml
services:
postgres:
image: timescale/timescaledb:latest-pg16
environment:
POSTGRES_DB: estate_sentry
Hypertable for Sensor Readings¶
-- Enable TimescaleDB
CREATE EXTENSION IF NOT EXISTS timescaledb;
-- Convert sensor_readings to hypertable
SELECT create_hypertable('sensor_readings', 'timestamp',
chunk_time_interval => INTERVAL '1 day',
if_not_exists => TRUE
);
-- Add compression (after 7 days)
SELECT add_compression_policy('sensor_readings',
compress_after => INTERVAL '7 days'
);
-- Add retention (optional)
SELECT add_retention_policy('sensor_readings',
drop_after => INTERVAL '1 year'
);
Optimized Queries¶
-- Last hour of readings for a sensor (uses chunk exclusion)
SELECT * FROM sensor_readings
WHERE sensor_id = 'uuid'
AND timestamp > NOW() - INTERVAL '1 hour';
-- Aggregated readings by minute
SELECT time_bucket('1 minute', timestamp) as bucket,
sensor_id,
avg((value->>'temperature')::float) as avg_temp
FROM sensor_readings
WHERE timestamp > NOW() - INTERVAL '24 hours'
GROUP BY bucket, sensor_id;
ChromaDB (Vector Database)¶
ChromaDB stores embeddings for the Sentry Intelligence system.
Collections¶
| Collection | Purpose | Data |
|---|---|---|
alerts |
Alert similarity search | Alert embeddings + metadata |
patterns |
Known threat patterns | Pattern embeddings |
incidents |
Historical incidents | Incident embeddings |
Configuration¶
# sentry/intelligence/vector_store.py
import chromadb
from chromadb.config import Settings
client = chromadb.Client(Settings(
chroma_db_impl="duckdb+parquet",
persist_directory="./data/chromadb"
))
Example Queries¶
# Search for similar alerts
results = alerts_collection.query(
query_embeddings=[alert_embedding],
n_results=5,
where={"severity": {"$in": ["HIGH", "CRITICAL"]}}
)
# Search threat patterns
results = patterns_collection.query(
query_texts=["multiple door sensors triggering in sequence"],
n_results=10
)
Event Log (Synchronization)¶
For multi-node deployments, an event log tracks all changes for synchronization.
Event Log Model¶
class EventLog(models.Model):
"""Append-only event log for node synchronization."""
node_id = models.UUIDField(db_index=True)
event_type = models.CharField(max_length=100)
entity_type = models.CharField(max_length=100)
entity_id = models.UUIDField()
payload = models.JSONField()
vector_clock = models.JSONField()
timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
synced_to = models.JSONField(default=list)
class Meta:
indexes = [
models.Index(fields=['timestamp', 'node_id']),
models.Index(fields=['entity_type', 'entity_id']),
]
Sync Queries¶
# Get events not yet synced to a node
EventLog.objects.exclude(
synced_to__contains=[target_node_id]
).filter(
timestamp__gt=last_sync_time
).order_by('timestamp')
Audit Log¶
Immutable audit trail for security compliance.
Audit Log Model¶
class AuditLog(models.Model):
timestamp = models.DateTimeField(auto_now_add=True)
event_type = models.CharField(max_length=50)
user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
ip_address = models.GenericIPAddressField(null=True)
resource_type = models.CharField(max_length=100)
resource_id = models.CharField(max_length=100, null=True)
old_value = models.JSONField(null=True)
new_value = models.JSONField(null=True)
metadata = models.JSONField(default=dict)
Retention¶
# Audit logs retained for 7 years (compliance)
AUDIT_RETENTION_DAYS = 365 * 7
MinIO (Object Storage)¶
S3-compatible storage for media files.
Bucket Structure¶
estate-sentry/
├── frames/ # Camera frames
│ └── {camera_id}/{date}/{timestamp}.jpg
├── recordings/ # Video recordings
│ └── {camera_id}/{date}/{start}_{end}.mp4
├── snapshots/ # Alert snapshots
│ └── {alert_id}/frame.jpg
└── exports/ # Report exports
└── {report_id}/report.pdf
Configuration¶
# settings.py
MINIO_ENDPOINT = os.environ.get('MINIO_ENDPOINT', 'localhost:9000')
MINIO_ACCESS_KEY = os.environ.get('MINIO_ACCESS_KEY')
MINIO_SECRET_KEY = os.environ.get('MINIO_SECRET_KEY')
MINIO_BUCKET = 'estate-sentry'
Future Enhancements¶
- Redis - Caching layer for frequent queries
- Elasticsearch - Full-text search for alerts and logs
- ClickHouse - Alternative for high-volume analytics