Architecture
System architecture and design of BuckPow.
Overview
BuckPow follows a layered architecture with clear separation of concerns:
- API Layer — FastAPI routers handle HTTP requests
- Service Layer — Business logic and data processing
- Model Layer — SQLAlchemy ORM models
- Database Layer — SQLite, PostgreSQL, or MySQL
High-Level Architecture
graph TB
subgraph Client
ESP[ESP32/ESP8266]
Browser[Web Browser]
end
subgraph BuckPow["BuckPow Server"]
API[FastAPI Application]
Services[Service Layer]
Models[SQLAlchemy Models]
DB[(Database)]
end
subgraph Frontend
HTMX[HTMX]
Tailwind[Tailwind CSS]
ChartJS[Chart.js]
end
ESP -->|HTTP POST| API
Browser -->|HTTP GET/POST| API
API --> Services
Services --> Models
Models --> DB
Browser --> HTMX
Browser --> Tailwind
Browser --> ChartJS
Directory Structure
buckpow/
├── src/
│ ├── __init__.py # App factory, lifespan, middleware
│ ├── main.py # Entrypoint: `fastapi run src/main.py`
│ ├── config.py # Settings (pydantic-settings)
│ ├── database.py # SQLAlchemy engine, session
│ ├── models.py # Shared Pydantic / ORM bases
│ ├── router.py # Router aggregation + health
│ ├── dependencies.py # FastAPI dependencies (re-exports)
│ ├── template_helpers.py # Jinja2 rendering helpers
│ ├── version.py # App version
│ ├── auth/ # Auth domain (models, schemas, router, service, deps)
│ ├── devices/ # Device domain (models, schemas, router, service)
│ ├── sessions/ # Session domain (models, schemas, router, service)
│ ├── measurements/ # Measurement domain (models, schemas, router, service)
│ ├── projects/ # Project domain (models, schemas, router, service)
│ ├── alerts/ # Alert domain (models, schemas, router, service)
│ ├── audit/ # Audit log domain (models, schemas, router, service)
│ ├── benchmark/ # Benchmark domain (models, schemas, router, service)
│ ├── settings/ # Settings domain (schemas, router, service)
│ ├── dashboard/ # Dashboard pages, API, service
│ ├── middleware/ # ASGI middleware (rate limiter)
│ ├── utils/ # Utility functions
├── templates/ # Jinja2 templates
├── static/ # CSS, JS
├── firmware/ # Arduino sketches
├── migrations/ # Alembic migrations
├── tests/ # Pytest suite (by domain)
├── scripts/ # Utility scripts
├── tasks/ # Task management
├── nginx/ # Nginx config for Docker
├── docs/ # Documentation source (MkDocs)
├── requirements/ # pip-compatible requirements (mirror pyproject.toml)
├── pyproject.toml # Single source of truth for deps, ruff, pytest
├── uv.lock # uv lockfile (reproducible installs)
├── mkdocs.yml # MkDocs config
├── alembic.ini # Alembic config
├── Dockerfile # Container build
├── docker-compose.yml # Production stack (PostgreSQL + Nginx)
├── docker-compose.cloud.yml # Cloud deployment stack
├── run.py # Alternative entrypoint
├── .pre-commit-config.yaml # Pre-commit hooks
├── .env.example # Environment template
├── .github/ # CI/CD workflows
└── LICENSE # MIT License
Request Flow
API Request
sequenceDiagram
participant Client
participant Router
participant Auth
participant Service
participant Model
participant DB
Client->>Router: HTTP Request
Router->>Auth: Verify JWT / API Key
Auth-->>Router: User / Device
Router->>Service: Business Logic
Service->>Model: Query / Update
Model->>DB: SQL Query
DB-->>Model: Result
Model-->>Service: Object
Service-->>Router: Response Data
Router-->>Client: JSON Response
Dashboard Request
sequenceDiagram
participant Browser
participant Router
participant Auth
participant Template
participant Static
Browser->>Router: HTTP GET
Router->>Auth: Verify JWT Cookie
Auth-->>Router: User
Router->>Template: Render Jinja2
Template-->>Browser: HTML
Browser->>Static: Fetch CSS/JS
Static-->>Browser: Assets
Browser->>Browser: HTMX init
Browser->>Router: API Polling (5s)
Router-->>Browser: JSON Data
Browser->>Browser: Update Charts
Models
Entity Relationship
erDiagram
User ||--o{ Project : owns
Project ||--o{ Device : contains
Project ||--o{ Session : contains
Device ||--o{ Session : has
Device ||--o{ Measurement : produces
Device ||--o{ Alert : triggers
Session ||--o{ Measurement : contains
User {
int id PK
string name
string email UK
string password
json settings
datetime created_at
}
Project {
int id PK
string name
text description
int owner_id FK
datetime created_at
datetime updated_at
}
Device {
int id PK
string device_id UK
string alias
text description
int sampling_interval
datetime last_seen
string status
boolean enabled
string firmware_version
string api_key UK
string local_ip
int project_id FK
float high_current_threshold
float high_power_threshold
float low_voltage_threshold
datetime created_at
datetime updated_at
}
Session {
int id PK
int device_id FK
string name
string target_device
text description
string status
int project_id FK
datetime started_at
datetime ended_at
datetime created_at
datetime updated_at
}
Measurement {
int id PK
int session_id FK
int device_id FK
float bus_voltage
float shunt_voltage
float load_voltage
float current
float power
float energy
datetime created_at
}
Alert {
int id PK
int device_id FK
string level
string message
datetime created_at
datetime resolved_at
}
Key Relationships
| Relationship |
Type |
Description |
| User → Project |
One-to-Many |
User owns projects |
| Project → Device |
One-to-Many |
Project contains devices |
| Project → Session |
One-to-Many |
Project contains sessions |
| Device → Session |
One-to-Many |
Device has many sessions |
| Device → Measurement |
One-to-Many |
Device produces measurements |
| Device → Alert |
One-to-Many |
Device triggers alerts |
| Session → Measurement |
One-to-Many |
Session contains measurements |
Service Layer
Services encapsulate business logic and are separated from HTTP handlers:
| Service |
Responsibility |
UserService |
User CRUD, password hashing |
DeviceService |
Device CRUD, API key management, online status |
SessionService |
Session lifecycle (create, start, stop) |
MeasurementService |
Measurement creation, chart data, statistics |
AlertService |
Alert creation, threshold checking, resolution |
ProjectService |
Project CRUD |
DashboardService |
Dashboard aggregates, summary stats |
AuditService |
Audit log creation |
Service Pattern
All services follow the same pattern:
class DeviceService:
def __init__(self, db: Session):
self.db = db
def get_all(self):
return self.db.query(Device).all()
def get_by_id(self, device_id):
return self.db.get(Device, device_id)
def create(self, **kwargs):
device = Device(**kwargs)
self.db.add(device)
self.db.commit()
return device
def update(self, device_id, **kwargs):
device = self.db.get(Device, device_id)
for key, value in kwargs.items():
setattr(device, key, value)
self.db.commit()
return device
Authentication
JWT User Authentication
- Token: HS256 JWT with
sub (user ID) and exp (expiry)
- Transport: Bearer header or httponly cookie
- Expiry: 7 days (configurable)
- Dependencies:
get_current_user, require_user
Device API Key Authentication
- Key: 64-character hex string
- Transport: Bearer header
- Lookup: Match key to device in database
- Dependency:
get_api_key_device
Rate Limiting
| Endpoint |
Method |
Limit |
Window |
/api/v1/auth/login |
POST |
5 |
60s |
/api/v1/measurements |
POST |
60 |
60s |
/api/v1/measurements/export/csv |
GET |
10 |
60s |
/api/v1/measurements/export/xlsx |
GET |
10 |
60s |
Frontend Architecture
Server-Rendered Pages
- Engine: Jinja2 templates
- Navigation: HTMX for AJAX-driven UI updates; full page loads for navigation
- Styling: Compiled Tailwind CSS with dark mode
- Charts: Chart.js with real-time updates
HTMX Pattern
HTMX is configured once in base.html to attach the CSRF token to every request, and components update specific elements without full page loads:
<script>
htmx.on('htmx:configRequest', function(evt) {
evt.detail.headers['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
});
</script>
<!-- API calls update specific elements -->
<div hx-get="/api/v1/dashboard" hx-trigger="every 5s">
<!-- Dashboard content -->
</div>
JavaScript Modules
| File |
Purpose |
format.js |
Unit formatting (fmtCurrent, fmtPower, fmtEnergy) |
dashboard.js |
Dashboard polling, charts, session selector |
benchmark.js |
Benchmark comparison, overlay chart |
charts.js |
Chart.js factory and options |
theme.js |
Dark/light/system theme toggle |
timestamp.js |
Timezone-aware timestamp formatting |
Database
Supported Backends
| Backend |
Connection String |
Use Case |
| SQLite |
sqlite:///instance/buckpow.db |
Development, single-user |
| PostgreSQL |
postgresql://user:pass@host:5432/db |
Production, multi-user |
| MySQL |
mysql+pymysql://user:pass@host:3306/db |
Production alternative |
Migrations
BuckPow uses Alembic for database migrations:
# Create a migration
alembic revision --autogenerate -m "description"
# Apply migrations
alembic upgrade head
# Rollback
alembic downgrade -1
SQLite Auto-Setup
For SQLite, tables are created automatically on first run:
if 'sqlite' in settings.DATABASE_URL:
Base.metadata.create_all(bind=engine)
command.stamp(alembic_cfg, 'head')
Error Handling
Exception Handlers
| Handler |
Status Code |
Description |
global_exception_handler |
500 |
Catches all unhandled exceptions |
http_exception_handler |
Various |
FastAPI HTTPException |
not_found_handler |
404 |
Route not found |
method_not_allowed_handler |
405 |
Method not allowed |
{
"error": "Error message",
"code": "ERROR_CODE"
}
Middleware Stack
graph LR
Request --> CORS[CORS Middleware]
CORS --> RateLimit[Rate Limiter]
RateLimit --> Router[FastAPI Router]
| Middleware |
Purpose |
CORSMiddleware |
Cross-origin resource sharing |
RateLimiterMiddleware |
Sliding window rate limiting |
Deployment
Docker Compose Stack
graph TB
subgraph Docker
Nginx[Nginx :80]
App[BuckPow :8000]
DB[PostgreSQL :5432]
end
Nginx --> App
App --> DB
| Service |
Image |
Port |
Purpose |
nginx |
nginx:alpine |
80 |
Reverse proxy, static files |
app |
Custom build |
8000 |
BuckPow application |
db |
postgres:16-alpine |
5432 |
Database |
Scaling Considerations
- Horizontal: Run multiple
app instances behind Nginx
- Database: PostgreSQL supports concurrent connections
- SQLite: Single-writer limitation — not suitable for production scaling
Security
Authentication
- JWT tokens with configurable expiry
- API keys for device authentication
- Password hashing with bcrypt
Authorization
- Owner-based access control for devices and projects
- Project ownership checks on mutations
- Rate limiting on sensitive endpoints
Data Protection
- Secrets not logged or exposed in responses
- API keys masked in API responses (first 6 +
**** + last 4)
- CORS configured for same-origin in production
Optimizations
- Connection pooling: SQLAlchemy
pool_pre_ping
- Lazy loading: Dynamic relationships on models
- Pagination: All list endpoints support pagination
- Indexing: Indexed columns on frequently queried fields
Database Indexes
| Table |
Index |
Columns |
devices |
device_id |
Unique |
devices |
api_key |
Unique |
measurements |
device_created |
(device_id, created_at) |
measurements |
session_created |
(session_id, created_at) |
alerts |
device_id |
Standard |
audit_logs |
created_at |
Standard |
audit_logs |
action |
Standard |