A small background-job service. Clients submit jobs over HTTP, an in-process worker runs them, and clients poll for the result.
- Node 24, TypeScript (run directly by Node, no build step)
- Fastify, zod
- SQLite through the built-in
node:sqlitemodule - vitest
npm install
npm run create-key -- my-laptop # prints an API key
npm run dev # http://localhost:3000curl -s localhost:3000/v1/jobs \
-H "x-api-key: $KEY" -H 'content-type: application/json' \
-d '{"type":"report.generate","payload":{"reportId":"q3","format":"csv"}}'npm test # vitest
npm run typecheck # tscAll /v1 routes except /v1/admin need an X-API-Key header. A key sees
only its own jobs.
| Method | Path | Does |
|---|---|---|
POST |
/v1/jobs |
Create a job: { type, payload?, priority?, maxAttempts?, runAt? } |
POST |
/v1/jobs/batch |
Create up to 100 jobs in one transaction: { jobs: [...] } |
GET |
/v1/jobs |
List jobs, newest first. Query: status, type, limit (1–100), cursor |
GET |
/v1/jobs/:id |
One job |
GET |
/v1/jobs/:id/events |
The job's history |
POST |
/v1/jobs/:id/cancel |
Cancel a queued or running job |
POST |
/v1/jobs/:id/retry |
Queue a failed job again |
GET |
/health |
Liveness and database check, no key needed |
Admin routes need X-Admin-Token (set ADMIN_TOKEN):
POST /v1/admin/api-keys, GET /v1/admin/api-keys,
DELETE /v1/admin/api-keys/:id, GET /v1/admin/stats, POST /v1/admin/purge.
| Type | Payload |
|---|---|
email.send |
{ to, subject, body } |
report.generate |
{ reportId, format: "csv" | "pdf", rows? } |
webhook.deliver |
{ url, event, body? } (https only) |
noop |
anything |
runAtis an ISO 8601 timestamp. A timestamp with no offset is read as UTC. The job runs at or after that time.- A failed attempt is retried after
RETRY_BACKOFF_MS, doubling each time, until the job has mademaxAttemptsattempts (defaultDEFAULT_MAX_ATTEMPTS). Then it isfailed. - Higher
priorityruns first.
Each API key may make RATE_LIMIT_MAX requests per RATE_LIMIT_WINDOW_MS.
Responses carry x-ratelimit-limit and x-ratelimit-remaining; a 429
carries retry-after.
| Variable | Default |
|---|---|
PORT |
3000 |
DB_PATH |
data/jobs.db |
ADMIN_TOKEN |
empty (admin API off) |
POLL_INTERVAL_MS |
1000 |
RETRY_BACKOFF_MS |
5000 |
DEFAULT_MAX_ATTEMPTS |
3 |
RATE_LIMIT_WINDOW_MS |
60000 |
RATE_LIMIT_MAX |
120 |
RUN_WORKER |
true |
LOG_LEVEL |
info |
See docs/ARCHITECTURE.md for how the parts fit.