Global Project Reference Patterns¶
IMPORTANT: Check this file FIRST before implementing ANY feature. These patterns apply to ALL components.
Table of Contents¶
- Code Organization
- Error Handling
- Logging and Monitoring
- Database Patterns
- API Design
- Security
- Testing
- Documentation
- Environment Configuration
- Anti-Patterns
Code Organization¶
Directory Structure¶
Pattern:
[backend/frontend]/
├── [framework-specific structure]
├── models/ # Data models
├── services/ # Business logic
├── controllers/ # Request handlers (backend) or components (frontend)
├── utils/ # Shared utilities
├── config/ # Configuration
└── tests/ # Test files mirroring source structure
Guidelines: - Mirror test directory structure to source - One component/module per file - Group related functionality in subdirectories - Keep files under 500 lines (split if larger)
Naming Conventions¶
Files:
- kebab-case.ext for all files
- Test files: [name].test.ext or test_[name].ext
Code:
- Functions/methods: camelCase (JavaScript/TypeScript) or snake_case (Python)
- Classes: PascalCase
- Constants: UPPER_SNAKE_CASE
- Private/internal: prefix with _ (Python) or use # (JavaScript private fields)
Import Organization¶
Pattern:
# Python example
# 1. Standard library
import os
import sys
# 2. Third-party libraries
from fastapi import FastAPI
from sqlalchemy import Column
# 3. Local imports
from .models import User
from .services import UserService
// TypeScript example
// 1. React/framework imports
import { useState, useEffect } from 'react';
// 2. Third-party libraries
import axios from 'axios';
// 3. Local imports
import { UserService } from '@/services/user';
import type { User } from '@/types';
Error Handling¶
Pattern: Structured Error Responses¶
Backend:
# Standard error response format
{
"error": {
"code": "VALIDATION_ERROR",
"message": "User-friendly error message",
"details": {
"field": "email",
"issue": "Invalid email format"
},
"timestamp": "2025-01-15T12:34:56Z",
"request_id": "uuid-here"
}
}
Error Categories:
- VALIDATION_ERROR - Input validation failures
- AUTHENTICATION_ERROR - Auth failures
- AUTHORIZATION_ERROR - Permission denied
- NOT_FOUND - Resource not found
- CONFLICT - State conflict (duplicate, version mismatch)
- INTERNAL_ERROR - Server errors
Pattern: Error Logging¶
Always log: - Error type and message - Request ID for tracing - User ID (if authenticated) - Stack trace (server-side only, never to client) - Context (what operation was being attempted)
Example (using structlog):
logger.error(
"Failed to create user",
error_type="ValidationError",
user_input=sanitized_input,
request_id=request_id,
src_ip=request.client.host,
exc_info=True # Include stack trace
)
Anti-Pattern: Generic Error Messages¶
❌ Don't:
✅ Do:
raise ValidationError("Email format invalid: missing @ symbol")
return {
"error": {
"code": "VALIDATION_ERROR",
"message": "Email address is invalid",
"details": {"field": "email", "issue": "missing @ symbol"}
}
}
Logging and Monitoring¶
See:
docs/prp/logging-prp.mdfor comprehensive logging implementation patterns, code examples, and anti-patterns.
Log Levels¶
- DEBUG - Detailed development info (never in production)
- INFO - Routine operations (user login, record created)
- WARNING - Unexpected but handled (rate limit approached, deprecated feature used)
- ERROR - Operation failed but app continues
- CRITICAL - System failure, immediate attention required
Key Requirements¶
- Single-line JSON format with timestamp first
- Use
structlogwith keyword arguments (notextra={}) - Always include:
request_id,user_id,src_ip,status_code,duration_ms - Use
unbind_contextvars()in async code (notclear_contextvars()) - Never log passwords, tokens, or API keys
Database Patterns¶
Pattern: Migration Management¶
Guidelines:
- Never modify existing migrations (create new one to fix)
- Use descriptive migration names: add_user_email_verification_column
- Include both upgrade and downgrade paths
- Test migrations on copy of production data before deploying
Verification:
# ALWAYS check live database schema
./scripts/db-schema.sh [table_name]
# Compare with models before writing code
Pattern: Query Optimization¶
Guidelines: - Use indexes for frequently queried columns - Avoid N+1 queries (use joins or eager loading) - Paginate large result sets - Use database-level constraints (foreign keys, unique, not null)
Example:
# ❌ N+1 query problem
users = session.query(User).all()
for user in users:
posts = session.query(Post).filter_by(user_id=user.id).all() # N queries
# ✅ Eager loading
users = session.query(User).options(joinedload(User.posts)).all() # 1 query
Pattern: Transactions¶
Use transactions for: - Multi-step operations that must complete together - Operations that modify multiple tables - Any operation where partial completion would leave invalid state
Example:
with session.begin():
user = User(email=email)
session.add(user)
profile = Profile(user_id=user.id, name=name)
session.add(profile)
# Both commit together or both rollback
API Design¶
Pattern: RESTful Endpoints¶
Standard CRUD:
- GET /resource - List resources (paginated)
- GET /resource/{id} - Get single resource
- POST /resource - Create new resource
- PUT /resource/{id} - Update entire resource
- PATCH /resource/{id} - Partial update
- DELETE /resource/{id} - Delete resource
Pattern: Response Format¶
Success:
Error: (See Error Handling section)
Pattern: Versioning¶
Use URL versioning:
When to create new version: - Breaking changes to request/response format - Removing fields - Changing field types
Maintain compatibility: - Adding optional fields is NOT a breaking change - Deprecate old versions with 6-month notice
Security¶
Pattern: Authentication¶
Guidelines: - Use secure, httpOnly cookies for session tokens (web) - Use JWT with short expiration for API tokens - Implement refresh token rotation - Hash passwords with bcrypt/argon2 (never MD5/SHA)
Pattern: Authorization¶
Check permissions at multiple levels: 1. API endpoint (route guard) 2. Service layer (business logic) 3. Database (row-level security if supported)
Example:
@require_auth
async def update_user(user_id: int, current_user: User):
# Check ownership
if user_id != current_user.id and not current_user.is_admin:
raise AuthorizationError("Cannot modify other users")
# Proceed with update
Pattern: Input Validation¶
ALWAYS validate: - Data types - Required fields - Format (email, phone, etc.) - Length limits - Allowed values (enums)
Validate at: 1. Client-side - Better UX 2. Server-side - Security (NEVER trust client)
Anti-Pattern: SQL Injection¶
❌ Never:
✅ Always use parameterized queries:
Testing¶
Test Coverage Requirements¶
- Critical paths: 100% (authentication, payment, data integrity)
- Business logic: 90%+
- Utilities: 80%+
- UI components: 70%+
Pattern: Test Organization¶
Mirror source structure:
Pattern: Test Naming¶
Format: test_[function]_[scenario]_[expected]
Examples:
def test_create_user_valid_input_returns_user():
...
def test_create_user_duplicate_email_raises_error():
...
def test_login_invalid_password_returns_401():
...
Pattern: Test Data¶
Use fixtures for reusable test data:
@pytest.fixture
def sample_user():
return User(email="test@example.com", name="Test User")
def test_user_creation(sample_user):
assert sample_user.email == "test@example.com"
CRITICAL: See TEST_CONFIG.md before writing tests.
Documentation¶
Code Comments¶
When to comment: - Complex algorithms or business logic - Non-obvious workarounds - Security considerations - Performance optimizations
When NOT to comment: - Obvious code ("increment counter") - Restating what code does - Outdated comments (update or delete)
Docstrings¶
Required for: - All public functions/methods - All classes - All modules
Format:
def calculate_total(items: List[Item], tax_rate: float) -> Decimal:
"""
Calculate total price including tax.
Args:
items: List of items to total
tax_rate: Tax rate as decimal (0.08 for 8%)
Returns:
Total price including tax
Raises:
ValueError: If tax_rate is negative
"""
Environment Configuration¶
Pattern: Environment Variables¶
Structure:
Access:
Pattern: Configuration Validation¶
Validate at startup:
required_vars = ["DATABASE_URL", "API_KEY", "SECRET_KEY"]
missing = [var for var in required_vars if not os.getenv(var)]
if missing:
raise EnvironmentError(f"Missing required env vars: {missing}")
Anti-Patterns¶
❌ Magic Numbers¶
Don't:
Do:
❌ God Objects/Functions¶
Don't: - 500-line functions - Classes with 50+ methods - Functions doing 10 different things
Do: - Split into smaller, focused functions - Follow Single Responsibility Principle - Extract related functionality to separate classes
❌ Premature Optimization¶
Don't: - Optimize before measuring - Add complexity for hypothetical performance gains - Cache everything "just in case"
Do: - Profile to find actual bottlenecks - Optimize critical paths only - Measure impact of optimization
❌ Ignoring Errors¶
Don't:
Do:
try:
risky_operation(entity_id=entity_id)
except SpecificException as e:
logger.error(
"Operation failed",
error_type=type(e).__name__,
error_message=str(e),
entity_id=entity_id,
operation="risky_operation",
exc_info=True
)
raise # Re-raise or handle appropriately
Summary Checklist¶
Before implementing ANY feature, verify:
- [ ] Checked
global.md(this file) - [ ] Checked component-specific PRP (if exists)
- [ ] Following code organization patterns
- [ ] Error handling implemented
- [ ] Logging configured
- [ ] Database patterns followed
- [ ] API design consistent
- [ ] Security considerations addressed
- [ ] Tests planned/written
- [ ] Documentation complete
- [ ] Environment variables configured
Project-Specific Patterns¶
Edge-to-Central Authentication¶
Pattern: API Key Authentication
All edge collector communication with the central server uses API keys over TLS.
HTTP Requests (all edge-to-central communication):
# Edge collector - common headers for all requests
headers = {"X-API-Key": config.api_key}
# Detection upload (batch)
response = await client.post(
f"{central_url}/api/v1/detections/batch",
headers=headers,
json={"detections": detections}
)
# Heartbeat (also polls for pending commands)
response = await client.post(
f"{central_url}/api/v1/heartbeat",
headers=headers,
json={"uptime_sec": uptime, "queue_depth": queue.size(), ...}
)
pending_commands = response.json().get("pending_commands", [])
# Command acknowledgment
response = await client.post(
f"{central_url}/api/v1/commands/{cmd_id}/ack",
headers=headers,
json={"status": "success", "message": "Config applied"}
)
Central Server Validation:
async def verify_api_key(api_key: str = Header(alias="X-API-Key")) -> Collector:
collector = await db.get_collector_by_api_key(api_key)
if not collector or not collector.is_active:
raise HTTPException(401, "Invalid or inactive API key")
return collector
@app.post("/api/v1/detections/batch")
async def receive_detections(
detections: List[Detection],
collector: Collector = Depends(verify_api_key)
):
# collector is authenticated and identified
...
Key Generation:
import secrets
def generate_api_key() -> str:
"""Generate a secure API key for a collector."""
return f"alpr_{secrets.token_urlsafe(32)}"
Detection Schema¶
Pattern: Normalized Detection Format
All camera adapters normalize detections to this standard schema before buffering:
@dataclass
class Detection:
id: str # UUID v4
collector_id: str # Edge collector identifier
timestamp: datetime # UTC, ISO 8601
camera: CameraInfo
plate: PlateInfo
vehicle: VehicleInfo
images: ImageSet
raw: dict # Original vendor payload (optional)
@dataclass
class CameraInfo:
id: str # Local camera identifier
mac: str # Normalized MAC (lowercase, no separators)
@dataclass
class PlateInfo:
text: str # Plate number (uppercase, no spaces)
confidence: float # 0.0 - 1.0
country: str | None # ISO country code
state: str | None # State/region code
@dataclass
class VehicleInfo:
direction: str | None = None # "entering" | "exiting" | "unknown"
vehicle_type: str | None = None # "car" | "truck" | "motorcycle" | "bus" | "van"
color: str | None = None
brand: str | None = None # Vehicle make (e.g., "Toyota", "Ford")
model: str | None = None # Vehicle model (e.g., "Camry", "F-150")
@dataclass
class ImageSet:
full: bytes | None # Full scene image (JPEG)
plate_crop: bytes | None # Plate close-up (JPEG)
MAC Address Normalization:
def normalize_mac(mac: str) -> str:
"""Normalize MAC address to lowercase, no separators."""
return mac.lower().replace(":", "").replace("-", "")
Plate Number Normalization:
def normalize_plate(plate: str) -> str:
"""Normalize plate number to uppercase, no spaces."""
return plate.upper().replace(" ", "").replace("-", "")
Heartbeat & Command Polling Patterns¶
Edge sends heartbeat every 60 seconds. Central responds with any pending commands.
Heartbeat Request (Edge → Central):
POST /api/v1/heartbeat
{
"timestamp": "2025-01-15T10:30:00Z",
"uptime_sec": 86400,
"queue_depth": 0,
"cameras_online": 2,
"disk_usage_percent": 15.5,
"config_version": "v3"
}
Heartbeat Response (Central → Edge):
{
"status": "ok",
"pending_commands": [
{
"id": "cmd-uuid-123",
"cmd": "config_update",
"timestamp": "2025-01-15T10:30:00Z",
"payload": { "cameras": [...] }
}
]
}
Command Types:
- config_update - Update collector configuration (cameras, upload settings)
- reboot - Reboot the Raspberry Pi (with optional delay)
- restart_service - Restart the collector service
- force_upload - Immediately upload all buffered events
Command Acknowledgment (Edge → Central):
POST /api/v1/commands/{cmd_id}/ack
{
"status": "success", # or "failed"
"message": "Config applied successfully"
}
Edge Implementation:
async def heartbeat_loop():
"""Send heartbeat every 60 seconds, process any pending commands."""
while True:
try:
response = await client.post(
f"{central_url}/api/v1/heartbeat",
headers={"X-API-Key": config.api_key},
json=get_health_status()
)
for cmd in response.json().get("pending_commands", []):
try:
await handle_command(cmd)
await send_ack(cmd["id"], "success")
except Exception as e:
logger.error(
"Command execution failed",
command_id=cmd["id"],
command_type=cmd.get("cmd"),
error_type=type(e).__name__,
error_message=str(e),
exc_info=True
)
await send_ack(cmd["id"], "failed", str(e))
except ConnectionError as e:
logger.warning(
"Heartbeat failed, will retry",
error_type=type(e).__name__,
error_message=str(e),
retry_interval_sec=60,
central_url=central_url
)
await asyncio.sleep(60)
Central Health Monitoring:
-- Collector status based on last heartbeat
SELECT
id, name, last_heartbeat,
CASE
WHEN last_heartbeat > NOW() - INTERVAL '2 minutes' THEN 'online'
WHEN last_heartbeat > NOW() - INTERVAL '10 minutes' THEN 'degraded'
ELSE 'offline'
END as status
FROM collectors;
Alerting Patterns¶
Pattern: Watchlist Matching
async def check_watchlist(plate_number: str) -> List[Alert]:
"""Check if plate matches any watchlist entries."""
normalized = normalize_plate(plate_number)
# Exact match
matches = await db.query(
Watchlist.plate_number == normalized
).all()
# Partial match (if enabled)
if settings.enable_partial_matching:
partial = await db.query(
Watchlist.plate_number.contains(normalized) |
literal(normalized).contains(Watchlist.plate_number)
).all()
matches.extend(partial)
return [create_alert(m) for m in matches]
Pattern: User Alert Subscriptions
async def notify_subscribers(alert: Alert):
"""Send alert to all subscribed users."""
subscribers = await db.query(
AlertSubscription.watchlist_id == alert.watchlist_id,
AlertSubscription.is_active == True
).all()
for sub in subscribers:
# WebSocket push
await websocket_manager.send_to_user(
sub.user_id,
{"type": "alert", "data": alert.to_dict()}
)
# Future: Push notification to mobile
Mobile-Responsive UI Patterns¶
Pattern: Tailwind Breakpoints
<!-- Mobile-first design -->
<div class="
flex flex-col <!-- Mobile: stack vertically -->
md:flex-row <!-- Tablet+: horizontal -->
lg:gap-8 <!-- Desktop: more spacing -->
">
<div class="w-full md:w-1/2 lg:w-1/3">...</div>
<div class="w-full md:w-1/2 lg:w-2/3">...</div>
</div>
<!-- Touch-friendly targets -->
<button class="
p-4 min-h-[44px] <!-- 44px minimum for touch -->
text-base <!-- Readable on mobile -->
md:p-2 md:text-sm <!-- Tighter on desktop -->
">
Click Me
</button>
Pattern: Responsive Tables
<!-- Card view on mobile, table on desktop -->
<div class="hidden md:block">
<table>...</table>
</div>
<div class="md:hidden">
<div class="card">...</div>
<div class="card">...</div>
</div>
Failure Handling Patterns¶
Edge Collector Failure Modes:
| Scenario | Behavior |
|---|---|
| Camera offline | Adapter marks degraded, continues polling, auto-reconnects |
| Central server unreachable | Buffer detections locally (up to 7 days), retry with backoff |
| Heartbeat fails | Log warning, retry on next 60-second interval |
| Pi crash/reboot | Systemd auto-restart, SQLite buffer survives reboot |
| Config error | Reject invalid config, keep running with previous, send failed ack |
Pattern: Exponential Backoff
async def upload_with_retry(detections: List[Detection], max_retries: int = 5):
"""Upload detections with exponential backoff."""
for attempt in range(max_retries):
try:
await upload_detections(detections)
return
except ConnectionError as e:
wait_time = min(300, 2 ** attempt * 30) # 30s, 60s, 120s, 240s, 300s
logger.warning(
"Upload failed, retrying",
retry_attempt=attempt + 1,
max_retries=max_retries,
retry_wait_sec=wait_time,
detections_queued=len(detections),
error_type=type(e).__name__,
error_message=str(e)
)
await asyncio.sleep(wait_time)
logger.error(
"Max retries exceeded, detections remain in buffer",
max_retries=max_retries,
detections_in_buffer=len(detections)
)
Last Updated: 2025-12-29 Maintained By: Development Team