raptor
raptor is a hawkBit-compatible over-the-air (OTA) update server, written in Rust. It speaks hawkBit’s DDI v1 device API — so SWUpdate, the RAUC hawkbit-updater, and other hawkBit clients work unchanged — and the core hawkBit Management API workflow, all from a single static binary and one config file.
Where a stock hawkBit deployment is Java + a relational database + RabbitMQ, raptor is one process backed by SQLite or Postgres. No broker, no JVM, no servlet container.
Highlights
- Drop-in DDI v1 — devices poll, download, and report feedback exactly as they do against hawkBit.
- Core Management API — targets, software modules, artifacts, distribution
sets, assignments, actions, rollouts, and target filters, with hawkBit-shaped
JSON, paging, sorting, and FIQL
q=filtering. - Rollouts — staged, threshold-driven group deployments.
- Target filters + auto-assignment — saved FIQL queries that assign a distribution set to matching devices automatically.
- Confirmation flow — optional device/operator confirmation before a deployment starts.
- One binary — an embedded web console ships inside the executable; SQLite by default, Postgres when you need it.
- Packaged — a Debian package with a hardened systemd unit.
Where to go next
- New here? Start with What is raptor? and the Quick Start.
- Ready to run it? See Installation and Your First Deployment.
- Coming from hawkBit? Read hawkBit Compatibility to see what is and isn’t implemented.
Note: raptor is young and its surface is a subset of hawkBit’s. The compatibility matrix is the source of truth for what exists today.
What is raptor?
raptor is a Rust reimplementation of the Eclipse hawkBit server, targeting drop-in API compatibility with the parts of hawkBit that device fleets use every day.
The problem it solves
hawkBit is the de-facto open-source backend for embedded/IoT software updates.
Its device-facing DDI protocol is spoken by mature clients — SWUpdate’s
suricatta module, the RAUC hawkbit-updater, and Collabora’s hawkbit Rust crate.
But a production hawkBit deployment brings a heavy operational footprint: a JVM,
a servlet container, a relational database, and (for the DMF device path)
RabbitMQ.
raptor keeps the API and drops the weight:
| hawkBit | raptor | |
|---|---|---|
| Runtime | JVM / Spring Boot | single static binary |
| Database | MySQL / Postgres | SQLite or Postgres |
| Message broker | RabbitMQ (DMF) | none |
| Device API | DDI v1 + DMF | DDI v1 |
| Deployment | multi-service | one process, one config file |
Because raptor implements hawkBit’s DDI v1 contract field-for-field, existing devices do not know the difference — you point them at raptor’s URL and they poll, download, and report just as before.
What raptor is
- A device update server: it tells devices what to install, serves the artifacts, and records their feedback.
- A fleet management API: create targets, upload firmware, compose distribution sets, assign updates, and run staged rollouts over REST.
- A single-fleet, single-tenant server. It accepts (and ignores) hawkBit’s tenant URL segment, so clients configured for a tenant still work, but there is no tenant isolation. Run one raptor per fleet.
What raptor is not (yet)
raptor implements a growing subset of hawkBit. It does not currently provide the DMF (AMQP) device path, multi-tenancy, tags, target types, metadata endpoints, maintenance windows, or the full set of action types. See hawkBit Compatibility for the authoritative list.
Design principles
- Compatibility is the contract. Clients branch on HTTP status codes and JSON field names; raptor matches hawkBit’s exactly, backed by golden-fixture and end-to-end tests against a real hawkBit client.
- One binary, one config file. Everything — including the web console — ships in the executable. Configuration is a single TOML file with environment overrides.
- Boring persistence. One SeaORM entity/migration set targets both SQLite and Postgres; migrations run automatically at startup.
Quick Start
This gets a raptor server running and deploys an update to a (virtual) device in a few commands. For production installs, see Installation.
1. Build and configure
$ cargo build --release
$ printf 'yourpassword\n' | ./target/release/raptor hash-password
$argon2id$v=19$m=19456,t=2,p=1$...
Put that hash into a raptor.toml:
bind = "0.0.0.0:8088"
database_url = "sqlite://raptor.db?mode=rwc" # or postgres://user:pass@host/db
artifact_dir = "./artifacts"
[ddi]
anonymous = true # dev only — no device auth
[mgmt]
username = "admin"
password_hash = "$argon2id$v=19$m=19456,t=2,p=1$..."
Warning:
anonymous = truedisables all device authentication. Use it for local experiments only. See Authentication for production setups.
2. Run the server
$ ./target/release/raptor serve --config raptor.toml
raptor listening bind=0.0.0.0:8088
The database is created and migrated automatically on first start.
3. Deploy an update
Using the Management API (HTTP Basic with the admin credentials):
# 1. a software module
curl -u admin:yourpassword -X POST localhost:8088/rest/v1/softwaremodules \
-H 'Content-Type: application/json' \
-d '[{"name":"rootfs","version":"1.0","type":"os"}]'
# 2. an artifact on module 1
curl -u admin:yourpassword -X POST localhost:8088/rest/v1/softwaremodules/1/artifacts \
-F 'file=@rootfs.img'
# 3. a distribution set bundling module 1
curl -u admin:yourpassword -X POST localhost:8088/rest/v1/distributionsets \
-H 'Content-Type: application/json' \
-d '[{"name":"release","version":"1.0","type":"os","modules":[{"id":1}]}]'
# 4. assign DS 1 to a device (auto-registered on first poll)
curl -u admin:yourpassword -X POST localhost:8088/rest/v1/targets/my-device/assignedDS \
-H 'Content-Type: application/json' -d '{"id":1,"type":"forced"}'
4. Poll as the device
$ curl localhost:8088/DEFAULT/controller/v1/my-device
{"config":{"polling":{"sleep":"00:05:00"}},
"_links":{"deploymentBase":{"href":".../deploymentBase/1"},
"configData":{"href":".../configData"}}}
The deploymentBase link tells the device an update is waiting. From here a real
hawkBit client downloads the artifacts and posts feedback. Walk through the full
cycle in Your First Deployment.
hawkBit Compatibility
raptor implements a subset of hawkBit, chosen to cover the device update workflow first. This page is the source of truth for what exists today.
Device API (DDI v1)
The DDI v1 contract is implemented field-for-field and verified with golden fixtures and an end-to-end test against a real hawkBit DDI client.
| Feature | Status |
|---|---|
Poll root, config.polling.sleep, _links | ✅ |
deploymentBase (download/update modes, chunks, artifacts) | ✅ |
| Deployment feedback state machine | ✅ |
cancelAction + cancel feedback | ✅ |
configData (attributes: merge / replace / remove) | ✅ |
installedBase | ✅ |
| Artifact download with HTTP Range (resume) | ✅ |
.MD5SUM companion endpoint | ✅ |
| Auto-registration (gateway token / anonymous) | ✅ |
confirmationBase confirmation flow | ✅ |
| Maintenance windows | ❌ |
| DMF (AMQP) device path | ❌ |
Management API
| Area | Status |
|---|---|
Targets CRUD, assignedDS, installedDS, actions, attributes | ✅ |
| Software modules CRUD + multipart artifact upload/list/download/delete | ✅ |
| Distribution sets CRUD + module composition | ✅ |
| Actions (per-target and fleet-wide list/filter) | ✅ |
| Rollouts (create/start/pause/resume/delete, deploy groups) | ✅ |
| Target filters + auto-assignment | ✅ |
| Per-target auto-confirm | ✅ |
Paging (offset/limit), sort=, q= FIQL on lists | ✅ |
Software-module / distribution-set / target types CRUD (composition drives complete; target-type/DS-type compatibility enforced) | ✅ |
Target / distribution-set tags CRUD, assign/unassign, q=tag==x | ✅ |
Metadata endpoints (targets / modules / DS, targetVisible on module entries) | ✅ |
All four action types + force escalation (PUT .../actions/{id}) and force-quit (DELETE ...?force=true) | ✅ |
| Rollout approval workflow, dynamic rollouts | ❌ |
| Maintenance windows | ❌ |
| Multi-assignment / action weights | ❌ |
Action types
hawkBit has forced, soft, downloadonly, and timeforced. raptor models all
four. timeforced starts out soft and escalates once its deadline passes;
downloadonly forces the download and never asks the device to install. An
operator can escalate a running action to forced with
PUT /rest/v1/targets/{controllerId}/actions/{actionId}, and force-quit one that
the device is not acknowledging with DELETE …/actions/{actionId}?force=true.
Auth
| Mechanism | Status |
|---|---|
| DDI target security token | ✅ |
| DDI shared gateway token | ✅ |
| DDI anonymous mode | ✅ |
| Management API HTTP Basic (single admin) | ✅ |
| Session cookie for the web console | ✅ |
| mTLS / certificate DDI auth | ❌ |
| Multiple users / roles, OIDC | ❌ |
Tenancy
raptor is single-tenant. The DDI URL’s /{tenant}/controller/v1/... segment
is accepted and ignored; all generated links use the tenant name DEFAULT.
There is no per-tenant data isolation — run one raptor instance per fleet.
Note: Items marked ❌ are tracked as issues on the GitHub repository. The schema is designed so these can be added without breaking existing deployments.
Installation
raptor is a single binary. You can build it from source or install the Debian package.
Debian / Ubuntu package
Prebuilt .deb packages are attached to each
GitHub Release:
$ sudo dpkg -i raptor_*.deb
The package installs:
/usr/bin/raptor— the server, with the web console embedded/etc/raptor/config.toml— default config, registered as a dpkg conffile (your edits survive upgrades)/usr/lib/systemd/system/raptor.service— a hardened systemd unit running as a transientDynamicUser, with state (SQLite DB + artifacts) under/var/lib/raptor
The service is enabled but not started on install, because you must set an admin password first:
$ raptor hash-password # type a password, copy the hash
$ sudoedit /etc/raptor/config.toml # paste into password_hash, pick DDI auth
$ sudo systemctl start raptor
See Running as a systemd Service for the unit’s hardening details and how to supply secrets safely.
From source
You need a recent stable Rust toolchain.
$ git clone https://github.com/rosterloh/raptor
$ cd raptor
$ cargo build --release
$ ./target/release/raptor --version
The binary lands at target/release/raptor.
Building with the web console
The embedded web console is behind the embed-ui Cargo feature. It requires the
Dioxus CLI (dx) to build the WASM bundle first:
$ cargo binstall dioxus-cli@0.7.10
$ dx build --release --package raptor-ui
$ cargo build --release --features embed-ui
Without embed-ui, the server runs identically but does not serve /ui.
Building the Debian package yourself
$ cargo install cargo-deb
$ dx build --release --package raptor-ui # populate the embedded UI
$ cargo deb -p raptor # -> target/debian/raptor_*.deb
Database
raptor supports SQLite and Postgres, selected by the database_url
scheme. Migrations run automatically at startup, so there is no separate migrate
step.
- SQLite:
sqlite://raptor.db?mode=rwc(themode=rwccreates the file) - Postgres:
postgres://user:pass@host/dbname
Next steps
- Configuration — the
raptor.tomlfile - Your First Deployment — an end-to-end walkthrough
Configuration
raptor reads a single TOML file (default raptor.toml, override with
--config). Every key can also be set via a RAPTOR_* environment variable,
which is useful for secrets and containers.
Minimal config
bind = "0.0.0.0:8088"
database_url = "sqlite://raptor.db?mode=rwc" # or postgres://user:pass@host/db
artifact_dir = "./artifacts"
[ddi]
gateway_token = "change-me" # or anonymous = true for dev
[mgmt]
username = "admin"
password_hash = "$argon2id$..." # from `raptor hash-password`
Only database_url, artifact_dir, and the [mgmt] credentials are required;
everything else has a default. See the
Configuration Reference for every key and its
default.
Environment overrides
Any key maps to an environment variable prefixed with RAPTOR_, with nested
tables joined by a double underscore (__):
export RAPTOR_BIND="127.0.0.1:9090"
export RAPTOR_DDI__ANONYMOUS=true
export RAPTOR_DDI__GATEWAY_TOKEN="super-secret"
export RAPTOR_MGMT__PASSWORD_HASH='$argon2id$...'
Environment values override the TOML file. This is the recommended way to inject
secrets — a plaintext gateway token in a world-readable config file is a
liability (the Debian package addresses this with a root-only raptor.env; see
Running as a systemd Service).
Generating the admin password hash
The Management API and web console authenticate against a single admin credential stored as an argon2id hash:
$ printf 'yourpassword\n' | raptor hash-password
$argon2id$v=19$m=19456,t=2,p=1$...
Copy the output into mgmt.password_hash.
Key groups at a glance
| Section | Purpose |
|---|---|
| top level | bind, database_url, artifact_dir, max_artifact_size, url, rollout_eval_interval_secs |
[ddi] | device-facing auth and polling: anonymous, gateway_token, polling_interval, confirmation_flow |
[mgmt] | Management API / web console admin: username, password_hash |
The url key
By default the _links in API responses are derived from the incoming request’s
Host header. If raptor sits behind a reverse proxy that rewrites the host, set
url to the externally visible base so devices receive dialable links:
url = "https://raptor.example.com"
Your First Deployment
This walks through a complete update cycle: create the content, assign it, and
watch a device install it. It uses curl for both the operator (Management API)
and the device (DDI API) sides.
Assume raptor is running on localhost:8088 with admin admin:yourpassword and
ddi.anonymous = true (so we can poll without a device token).
1. Create a software module
A software module is a named, versioned unit of a given type (os,
firmware, runtime, or application).
curl -u admin:yourpassword -X POST localhost:8088/rest/v1/softwaremodules \
-H 'Content-Type: application/json' \
-d '[{"name":"rootfs","version":"1.0","type":"os"}]'
# -> [{"id":1, ...}]
2. Upload an artifact
Artifacts are uploaded as multipart form data. raptor streams the bytes to disk and computes the SHA-1, MD5, and SHA-256 hashes as it goes.
curl -u admin:yourpassword -X POST localhost:8088/rest/v1/softwaremodules/1/artifacts \
-F 'file=@rootfs.img'
# -> {"id":1,"providedFilename":"rootfs.img","size":..., "hashes":{...}}
3. Compose a distribution set
A distribution set (DS) bundles one or more modules into a releasable unit. A
DS is complete once it has the modules its type requires.
curl -u admin:yourpassword -X POST localhost:8088/rest/v1/distributionsets \
-H 'Content-Type: application/json' \
-d '[{"name":"release","version":"1.0","type":"os","modules":[{"id":1}]}]'
# -> [{"id":1,"complete":true, ...}]
4. Assign the DS to a target
You do not need to create the target first — an unknown controller ID is auto-registered on its first poll. Assigning creates an action (the record of one deployment).
curl -u admin:yourpassword -X POST localhost:8088/rest/v1/targets/my-device/assignedDS \
-H 'Content-Type: application/json' -d '{"id":1,"type":"forced"}'
# -> {"assigned":1,"alreadyAssigned":0,"total":1,"assignedActions":[{"id":1}]}
5. Device poll
The device polls the DDI root. Because an action is pending, the response carries
a deploymentBase link.
curl localhost:8088/DEFAULT/controller/v1/my-device
# _links.deploymentBase -> .../deploymentBase/1
6. Fetch the deployment
curl localhost:8088/DEFAULT/controller/v1/my-device/deploymentBase/1
The response describes the download/update modes and lists each module’s artifacts with hashes, sizes, and download links. A real client (SWUpdate, RAUC) downloads the artifacts from those links (which support HTTP Range for resume).
7. Report feedback
The device reports progress, then a final result. Feedback drives the action state machine.
# progress
curl -X POST localhost:8088/DEFAULT/controller/v1/my-device/deploymentBase/1/feedback \
-H 'Content-Type: application/json' \
-d '{"status":{"execution":"proceeding","result":{"finished":"none"}}}'
# success
curl -X POST localhost:8088/DEFAULT/controller/v1/my-device/deploymentBase/1/feedback \
-H 'Content-Type: application/json' \
-d '{"status":{"execution":"closed","result":{"finished":"success"}}}'
On closed/success the action becomes finished and the target’s
updateStatus becomes in_sync. A subsequent poll no longer offers a
deploymentBase, and installedBase now reflects the installed DS.
8. Verify
curl -u admin:yourpassword localhost:8088/rest/v1/targets/my-device
# "updateStatus": "in_sync"
curl -u admin:yourpassword localhost:8088/rest/v1/targets/my-device/actions
# the action is "finished"
That’s a full cycle. From here, explore Rollouts to stage a deployment across many devices, or Target Filters to assign automatically.
Targets & Auto-Registration
A target is a device raptor can update, identified by a unique
controllerId. Targets carry a security token, a reported set of attributes, and
an updateStatus.
Update status
Every target has an updateStatus reflecting where it is in the update cycle:
| Status | Meaning |
|---|---|
unknown | created via the Management API, never polled |
registered | known to the server, no update assigned |
pending | an update is assigned and in progress |
in_sync | running the assigned distribution set |
error | the last deployment failed |
Creating targets
Explicitly (Management API)
curl -u admin:pw -X POST localhost:8088/rest/v1/targets \
-H 'Content-Type: application/json' \
-d '[{"controllerId":"device-42","name":"Device 42"}]'
The request body is an array, so you can create many at once. A securityToken
is generated if you don’t supply one.
Automatically (auto-registration)
An unknown controllerId that polls the DDI API is created on the spot with
status registered — hawkBit’s plug-and-play behavior. Auto-registration
requires the poll to be authenticated by the shared gateway token, or DDI
anonymous mode to be on. See Authentication.
Listing and filtering
The list endpoint supports paging, sorting, and FIQL:
curl -u admin:pw 'localhost:8088/rest/v1/targets?offset=0&limit=50&sort=controllerId:ASC'
curl -u admin:pw 'localhost:8088/rest/v1/targets?q=updateStatus==error'
Filterable fields include controllerId (alias id), name, description,
updateStatus, lastControllerRequestAt, and address. See
Filtering with FIQL.
Last-seen address
A target’s address / ipAddress is recorded from its DDI polls, so a device
that self-registers shows one without any Management API call. By default it’s
the socket peer address.
Behind a reverse proxy the socket peer is the proxy, so point raptor at the header carrying the real address:
[ddi]
trusted_proxy_header = "x-forwarded-for"
This is unset by default because a device can put whatever it likes in that
header — only set it when a proxy you control is rewriting it. raptor reads the
rightmost entry, the hop appended by the proxy directly in front of it;
entries to the left are caller-supplied and so spoofable. Values that don’t parse
as an IP are ignored. Both X-Forwarded-For lists and RFC 7239 Forwarded
(for=…) syntax are understood, with or without a port.
Attributes
Devices report key/value attributes (hardware revision, OS version, …) via
the DDI configData endpoint. Retrieve them with:
curl -u admin:pw localhost:8088/rest/v1/targets/device-42/attributes
# {"hw":"rev2","os":"linux"}
Attributes are set by the device, in three modes — merge (default), replace,
and remove — described in the DDI API reference.
Note: target attributes (device-reported) are distinct from hawkBit metadata (operator-set key/value pairs), which raptor does not yet implement.
Poll status
When a target has polled at least once, its representation includes a
pollStatus block with the last request time, the next expected request time
(derived from the configured polling interval), and an overdue flag — handy for
spotting devices that have gone quiet.
Software Modules & Artifacts
A software module is a named, versioned unit of updatable content. An artifact is a file belonging to a module (the actual firmware image, package, etc.).
Module types
raptor seeds four software-module types: os, firmware, runtime, and
application. Types are read-only in raptor (created via migration); you
reference them by key when creating a module.
curl -u admin:pw -X POST localhost:8088/rest/v1/softwaremodules \
-H 'Content-Type: application/json' \
-d '[{"name":"rootfs","version":"1.0","type":"os","vendor":"ACME"}]'
The combination of name + version + type is unique; a duplicate returns 409 Conflict.
Uploading artifacts
Artifacts are uploaded as multipart/form-data. raptor streams the upload to
disk while computing the SHA-1, MD5, and SHA-256 hashes in one pass:
curl -u admin:pw -X POST localhost:8088/rest/v1/softwaremodules/1/artifacts \
-F 'file=@rootfs.img'
The response includes the computed hashes and the byte size. Maximum upload size
is governed by max_artifact_size (see the
Configuration Reference).
Content-addressed storage
Blobs are stored once per SHA-256, laid out git-object-style:
<artifact_dir>/<sha256[0..2]>/<sha256>
Uploading identical content twice stores the bytes once and adds a second reference; the blob is removed from disk only when the last artifact row referencing it is deleted. This dedup is transparent — each module still sees its own artifact row with its own filename.
Listing, downloading, deleting
# list a module's artifacts
curl -u admin:pw localhost:8088/rest/v1/softwaremodules/1/artifacts
# download (operator side)
curl -u admin:pw -O localhost:8088/rest/v1/softwaremodules/1/artifacts/1/download
# delete
curl -u admin:pw -X DELETE localhost:8088/rest/v1/softwaremodules/1/artifacts/1
Device-side download
Devices fetch artifacts through the DDI API, not the Management API:
GET /{tenant}/controller/v1/{cid}/softwaremodules/{moduleId}/artifacts/{filename}
This endpoint supports HTTP Range requests (RFC 7233), so an interrupted
download resumes rather than restarting — important for large firmware images
over flaky links. A companion {filename}.MD5SUM endpoint returns the
md5sum-file format some clients verify against.
Distribution Sets
A distribution set (DS) is the releasable unit you assign to devices: a named, versioned bundle of software modules.
DS types
raptor seeds three distribution-set types: os, os_app, and app. Like
module types, they are read-only. The type determines which module types a DS
needs to be considered complete.
Creating a distribution set
curl -u admin:pw -X POST localhost:8088/rest/v1/distributionsets \
-H 'Content-Type: application/json' \
-d '[{"name":"release","version":"1.0","type":"os","modules":[{"id":1}]}]'
You can pass modules inline (as above) or add them afterward:
curl -u admin:pw -X POST localhost:8088/rest/v1/distributionsets/1/assignedSM \
-H 'Content-Type: application/json' -d '[{"id":2}]'
Completeness
A DS is complete when it contains the modules its type requires. Only complete
distribution sets can be assigned or deployed — assigning an incomplete DS (or
attaching one as a target-filter auto-assignment) returns 400 Bad Request.
curl -u admin:pw localhost:8088/rest/v1/distributionsets/1
# {"id":1,"complete":true, "modules":[...], ...}
Listing and filtering
Distribution sets support the usual paging, sorting, and FIQL query parameters:
curl -u admin:pw 'localhost:8088/rest/v1/distributionsets?q=name==release*&sort=version:DESC'
Lifecycle
Distribution sets are referenced by actions and rollouts. raptor keeps the content-addressing scheme independent of DS identity, so the same artifact bytes can back many distribution sets without duplication.
Assignments & Actions
An action is the record of one deployment: a distribution set being rolled out to one target. Assigning a DS creates an action; the device’s feedback drives it to completion.
Assigning a distribution set
curl -u admin:pw -X POST localhost:8088/rest/v1/targets/device-42/assignedDS \
-H 'Content-Type: application/json' -d '{"id":1,"type":"forced"}'
The type is the action type. All four of hawkBit’s are supported, and each maps
to the download/update handling modes the device is given in
deploymentBase:
type | download | update | Meaning |
|---|---|---|---|
forced (default) | forced | forced | install as soon as possible |
soft | attempt | attempt | the device may defer per its own policy |
timeforced | attempt → forced | attempt → forced | soft until forcetime, forced after |
downloadonly | forced | skip | fetch the artifacts, do not install |
An unknown type is rejected with 400.
timeforced takes a forcetime (epoch millis) alongside the type:
curl -u admin:pw -X POST localhost:8088/rest/v1/targets/device-42/assignedDS \
-H 'Content-Type: application/json' \
-d '{"id":1,"type":"timeforced","forcetime":1767225600000}'
Before that instant the device sees attempt; after it, forced — no server-side
job is involved, the mode is computed per request. Omitting forcetime means
“already reached”, so the action behaves as forced immediately (matching
hawkBit’s default of 0). Note the request body spells it all-lowercase
forcetime while the action response uses forceTime; that asymmetry is
hawkBit’s and raptor mirrors it.
A downloadonly action completes when the device reports downloaded
feedback rather than closed. Because nothing was installed, the target’s
installedDS is deliberately left untouched — only assignedDS reflects the
distribution set. The action ends with status: finished and
detailStatus: downloaded.
Escalating a running action
A soft or timeforced action can be pushed through immediately:
curl -u admin:pw -X PUT localhost:8088/rest/v1/targets/device-42/actions/7 \
-H 'Content-Type: application/json' -d '{"forceType":"forced"}'
The next deploymentBase the device fetches carries forced. Escalating an
action that is no longer active returns 410 Gone.
One active action per target
raptor enforces hawkBit’s default invariant: a target has at most one active action. Assigning a new DS to a target that already has an active action cancels the old one and starts the new deployment. (hawkBit’s opt-in multi-assignment mode with action weights is not implemented.)
Action states
| State | active | Meaning |
|---|---|---|
wait_for_confirmation | yes | awaiting confirmation before deploying (see Confirmation Flow) |
running | yes | device has been told to deploy |
canceling | yes | cancellation requested, awaiting device acknowledgement |
canceled | no | cancellation confirmed (or forced) |
finished | no | deployment succeeded |
error | no | deployment failed |
Each transition and every piece of device feedback appends an ActionStatus history row (with optional messages).
Inspecting actions
# all actions on one target (newest first)
curl -u admin:pw localhost:8088/rest/v1/targets/device-42/actions
# a single action
curl -u admin:pw localhost:8088/rest/v1/targets/device-42/actions/1
# fleet-wide, filterable
curl -u admin:pw 'localhost:8088/rest/v1/actions?q=detailStatus==error'
The action JSON exposes status (pending while active, else finished) and
detailStatus (the fine-grained state from the table above).
Status history
Every state change an action goes through — assignment, each piece of device feedback, cancellation — is recorded as a status entry. List them with:
# chronological (oldest first); pass ?sort=id:DESC for newest first
curl -u admin:pw localhost:8088/rest/v1/targets/device-42/actions/1/status
Each entry has a type (the reported status, e.g. running, finished,
canceled), any messages the device or server attached, and reportedAt.
The list supports the usual offset/limit/sort paging.
Cancelling
# request cancellation (device must acknowledge)
curl -u admin:pw -X DELETE localhost:8088/rest/v1/targets/device-42/actions/1
# force-cancel server-side (no device acknowledgement)
curl -u admin:pw -X DELETE 'localhost:8088/rest/v1/targets/device-42/actions/1?force=true'
A normal cancel moves the action to canceling and offers the device a
cancelAction link; the device confirms via cancel feedback, moving it to
canceled. A forced cancel closes the action immediately.
Installed vs assigned
GET /rest/v1/targets/{cid}/assignedDS— the DS currently assigned (what the device should run).GET /rest/v1/targets/{cid}/installedDS— the DS the device last successfully installed.
Both return 204 No Content when there is nothing to report.
Rollouts
A rollout deploys a distribution set across many targets in stages, advancing from one group to the next only when success thresholds are met — so a bad update is caught on a small group before it reaches the whole fleet.
How it works
- You create a rollout from a FIQL target filter, a distribution set, and a number of groups. Matching targets are split evenly across the groups at creation time.
- Each group has a success threshold and an error threshold (percentages).
- Starting the rollout schedules the first group only — its targets get the DS assigned.
- A background evaluator watches each running group:
- When the error threshold is reached, the group and rollout pause.
- When the success threshold is reached, the group finishes and the next group is scheduled.
- When the last group finishes, the rollout is finished.
Creating a rollout
curl -u admin:pw -X POST localhost:8088/rest/v1/rollouts \
-H 'Content-Type: application/json' \
-d '{
"name": "fleet-1.1",
"distributionSetId": 1,
"targetFilterQuery": "controllerId==device-*",
"amountGroups": 3,
"successCondition": {"condition":"THRESHOLD","expression":"90"},
"errorCondition": {"condition":"THRESHOLD","expression":"20"}
}'
amountGroupssplits matching targets into that many groups.successCondition.expression/errorCondition.expressionare percentages (0–100). IferrorConditionis omitted, the error threshold never trips.typeis the action type every action the rollout creates inherits —forced(default),soft,timeforcedordownloadonly— withforcetimealongside it fortimeforced. A staged download-then-install is therefore adownloadonlyrollout followed by aforcedone over the same filter. An unknown type is rejected with400.
The rollout starts in ready.
Lifecycle operations
curl -u admin:pw -X POST localhost:8088/rest/v1/rollouts/1/start
curl -u admin:pw -X POST localhost:8088/rest/v1/rollouts/1/pause
curl -u admin:pw -X POST localhost:8088/rest/v1/rollouts/1/resume
curl -u admin:pw -X DELETE localhost:8088/rest/v1/rollouts/1
- start —
ready→running; schedules the first group. - pause —
running→paused; the evaluator ignores paused rollouts. - resume —
paused→running; re-evaluates immediately. - delete — cancels any active actions in the rollout and removes it.
Inspecting groups
# deploy groups with per-group status and target counts
curl -u admin:pw localhost:8088/rest/v1/rollouts/1/deploygroups
# one group
curl -u admin:pw localhost:8088/rest/v1/rollouts/1/deploygroups/5
# the controllerIds in a group
curl -u admin:pw localhost:8088/rest/v1/rollouts/1/deploygroups/5/targets
Tracking progress
Rollouts and groups both carry totalTargetsPerStatus, hawkBit’s breakdown of
their targets by deployment outcome:
{
"id": 1, "name": "fleet-1.1", "status": "running", "totalTargets": 9,
"totalTargetsPerStatus": {
"notstarted": 0, "scheduled": 3, "running": 3,
"error": 1, "finished": 2, "cancelled": 0
}
}
notstarted— the rollout has not been started, so nothing is deployed yet.scheduled— the group is waiting its turn; raptor creates actions only when a group is scheduled, so these targets have no action yet.running— an action is in flight (includingcancelingand, with the confirmation flow on,wait_for_confirmation).finished/error/cancelled— the action’s terminal state.
A rollout’s counts are the sum of its groups’. The web console renders both as progress bars — see the Web Console guide.
Evaluator cadence
The background evaluator runs every rollout_eval_interval_secs seconds
(default 5). Lower it for snappier progression in testing, raise it to reduce
load on large fleets. See the
Configuration Reference.
Note: hawkBit’s rollout approval workflow and dynamic rollouts (groups that keep absorbing newly-matching targets) are not yet implemented. Group membership is a static snapshot taken at creation time.
Target Filters & Auto-Assignment
A target filter is a saved FIQL query. On its own it’s a convenient, named way to select devices. Attach a distribution set to it, and it becomes an auto-assignment rule: matching targets receive that DS automatically — now, and as new matching devices appear.
Creating a filter
curl -u admin:pw -X POST localhost:8088/rest/v1/targetfilters \
-H 'Content-Type: application/json' \
-d '{"name":"beta-ring","query":"controllerId==beta-*"}'
The query is validated against the target field map at write time — an invalid
FIQL expression returns 400 Bad Request, and a duplicate name returns 409 Conflict. Update, fetch, list, and delete follow the usual REST shape:
curl -u admin:pw -X PUT localhost:8088/rest/v1/targetfilters/1 \
-H 'Content-Type: application/json' -d '{"query":"name==beta-*"}'
curl -u admin:pw localhost:8088/rest/v1/targetfilters
curl -u admin:pw -X DELETE localhost:8088/rest/v1/targetfilters/1
Attaching an auto-assign distribution set
curl -u admin:pw -X POST localhost:8088/rest/v1/targetfilters/1/autoAssignDS \
-H 'Content-Type: application/json' \
-d '{"id":1,"type":"forced"}'
id— the distribution set to assign. It must be complete (400otherwise).type—forced(default) orsoft.
Attaching immediately assigns the DS to every currently-matching target. Read or detach the attachment with:
curl -u admin:pw localhost:8088/rest/v1/targetfilters/1/autoAssignDS # -> the DS, or 204
curl -u admin:pw -X DELETE localhost:8088/rest/v1/targetfilters/1/autoAssignDS
From the web console
Everything above is also available under Target filters in the web console:
- New filter / Edit — name plus FIQL query. The query is checked against
the live target list as you type, so the form shows how many targets it matches
(or the parser’s complaint) before you save; the API’s
400/409messages are shown against the query and name fields. - Auto-assign… — pick a distribution set and the action type (
forcedorsoft), or Detach to remove the attachment. An incomplete set is reported inline rather than as a raw failure. - The Auto-assign column names the attached set, so you can see which filters are live rules at a glance.
When auto-assignment runs
A matching target receives the DS:
- On registration — a device auto-registering via DDI that matches a filter
is assigned in the same poll (its very first poll can already return a
deploymentBase). - On attribute change — updating a target via
configDatare-evaluates the filters. - Periodically — a background sweep (sharing the rollout evaluator’s task) catches targets created through other paths.
Non-disruptive by design
Auto-assignment never disturbs work in flight. A matching target is skipped if it already has that DS assigned, or if it has any active action. So a device mid-deployment is never clobbered by an auto-assign rule; it’s picked up on a later sweep once it’s idle.
Note: FIQL auto-assignment matches on the standard target fields (
controllerId,name,updateStatus, …). Matching on device-reported attributes is not yet supported.
Tags
A tag is a free-form label you attach to targets or distribution sets:
beta, eu-west, kiosk, qa. Tags carry no behaviour of their own — they
exist so you can organise a fleet and then select it again with the tag== FIQL
term, in list queries, saved target filters and rollouts.
Target tags and distribution-set tags are separate namespaces: a beta target
tag and a beta DS tag are unrelated tags.
Creating tags
The create body is an array, like the other Management API create endpoints:
curl -u admin:pw -X POST localhost:8088/rest/v1/targettags \
-H 'Content-Type: application/json' \
-d '[{"name":"beta","description":"early access","colour":"#00ff00"}]'
name is required and unique (409 Conflict on a duplicate); description and
colour are optional. Distribution-set tags are identical, at
/rest/v1/distributionsettags.
Fetch, update and delete follow the usual REST shape. PUT only changes the
fields present in the body:
curl -u admin:pw localhost:8088/rest/v1/targettags/1
curl -u admin:pw -X PUT localhost:8088/rest/v1/targettags/1 \
-H 'Content-Type: application/json' -d '{"colour":"#ff0000"}'
curl -u admin:pw -X DELETE localhost:8088/rest/v1/targettags/1
Deleting a tag deletes its assignments, not the tagged entities. Removing
the beta tag leaves every target that carried it in place, simply untagged.
Tag lists support the usual paging, sort= and q= parameters over id,
name, description and colour:
curl -u admin:pw 'localhost:8088/rest/v1/targettags?q=name==be*&sort=name:ASC'
Assigning tags
Assign one target by controller id, or several at once with a JSON array body:
curl -u admin:pw -X POST localhost:8088/rest/v1/targettags/1/assigned/dev-1
curl -u admin:pw -X POST localhost:8088/rest/v1/targettags/1/assigned \
-H 'Content-Type: application/json' -d '["dev-2","dev-3"]'
Assignment is idempotent — re-assigning an already-tagged target succeeds and
changes nothing, so bulk calls are safe to retry. An unknown controller id fails
the whole call with 404.
Unassign with the same paths and DELETE, and list what a tag holds with
GET .../assigned (a paged list of full target objects, accepting offset,
limit, sort= and q=):
curl -u admin:pw -X DELETE localhost:8088/rest/v1/targettags/1/assigned/dev-1
curl -u admin:pw -X DELETE localhost:8088/rest/v1/targettags/1/assigned \
-H 'Content-Type: application/json' -d '["dev-2","dev-3"]'
curl -u admin:pw 'localhost:8088/rest/v1/targettags/1/assigned?limit=100'
The reverse lookup — the tags carried by one entity — is a raptor extension, since hawkBit only exposes the tag-to-entity direction:
curl -u admin:pw localhost:8088/rest/v1/targets/dev-1/tags
curl -u admin:pw localhost:8088/rest/v1/distributionsets/7/tags
Distribution-set tags work the same way, keyed by distribution set id:
curl -u admin:pw -X POST localhost:8088/rest/v1/distributionsettags/1/assigned/7
curl -u admin:pw -X POST localhost:8088/rest/v1/distributionsettags/1/assigned \
-H 'Content-Type: application/json' -d '[8,9]'
curl -u admin:pw localhost:8088/rest/v1/distributionsettags/1/assigned
In the web console
The web console has a Tags page with a tab per kind, where tags are created, recoloured and deleted, alongside a count of how many targets or sets carry each one. A target’s or distribution set’s own tags appear on its detail page, where Manage… assigns and unassigns them, and the targets and distributions lists gain a tag dropdown that filters the list to one tag — combined with whatever is in the search box.
Filtering by tag
tag is available as a FIQL field on /rest/v1/targets and
/rest/v1/distributionsets:
curl -u admin:pw 'localhost:8088/rest/v1/targets?q=tag==beta'
curl -u admin:pw 'localhost:8088/rest/v1/targets?q=tag!=beta'
curl -u admin:pw 'localhost:8088/rest/v1/targets?q=tag=in=(beta,canary)'
curl -u admin:pw 'localhost:8088/rest/v1/distributionsets?q=tag==qa'
It accepts ==, !=, =in= and =out= (plus * wildcards on the tag name);
the ordering operators return 400. Negation applies to the membership rather
than the name, so tag!=beta means “not tagged beta” — a target that also
carries stable is still excluded.
Because saved target filters compile the same field map, tag== works there
too, which makes it a natural way to scope a rollout:
curl -u admin:pw -X POST localhost:8088/rest/v1/targetfilters \
-H 'Content-Type: application/json' \
-d '{"name":"beta-ring","query":"tag==beta"}'
Combine it with any other term to narrow further:
tag==beta ; updateStatus==error
tag==eu-* , tag==us-*
Confirmation Flow
By default an assignment becomes an active deployment immediately. With the
confirmation flow enabled, a new assignment first enters a
wait_for_confirmation state and does not deploy until it is confirmed —
either by the device over DDI, or by an operator activating auto-confirm.
This mirrors hawkBit’s confirmation flow (confirmationBase), which clients like
the RAUC hawkbit-updater support.
The client has to implement
confirmationBase. With the flow on, a waiting target’s poll offersconfirmationBaseand nodeploymentBase. A client that doesn’t parse that link — the mainline Zephyr hawkbit client (subsys/mgmt/hawkbit) ignores it entirely, as do older SWUpdate/RAUC setups — sees nothing it understands, so it polls forever and never installs, with no error reported anywhere. raptor logs a warning at startup when the flow is on. Useauto_confirm_default(below) or per-target auto-confirm for those devices.
Enabling the flow
It’s a server-wide toggle in the config:
[ddi]
confirmation_flow = true
When off (the default), behavior is exactly as before — assignments go straight
to running.
What a device sees
With the flow on, a target’s poll offers a confirmationBase link instead of
deploymentBase:
$ curl localhost:8088/DEFAULT/controller/v1/device-42
# _links.confirmationBase -> .../confirmationBase/7
GET .../confirmationBase/{actionId} returns the pending deployment (the same
chunk/artifact shape as deploymentBase, under a confirmation key) so the
device can decide. The device then confirms or denies:
# confirm -> action goes to running; next poll offers deploymentBase
curl -X POST localhost:8088/DEFAULT/controller/v1/device-42/confirmationBase/7/feedback \
-H 'Content-Type: application/json' \
-d '{"confirmation":"confirmed","details":["operator approved"]}'
# deny -> action stays waiting (a "denied" ActionStatus row is recorded)
curl -X POST localhost:8088/DEFAULT/controller/v1/device-42/confirmationBase/7/feedback \
-H 'Content-Type: application/json' \
-d '{"confirmation":"denied","details":["not now"]}'
A denied action remains in wait_for_confirmation; the device may confirm later,
or an operator can cancel it.
Auto-confirm
A target can be set to auto-confirm, so assignments skip the wait state entirely. Toggle it from the Management API:
curl -u admin:pw localhost:8088/rest/v1/targets/device-42/autoConfirm
# {"active": false}
curl -u admin:pw -X POST localhost:8088/rest/v1/targets/device-42/autoConfirm/activate
curl -u admin:pw -X POST localhost:8088/rest/v1/targets/device-42/autoConfirm/deactivate
Or by the device itself over DDI:
curl -X POST localhost:8088/DEFAULT/controller/v1/device-42/confirmationBase/activateAutoConfirm
curl -X POST localhost:8088/DEFAULT/controller/v1/device-42/confirmationBase/deactivateAutoConfirm
Activating auto-confirm releases any already-pending actions on that target —
they transition straight to running. New assignments to an auto-confirm target
never enter the wait state.
Auto-confirm by default
To run the flow only for the devices that actually support it, have every new target start out auto-confirming and deactivate it on the ones you want to confirm explicitly:
[ddi]
confirmation_flow = true
auto_confirm_default = true
This applies to targets created either way — DDI self-registration or
POST /rest/v1/targets. It only affects new targets; existing ones keep
whatever auto-confirm state they have.
Operator confirm/deny
There is currently no per-action operator confirm/deny over the Management API — device-driven confirm/deny is DDI-only. The operator path is to activate auto-confirm on the target (which releases pending actions), or to cancel the action.
Filtering with FIQL
Every Management API list endpoint accepts a q= query in FIQL (Feed Item
Query Language, the same dialect hawkBit uses, also called RSQL). raptor compiles
it to a database query.
Grammar
Comparison operators:
| Operator | Meaning |
|---|---|
== | equal (supports * wildcards → SQL LIKE) |
!= | not equal (supports * wildcards) |
=lt= | less than |
=le= | less than or equal |
=gt= | greater than |
=ge= | greater than or equal |
=in= | in a list: field=in=(a,b,c) |
=out= | not in a list |
Logical operators:
;— AND,— OR- AND binds tighter than OR; use parentheses to group.
Wildcards: * in a value becomes a SQL LIKE wildcard, so
controllerId==dev-* matches everything starting with dev-.
Examples
updateStatus==error
controllerId==beta-*
updateStatus=in=(pending,error)
name==prod-* ; updateStatus==in_sync
updateStatus==error , updateStatus==pending
URL-encode the query when passing it on the command line:
curl -u admin:pw 'localhost:8088/rest/v1/targets?q=updateStatus%3D%3Derror'
Filterable fields
Each resource exposes its own field map; an unknown field returns 400 Bad Request. Common maps:
- targets —
id/controllerId,name,description,updateStatus,lastControllerRequestAt,address,tag - distribution sets —
id,name,version,description,complete,tag - target tags / DS tags —
id,name,description,colour - actions —
id,active,detailStatus - rollouts —
id,name,status - target filters —
id,name
Boolean fields (e.g. active) accept true/false and compile to typed boolean
comparisons.
The tag field
tag is not a column but a membership test against the entity’s tags, so it
only accepts ==, !=, =in= and =out= — the ordering operators return
400. Negation applies to the membership, not the tag name: tag!=beta means
“not tagged beta”, so a target tagged both beta and stable is excluded.
tag==beta # targets tagged beta
tag!=beta # targets not tagged beta
tag=in=(beta,canary) # tagged with either
tag==beta ; updateStatus==error # tagged beta and in error
The same term works on /rest/v1/distributionsets and inside saved target
filter queries (and therefore rollout target filters).
Where FIQL is used
Beyond q= on list endpoints, the same grammar drives:
- Rollout target selection (
targetFilterQuery). - Target filter queries and their auto-assignment matching.
The query is validated when a rollout or target filter is created, so an invalid expression is rejected up front rather than failing silently later.
Authentication
raptor has two independent auth zones: the device-facing DDI zone and the operator-facing Management zone.
DDI zone (devices)
Devices authenticate on every DDI request with one of:
- Target security token —
Authorization: TargetToken <token>, matched against the target’s stored token. Each target has its own token (generated at creation, or supplied). - Gateway token —
Authorization: GatewayToken <token>, a single shared token from config. A valid gateway token also enables auto-registration of unknown controller IDs.
Configure them under [ddi]:
[ddi]
gateway_token = "shared-secret-for-registration"
Anonymous mode
[ddi]
anonymous = true
This disables DDI authentication entirely — any controller ID can poll and register. It’s convenient for local development and should not be used in production. It is off by default.
Management zone (operators)
The Management API and web console authenticate against a single admin credential using HTTP Basic:
[mgmt]
username = "admin"
password_hash = "$argon2id$..." # from `raptor hash-password`
The password is stored as an argon2id hash, never in plaintext. Generate it with:
$ printf 'yourpassword\n' | raptor hash-password
The web console also issues a session cookie (via POST /rest/v1/login) so
browser users don’t send Basic credentials on every request.
Note: raptor has a single admin user in this version. Multiple users, roles, OIDC, and certificate-based (mTLS) DDI auth are not yet implemented.
Deployment guidance
- Terminate TLS at a reverse proxy in front of raptor; the DDI tokens and Basic credentials are bearer secrets that must not travel in cleartext.
- Set
urlto the externally visible base URL so_linksare dialable through the proxy (see Configuration). - Prefer environment variables or the systemd
raptor.envfile for the gateway token and password hash rather than committing them to a shared config file (see Running as a systemd Service).
Using raptor with Zephyr
Zephyr’s mainline hawkBit client (subsys/mgmt/hawkbit) works against raptor’s
DDI API unchanged. This page is the minimal wiring, plus the two footguns worth
knowing before you flash a fleet.
Kconfig symbols below are from Zephyr’s subsys/mgmt/hawkbit/Kconfig; the
compatibility matrix at the end records what the client actually exercises.
Minimal configuration
Device side (prj.conf):
CONFIG_HAWKBIT=y
CONFIG_HAWKBIT_SERVER="ota.example.com"
CONFIG_HAWKBIT_PORT=8088
CONFIG_HAWKBIT_TENANT="DEFAULT"
CONFIG_HAWKBIT_POLL_INTERVAL=5 # minutes; 1..43200
# Pick one auth mode (see below)
CONFIG_HAWKBIT_DDI_GATEWAY_SECURITY=y
CONFIG_HAWKBIT_DDI_SECURITY_TOKEN="shared-registration-secret"
# If raptor is behind TLS
# CONFIG_HAWKBIT_USE_TLS=y
If CONFIG_HAWKBIT_SERVER is a hostname rather than an IP, the device also needs
CONFIG_DNS_RESOLVER=y.
Server side (raptor.toml):
url = "http://ota.example.com:8088"
[ddi]
gateway_token = "shared-registration-secret"
polling_interval = "00:05:00"
Keep polling_interval and CONFIG_HAWKBIT_POLL_INTERVAL consistent — raptor
advertises its value as config.polling.sleep (HH:MM:SS), which the client
reads and honours, so the Kconfig value is really just the pre-first-poll default.
Choosing an auth mode
The client sends exactly one of two headers, selected at build time:
| Kconfig choice | Header sent | raptor side |
|---|---|---|
CONFIG_HAWKBIT_DDI_GATEWAY_SECURITY=y | Authorization: GatewayToken <token> | [ddi] gateway_token = "<token>" |
CONFIG_HAWKBIT_DDI_TARGET_SECURITY=y | Authorization: TargetToken <token> | the target’s own securityToken |
Either way the token value goes in CONFIG_HAWKBIT_DDI_SECURITY_TOKEN.
Gateway token is one shared secret for the whole fleet and permits auto-registration: a device that polls with it is created on first contact. Good for bringing a fleet up, weaker blast radius if extracted from one device.
Target token is per-device, so the target must exist in raptor first, with a token you provision into the firmware:
curl -u admin:pw -X POST localhost:8088/rest/v1/targets \
-H 'Content-Type: application/json' \
-d '[{"controllerId": "device-42", "securityToken": "per-device-secret"}]'
See Authentication for the full picture. [ddi] anonymous = true disables DDI auth entirely — convenient for a bring-up on a lab network,
never in production.
Footgun 1: the tenant must be DEFAULT
Zephyr’s CONFIG_HAWKBIT_TENANT defaults to "default", and raptor is
single-tenant: it accepts any tenant segment but emits DEFAULT in every link it
returns. Case doesn’t matter (default and DEFAULT are the same tenant here),
but anything else leaves the device following hrefs that disagree with its own
configuration, and would break outright if multi-tenancy ever lands. raptor logs a
warning the first time it sees one. Set it to DEFAULT and move on.
Footgun 2: don’t enable the confirmation flow
The client parses exactly three _links keys from the base poll —
deploymentBase, configData, cancelAction. It has no confirmationBase
handling at all.
So with [ddi] confirmation_flow = true, a waiting target is offered only
confirmationBase, the device sees no link it understands, and it polls forever
without installing and without reporting an error. If you need the flow for other
clients, keep Zephyr devices out of it with:
[ddi]
confirmation_flow = true
auto_confirm_default = true # new targets auto-confirm
See Confirmation Flow.
Compatibility matrix
What the mainline Zephyr client uses, and raptor’s status for each. “Verified”
means covered by raptor’s integration tests, including a JSON-contract test
(zephyr_client_json_contract) pinning the exact response shape the client’s
strict JSON descriptors require.
| DDI feature | Zephyr client | raptor |
|---|---|---|
Base poll config.polling.sleep (HH:MM:SS) | reads and honours | ✅ verified |
_links.deploymentBase | parsed | ✅ verified |
_links.configData | parsed; uploads whenever present | ✅ verified — advertised only when attributes are wanted, so devices don’t re-upload every poll |
_links.cancelAction | parsed | ✅ verified |
_links.confirmationBase | not parsed | ⚠️ raptor supports it; do not enable for Zephyr (footgun 2) |
configData PUT, mode: "merge" | sends on every poll carrying the link | ✅ verified |
deploymentBase chunks/artifacts | parsed | ✅ verified |
Artifact hashes.sha256 | verified against flashed image | ✅ verified |
Artifact _links.download-http | the link it downloads from | ✅ verified — see download-http vs download |
Range: resume (CONFIG_HAWKBIT_SAVE_PROGRESS) | sends bytes=N- | ✅ verified — 206 + Content-Range |
Feedback execution / result | closed, proceeding, canceled, scheduled, rejected, resumed, none / success, failure, none | ✅ verified |
Authorization: TargetToken / GatewayToken | either, chosen at build time | ✅ verified |
| Multi-tenancy | CONFIG_HAWKBIT_TENANT | ⚠️ single-tenant, DEFAULT only (footgun 1) |
Verifying against a real device
raptor’s test suite drives its DDI API with the Rust hawkbit crate and the
JSON-contract test above — not with the Zephyr client itself, which needs
hardware or QEMU. For an end-to-end check, build Zephyr’s
samples/subsys/mgmt/hawkbit sample against a raptor instance and watch the
server log: a successful cycle is a base poll, a configData PUT, a
deploymentBase GET, artifact GETs, then feedback with execution: "closed".
Running as a systemd Service
The Debian package installs a hardened systemd unit at
/usr/lib/systemd/system/raptor.service. This guide covers how it’s wired and
how to operate it.
Layout
| Path | Purpose |
|---|---|
/usr/bin/raptor | the binary (web console embedded) |
/etc/raptor/config.toml | config, a dpkg conffile (edits survive upgrades) |
/var/lib/raptor/ | state: SQLite DB and artifact blobs |
/etc/raptor/raptor.env | optional, root-only secrets (not shipped) |
First start
The service is enabled but not started on install — you must set an admin password first:
$ raptor hash-password # type a password, copy the hash
$ sudoedit /etc/raptor/config.toml # set password_hash, choose DDI auth
$ sudo systemctl start raptor
$ systemctl status raptor
State directory
The unit uses systemd’s DynamicUser=yes with StateDirectory=raptor. That
means:
- raptor runs as a transient, unprivileged user allocated at runtime — there
is no
raptoruser to manage and nopostinstchown. /var/lib/raptoris created and owned by that user automatically, mode0750.
The default config points both the database and the artifact store there:
database_url = "sqlite:///var/lib/raptor/raptor.db?mode=rwc"
artifact_dir = "/var/lib/raptor/artifacts"
/var/lib is the FHS location for persistent, service-owned state — the
content-addressed artifact blobs are the source of truth (not regenerable
cache), which is why they live here rather than under /var/cache.
Secrets
The config file is world-readable because the DynamicUser must read it. Keep plaintext secrets out of it. Put them in a root-only environment file, which systemd reads before dropping privileges:
$ sudo install -m 600 /dev/null /etc/raptor/raptor.env
$ echo 'RAPTOR_DDI__GATEWAY_TOKEN=super-secret' | sudo tee -a /etc/raptor/raptor.env
$ sudo systemctl restart raptor
The unit loads it via EnvironmentFile=-/etc/raptor/raptor.env (the leading -
makes it optional). The mandatory password_hash is an argon2 hash and is safe
to keep in the config.
Hardening
The unit ships with a broad systemd sandbox: ProtectSystem=strict,
ProtectHome=yes, PrivateTmp=yes, NoNewPrivileges=yes,
MemoryDenyWriteExecute=yes, a restricted SystemCallFilter=@system-service, an
empty CapabilityBoundingSet, and address-family restrictions. raptor needs no
Linux capabilities because it binds a high port (8088) by default.
Note: If you change
bindto a privileged port (< 1024), grant the capability explicitly withAmbientCapabilities=CAP_NET_BIND_SERVICEvia a drop-in (systemctl edit raptor), or bind a high port and let the reverse proxy front it.
Logs
raptor logs to stdout/stderr, captured by the journal:
$ journalctl -u raptor -f
Adjust verbosity with the RUST_LOG env var (e.g. RUST_LOG=raptor=debug) via a
systemctl edit drop-in or raptor.env.
Web Console
raptor ships an optional web console — a Dioxus WASM single-page app — embedded directly in the binary.
Enabling it
The console is behind the embed-ui Cargo feature. The Debian package and the
release binaries are built with it on; if you build from source, include the
feature (see Installation):
$ dx build --release --package raptor-ui
$ cargo build --release --features embed-ui
Without the feature the server runs identically but does not serve the UI routes.
Accessing it
Browse to /ui:
http://localhost:8088/ui
Log in with the same admin credentials as the Management API. The console
authenticates via POST /rest/v1/login and holds a session cookie, so you don’t
re-enter credentials on every request.
What it covers
The console surfaces the core read/observe workflow and common actions:
- a dashboard (fleet counters, the actions feed, active rollouts, and the server’s configuration),
- targets and target detail,
- target filters and auto-assignment,
- tags — create and edit target and distribution-set tags, tag an entity from its detail page, and filter the target and distribution lists by tag,
- distribution sets and detail,
- software modules and detail,
- rollouts and rollout detail,
- the actions feed.
Dashboard
The counter tiles — targets, in sync, pending, error, running actions — come
from GET /rest/v1/system/statistics, so the server counts the whole fleet and
the numbers stay correct however large it grows. The active-rollout and
recent-action lists below them are feeds rather than counts and read the
ordinary list endpoints. Everything refreshes on the console’s 5s polling.
The System configuration card at the foot of the dashboard shows the tenant
configuration devices see (GET /rest/v1/system/configs): the polling interval,
whether the confirmation flow is on, and which authentication modes are enabled.
It is read-only — raptor takes its configuration from raptor.toml, and the API
answers writes to these keys with 403. See
Configuration to change any of them.
Rollouts
The rollouts list shows each rollout’s status and a progress bar of its targets; the detail page adds the deploy groups, each with its own bar and a legend of targets per status (finished / running / error / cancelled / scheduled / not started), refreshed by the same 5s polling as the rest of the console.
Lifecycle buttons — Start, Pause, Resume, Delete — appear for the transitions the rollout’s current status allows, so an operator can drive a rollout end to end without touching the API. Creating a rollout is still API-only; see the Rollouts guide.
Note: The console tracks the API and lags it slightly. A page for the confirmation flow is planned (tracked as an issue on GitHub). Anything not yet in the UI is always available through the Management API.
Architecture
raptor is a modular monolith: one process, one binary, with clear internal seams between the HTTP layer, the domain logic, and persistence.
Workspace
raptor/
├── Cargo.toml # workspace
├── raptor/ # main crate (bin + lib)
│ └── src/
│ ├── main.rs # CLI (serve, hash-password), startup, background tasks
│ ├── app.rs # router assembly
│ ├── config.rs # TOML + RAPTOR_* env overrides
│ ├── api/
│ │ ├── ddi/ # /{tenant}/controller/v1/... device handlers
│ │ └── mgmt/ # /rest/v1/... operator handlers
│ ├── domain/ # deployment state machine, rollouts, auto-assign
│ ├── entity/ # SeaORM entities
│ ├── fiql/ # FIQL/RSQL parser -> SeaORM Condition
│ ├── storage.rs # content-addressed artifact store
│ └── auth/ # tower middleware for both auth zones
├── raptor-api-types/ # shared Management API DTOs (also compiles to wasm)
├── raptor-ui/ # Dioxus web console (wasm32)
└── migration/ # sea-orm-migration crate (one set, both DBs)
Stack
- axum — HTTP routing and extractors.
- SeaORM — one entity/migration definition
targeting both SQLite and Postgres; the backend is chosen by the
database_urlscheme. - tokio — async runtime; also drives background tasks.
- winnow — the FIQL parser.
- sha2 / sha1 / md-5 — artifact hashing.
- argon2 — admin password hashing.
- Dioxus — the optional embedded web console.
Request path
- A request hits axum. A tower middleware enforces the right auth zone —
ddi_authfor/{tenant}/controller/v1/...,mgmt_authfor/rest/v1/.... - The handler (
api/ddiorapi/mgmt) validates input and calls into the domain layer. - The domain layer owns the rules — the action state machine, rollout evaluation, auto-assignment — and talks to persistence through entities.
- Artifact bytes bypass the database: they stream to/from the content-addressed store on disk.
Background tasks
A single tokio task runs on a fixed interval
(rollout_eval_interval_secs, default 5s) and performs two jobs:
- Rollout evaluation — advances or pauses running rollout groups based on action outcomes.
- Auto-assignment sweep — assigns target-filter distribution sets to newly-matching targets.
Shared DTOs
The raptor-api-types crate holds the Management API request/response types. It
compiles to wasm32 as well as native, so the server and the Dioxus web console
share exactly the same type definitions — the JSON contract can’t drift between
them. Round-trip tests assert the JSON shape matches hawkBit.
Persistence & migrations
There is one SeaORM entity set and one ordered list of migrations. They
run automatically at startup against whichever database database_url points to,
so upgrading raptor and migrating the schema is a single step: start the new
binary.
Domain Model
The entities below are the nouns of raptor. Understanding how they relate makes the API and the update lifecycle straightforward.
SoftwareModuleType DistributionSetType
│ │
▼ ▼
SoftwareModule ──< DsModule >── DistributionSet
│ │
▼ │ assigned / installed
Artifact ▼
(blob on disk) Target ──< Action >── (a deployment)
▲ │ │
│ ▼ ▼
content-addressed TargetAttribute ActionStatus
(+ messages)
Rollout ──< RolloutGroup >── RolloutTargetGroup
TargetFilter (optional auto-assign DS)
Core entities
- Target — a device, keyed by unique
controllerId. Holds a security token, anupdateStatus(unknown→registered→pending→in_sync/error), the last poll time and request address, anauto_confirmflag, and pointers to its assigned and installed distribution sets. - TargetAttribute — key/value pairs a device reports via DDI
configData. - SoftwareModule — a named, versioned unit of a seeded type (
os,firmware,runtime,application). Owns artifacts. - Artifact — a file (filename, size, sha1/md5/sha256) belonging to one module. The blob is stored once per sha256 on disk; artifact rows are references to it.
- DistributionSet — a named, versioned bundle of modules of a seeded type
(
os,os_app,app).completewhen it has the modules its type requires; only complete sets are assignable. - Action — one deployment of a DS to a target. Carries a
status(the action state machine), anactiveflag, and aforced/soft indicator. May belong to a rollout group. Invariant: at most one active action per target. - ActionStatus — an append-only history row (with optional messages) written on every device feedback and every server-side transition.
Rollout entities
- Rollout — a staged deployment of a DS to the targets matched by a FIQL filter, split into groups, with success/error thresholds.
- RolloutGroup — one stage; has its own status and thresholds.
- RolloutTargetGroup — the membership join: which targets are in which group (a static snapshot taken at creation).
Target filters
- TargetFilter — a saved FIQL query with an optional attached auto-assign distribution set and action type. Drives auto-assignment.
Seeded types
Software-module and distribution-set types are read-only in this version, created by the initial migration:
- Software-module types:
os,firmware,runtime,application - Distribution-set types:
os,os_app,app
Tenancy
There is no tenant column. raptor is single-tenant; the DDI tenant URL segment is
accepted and ignored, and all generated links use DEFAULT.
Update Lifecycle
This is the heart of raptor: how an assignment becomes an installed update, and how each party’s actions move the state machine.
The happy path
operator server device
──────── ────── ──────
assignedDS ──────────▶ Action(running), target=pending
◀────── poll (sees deploymentBase)
◀────── GET deploymentBase
(streams artifacts) ◀────── download artifacts
◀────── feedback: proceeding
ActionStatus += proceeding
◀────── feedback: closed/success
Action(finished), target=in_sync
◀────── poll (no deploymentBase;
installedBase set)
- Assign.
POST /rest/v1/targets/{id}/assignedDScreates an Action inrunning(orwait_for_confirmationif the confirmation flow is on), and sets the target topending. Any prior active action is cancelled. - Poll. The device polls the DDI root and sees
_links.deploymentBase. - Fetch & download. The device gets
deploymentBase/{actionId}and downloads the listed artifacts (with HTTP Range resume). - Feedback. The device reports
proceeding, then a final result. Each report appends an ActionStatus row. - Complete. On
closed/successthe Action becomesfinished, the target becomesin_sync, andinstalledBasereflects the deployment. Onclosed/failurethe Action becomeserrorand the target becomeserror.
Feedback vocabulary
Device feedback is {"status":{"execution": ..., "result":{"finished": ...}}}.
execution∈proceeding,scheduled,resumed,downloading,downloaded,canceled,rejected,closed.result.finished∈none,success,failure.
Only closed (with success or failure) is terminal; the others are recorded
as history and leave the action active.
Confirmation flow
With the confirmation flow enabled, a new
assignment lands in wait_for_confirmation and the poll offers confirmationBase
instead of deploymentBase. A confirmed feedback moves it to running; the
next poll then follows the happy path above.
Cancellation
operator server device
──────── ────── ──────
DELETE action ────────▶ Action(canceling)
◀────── poll (sees cancelAction)
◀────── GET cancelAction
◀────── cancel feedback: closed
Action(canceled); assigned reverts to installed
A normal cancel sets the action to canceling and offers the device a
cancelAction link; the device confirms and the action becomes canceled. A
forced cancel (?force=true) closes it immediately, without waiting for the
device.
Auto-registration
An unknown controllerId that polls with a valid gateway token (or in anonymous
mode) is created on the spot as registered. If an
auto-assignment filter matches it, the DS is
assigned during that same poll — so a brand-new device can receive a
deploymentBase on its very first request.
Configuration Reference
raptor reads a TOML file (default raptor.toml, override with
serve --config <path>). Every key can be overridden by a RAPTOR_*
environment variable; nested tables use a __ separator (e.g.
RAPTOR_DDI__ANONYMOUS).
Top level
| Key | Type | Default | Description |
|---|---|---|---|
bind | socket addr | 0.0.0.0:8088 | address the HTTP server listens on |
database_url | string | (required) | sqlite://… or postgres://…; selects the backend |
artifact_dir | path | (required) | root of the content-addressed artifact store |
max_artifact_size | integer (bytes) | 1073741824 (1 GiB) | maximum artifact upload size |
url | string | (unset) | external base URL for _links; derived from the Host header when unset |
rollout_eval_interval_secs | integer | 5 | how often the background evaluator / auto-assign sweep runs |
[ddi] — device-facing API
| Key | Type | Default | Description |
|---|---|---|---|
anonymous | bool | false | disable all DDI auth (dev only) |
gateway_token | string | (unset) | shared token; enables auto-registration |
polling_interval | string HH:MM:SS | 00:05:00 | poll sleep advertised to devices |
confirmation_flow | bool | false | require confirmation before a deployment starts |
auto_confirm_default | bool | false | give newly created targets autoConfirm, so confirmation_flow can’t strand confirmation-unaware clients |
artifact_http_url | string | (unset) | plain-HTTP base advertised in the DDI download-http links; unset means they reuse url |
trusted_proxy_header | string | (unset) | header to read the device address from behind a reverse proxy, e.g. x-forwarded-for; unset uses the socket peer |
[mgmt] — Management API / web console
| Key | Type | Default | Description |
|---|---|---|---|
username | string | (required) | admin username |
password_hash | string | (required) | argon2id hash from raptor hash-password |
Example
bind = "0.0.0.0:8088"
database_url = "postgres://raptor:raptor@localhost/raptor"
artifact_dir = "/var/lib/raptor/artifacts"
max_artifact_size = 2147483648 # 2 GiB
url = "https://raptor.example.com"
rollout_eval_interval_secs = 10
[ddi]
anonymous = false
gateway_token = "shared-registration-secret"
polling_interval = "00:05:00"
confirmation_flow = true
[mgmt]
username = "admin"
password_hash = "$argon2id$v=19$m=19456,t=2,p=1$..."
Environment overrides
RAPTOR_BIND=127.0.0.1:9090
RAPTOR_DATABASE_URL=sqlite://raptor.db?mode=rwc
RAPTOR_DDI__ANONYMOUS=true
RAPTOR_DDI__GATEWAY_TOKEN=super-secret
RAPTOR_MGMT__PASSWORD_HASH='$argon2id$...'
Environment values take precedence over the TOML file — the recommended way to inject secrets.
Management API Reference
Operator-facing REST API under /rest/v1, authenticated with HTTP Basic (or a
session cookie). All list endpoints accept offset, limit, sort=field:ASC|DESC,
and q=<FIQL>, and return the hawkBit paged envelope {content, total, size}.
Base URL examples assume localhost:8088.
Handlers live under raptor/src/api/mgmt/, one module per resource family,
each exposing a routes() that mod.rs merges — e.g. target endpoints in
targets.rs, distribution-set/software-module/target type endpoints in
types/.
Auth & session
| Method | Path | Description |
|---|---|---|
POST | /rest/v1/login | exchange credentials for a session cookie |
POST | /rest/v1/logout | clear the session |
GET | /rest/v1/session | 204 if the request is authenticated, 401 if not |
GET | /health | liveness probe (returns ok) |
Targets
| Method | Path | Description |
|---|---|---|
POST | /rest/v1/targets | create targets (JSON array) |
GET | /rest/v1/targets | list (paging/sort/FIQL) |
GET | /rest/v1/targets/{cid} | get one |
PUT | /rest/v1/targets/{cid} | update name/description/token/requestAttributes |
DELETE | /rest/v1/targets/{cid} | delete |
GET | /rest/v1/targets/{cid}/attributes | device-reported attributes |
POST / DELETE | /rest/v1/targets/{cid}/targettype | assign / unassign the target type |
POST / GET | /rest/v1/targets/{cid}/metadata | create (JSON array) / list metadata |
| GET / PUT / DELETE | /rest/v1/targets/{cid}/metadata/{key} | get / update / delete one entry |
| POST | /rest/v1/targets/{cid}/assignedDS | assign a DS (creates an action) |
| GET | /rest/v1/targets/{cid}/assignedDS | currently assigned DS (or 204) |
| GET | /rest/v1/targets/{cid}/installedDS | last installed DS (or 204) |
| GET | /rest/v1/targets/{cid}/actions | actions for this target |
| GET | /rest/v1/targets/{cid}/actions/{aid} | one action |
| PUT | /rest/v1/targets/{cid}/actions/{aid} | escalate the force type, {"forceType":"forced"} |
| GET | /rest/v1/targets/{cid}/actions/{aid}/status | action status history (paging/sort) |
| DELETE | /rest/v1/targets/{cid}/actions/{aid} | cancel (?force=true to force) |
| GET | /rest/v1/targets/{cid}/autoConfirm | auto-confirm state |
| POST | /rest/v1/targets/{cid}/autoConfirm/activate | enable auto-confirm |
| POST | /rest/v1/targets/{cid}/autoConfirm/deactivate | disable auto-confirm |
A target’s requestAttributes flag controls whether its DDI poll advertises the
configData link. It is set when the target is created and cleared once the
device uploads its attributes; re-arm it to ask for a fresh upload:
curl -u admin:pw -X PUT localhost:8088/rest/v1/targets/device-42 \
-H 'Content-Type: application/json' -d '{"requestAttributes": true}'
Embedded set and tag references
GET /rest/v1/targets and GET /rest/v1/targets/{cid} include the target’s
installed and assigned distribution sets, and its tags, inline:
{
"controllerId": "dev-a91f3c",
"updateStatus": "pending",
"installedDs": { "id": 4, "name": "gw-linux", "version": "2026.05", "type": "os_app" },
"assignedDs": { "id": 7, "name": "gw-linux", "version": "2026.07", "type": "os_app" },
"tags": [{ "id": 2, "name": "linux-gw", "colour": "#4f9cf9" }]
}
A raptor extension, and additive: hawkBit exposes these only as installedDS,
assignedDS and per-tag reverse lookups, so a list view could otherwise only
fetch them one request per row. The fields are omitted when absent — no
installed set is different from an empty one — and every other endpoint that
returns a target (create, tag assignment) serialises exactly as before.
Resolved in a fixed number of queries for the whole page regardless of page size,
because assigned_ds_id and installed_ds_id are columns on the target row.
The set type is the type key (os, app, os_app), not its id: it is what
says whether a given device class can install that set at all.
Software modules & artifacts
| Method | Path | Description |
|---|---|---|
POST / GET | /rest/v1/softwaremodules | create / list |
GET / PUT / DELETE | /rest/v1/softwaremodules/{id} | get / update / delete |
POST / GET | /rest/v1/softwaremodules/{id}/artifacts | upload (multipart) / list |
GET / DELETE | /rest/v1/softwaremodules/{id}/artifacts/{aid} | get / delete |
GET | /rest/v1/softwaremodules/{id}/artifacts/{aid}/download | download |
POST / GET | /rest/v1/softwaremodules/{id}/metadata | create (JSON array) / list metadata |
GET / PUT / DELETE | /rest/v1/softwaremodules/{id}/metadata/{key} | get / update / delete one entry (targetVisible surfaces to devices) |
Distribution sets
| Method | Path | Description |
|---|---|---|
POST / GET | /rest/v1/distributionsets | create / list |
GET / PUT / DELETE | /rest/v1/distributionsets/{id} | get / update / delete |
POST | /rest/v1/distributionsets/{id}/invalidate | invalidate (stops rollouts / auto-assign, cancels actions) |
POST / GET | /rest/v1/distributionsets/{id}/assignedSM | add / list modules |
POST / GET | /rest/v1/distributionsets/{id}/metadata | create (JSON array) / list metadata |
GET / PUT / DELETE | /rest/v1/distributionsets/{id}/metadata/{key} | get / update / delete one entry |
Actions (fleet-wide)
| Method | Path | Description |
|---|---|---|
GET | /rest/v1/actions | list all actions (paging/sort/FIQL) |
GET | /rest/v1/system/configs | tenant configuration (read-only; file-driven) |
GET / PUT / DELETE | /rest/v1/system/configs/{key} | one config key (writes → 403) |
GET | /rest/v1/system/statistics | fleet counters (targets/actions/…), optional q= |
Scoping statistics to a filter
/rest/v1/system/statistics accepts an optional FIQL q=, the same query the
target list and saved target filters take:
curl -u admin:pw 'localhost:8088/rest/v1/system/statistics?q=tag%3D%3Dlinux-gw'
q narrows the target counters — totalTargets and targetsByStatus — to
the matching targets. It lets a caller read one saved filter’s in-sync /
pending / error split in a single request rather than one request per status.
The catalogue and action counters (totalDistributionSets,
totalSoftwareModules, totalActions, totalRollouts, activeActions) stay
fleet-wide whether or not q is set: scoping “how many distribution sets exist”
to a set of targets has no meaning, and reporting them as 0 would read as
missing data. An unparseable or unknown-field query returns 400.
Types
Software-module, distribution-set and target types support full CRUD. The
default os / firmware / runtime / application module types and os /
os_app / app DS types are seeded, and a DS type’s mandatory module types
drive whether a distribution set is complete. Deleting a type that is still in
use returns 409.
| Method | Path | Description |
|---|---|---|
POST / GET | /rest/v1/softwaremoduletypes | create / list module types |
GET / PUT / DELETE | /rest/v1/softwaremoduletypes/{id} | get / update (description) / delete |
POST / GET | /rest/v1/distributionsettypes | create (with mandatory/optional module types) / list |
GET / PUT / DELETE | /rest/v1/distributionsettypes/{id} | get / update (description) / delete |
GET / POST | /rest/v1/distributionsettypes/{id}/mandatorymoduletypes | list / add mandatory module type |
DELETE | /rest/v1/distributionsettypes/{id}/mandatorymoduletypes/{mid} | remove mandatory module type |
GET / POST | /rest/v1/distributionsettypes/{id}/optionalmoduletypes | list / add optional module type |
DELETE | /rest/v1/distributionsettypes/{id}/optionalmoduletypes/{mid} | remove optional module type |
POST / GET | /rest/v1/targettypes | create (with compatible DS types) / list |
GET / PUT / DELETE | /rest/v1/targettypes/{id} | get / update / delete |
GET / POST | /rest/v1/targettypes/{id}/compatibledistributionsettypes | list / add compatible DS type |
DELETE | /rest/v1/targettypes/{id}/compatibledistributionsettypes/{dsid} | remove compatible DS type |
Tags
Target and distribution-set tags are free-form labels (name, description,
colour) used for fleet organisation and as the tag== FIQL term on the target
and distribution-set lists. Deleting a tag removes its assignments; the tagged
targets and sets are untouched. Assignment is idempotent — re-assigning an
already-tagged entity succeeds without creating a duplicate.
| Method | Path | Description |
|---|---|---|
POST / GET | /rest/v1/targettags | create (array body) / list |
GET / PUT / DELETE | /rest/v1/targettags/{id} | get / update / delete |
GET | /rest/v1/targettags/{id}/assigned | list tagged targets (paging, sort=, q=) |
GET | /rest/v1/targets/{cid}/tags | tags carried by one target (raptor extension) |
POST / DELETE | /rest/v1/targettags/{id}/assigned | bulk assign / unassign, body ["dev-1","dev-2"] |
POST / DELETE | /rest/v1/targettags/{id}/assigned/{cid} | assign / unassign one target |
POST / GET | /rest/v1/distributionsettags | create (array body) / list |
GET / PUT / DELETE | /rest/v1/distributionsettags/{id} | get / update / delete |
GET | /rest/v1/distributionsettags/{id}/assigned | list tagged distribution sets |
GET | /rest/v1/distributionsets/{id}/tags | tags carried by one set (raptor extension) |
POST / DELETE | /rest/v1/distributionsettags/{id}/assigned | bulk assign / unassign, body [1,2] |
POST / DELETE | /rest/v1/distributionsettags/{id}/assigned/{dsid} | assign / unassign one set |
Rollouts
| Method | Path | Description |
|---|---|---|
POST / GET | /rest/v1/rollouts | create / list |
GET / DELETE | /rest/v1/rollouts/{id} | get / delete |
POST | /rest/v1/rollouts/{id}/start | start (schedules first group) |
POST | /rest/v1/rollouts/{id}/pause | pause |
POST | /rest/v1/rollouts/{id}/resume | resume |
GET | /rest/v1/rollouts/{id}/deploygroups | list groups |
GET | /rest/v1/rollouts/{id}/deploygroups/{gid} | one group |
GET | /rest/v1/rollouts/{id}/deploygroups/{gid}/targets | controllerIds in a group |
Rollout and group payloads carry totalTargetsPerStatus (notstarted, scheduled,
running, error, finished, cancelled) — see the
Rollouts guide.
Target filters
| Method | Path | Description |
|---|---|---|
POST / GET | /rest/v1/targetfilters | create / list |
GET / PUT / DELETE | /rest/v1/targetfilters/{id} | get / update / delete |
GET / POST / DELETE | /rest/v1/targetfilters/{id}/autoAssignDS | read / attach / detach auto-assign DS |
Common status codes
| Code | When |
|---|---|
200 / 201 | success / created |
204 | no content (e.g. no assigned DS) |
400 | invalid FIQL or malformed body |
401 | bad or missing credentials |
404 | unknown entity |
409 | duplicate key (e.g. module name+version+type) |
410 | feedback for a non-active action |
See Error Codes for the response body shape.
DDI API Reference
The device-facing API, under /{tenant}/controller/v1/{controllerId}. Requests
are authenticated by target token, gateway token, or anonymous mode — see
Authentication.
Configure clients with tenant
DEFAULT. raptor is single-tenant: thetenantpath segment is accepted and ignored, and every emitted link saysDEFAULT. A device configured with anything else (Zephyr:CONFIG_HAWKBIT_TENANT) still works — it just follows hrefs that disagree with its own config, and it would break if multi-tenancy landed later. raptor logs a warning the first time it sees a non-DEFAULTpoll.
Response JSON matches the hawkBit DDI v1 schemas field-for-field.
Handlers live under raptor/src/api/ddi/, one module per resource: poll root
in root.rs, deployment/installed base in deployment.rs, feedback/cancel in
feedback.rs, confirmation flow in confirmation.rs, and artifacts in
artifacts.rs, all wired together in mod.rs.
Endpoints
| Method | Path (under /{tenant}/controller/v1/{cid}) | Description |
|---|---|---|
GET | / | poll root: config.polling.sleep + _links |
PUT | /configData | report device attributes (merge / replace / remove) |
GET | /deploymentBase/{actionId} | the deployment to install |
POST | /deploymentBase/{actionId}/feedback | deployment progress/result |
GET | /confirmationBase/{actionId} | pending deployment awaiting confirmation |
POST | /confirmationBase/{actionId}/feedback | confirm / deny |
POST | /confirmationBase/activateAutoConfirm | device enables auto-confirm |
POST | /confirmationBase/deactivateAutoConfirm | device disables auto-confirm |
GET | /cancelAction/{actionId} | cancellation to acknowledge |
POST | /cancelAction/{actionId}/feedback | confirm cancellation |
GET | /installedBase/{actionId} | last successfully installed deployment |
GET | /softwaremodules/{moduleId}/artifacts | artifact list for a module |
GET | /softwaremodules/{moduleId}/artifacts/{filename} | artifact download (HTTP Range) |
GET | /softwaremodules/{moduleId}/artifacts/{filename}.MD5SUM | md5sum-file |
Poll root
{
"config": { "polling": { "sleep": "00:05:00" } },
"_links": {
"configData": { "href": ".../configData" },
"deploymentBase": { "href": ".../deploymentBase/7" }
}
}
Which _links appear depends on the target’s state: deploymentBase when an
action is running, confirmationBase when it’s wait_for_confirmation,
cancelAction when it’s canceling, and installedBase once something has been
installed.
configData appears only while raptor actually wants attributes — a freshly
registered target, or one an operator has re-armed with requestAttributes (see
Management API). It disappears as soon as a
configData PUT arrives. This matters because some clients (the Zephyr hawkbit
client among them) re-upload their entire attribute set on every poll that
carries the link, which is wasted uplink on a large or cellular fleet.
deploymentBase
{
"id": "7",
"deployment": {
"download": "forced",
"update": "forced",
"chunks": [
{ "part": "os", "version": "1.0", "name": "rootfs",
"artifacts": [
{ "filename": "rootfs.img", "size": 12345,
"hashes": { "sha1": "...", "md5": "...", "sha256": "..." },
"_links": { "download-http": {"href": "..."},
"md5sum-http": {"href": "..."} } }
],
"metadata": [ { "key": "signature", "value": "sig-1" } ] }
]
},
"actionHistory": { "status": "RUNNING", "messages": [] }
}
download/update follow the action’s type, matching hawkBit’s
calculateDownloadType/calculateUpdateType:
| Action type | download | update |
|---|---|---|
forced | forced | forced |
soft | attempt | attempt |
timeforced, before forcetime | attempt | attempt |
timeforced, after forcetime | forced | forced |
downloadonly | forced | skip |
The modes are computed per request, so a timeforced action flips on its own
once the deadline passes. See Assignments & Actions for
the operator side. The confirmationBase response is identical but keyed
confirmation instead of deployment.
A chunk’s metadata array carries any software-module metadata marked
targetVisible (see the Management API). The key is omitted entirely when a
module has no visible metadata.
Feedback
{ "status": { "execution": "closed", "result": { "finished": "success" } } }
execution∈proceeding,scheduled,resumed,downloading,downloaded,canceled,rejected,closed.result.finished∈none,success,failure.
closed is terminal for every action type. For a downloadonly action
downloaded is also terminal — that is the whole job, so the action closes
with detailStatus: downloaded and the target’s installed DS is left unchanged.
For all other types downloaded is recorded as progress only. Posting feedback
to a non-active action returns 410 Gone.
Confirmation feedback uses a different body:
{ "confirmation": "confirmed", "details": ["…"] } // or "denied"
configData
{ "mode": "merge", "data": { "hw": "rev2", "os": "linux" } }
mode ∈ merge (default; upsert keys), replace (drop all, then set), remove
(delete the listed keys). Extra legacy fields in the body are ignored.
Artifact download & Range
The artifact download endpoint honors HTTP Range (RFC 7233) so an interrupted
download resumes with a 206 Partial Content response rather than restarting.
download-http vs download
Each artifact carries up to two link families, following hawkBit’s convention:
download-http / md5sum-http are the plain-HTTP URLs, download / md5sum
the HTTPS ones. Clients pick one and take the scheme from the href itself — the
Zephyr hawkbit client uses download-http.
raptor only advertises a genuinely different plain-HTTP URL when you have told it
one is reachable, via [ddi] artifact_http_url:
url = "https://ota.example.com"
[ddi]
artifact_http_url = "http://dl.example.com:8088"
| Config | download-http | download |
|---|---|---|
url http, no artifact_http_url | the url (http) | (absent) |
url https, no artifact_http_url | the url (https) | the url (https) |
url https + artifact_http_url | artifact_http_url (http) | the url (https) |
The middle row is why download-http can carry an https:// href: on a TLS-only
deployment there is no plain-HTTP port to point at, and emitting one anyway would
break every client that follows the link. Set artifact_http_url when you want
device downloads to bypass TLS.
CLI Reference
The raptor binary has two subcommands.
raptor serve
Runs the server: loads config, connects to the database, applies migrations, starts the background evaluator, and serves HTTP.
$ raptor serve [--config <path>]
| Flag | Default | Description |
|---|---|---|
--config <path> | raptor.toml | path to the TOML config file |
Migrations are applied automatically before the listener starts. The server logs
to stdout/stderr; control verbosity with the RUST_LOG environment variable,
e.g. RUST_LOG=raptor=debug,tower_http=info.
$ RUST_LOG=raptor=debug raptor serve --config /etc/raptor/config.toml
raptor listening bind=0.0.0.0:8088
raptor hash-password
Reads a password from stdin and prints its argon2id hash for use in
mgmt.password_hash.
$ printf 'yourpassword\n' | raptor hash-password
$argon2id$v=19$m=19456,t=2,p=1$...
Pipe from a file or a secrets manager rather than typing interactively in scripts. The output is safe to store in the config file (it’s a hash, not the password).
Version
$ raptor --version
Error Codes
raptor returns hawkBit-shaped error bodies so that existing clients — which
branch on the HTTP status code and sometimes the errorCode string — behave
identically against raptor.
Body shape
{
"exceptionClass": "org.eclipse.hawkbit.repository.exception.EntityNotFoundException",
"errorCode": "hawkbit.server.error.repo.entitiyNotFound",
"message": "target not found"
}
The exceptionClass and errorCode strings mirror hawkBit’s for the covered
cases (including hawkBit’s historical entitiy spelling), because some clients
match on them. Most clients branch on the status code, which is the hard
contract.
Status codes
| Code | Meaning | Example |
|---|---|---|
400 Bad Request | invalid FIQL or malformed body | q=bogusField==1, incomplete DS assignment |
401 Unauthorized | bad or missing credentials | wrong target token, missing Basic auth |
404 Not Found | unknown entity | target / module / action doesn’t exist |
409 Conflict | duplicate key | module name+version+type, duplicate filter name |
410 Gone | feedback for a non-active action | device reports on a finished/canceled action |
Notable errorCode strings
| errorCode | Paired status |
|---|---|
hawkbit.server.error.repo.entitiyNotFound | 404 |
hawkbit.server.error.rest.body.notReadable | 400 |
hawkbit.server.error.unauthorized | 401 |
hawkbit.server.error.repo.entitiyAlreadyExists | 409 |
hawkbit.server.error.repo.actionNotActive | 410 |
Auth responses
401 from the Management zone includes a WWW-Authenticate: Basic header so
tools prompt for credentials. The web console’s own session checks use a “quiet”
variant that omits the header, to avoid triggering the browser’s native Basic-auth
dialog on the single-page app.
FAQ
Is raptor a drop-in replacement for hawkBit?
For the DDI v1 device protocol, yes — devices using SWUpdate, the RAUC hawkbit-updater, or other hawkBit DDI clients work unchanged. For the Management API, raptor implements the core workflow but not every endpoint; see hawkBit Compatibility.
Which databases are supported?
SQLite and Postgres, selected by the database_url scheme. Migrations run
automatically at startup. SQLite is great for small/single-node deployments;
Postgres for larger fleets or when you want an external managed database.
Does raptor need RabbitMQ?
No. raptor implements only the HTTP-based DDI device path, not hawkBit’s DMF (AMQP) path, so there is no broker to run.
Can I run multiple tenants on one raptor?
No. raptor is single-tenant. It accepts and ignores the tenant segment in DDI URLs (so tenant-configured clients still work), but there is no data isolation. Run one raptor instance per fleet.
How do devices authenticate?
Per-target security tokens, a shared gateway token (which also enables auto-registration), or anonymous mode for development. See Authentication.
Where does raptor store artifacts?
In a content-addressed store on local disk under artifact_dir, laid out as
<sha256[0..2]>/<sha256>. Identical content is stored once. The Debian package
defaults this to /var/lib/raptor/artifacts.
Can I offload artifact storage to S3?
Not yet — storage is local disk only. S3/object-storage backends are a tracked enhancement.
How do I change the admin password?
Generate a new hash with raptor hash-password, put it in mgmt.password_hash
(or the RAPTOR_MGMT__PASSWORD_HASH env var), and restart. There is a single
admin user in this version.
How do I make a deployment wait for approval?
Enable the confirmation flow
([ddi] confirmation_flow = true). Assignments then wait for the device (or an
operator via auto-confirm) before deploying. Note this is per-assignment
confirmation, not hawkBit’s separate rollout approval workflow, which isn’t
implemented.
Why is my deploymentBase returning 404?
The action is not in a state that serves a deployment. If the confirmation flow
is on, the device should be using confirmationBase until the action is
confirmed; deploymentBase only serves running actions. Check the action’s
detailStatus via GET /rest/v1/targets/{cid}/actions/{aid}.
How do I upgrade raptor?
Install the new binary/package and restart. Schema migrations apply automatically
on startup. With the Debian package, your /etc/raptor/config.toml edits are
preserved (it’s a dpkg conffile).
Where do I report bugs or request features?
On the GitHub repository. The compatibility matrix lists what’s not yet implemented; most of those items have tracking issues.