How the queue works #
The web app writes a row into the jobs table. The worker claims rows with SELECT … FOR UPDATE SKIP LOCKED, runs the handler, and records the outcome. A Postgres LISTEN/NOTIFY trigger wakes the worker the moment a job is inserted, so latency is milliseconds, and a poll interval catches anything missed.
Lifecycle: queued → running → succeeded | failed (retry with backoff) | cancelled. Each running job heartbeats; a reaper re-queues jobs whose worker died.
Job types #
| Type | Purpose |
|---|---|
mail.send | Transactional email. |
webhook.deliver | One webhook delivery attempt. |
push.send | APNs push to registered devices. |
notify.digest | Daily notification digest per user. |
prune.jobs prune.invitations prune.notifications prune.webhook_deliveries | Retention. |
Schedules #
The scheduler reads the schedules table every few seconds and enqueues one job per due row, then advances next_run_at by the row's interval. It only enqueues; the worker does the work. Schedules are plain rows: pause, retune or add one with SQL, or toggle them under Admin → System → Schedules.
Monitoring #
Admin → System → Jobs shows recent jobs with their state, queue-health counts, and actions to send a test email, stop a running job, or retry a failed one. Both Go services log structured lines to stdout; point your log shipper at the containers.