Skip to Content
Getting started

Getting started

Prerequisites

  • Node.js 18+
  • PostgreSQL (same database for pg-boss and app migrations)

Install

git clone https://github.com/blockqueue/bq-queue.git cd bq-queue npm install cd apps/queue-service

Environment

cp .env.example .env

Set these (names matter; .env.example may show DATABASE_URL, but the service reads POSTGRES_DATABASE_URL):

PORT=9000 NODE_ENV=development SCHEDULER_CONFIG_DIR=./config POSTGRES_DATABASE_URL=postgresql://user:pass@localhost:5432/bq_queue POSTGRES_SSL=false REQUEST_SIGNING_SECRET=replace-with-a-long-random-secret SIGNATURE_HEADER=x-bq-queue-request-signature

PORT must be between 3000 and 65535.

Queue config

Copy a sample and point real endpoints/secrets at it. Do not leave *.sample.yml in the config directory if you also keep production files there. Every *.yml / *.yaml file is loaded.

cp config/prj-a-config.sample.yml config/prj-a.yml

Minimal queue:

queues: WEBHOOK_DELIVERY: endpoint: 'http://127.0.0.1:4000/hooks/deliver' method: 'POST' signature_secret: ${WEBHOOK_SIGNING_SECRET} max_concurrent_jobs: 5 retryLimit: 3 retryDelay: 30

Export the queue secret in the same shell/env as the service:

export WEBHOOK_SIGNING_SECRET=another-long-random-secret

More YAML options: Configuration.

Migrate, build, run

npm run db:apply:migration npm run build npm run start

Health check (no signature):

curl -s http://127.0.0.1:9000/api/health # {"ok":true}

Docker Compose in the repo root can start Postgres, the service, and a dashboard for queues, jobs, and schedules. Mount your YAML dir and set SCHEDULER_CONFIG_DIR (often /config in the image).

Enqueue your first job

Job bodies for one-off and schedule are JSON arrays. Sign the exact bytes you send.

Node example:

import crypto from 'node:crypto'; const secret = process.env.REQUEST_SIGNING_SECRET; const body = JSON.stringify([ { idempotencyKey: 'invoice-42-send', queue: 'WEBHOOK_DELIVERY', payload: { invoiceId: 42 }, }, ]); const t = Math.floor(Date.now() / 1000); const v1 = crypto .createHmac('sha512', secret) .update(`${t}.`) .update(body) .digest('hex'); const res = await fetch('http://127.0.0.1:9000/api/jobs/one-off', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-bq-queue-request-signature': `t=${t},v1=${v1}`, }, body, }); console.log(res.status, await res.json()); // 202 { jobs: [ { jobId: '...' } ] }

Handle the delivery (same machine)

Point the queue endpoint at a small listener (port 4000 in the YAML above). Use the queue secret (WEBHOOK_SIGNING_SECRET), not REQUEST_SIGNING_SECRET.

import crypto from 'node:crypto'; import express from 'express'; function verifySignature(rawBody, headerValue, secret) { const match = /^t=(\d+),v1=([a-f0-9]+)$/i.exec(headerValue ?? ''); if (!match) return false; const t = Number(match[1]); const v1 = match[2]; if (Math.abs(Math.floor(Date.now() / 1000) - t) > 300) return false; const expected = crypto .createHmac('sha512', secret) .update(`${t}.`) .update(rawBody) .digest('hex'); try { return crypto.timingSafeEqual( Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'), ); } catch { return false; } } const app = express(); const QUEUE_SECRET = process.env.WEBHOOK_SIGNING_SECRET; app.post( '/hooks/deliver', express.raw({ type: 'application/json' }), (req, res) => { const raw = req.body.toString('utf8'); const header = req.headers['x-bq-queue-request-signature']; if (!verifySignature(raw, header, QUEUE_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); } const { data } = JSON.parse(raw); console.log('job payload', data); // { invoiceId: 42 } return res.status(200).json({ ok: true }); }, ); app.listen(4000);

Start this before (or while) you enqueue. Non-2xx responses are retried. More detail: Receiving jobs and Request signing.

Example: publish later

Your CMS schedules a post. It does not sleep in-process or rely on a replica’s cron. Add a POST_PUBLISHING queue in YAML (same shape as WEBHOOK_DELIVERY above), then enqueue with runAt:

import crypto from 'node:crypto'; const secret = process.env.REQUEST_SIGNING_SECRET; const postId = 42; const body = JSON.stringify([ { idempotencyKey: `post-${postId}-publish`, queue: 'POST_PUBLISHING', payload: { postId, action: 'publish' }, runAt: '2026-03-23T15:00:00Z', }, ]); const t = Math.floor(Date.now() / 1000); const v1 = crypto .createHmac('sha512', secret) .update(`${t}.`) .update(body) .digest('hex'); await fetch('http://127.0.0.1:9000/api/jobs/one-off', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-bq-queue-request-signature': `t=${t},v1=${v1}`, }, body, });

At runAt, queue-service POSTs your POST_PUBLISHING endpoint with { timestamp, data: { postId, action: 'publish' } }. Your handler publishes and returns 200. Resubmitting the same idempotencyKey cancels the previous job and enqueues a new one (useful if the user changes the publish time).

Example: daily digest (YAML cron)

Fixed recurring work stays in config. No API call every morning:

queues: REPORT_DIGEST: endpoint: 'http://127.0.0.1:4000/hooks/digest' method: 'POST' signature_secret: ${REPORT_DIGEST_SIGNING_SECRET} cron_jobs: - name: 'daily-report-digest' queue: 'REPORT_DIGEST' schedule: '0 8 * * *' timezone: 'UTC' payload: type: 'daily_digest'

Every day at 08:00 UTC the worker hits /hooks/digest with data: { type: 'daily_digest' }. Verify with REPORT_DIGEST_SIGNING_SECRET. Change the schedule by editing YAML and restarting the service.

For “next Monday only” from app logic, use POST /api/jobs/schedule instead. Details: Configuration and HTTP API.

Images and deploy

Published images (tag releases):

docker pull ghcr.io/blockqueue/bq-queue:<tag> # optional UI docker pull ghcr.io/blockqueue/bq-queue-ui:<tag>

In-repo examples:

  • Root docker-compose.yml: Postgres + queue-service + dashboard for local work
  • docker/docker-compose.serve.yml: pull a published ghcr.io/blockqueue/bq-queue tag, mount YAML at /config, set SCHEDULER_CONFIG_DIR=/config

Run on any orchestrator you use (Compose, Swarm, Kubernetes, and so on). Typical layout: queue-service on an internal network with one or more replicas, Postgres reachable only from that network, app services as queue endpoint hosts (service DNS names), secrets via env or your secret store, config as a mounted directory or config object. Put TLS and access control at the edge if anything must cross trust boundaries. Keep the dashboard on a private address.

Next

Last updated on