How Stafett works

Stafett is an outbound webhook queue that runs inside your own infrastructure. Your services hand it a payload, the headers to send, and a destination URL. Stafett stores the job in your Postgres, then delivers it as an HTTP POST with retries, backoff, and a dead-letter queue. It can sign each attempt with Standard Webhooks, or leave the request exactly as you built it.

Nothing leaves your network except the deliveries themselves. Stafett never calls stafett.dev.

The pieces

Stafett is two container images, one per role. You deploy each separately, and both point at the same Postgres database.

 YOUR NETWORK (e.g. one Kubernetes namespace)
 ┌────────────────────────────────────────────────────────────────┐
 │                                                                │
 │  your services                                                 │
 │      │ POST /enqueue                                           │
 │      ▼                                                         │
 │  ┌──────────────┐   commit job    ┌───────────────────────┐    │
 │  │ stafett-     │────────────────►│ your Postgres         │    │
 │  │ enqueue      │                 │ schema "stafett"      │    │
 │  └──────────────┘                 │   pgqueuer            │    │
 │                                   │   pgqueuer_log        │    │
 │                                   └───────┬───────────▲───┘    │
 │                           NOTIFY, dequeue │           │ result │
 │                                           ▼           │        │
 │                                   ┌───────────────────┴───┐    │
 │                                   │ stafett-deliver       │    │
 │                                   │                       │    │
 │                                   └───────────┬───────────┘    │
 └───────────────────────────────────────────────┼────────────────┘
                                                 │ POST, signed if
                                                 │ deliver has a secret
                                                 ▼
              destination (internal service or third party)

They are two images, one per role, because the roles scale on different signals. Enqueue scales with your request rate; delivery scales with backlog and how fast destinations answer. A deliver pod that crashes never takes POST /enqueue down with it.

One job, start to finish

your service      enqueue         Postgres           deliver       destination
    │                │               │                  │               │
    │ POST /enqueue  │               │                  │               │
    │───────────────►│ validate      │                  │               │
    │                │ INSERT job    │                  │               │
    │                │──────────────►│                  │               │
    │                │ COMMIT        │ NOTIFY           │               │
    │                │──────────────►│─────────────────►│               │
    │  202 {id}      │               │                  │               │
    │◄───────────────│               │ dequeue, lock    │               │
    │                │               │◄─────────────────│               │
    │                │               │                  │ POST          │
    │                │               │                  │──────────────►│
    │                │               │                  │     2xx       │
    │                │               │ delete row,      │◄──────────────│
    │                │               │ log "successful" │               │
    │                │               │◄─────────────────│               │
  1. enqueue validates the request, writes the job, and commits. Only then does it answer 202 with the job id.
  2. Postgres NOTIFY wakes a deliver pod within milliseconds. One that misses the notification finds the job on its next poll, at most 5 seconds later.
  3. deliver locks the job, so no other replica takes it, and POSTs the payload. With a signing secret, it adds Standard Webhooks signature headers.
  4. A 2xx removes the job from the queue and logs it as delivered. Anything else either schedules a retry or moves the job to the dead-letter queue.

Retries and the dead-letter queue

         POST /enqueue
              │
              ▼
       ┌──────────────┐   DELETE /jobs/{id}   ┌────────────┐
       │    queued    │──────────────────────►│  canceled  │
       └──────┬───────┘                       └────────────┘
              │ deliver picks it up                 ▲
              ▼                                     │ DELETE
       ┌──────────────┐  5xx, 429, timeout,   ┌─────┴──────┐
       │  delivering  │── connect error ─────►│  retrying  │
       │              │◄──────────────────────│            │
       └──┬────────┬──┘   backoff elapsed     └────────────┘
          │        │
      2xx │        │ 3xx or 4xx, or a retryable
          │        │ failure on the last attempt
          ▼        ▼
    ┌───────────┐ ┌──────┐
    │ delivered │ │ dead │
    └───────────┘ └──┬───┘
                     │
                     └── pgq requeue <id> (replay) ──► retrying

Delivery guarantee

Delivery is at-least-once, and typically once. If an attempt times out or the connection drops after the request went out, Stafett cannot know whether the destination processed it, so it retries. Receivers must tolerate repeats: dedupe on the webhook-id header, which stays the same across every retry and replay of one job. With signing off, Stafett adds no id, so send one of your own as a Stafett-Header-<name> header.

Stafett does not guarantee ordering. Jobs run concurrently across replicas, and a retrying job falls behind newer ones.

What Stafett stores

Everything lives in schema stafett in your database, created by pgqueuer's own upgrade (setup):

Table Holds
stafett.pgqueuer One row per job that is queued, in flight, or dead. Deleted when the job is delivered.
stafett.pgqueuer_log History: enqueue, every attempt with its reason, cancel, replay. Grows until you prune it (Operations).
stafett.pgqueuer_statistics Per-second counts rolled up from the log. Also grows until pruned.
stafett.pgqueuer_schedules pgqueuer's cron schedules. Stafett does not use it.

setup also creates pgqueuer's status type, and the function and trigger that send NOTIFY on new jobs.

The signing secret lives only in deliver's environment, never in the database. Someone who can read the database still cannot forge a signed delivery. Headers you enqueue, tokens included, are stored with the job.

Stafett's NOTIFY channel is stafett_ch_pgqueuer, so it does not collide with a pgqueuer you already run in the same database.