Guidance for Claude Code when working in this repository.
Canonical docs: docs.sidequestjs.com (source under
packages/docs/). When end-user behavior is in doubt, read the docs first — they cover the public API in detail. This file documents what the docs don't make obvious to someone modifying the codebase.
Production-grade distributed background job processor for Node.js. Persists jobs in a SQL or document database (no Redis), exposes a fluent API + management dashboard, and isolates execution in worker threads. LGPL-3.0, published as sidequest + separate @sidequest/*-backend driver packages.
Monorepo on Yarn 4 (Berry, via Corepack) + Turbo. Workspaces declared in root package.json: packages/*, packages/backends/*, examples.
| Package | Public name | Role |
|---|---|---|
packages/sidequest |
sidequest |
Umbrella package end users install. Exposes Sidequest, re-exports the rest. Source is intentionally thin — mostly the Sidequest static class and operations facade. |
packages/engine |
@sidequest/engine |
Orchestration. Owns the Engine, Dispatcher, QueueManager, ExecutorManager, JobBuilder, JobTransitioner, cron registry, routines (cleanup, stale recovery), shared runner pool. |
packages/core |
@sidequest/core |
Shared primitives: Job base class, schema/types (JobData, QueueConfig, etc.), state transitions, logger (Winston), uniqueness, tools. |
packages/dashboard |
@sidequest/dashboard |
Express + EJS + HTMX + Tailwind/DaisyUI web UI. Can run standalone via SidequestDashboard. |
packages/cli |
@sidequest/cli |
sidequest / sq CLI for config, migrate, rollback. |
packages/docs |
(private) | VitePress site → docs.sidequestjs.com. |
packages/backends/backend |
@sidequest/backend |
Backend interface + SQLBackend base (Knex-based). |
packages/backends/backend-test |
@sidequest/backend-test |
Conformance suite every backend driver runs. |
packages/backends/{postgres,mysql,sqlite,mongo} |
@sidequest/{name}-backend |
Driver implementations. SQL backends extend SQLBackend (Knex); Mongo is its own thing. |
examples/ |
(private) | Runnable usage examples. Useful but incomplete — never treat as the source of truth. |
tests/integration/ at the root holds cross-package integration tests.
# Setup (once)
corepack enable && yarn install
# Build everything (Turbo orchestrates per-package Rollup)
yarn build
# Watch mode for everything, including docs site at http://localhost:5173
yarn dev
# Unit tests (excludes backend conformance + tests/integration)
yarn test
# Backend conformance + integration: needs the DBs running
yarn db:all # spins up pg, mysql, mongo via docker
yarn test:all # all tests including backends
yarn test:integration # cross-package integration suite
yarn db:all:stop
# Lint / format
yarn lint
yarn format
# Single-file vitest run (run from repo root)
yarn vitest packages/engine/src/job/job-builder.test.ts
# Clean
yarn clean # rimraf packages/**/dist + .turboVitest is the test runner across the repo (Mocha/Chai/Sinon are gone). Rollup builds each package; don't reach for tsc to build — Turbo + Rollup own that. Per-package vitest.config.js extends vitest.base.config.js at the root.
Node ≥ 22.6.0 required. TypeScript jobs run natively on Node ≥ 23.6.0.
┌─────────────────┐ fork() + IPC ┌──────────────────────────────────┐
│ App process │ ─────────────────► │ Engine worker process │
│ • Sidequest API │ │ ├─ Dispatcher (polls backend) │
│ • enqueue/build │ │ ├─ QueueManager / ExecutorMgr │
│ • Dashboard │ │ └─ piscina pool (worker threads)│
└─────────────────┘ │ └─ runs Job#run in thread │
└──────────────────────────────────┘
│
▼
┌──────────────────┐
│ Backend (DB) │
└──────────────────┘
- Two layers of isolation, deliberately:
- The engine itself runs in a child process forked from the app (
child_process.fork). Crashes in job code can't kill the host app. - Inside the engine process, jobs execute in worker threads via
piscina— that's the runner pool inpackages/engine/src/shared-runner/.minThreads/maxThreads/idleWorkerTimeoutconfigure piscina.
- The engine itself runs in a child process forked from the app (
- Dispatcher polls the backend at
jobPollingInterval(default 100 ms), claims jobs atomically, and hands them to the executor. Increasing this interval reduces DB load but adds start latency. - Backends are Knex-based for SQL (Postgres/MySQL/SQLite); the
Backendinterface andSQLBackendbase live inpackages/backends/backend. Drivers are dynamicallyimport()ed by their npm name (thedriverconfig string). - Atomicity matters.
claimPendingJobmust claim under contention without double-issuing. The conformance suite in@sidequest/backend-testenforces this — run it whenever you touch a backend.
Sidequest.configure(config)— set up engine, run migrations, don't start workers. Use this when an instance only enqueues.configureis idempotent and a subsequentstart()will ignore any new config.Sidequest.start(config?)—configure+ spin up the engine fork + dashboard. Callingstart()afterconfigure()reuses the configured options.Sidequest.stop()— graceful: drain running jobs, close backend, stop dashboard. Safe to start again afterward.Sidequest.build(JobClass)→JobBuilder(fluent:.queue() .timeout() .maxAttempts() .retryDelay() .backoffStrategy() .availableAt() .unique() .with(...ctorArgs) .enqueue(...runArgs) | .schedule(cron, ...runArgs)).Sidequest.job—.get,.list,.count,.cancel,.run,.snooze,.findStale,.deleteFinished.Sidequest.queue—.get,.list,.create,.pause,.activate,.toggle,.setConcurrency,.setPriority.Jobclass (@sidequest/core) withasync run(...args). Runtime metadata (this.id,this.attempt, etc.) is injected after construction, only available insiderun. Convenience methods insiderun:return this.complete(result)/this.fail(reason)/this.retry(reason, delay?)/this.snooze(delay). You mustreturnthem — calling without returning is a no-op.SidequestDashboard(@sidequest/dashboard) — standalone dashboard against a shared backend.
These are the things that aren't visually loud in either code or docs:
- Queue config in
start({ queues })overrides DB state. If an operator changed concurrency/priority via the dashboard, restarting with that queue listed will reset it. Only queues explicitly named are touched. - Cron schedules are in-memory only.
JobBuilder.schedule(...)registers vianode-cron; it is not persisted. Multi-instance deployments will all schedule the same job — useunique({ period: 'minute' | 'hour' | ... })to deduplicate, or run scheduling on a single node. - Auto-resolution uses stack traces to find the job's source file. This breaks under bundling (Next/Nuxt/serverless). The escape hatch is
manualJobResolution: true+ asidequest.jobs.jsre-export file. The engine walks parent dirs fromcwdto find it unlessjobsFilePathis set; relativejobsFilePathis resolved against the file that calledconfigure/start, notcwd. - SQLite + concurrency > 1 =
SQLITE_BUSY. Single-writer file locking. Either keepmaxConcurrentJobs: 1or use a real DB. Always use a separate.sqlitefile from the host app. - Stale job recovery is automatic. Jobs stuck in
running/claimedpast the configured age get reset by the routine inpackages/engine/src/routines/release-stale-jobs.ts. Don't reinvent it. - Backend driver loading is
import(driver). The driver string is the npm package name; the package mustexport defaultthe backend class. Same goes for custom drivers — relative paths work but are sensitive tocwd. - Dashboard without
authis wide open. Any job-mutation operation (cancel, re-run) is exposed. Treatauth: undefinedas dev-only.
- Default answer to a new dependency is no. Don't add a library because it's popular — Sidequest deliberately keeps its dep graph small. Prefer stdlib or what's already in the tree. If a new dep is genuinely the right call, name the alternatives considered before adding.
- Don't wrap things "just in case." No backwards-compat shims, no feature flags for hypothetical use cases, no validation at internal boundaries.
- Match existing structure. New engine concerns go under
packages/engine/src/<area>; cross-cutting types belong in@sidequest/core. Don't create a new package for a small piece. - Tests live next to the code.
foo.ts+foo.test.tsin the same folder; integration tests undertests/integration/. Backend changes must keep@sidequest/backend-testgreen for every driver. - JSDoc every exported entity.
CONTRIBUTING.mdrequires JSDoc-style docstrings on all exports; match that when adding public API. - Commits follow Conventional Commits (commitlint + Husky enforce it). semantic-release publishes from
master.
dist/at the repo root is legacy — not produced by the current build, safe to delete/ignore. Build artifacts live inpackages/*/dist/andpackages/backends/*/dist/.- The pre-1.0
lib/-based POC (Redis + child_process.fork +Taskclass +sidequest-config.json) is gone. If something still references it, it's stale.