A modern, fast, type-safe REST API for ZoneMinder
Rebuilding ZoneMinder's aging Perl/PHP API surface as a single, well-tested Rust service β with live streaming, fine-grained access control, and OpenAPI docs baked in.
ZoneMinder is a rock-solid surveillance platform, but its API grew organically across Perl, PHP, and CGI over two decades. zm_api replaces that surface with one cohesive service:
- π¦ One binary, one language β no PHP-FPM, no CGI, no Perl runtime to babysit.
- β‘ Fast & async β built on Axum + Tokio; streaming endpoints don't block the API.
- π Secure by default β JWT auth, per-feature RBAC, and row-level monitor ACLs.
- π Self-documenting β every endpoint is in an auto-generated OpenAPI spec + Swagger UI.
- π§ͺ Actually tested β ~600 unit + integration tests, with a coverage gate in CI.
- ποΈ Drop-in schema β talks directly to an existing ZoneMinder MySQL/MariaDB database.
Full CRUD for monitors, events, frames, zones, and event metadata β paginated, filterable, and searchable. Monitor state & alarm control. Per-monitor snapshots straight from the live stream.
Three delivery paths from one API:
- HLS β fragmented-MP4 playlists for any HTML5
<video>element. - MSE β low-latency fMP4 pushed over a WebSocket.
- WebRTC β native peer connection with ICE/SDP signaling.
Plus recorded-event playback (video, byte-range seeking, thumbnails).
Pan/tilt/zoom with native protocol drivers and a Perl-bridge fallback β ONVIF and vendor protocols, presets, and continuous control.
- JWT access + refresh tokens.
- Feature RBAC β ZoneMinder's 8 permission columns (
Stream,Events,Control,Monitors,Groups,Devices,Snapshots,System) enforced on every route. - Row-level ACLs β
Monitors_Permissions/Groups_Permissionsresolved per request, so a user only ever sees the monitors they're granted (default-allow, fully backward compatible).
Daemon supervision (zmc/zma) with a Unix-socket IPC shim for legacy zmdc.pl compatibility,
storage & server management, configs, logs, montage layouts, and system control β
all behind graceful SIGTERM/SIGINT shutdown.
TLS with optional ACME/Let's Encrypt, security headers, gzip/brotli compression (streaming routes excluded), request-body limits, and opt-in per-IP rate limiting.
A clean, one-way layered flow β handlers never touch the database directly.
flowchart TD
Client["π Client / Browser / ZM UI"] -->|HTTPS Β· JWT| Stack
subgraph Stack["Middleware stack"]
direction LR
T[trace] --> C[compression] --> H[security headers] --> R[rate limit] --> O[CORS]
end
Stack --> Auth{"auth Β· RBAC Β· monitor ACL"}
Auth -->|allowed| Handlers["π₯ Handlers β extraction & validation"]
Auth -->|denied| Reject["401 / 403 / 404"]
Handlers --> Services["βοΈ Services β business logic"]
Services --> Repos["ποΈ Repositories β SeaORM queries"]
Repos --> DB[("π’οΈ MariaDB / MySQL<br/>ZoneMinder schema")]
Handlers -.live & playback.-> Streaming["π¬ HLS Β· MSE Β· WebRTC"]
Streaming -.-> Cameras[("π· ZoneMinder capture daemons")]
| Layer | Path | Responsibility |
|---|---|---|
| Routes | src/routes/ |
Endpoint wiring & middleware layering |
| Handlers | src/handlers/ |
Request extraction, validation, response mapping |
| Services | src/service/ |
Business logic |
| Repositories | src/repo/ |
Database queries |
| Entities | src/entity/ |
SeaORM models (generated from the ZM schema) |
| DTOs | src/dto/ |
Request/response types (OpenAPI schemas) |
| Language | Rust (edition 2021) |
| Web framework | Axum 0.8 |
| Async runtime | Tokio |
| ORM | SeaORM 1.1 (MySQL/MariaDB) |
| API docs | utoipa + Swagger UI |
| Auth | JSON Web Tokens (jsonwebtoken) |
| Streaming | webrtc, fMP4/MSE, HLS, retina (RTSP) |
| Media | FFmpeg (ffmpeg-next) for H.264 β JPEG |
- Rust (current stable) β install via rustup
- MariaDB / MySQL with a ZoneMinder schema
- FFmpeg dev libraries β
libavutil-dev libavcodec-dev libavformat-dev libavfilter-dev libavdevice-dev libswscale-dev libswresample-dev(Debian/Ubuntu) orbrew install ffmpeg
# 1. Clone
git clone https://github.com/SteveGilvarry/zm-api.git
cd zm-api
# 2. Spin up a local test database (Docker / Apple Container)
./scripts/db-manager.sh start
./scripts/db-manager.sh mysql # load the ZoneMinder schema
# 3. Build & run
cargo run # uses settings/base.toml
APP_PROFILE=prod cargo run # production profileThe API comes up on the address/port from your active profile (see settings/).
Once running, open the interactive docs:
| π§ Swagger UI | http://<host>:<port>/swagger-ui |
| π OpenAPI spec | http://<host>:<port>/api-docs/openapi.json |
Settings are layered, last one wins:
settings/base.tomlβ defaultssettings/{APP_PROFILE}.tomlβdevΒ·testΒ·test-dbΒ·prod- Environment variables β prefix
APP_, nested keys use__
APP_PROFILE=prod
APP_DB__HOST=10.0.0.5 # overrides db.host
APP_CONFIG_DIR=/etc/zm_api # alternate config directoryπ‘ Local profiles like
settings/dev.tomlare gitignored β keep secrets out of version control.
# Unit + non-DB tests
cargo test --all-features
# Full integration suite (needs the test database)
./scripts/db-manager.sh start && ./scripts/db-manager.sh mysql
APP_PROFILE=test-db cargo test --test '*' -- --include-ignored
# Coverage report
cargo llvm-cov --all-features --ignore-filename-regex '/(entity|migration)/' \
-- --include-ignoredCI runs the suite on every push and gates line coverage β it can't regress below the floor. Currently ~59% and climbing, ~600 tests across unit + per-domain integration files.
src/
βββ routes/ Axum routers & middleware wiring
βββ handlers/ HTTP handlers
βββ service/ Business logic
βββ repo/ Database query layer
βββ entity/ SeaORM entities (generated from the ZM schema)
βββ dto/ Request/response DTOs
βββ streaming/ HLS / MSE / WebRTC pipelines
βββ ptz/ PTZ control drivers
βββ daemon/ ZoneMinder daemon supervision
βββ configure/ Config loading
βββ error/ AppError + HTTP mapping
scripts/ DB management, JWT key generation, CI helpers
settings/ Layered TOML configuration
docs/ Deployment, TLS, PTZ & streaming design notes
Before opening a PR, make sure the local quality gates pass:
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-featuresWork tests-first, keep changes focused, and treat src/entity/ as generated artifacts.
See CLAUDE.md for the full development workflow and conventions.
zm_api is dual-licensed:
- π Open source β AGPL-3.0. Free to use, modify, and self-host. If you run a modified version as a network service, the AGPL requires you to publish your changes.
- πΌ Commercial license. For embedding zm_api in a closed-source product, or running a modified version as a hosted service without the AGPL's source-sharing obligation, a commercial license is available. Contact the maintainer to enquire.
Contributions are accepted under a Contributor License Agreement so the project
can be offered under both licenses β see CONTRIBUTING.md.
The
db/*.sqlschema files are from ZoneMinder and remain under its GPL-2.0 license.