Getting started
Run Stafett on your laptop with Docker Compose, send one job, and watch a receiver verify its signature. It takes about five minutes. For a cluster, continue with Deployment afterwards.
You need Docker with Compose and a checkout of the Stafett repository. The images are not published yet, so Compose builds them from the checkout.
1. Start the stack
From the repository root:
docker compose -f examples/quickstart/docker-compose.yml up --build
This starts five containers:
db Postgres 17
setup installs schema "stafett", then exits
enqueue the stafett-enqueue image: the HTTP API, on 127.0.0.1:8000
deliver the stafett-deliver image: signs and sends each job
receiver a tiny webhook endpoint that verifies signatures
enqueue and deliver wait for setup to finish. enqueue listens on
loopback only, because it has no authentication.
2. Enqueue a job
curl -X POST localhost:8000/enqueue \
-H 'content-type: application/json' \
-H 'stafett-url: http://receiver:9000/' \
--data-binary '{"hello": "world"}'
Stafett answers 202 Accepted once the job is committed to Postgres:
{"id": 1, "status": "queued", "status_url": "/jobs/1"}
202 means the job is stored, not that it was delivered.
3. Check the result
curl localhost:8000/jobs/1
{"id": 1, "status": "delivered", "attempts": 1, "created_at": "2026-11-03T09:00:00Z"}
The receiver logs each delivery it verified:
docker compose -f examples/quickstart/docker-compose.yml logs receiver
receiver-1 | verified 019a4b7c-6c4a-7b32-9f21-2c0a5e4d9b11: {'hello': 'world'}
4. Verify signatures in your own receiver
The quickstart's deliver has a signing secret, so every delivery carries
Standard Webhooks headers: webhook-id, webhook-timestamp, and
webhook-signature. (Leave STAFETT_SIGNING_SECRET unset and deliver
signs nothing: the request carries only the headers you set, plus a default
user-agent.) Verify them over the raw body with any Standard Webhooks library,
using the same secret deliver has in STAFETT_SIGNING_SECRET:
from standardwebhooks import Webhook
webhook = Webhook("whsec_…")
payload = webhook.verify(raw_body, headers) # raises if the signature is bad
Verify the bytes you received. Parsing the JSON and re-encoding it first
breaks the signature. The quickstart's receiver is
examples/quickstart/receiver.py.
Delivery is at-least-once: a job can occasionally arrive more than once, for
example after a timeout. Dedupe on webhook-id; it is the same for every
retry of one job.
5. See a failure and a replay
Send a job to a destination that does not answer:
curl -X POST localhost:8000/enqueue \
-H 'content-type: application/json' \
-H 'stafett-url: http://nothing-here:9000/' \
--data-binary '{}'
After five attempts (about 20 seconds) GET /jobs/{id} shows dead. List
dead jobs and replay one with pgqueuer's pgq tool, which ships in both
images:
docker compose -f examples/quickstart/docker-compose.yml exec deliver sh -c \
'PGDSN=$DATABASE_URL PGQUEUER_SCHEMA=stafett PGQUEUER_CHANNEL=stafett_ch_pgqueuer pgq failed'
docker compose -f examples/quickstart/docker-compose.yml exec deliver sh -c \
'PGDSN=$DATABASE_URL PGQUEUER_SCHEMA=stafett PGQUEUER_CHANNEL=stafett_ch_pgqueuer pgq requeue <id>'
More in Operations.
Next
- How it works: the architecture and a job's life.
- Deployment: Postgres requirements, the Helm chart
(
helm repo add stafett https://stafett.dev/charts), and a plain install.yaml. - API reference: every field, status, and header.