Over-the-air updates for fleets of devices β and for the machines that only make sense updated together.
Speaks Eclipse hawkBit 1.1.0, so your devices and tools work unchanged.
Adds release channels, centres, and an orchestrator for systems of devices.
π Documentation Β· π Get started Β· π±οΈ Console guide Β· β¨οΈ API reference
An update server has one job: get the right software onto the right machines without breaking any of them. Qawk does that job the way a real fleet works.
π It is hawkBit, so nothing has to change. All 16 operations of the device API and all 153 of the Management API, with hawkBit's JSON, status codes, error codes, query language, paging and download headers. A device running SWUpdate's suricatta is pointed at a different host and carries on. A script written for hawkBit keeps working.
π¦ And then it is more than hawkBit. Releases move down a pipeline β
dev β beta β prod β through a gate that will not open until enough devices have
run the release for long enough. Devices are grouped by where they physically
are. Machines that work together are updated in the order you decide, and put
back together when one of them fails.
dev ββpromoteβββΆ beta ββpromoteβββΆ prod
β β β
given directly gate opens gate + a
by itself second person
| π hawkBit, the same | All 16 device operations and all 153 management operations. A contract test replays a flow recorded from a real hawkBit 1.1.0 and compares every answer field by field; the five deliberate differences are listed with their reasons. |
| π¦ Channels and a release pipeline | Devices join by rule (attribute.ring==beta); channels chain dev β beta β prod; a release is promoted through a gate (devices on it, share of success, soak time) and, when asked, a second person's approval. It goes out in waves and halts by itself over an error threshold. Freezes. Temporary channels whose devices go home afterwards. |
| π§© Systems (the orchestrator) | Devices that work together, updated as one, after Mender Orchestrator β components in a chosen order, the whole system rolled back when one device fails, and one system taken again once you know why. Mender's topology and manifest YAML in and out. Runs server-side: nothing is installed on the devices. |
| π’ Centres | A device says where it is (attribute.centerid), a centre goes in a channel, and all its devices follow. You move a place, not a list of machines. |
| β° Scheduling | Maintenance windows (a Quartz cron, a duration, an offset), rollouts that start at a set time, time-forced and download-only deployments. |
| π₯ Users | Users and roles in the database with hawkBit's permissions; built-in admin, operator, release-manager, viewer; personal API tokens; users from a file at startup. |
| π Audit log | Every change and every refused sign-in: who, what, when, from where. |
| π HTTPS, on its own | A certificate and a key and it terminates TLS itself β TLS 1.2 floor, 1.3 when the client can. Mutual TLS with QAWK_TLS_CLIENT_CA; a redirect for devices still on the old plain-HTTP URL. Or put a proxy in front, as before. |
| π Operations | Prometheus at /metrics, OpenTelemetry over OTLP, /live and /health, download progress per device, background jobs elected through PostgreSQL so any number of instances can run. |
| β‘ Scale | Measured with 10,000 devices polling every 30 s: 333 requests/s, p99 2 ms. |
π₯οΈ The console (console/) β dashboard, targets with the columns
you choose, distribution sets and modules with upload checks, deployments by
device, type or query with a live match count, rollouts, filters and
auto-assignment, channels and the pipeline, systems and centres, users, roles,
tokens and the audit log, two themes, notifications. Plain JavaScript, no build
step. Against a stock hawkBit it shows the hawkBit part and hides the rest.
π§ͺ Tools, in the server image β qawk-sim: simulated devices that register,
poll, take updates and report how they went. qawk-load: a load generator.
Needs docker, python3 and curl, and ports 8080 and 8090 free.
git clone https://github.com/padovanl/qawk.git
cd qawk
demo/start.shIt builds the images, starts PostgreSQL, the server and the console, loads sample data and starts simulated devices:
- π¦ a catalogue:
app1.0.0 β 1.2.0,app-broken1.3.0 (the simulated devices fail anything named broken, on purpose),os,device2-fw,device3-fw; - π¦ channels
devβbetaβprod(prod needs approval) and a temporaryexpo; - π about 270 simulated devices and 16 simulated systems β a
device1with twodevice2and adevice3; - π’ four centres, each holding both kinds of machine:
c01/c02inbeta,c03/c04inprod. Every device in them reports itscenteridand follows its centre, the way a real fleet is arranged βdevandexpohave none and reach their channel by rule instead; βΆοΈ devalready hasapp1.1.0, so something is moving when you open the page.
π Open http://localhost:8090, sign in as admin / changeme.
demo/start.sh seed # load the sample data again
demo/start.sh down # stop and remove everything (the demo keeps no data)| Variable | Default | |
|---|---|---|
QAWK_ADMIN_PASSWORD |
changeme |
the administrator's password |
QAWK_PORT / CONSOLE_PORT |
8080 / 8090 |
the server, the console |
PROD_DEVICES |
200 |
simulated devices in prod |
SYSTEMS |
16 |
simulated systems |
Things to try π― β promote dev's release to beta from Fleets and watch
the waves Β· give dev the broken release and watch it halt by itself Β· approve
a release into prod Β· deploy manifest device-system-2.0 in Orchestrator and
watch sixteen systems update in order.
Build them β you need only Docker, and the console has nothing to install: no npm, no bundler, no build step.
docker build -t qawk:local server/
docker build -t qawk-console:local console/π¦ No images are published yet. When you publish your own, give the server its version β it reports it at
/qawk/v1/infoand on the console's About page:V=0.1.0 docker build --build-arg VERSION=$V -t <you>/qawk:$V server/ docker build -t <you>/qawk-console:$V console/
docker network create qawk
docker run -d --name qawk-db --network qawk --restart unless-stopped \
-v qawk-db:/var/lib/postgresql/data \
-e POSTGRES_USER=qawk -e POSTGRES_PASSWORD='db-secret' -e POSTGRES_DB=qawk \
postgres:16-alpine
docker run -d --name qawk --network qawk --restart unless-stopped -p 8080:8080 \
-v qawk-artifacts:/var/lib/qawk \
-e QAWK_DATABASE_URL='postgres://qawk:db-secret@qawk-db:5432/qawk?sslmode=disable' \
-e QAWK_ADMIN_PASSWORD='a-long-admin-password' \
qawk:local
docker run -d --name qawk-console --network qawk --restart unless-stopped -p 8090:8090 \
-e HB_URL=http://qawk:8080 \
qawk-console:localOn its first start Qawk creates its schema, the built-in roles and hawkBit's
default types. Back up two things: the database, and /var/lib/qawk.
β οΈ SetQAWK_ADMIN_PASSWORD. Without it the administrator's password isadmin. That administrator is never stored: it always works, which is how a new server is set up and how a lost password is fixed.
π Use HTTPS. Devices send a bearer token on every poll. Give Qawk a certificate and it terminates TLS itself:
-e QAWK_LISTEN=':8443' -e QAWK_TLS_CERT=/tls/cert.pem -e QAWK_TLS_KEY=/tls/key.pem
QAWK_TLS_CLIENT_CAdemands a certificate from the device too (mutual TLS), andQAWK_REDIRECT_HTTPpoints the old plain-HTTP address at the new one. A proxy in front is still fine β see HTTPS, which also shows how to make a certificate to test with.
π Compose, Kubernetes, every environment variable, users from a file, backup and upgrade: Installing the server.
Devices poll http://<server>:8080/<tenant>/controller/v1/<controller id>,
exactly as they poll hawkBit. With a gateway token they register themselves at
their first poll.
For SWUpdate, the suricatta section of swupdate.cfg:
suricatta: {
url = "https://updates.example.com";
tenant = "DEFAULT";
id = "device-0001";
gatewaytoken = "a-random-token";
};
A device's attributes are what channels, centres and systems are built on β
attribute.ring, attribute.centerid, attribute.device_type,
attribute.device. Get them right and the rest arranges itself.
π Connecting devices
# 50 devices reporting ring=lab, polling every 30 s
demo/simulate.sh --url http://localhost:8080 --token <token> --fleet lab:50
# 16 systems -- a device1 with two device2 and a device3 -- in four centres
demo/simulate.sh --name centres --url http://localhost:8080 --token <token> --systems 16
demo/simulate.sh --list # what is running
demo/simulate.sh --stop # stop it allThey are not containers and there is no application inside them: one qawk-sim
process runs every device as its own client of the real device API. They
simulate only the installation and the rollback β no artifact is ever
downloaded, the install is a wait, and a failing device reports a rollback that
never had anything to undo. Registration, attributes, polling, the deployment
offered and every feedback message are real, which is all the server ever sees.
π What is real and what is
pretended
π₯ Make things fail on purpose β because you cannot trust a safety mechanism you have never seen fire:
a module named broken |
fails on every device β watch a release halt |
-- -fail-rate 0.1 |
one deployment in ten fails at random |
-- -fail-where device=device-07,device_type=device3,set=2.0 |
one component of one system β watch the whole system roll back |
β‘ Load. qawk-load measures how many polls a server holds:
docker run --rm --network host --ulimit nofile=65536:65536 --entrypoint qawk-load \
qawk:local -url http://localhost:8080 -token <token> \
-devices 10000 -interval 30s -ramp 30s -duration 180s -act| π Get started | The demo, Docker Hub, a build from source, or no Docker at all |
| π‘ Concepts | How the pieces fit, and a glossary |
| π±οΈ Using the console | For whoever ships the update. No command line anywhere in it |
| π¦ Channels and releases | Gates, approvals, waves, thresholds, freezes |
| π’ Centres | Moving a place instead of a list of machines |
| π§© The orchestrator | Systems, topologies, manifests, rollback |
| π Connecting devices | SWUpdate, tokens, attributes, simulation |
| ποΈ Installing the server | Docker, compose, Kubernetes, TLS, backup |
| π₯ Users and audit | Roles, tokens, what is recorded |
| π Operations | Metrics, alerts, scaling, troubleshooting |
| β¨οΈ API reference | 91 endpoints, each with the call in curl, Python, JavaScript, Go and PowerShell |
| π hawkBit compatibility | What is implemented, what differs and why, how to migrate |
To read the site locally, without publishing anything:
docs/serve.sh # http://localhost:8099The full server reference β every route, every rule, the numbers behind the
defaults β is server/README.md.
Server β Go 1.25:
cd server
go build ./... && go vet ./... && go test ./...or without Go installed:
docker run --rm -v "$PWD/server":/src -w /src golang:1.25-bookworm go test ./...
End-to-end, against a scratch server (they change its configuration and create data):
python3 server/test/contract.py http://localhost:8080 # the hawkBit contract
python3 server/test/pipeline.py http://localhost:8080 # channels and the pipeline
python3 server/test/rollouts.py http://localhost:8080
python3 server/test/scheduled.py http://localhost:8080
python3 server/test/systems.py http://localhost:8080
python3 server/test/centres.py http://localhost:8080
python3 server/test/orchestrated.py http://localhost:8080
python3 server/test/autopromote.py http://localhost:8080Console β Node 18+, for the tests only:
node console/test/smoke.mjs # the module tree loads; no server needed
node console/test/compat.mjs # every endpoint called is in the compatibility list
node console/test/fiql-live.mjs http://localhost:8080 admin changemeAfter changing the console, console/run-console.sh rebuild restarts the demo's
console alone.
See CONTRIBUTING.md.
Bug fixes, documentation and tests need no ceremony β send them. For anything larger, open an issue first: Qawk has strong opinions about how it behaves, and a change that crosses one of them wants a conversation before it wants code.
| CONTRIBUTING.md | How to run it, what the checks are, and what review will ask of you |
| CODE_OF_CONDUCT.md | Be decent. Argue about the code, not the person |
| SECURITY.md | Found a hole? Do not open an issue β report it privately |
| CHANGELOG.md | What changed, for someone deciding whether to upgrade |
| LICENSING.md | The licence, in full, and why |
Every source file carries an SPDX header, and CI refuses a pull request that adds one without.
server/ Qawk
cmd/qawk/ the server
cmd/qawk-sim/ simulated devices
cmd/qawk-load/ load generator
internal/ APIs, service, store, users, metrics (server/README.md)
reference/ hawkBit's API descriptions (EPL-2.0 β see its README)
deploy/kubernetes/ manifests
test/ end-to-end tests
console/ the web console: index.html, js/, serve.py, test/
demo/ start.sh, simulate.sh, seed.py, compose.yml
docs/ the documentation site (GitHub Pages), docs/serve.sh
.github/ CI, the release workflow, issue and PR templates
AGPL-3.0-or-later Β· Copyright Β© 2026 Luca Padovan
- β Run it on your own fleet β modified or not, you owe nothing.
- β Change it, and share the changes under the same licence.
- β Take it, close it, sell it as your own β that is what this licence prevents.
- πΌ Need other terms? Open an issue. Commercial licences are available.
π If you change Qawk and let other people use it over a network, they are entitled to your version's source. Set
QAWK_SOURCE_URLto your own repository: it is answered by/qawk/v1/infoand shown on the console's About page, which is how that offer reaches the people entitled to it.
Why the AGPL and not MIT, the Apache-2.0 alternative, the third-party components, and the EPL-2.0 exception the embedded hawkBit files need: LICENSING.md and NOTICE.
hawkBit is a trademark of the Eclipse Foundation; Mender is a trademark of Northern.tech AS. Qawk is affiliated with neither.