Vistralio — Architecture
Process model
Vistralio runs as a split set of systemd services behind nginx:
| Service | Unit | Port | Purpose |
|---|---|---|---|
| Connect | vistralio-connect.service |
8000 | Main REST API, auth, admin/business logic, settings, tenants, licensing runtime, API Explorer |
| Media | vistralio-media.service |
8200 | /api/streams/*, edge WebSocket, bridge tunnel lifecycle, snapshot/live proxy |
| Core | vistralio-core.service |
8300 | Built SPA shell and static frontend assets |
| Worker | vistralio-worker.service |
— | Per-camera detection loops, recording child processes, event creation, retention sweeps, frame sharing |
| License Server | vistralio-ls.service |
8100 | Standalone license server UI/API |
| MQTT | vistralio-mqtt.service |
1883/8883 | Embedded MQTT broker for notifications and Home Assistant |
nginx routes per virtual host:
saas.vistralio.co.uk (full UI + API):
/api/streams/*and/api/edge/ws→vistralio-media(:8200)/api/*→vistralio-connect(:8000)- everything else →
vistralio-core(:8300)
connect.vistralio.co.uk (primary external API entry point):
- root
/→ redirects to/api/ /api/streams/*→vistralio-media(:8200)/api/*→vistralio-connect(:8000)- everything else → 404
updates.vistralio.co.uk — update endpoints only (/api/updates/*)
license-server.vistralio.co.uk → vistralio-ls (:8100)
Core data model
Single MariaDB database (sentinel), tenant-aware. Tenant-scoped rows include:
- cameras / devices, events, schedules
- areas (sites), edge routers
- known faces / plates, event types
- activity log entries, groups
Admins can override tenant context. Standard users operate inside their current tenant only.
Devices, streams, and bridge routing
The product surface is Devices; the API namespace remains /api/cameras
for compatibility. Each device has:
- a
device_type:camera,camera_speaker, ordoorbell - a
connection_method:directorbridge - up to four path slots:
detect,live,record,talk
Direct mode — the server dials the device RTSP URL directly via FFmpeg. FFmpeg decodes H264/MJPEG, scales frames to max 1280 px wide, and outputs MJPEG frames to a pipe. The media service parses JPEG markers (FFD8/FFD9) and pushes frames to WebSocket clients.
Bridge mode — three components work together:
vistralio-workeropens two RTSP connections via the bridge tunnel: one for the recording child process, one for the detection child.- After each detection frame the worker writes the latest JPEG to
/dev/shm/vistralio/cam_{id}.jpg(atomic: write.tmpthen rename). vistralio-mediareads from that shared file for live stream WebSocket clients — zero extra RTSP connections, so the 2-connection hardware limit is never breached.
Detection pipeline
RTSP/HTTP frame ──▶ detector (YOLO / ALPR / face)
│
├──▶ Event (DB) + snapshot + clip metadata
├──▶ Notifier (MQTT / SMTP)
└──▶ /dev/shm JPEG for live view (bridge cameras)
Detection supports:
- full-frame scanning when a label has no zones
- named zones for event routing and wording
- separate masks for ignored areas
- per-object confidence overrides
- per-zone threshold and dwell time
Worker dynamic camera loading
The worker loads cameras at startup, then runs camera_watcher() which polls
the database every 30 seconds. Any newly-enabled camera is started (recording
child + detection child) without restarting the service. New cameras are live
within ~30 seconds of being saved in the UI.
Recordings and event media
The worker records segmented MP4 files (default 60 seconds per segment). It also writes event snapshots, object crops, and plate crops alongside recordings.
vistralio-media serves live stream WebSocket frames and snapshots.
recordings.py serves segment playback, exports, and event media.
Authentication and authorization
- Main API: short-lived JWTs (HS256, secret in
/etc/vistralio/vistralio.yaml) - Media URLs: separate short-lived media tokens (5-minute TTL)
- Optional 2FA (TOTP)
- Tenant switching for multi-tenant accounts
RBAC is string-based. settings.admin is a broad override.
DNS model
Two public hostnames are tracked in the settings table:
| Setting key | Default | Purpose |
|---|---|---|
dns.current |
(inferred from request Host) | Web UI hostname |
dns.api_hostname |
connect.vistralio.co.uk |
API / Connect hostname |
The API hostname controls edge router enrollment commands, the API Explorer link, and external integration docs. Changing it does not require an nginx reload — it is a pure settings-table update.
The web hostname uses a staged dry-run flow (stage → probe → promote) because
changing it requires an nginx server_name reload and TLS certificate
re-issuance. scripts/apply-dns.sh performs that reload.
Connection pool
SQLAlchemy: pool_size=20, max_overflow=40, pool_pre_ping=True,
pool_recycle=3600. Handles ~60 concurrent DB-holding requests without
queueing. The events list endpoint also uses a 2-second server-side TTL cache
(per tenant + query params) to reduce DB load under concurrent users.
Licensing
Licensing is enforced at runtime in the API and worker layers, gating: camera count, user count, tenant count, exports, branding, SMTP, face and plate recognition. Federated licensing (primary → downstream → instance) is supported.
Security posture
- Fernet-encrypted credential storage (camera passwords, SMTP, license keys)
- JWT secret in
/etc/vistralio/vistralio.yaml(never in DB) - Short-lived media tokens instead of reusing main JWT
- Tenant isolation at the SQLAlchemy query layer
- Bridge device fingerprint pinning after first enrollment
- Secret redaction in the activity log
Extension points
- Detectors in
backend/app/detectors/ - Notifier integrations in
backend/app/services/notifier.py - Edge bridge protocol in
backend/app/api/edge.py - License entitlements in
backend/app/services/licensing.py - Admin UI sections in
frontend/src/pages/Admin.jsx