Backend Architecture¶
The backend is a Python 3.13 API service built with FastAPI. It handles authentication, user, device, cluster, and agent management, plus Wake-on-LAN dispatch coordination.
Technology Stack¶
| Concern | Library | Version |
|---|---|---|
| Web framework | FastAPI | 0.135 |
| ASGI server | Uvicorn | 0.41 |
| ORM | SQLModel + SQLAlchemy | 0.0.37 / 2.0 |
| Database driver | psycopg2-binary | 2.9 |
| Password hashing | pwdlib (Argon2 + bcrypt) | 0.3 |
| JWT | PyJWT | 2.11 |
| OIDC client | Authlib | 1.6 |
| Settings management | pydantic-settings | 2.13 |
| HTTP client (agent dispatch) | httpx | 0.28 |
Directory Structure¶
backend/
├── main.py # FastAPI app entry, lifespan, middleware registration
├── requirements.txt # Pinned dependencies
├── pyproject.toml # Project metadata and dev dependencies (uv)
└── powerbeacon/
├── core/
│ ├── config.py # Settings class (pydantic-settings, reads .env)
│ ├── db.py # SQLAlchemy engine, init_db()
│ ├── deps.py # FastAPI Depends: SessionDep, CurrentUser, etc.
│ └── security.py # JWT creation, password hash/verify
├── models/
│ ├── users.py # User, UserPublic, UserCreate, UserRole enum
│ ├── devices.py # Device, device DTOs, and device-agent summaries
│ ├── agents.py # Agent, AgentRegistration, AgentHeartbeat, AgentStatus
│ ├── clusters.py # Cluster models and cluster detail DTOs
│ ├── links.py # DeviceAgentLink many-to-many association table
│ ├── config.py # Config DB models (OIDC settings)
│ └── generic.py # Token, TokenPayload, Message, ErrorResponse
├── routes/
│ ├── login.py # POST /api/auth/login, GET /api/auth/me, OIDC callbacks
│ ├── users.py # CRUD /api/users
│ ├── devices.py # CRUD /api/devices, POST /api/devices/{id}/wake
│ ├── agents.py # Register, heartbeat, list, delete /api/agents
│ ├── clusters.py # CRUD /api/clusters, detail, and cluster wake operations
│ ├── config.py # OIDC configuration /api/config
│ └── setup.py # First-run setup /api/setup
├── crud/
│ ├── user_crud.py # User DB operations
│ ├── agent_crud.py # Agent DB operations
│ └── config_crud.py # Config DB operations
└── services/
├── agent_service.py # Low-level agent HTTP communication
├── inventory_service.py # DTO serialization helpers for devices, agents, clusters
├── wake_service.py # Multi-agent wake orchestration for devices and clusters
└── oidc.py # Authlib OAuth client factory
Application Lifecycle¶
main.py defines a lifespan context manager that runs init_db() on startup. init_db() imports all SQLModel table classes and calls SQLModel.metadata.create_all(engine) to create tables if they are missing. This covers fresh environments without requiring a separate migration step.
Middleware registration order in main.py:
SessionMiddleware— required for Authlib/OIDC OAuth flows.CORSMiddleware— configured fromsettings.cors_origins.
All route modules are attached under a single /api prefix via APIRouter.
Configuration¶
Production Secrets
JWT_SECRETmust be a strong, random value (minimum 32 characters).- Never commit secrets to version control.
- Rotate
JWT_SECRETperiodically; existing tokens will become invalid. - Use a secrets manager (Vault, AWS Secrets Manager, etc.) in production.
Configuration is handled by a Settings class (powerbeacon/core/config.py) using pydantic-settings. Values are read from the environment or a .env file at the root of the backend directory.
| Variable | Default | Description |
|---|---|---|
DB_URL |
postgresql+psycopg2://powerbeacon:changeMe@db:5432/powerbeacon |
Database connection string |
JWT_SECRET |
(insecure default) | HMAC secret for JWT signing — must be changed in production |
JWT_EXPIRATION_HOURS |
24 |
JWT lifetime in hours |
FRONTEND_URL |
http://localhost:5173 |
Used for OIDC callback redirect |
CORS_ORIGINS |
["http://localhost:3000","http://localhost:5173"] |
Allowed CORS origins |
PASSWORD_MIN_LENGTH |
8 |
Minimum password length enforced at creation |
Data Models¶
User¶
users table
├── id UUID PK
├── username str unique, indexed
├── email str unique, nullable
├── full_name str nullable
├── hashed_password str
├── role enum: superuser | admin | user | viewer
├── is_active bool
└── created_at timestamptz
Role hierarchy governs all authorization checks in route handlers:
| Role | Capabilities |
|---|---|
superuser |
Full access including user management, settings |
admin |
Manage users, devices, agents; wake devices |
user |
Manage and wake own devices; view agents |
viewer |
Read-only: view devices |
Device¶
devices table
├── id UUID PK
├── name str
├── mac_address str unique, indexed
├── ip_address str optional, indexed
├── os_type enum: linux | windows | macos
├── is_active bool
├── description str optional
├── tags JSON (list of strings)
├── cluster_id UUID FK → clusters.id (optional)
├── owner_id UUID FK → users.id (optional)
├── created_at timestamptz
└── updated_at timestamptz
Devices are associated with agents through the device_agent_links table. This allows one device to fan out a wake request to multiple agents.
Cluster¶
clusters table
├── id UUID PK
├── name str
├── description str optional
├── tags JSON (list of strings)
├── owner_id UUID FK → users.id (optional)
├── created_at timestamptz
└── updated_at timestamptz
Clusters group devices and agents for organization and wake orchestration. Devices still control their own final list of wake agents, but those agents must belong to the same cluster as the device.
Agent¶
agents table
├── id UUID PK
├── hostname str
├── ip str indexed (IPv4/IPv6)
├── port int default 18080
├── os enum: linux | windows | darwin
├── version str
├── cluster_id UUID FK → clusters.id (optional)
├── token str indexed (bearer token)
├── status enum: online | offline
├── last_seen timestamptz
└── created_at timestamptz
The token field is a randomly generated secret issued at agent registration. The backend uses it to authenticate outbound WOL dispatch requests to the agent. Agents use the same token in the Authorization: Bearer header when sending heartbeats.
Authentication¶
Authentication Methods
PowerBeacon supports two authentication modes. Choose one based on your deployment environment.
Use case: Small teams, development, environments without SSO infrastructure.
sequenceDiagram
participant Client
participant API as /api/auth/login
participant Database
Client->>API: POST with username & password
API->>Database: Fetch user by username
Database-->>API: User record
API->>API: Verify password (Argon2)
alt Success
API->>API: Sign HS256 JWT<br/>(sub: user_id, exp: now+24h)
API-->>Client: {access_token}
else Failure
API-->>Client: 401 Unauthorized
end
Configuration:
Password Hashing:
- Primary: Argon2 (modern, resistant to GPU attacks)
- Legacy fallback: bcrypt (for imported users)
- Minimum length: 8 characters (configurable)
Use case: Enterprise deployments with single sign-on (Keycloak, Auth0, Okta, etc.).
sequenceDiagram
participant Client
participant Backend
participant Provider as OIDC Provider
Client->>Backend: GET /api/auth/login/oauth
Backend->>Provider: Redirect to authorization endpoint
Provider-->>Client: Login form
Client->>Provider: Authenticate
Provider-->>Backend: Callback with auth code
Backend->>Provider: Exchange code for tokens
Provider-->>Backend: access_token + userinfo
Backend->>Backend: Create/update local user
Backend->>Backend: Sign local JWT
Backend-->>Client: Redirect with JWT in query
Configuration:
OIDC_ISSUER_URL=https://keycloak.example.com/realms/master
OIDC_CLIENT_ID=powerbeacon
OIDC_CLIENT_SECRET=your-client-secret
Features:
- Automatic user provisioning on first login
- User attributes synced from provider (email, full name)
- Works with any OpenID Connect-compliant provider
JWT Validation (FastAPI Dependency)¶
deps.py defines get_current_user() as a FastAPI dependency. It:
- Extracts the bearer token via
OAuth2PasswordBearer. - Decodes and validates the JWT using
PyJWTwith the configured secret andHS256algorithm. - Reads the
subclaim, fetches theUserfrom the database, and confirms the user is active. - Returns the
Userobject; any route that declaresCurrentUseras a parameter receives it automatically.
Route Map¶
| Method | Path | Auth required | Description |
|---|---|---|---|
POST |
/api/auth/login |
No | Local token login |
GET |
/api/auth/me |
User | Current user info |
GET |
/api/auth/login/oauth |
No | OIDC redirect |
GET |
/api/auth/callback |
No | OIDC callback |
GET |
/api/devices/ |
User | List all devices |
GET |
/api/devices/{id} |
User | Get device |
POST |
/api/devices/ |
User (non-viewer) | Create device |
PUT |
/api/devices/{id} |
Owner, admin, or superuser | Update device |
DELETE |
/api/devices/{id} |
Owner, admin, or superuser | Delete device |
POST |
/api/devices/{id}/wake |
User | Dispatch WOL |
GET |
/api/clusters/ |
User | List clusters |
GET |
/api/clusters/{id} |
User | Get cluster details |
POST |
/api/clusters/ |
User (non-viewer) | Create cluster |
PUT |
/api/clusters/{id} |
Owner, admin, or superuser | Update cluster |
DELETE |
/api/clusters/{id} |
Owner, admin, or superuser | Delete cluster |
POST |
/api/clusters/{id}/wake |
User | Wake all devices in a cluster |
POST |
/api/agents/register |
No (agent call) | Register agent |
POST |
/api/agents/heartbeat |
Agent token | Agent heartbeat |
GET |
/api/agents/ |
User (non-viewer) | List agents |
DELETE |
/api/agents/{id} |
Admin+ | Remove agent |
GET |
/api/users/ |
Admin+ | List users |
POST |
/api/users/ |
Superuser | Create user |
PUT |
/api/users/{id} |
Superuser or self | Update user |
DELETE |
/api/users/{id} |
Superuser | Delete user |
GET |
/api/config/oidc |
Admin+ | Get OIDC config |
PUT |
/api/config/oidc |
Superuser | Update OIDC config |
GET |
/api/setup/status |
No | Setup completion status |
POST |
/api/setup/ |
No | Initial admin setup |
GET |
/health |
No | Health probe |
WOL Dispatch Flow (Backend Side)¶
flowchart TD
Start["User calls POST /api/devices/{id}/wake"]
Auth["Authenticate JWT"]
Load["Load Device from database"]
Check{"Device has<br/>associated agents?"}
NotFound["❌ No agents associated<br/>Return error"]
Found["✅ Load associated agents"]
Build["Build WOL request<br/>{mac, broadcast, port}"]
Dispatch["HTTP POST to each online agent<br/>Authorization: Bearer {token}"]
AgentCheck{"At least one agent<br/>responds 200?"}
Success["✅ Return success<br/>to client"]
Failure["❌ Log partial or total failure<br/>Return error"]
Start --> Auth
Auth --> Load
Load --> Check
Check -->|No| NotFound
Check -->|Yes| Found
Found --> Build
Build --> Dispatch
Dispatch --> AgentCheck
AgentCheck -->|Yes| Success
AgentCheck -->|No| Failure
style Start fill:#3b82f6,stroke:#1e40af,color:#fff
style Success fill:#10b981,stroke:#047857,color:#fff
style Failure fill:#ef4444,stroke:#dc2626,color:#fff
style NotFound fill:#f97316,stroke:#c2410c,color:#fff
When a client calls POST /api/devices/{id}/wake:
- Route handler loads the
Devicefrom the database. - The backend resolves every associated
Agentlinked to that device. - Online agents are dispatched in turn through
wake_service, which usesagent_service.dispatch_wol()for the actual HTTP call. AgentService.dispatch_wol()builds the JSON payload and callsPOST http://{agent.ip}:{agent.port}/wolusinghttpx, includingAuthorization: Bearer {agent.token}.- If at least one associated online agent succeeds, the request returns success to the caller.
- If no agent succeeds, or if every associated agent is offline, the route returns an error.
Database Initialization¶
init_db() uses SQLModel.metadata.create_all(engine) with pool_pre_ping=True on the engine so stale connections are detected and replaced automatically. In production, consider replacing create_all with Alembic migrations for controlled schema evolution.
Built-in API Documentation¶
When the backend is running, interactive API documentation is available at:
- Swagger UI:
http://localhost:8000/api/docs - OpenAPI spec:
http://localhost:8000/api/openapi.json