{"_id":"@bitfootco/fabrikk","_rev":"3-c50331cc687b6a5402b0eab6bdb3f95f","name":"@bitfootco/fabrikk","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.0":{"name":"@bitfootco/fabrikk","version":"0.1.0","_id":"@bitfootco/fabrikk@0.1.0","maintainers":[{"name":"lukechanning","email":"luke@bitfoot.co"}],"dist":{"shasum":"9cb17955fa16eb321458f398f93ba54b2bad59f0","tarball":"https://registry.npmjs.org/@bitfootco/fabrikk/-/fabrikk-0.1.0.tgz","fileCount":63,"integrity":"sha512-n8lCC86pLuKVDu3ZUx5iWbGxRYMupH/+EnRgIiYIwONRGoLskGSp0iGnUoFud85i2y36OvZ7HyLOSGCq4JjjGA==","signatures":[{"sig":"MEQCIEslfKgigXlwrbMAleuEw/g5aWbiljuDf8d5GignVo5LAiAhh8RCd6pHYahWiQfEV31hUrtnkgkp7e3pRbaBYJ4a+g==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":122891},"main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","require":"./dist/index.js"}},"gitHead":"1da5cc9aeb94ee5744e7f0b64a6b40276d0c659c","scripts":{"lint":"eslint --no-error-on-unmatched-pattern src test","test":"node --env-file=.env node_modules/.bin/vitest run","build":"tsc","prepare":"husky","typecheck":"tsc --noEmit","test:watch":"node --env-file=.env node_modules/.bin/vitest"},"_npmUser":{"name":"lukechanning","email":"luke@bitfoot.co"},"_npmVersion":"10.8.2","description":"Postgres-native job queue for TypeScript. Bare by default. Powerful by choice.","directories":{},"lint-staged":{"*.{json,md}":["prettier --write"],"*.{ts,js,mjs}":["eslint --fix","prettier --write"]},"_nodeVersion":"20.20.2","dependencies":{"pg":"^8.13.0"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.0","eslint":"^9.27.0","vitest":"^3.2.0","prettier":"^3.5.0","@types/pg":"^8.11.0","@eslint/js":"^9.27.0","typescript":"^5.8.0","@types/node":"^18.19.0","lint-staged":"^15.5.0","typescript-eslint":"^8.32.0"},"_npmOperationalInternal":{"tmp":"tmp/fabrikk_0.1.0_1778902080047_0.6711311964937534","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@bitfootco/fabrikk","version":"0.1.1","_id":"@bitfootco/fabrikk@0.1.1","maintainers":[{"name":"lukechanning","email":"luke@bitfoot.co"}],"dist":{"shasum":"2d0bc9b86ad08d451d99cb9b5b46cae472d2d0ba","tarball":"https://registry.npmjs.org/@bitfootco/fabrikk/-/fabrikk-0.1.1.tgz","fileCount":63,"integrity":"sha512-jBqKxRZaSSOx6RFJCtfRt/kt2Aqj6SJ/dbn8nagzlvyW46ybH4MCeDttIWpJwwCwz5f+iPTkquGpypIe+wtecw==","signatures":[{"sig":"MEYCIQDLHD3O/jcc9DobXB5l+u6rhhlW32vP3kVAvO8jVfStbAIhAMOb9eU4xJ0mq8NeZUnEZGWVrF4JR+OvUuDqWj8bqqew","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":124283},"main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","require":"./dist/index.js"}},"gitHead":"cf7ea04038d0c361526887587fe09d770053ad38","scripts":{"lint":"eslint --no-error-on-unmatched-pattern src test","test":"node --env-file=.env node_modules/.bin/vitest run","build":"tsc","prepare":"husky","typecheck":"tsc --noEmit","test:watch":"node --env-file=.env node_modules/.bin/vitest"},"_npmUser":{"name":"lukechanning","email":"luke@bitfoot.co"},"_npmVersion":"10.8.2","description":"Postgres-native job queue for TypeScript. Bare by default. Powerful by choice.","directories":{},"lint-staged":{"*.{json,md}":["prettier --write"],"*.{ts,js,mjs}":["eslint --fix","prettier --write"]},"_nodeVersion":"20.20.2","dependencies":{"pg":"^8.13.0"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.0","eslint":"^9.27.0","vitest":"^3.2.0","prettier":"^3.5.0","@types/pg":"^8.11.0","@eslint/js":"^9.27.0","typescript":"^5.8.0","@types/node":"^18.19.0","lint-staged":"^15.5.0","typescript-eslint":"^8.32.0"},"_npmOperationalInternal":{"tmp":"tmp/fabrikk_0.1.1_1780559814252_0.194645365560965","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@bitfootco/fabrikk","version":"0.1.2","description":"Postgres-native job queue for TypeScript. Bare by default. Powerful by choice.","main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","require":"./dist/index.js"}},"scripts":{"build":"tsc","lint":"eslint --no-error-on-unmatched-pattern src test","typecheck":"tsc --noEmit","test":"node --env-file=.env node_modules/.bin/vitest run","test:watch":"node --env-file=.env node_modules/.bin/vitest","prepare":"husky"},"dependencies":{"pg":"^8.13.0"},"devDependencies":{"@eslint/js":"^9.27.0","@types/node":"^18.19.0","@types/pg":"^8.11.0","eslint":"^9.27.0","husky":"^9.1.0","lint-staged":"^15.5.0","prettier":"^3.5.0","typescript":"^5.8.0","typescript-eslint":"^8.32.0","vitest":"^3.2.0"},"lint-staged":{"*.{ts,js,mjs}":["eslint --fix","prettier --write"],"*.{json,md}":["prettier --write"]},"_id":"@bitfootco/fabrikk@0.1.2","gitHead":"dc6cb08ea091360e8b694d08f30b56b78de819da","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-483dQGp4IquEak8lWlUIxca9jW26IGZx5uWkdqalS1Jii7Irag456sYYQWf3yD0mw+zhXFLEvxIVtlfrjuamWQ==","shasum":"4818214db361f9d0d7cb5272790b941d90900424","tarball":"https://registry.npmjs.org/@bitfootco/fabrikk/-/fabrikk-0.1.2.tgz","fileCount":63,"unpackedSize":124526,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDfU4Hyk2+PgKgNoehKZXuWbUcdYMsoxhlSLuAcerd++AIhAOKBTRC2b/19RrHu5t4Y1qmQeb3x7sOovrV3753NP9O1"}]},"_npmUser":{"name":"lukechanning","email":"luke@bitfoot.co"},"directories":{},"maintainers":[{"name":"lukechanning","email":"luke@bitfoot.co"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/fabrikk_0.1.2_1784752434812_0.935909490174291"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-16T03:27:59.950Z","modified":"2026-07-22T20:33:55.100Z","0.1.0":"2026-05-16T03:28:00.227Z","0.1.1":"2026-06-04T07:56:54.405Z","0.1.2":"2026-07-22T20:33:54.949Z"},"description":"Postgres-native job queue for TypeScript. Bare by default. Powerful by choice.","maintainers":[{"name":"lukechanning","email":"luke@bitfoot.co"}],"readme":"# fabrikk\n\n> A Postgres-native job queue for TypeScript. Bare by default. Powerful by choice.\n\nFabrikk is a zero-dependency job queue built on Postgres and TypeScript. The base install fits in your head. When you need more — retries, cron, rate limiting, a dashboard API — you opt in, one battery at a time.\n\n---\n\n## Why Fabrikk?\n\nPostgres-backed queues are a well-trodden pattern, and there are good libraries out there. Fabrikk has a specific opinion: **you shouldn't have to learn a framework to use a queue.**\n\n- The bare API is three concepts: define a job, enqueue it, work it\n- TypeScript types flow from definition through to your worker handler — no casting\n- Every advanced feature is opt-in via config — unused features have zero footprint\n- The library owns its schema — no migrations, no setup scripts, it self-heals on startup\n- Graceful shutdown is built in — every worker receives an `AbortSignal`\n\n---\n\n## Install\n\n```bash\nnpm install @bitfootco/fabrikk\n```\n\nRequires Node.js 18+ and Postgres 14+.\n\n---\n\n## Quickstart\n\nThis is the entire bare API.\n\n```ts\nimport { Queue } from '@bitfootco/fabrikk'\n\n// 1. Define your job types\ntype Jobs = {\n  send-email: { to: string; subject: string; body: string }\n  resize-image: { imageId: string; width: number }\n}\n\n// 2. Create a queue — it manages its own schema\nconst queue = new Queue<Jobs>({\n  connectionString: process.env.DATABASE_URL,\n})\n\n// 3. Enqueue a job\nawait queue.enqueue('send-email', {\n  to: 'hi@example.com',\n  subject: 'Hello',\n  body: 'World',\n})\n\n// 4. Work jobs — payload is fully typed\nqueue.work('send-email', async (job, signal) => {\n  await sendEmail(job.payload) // payload: { to, subject, body } — no casting needed\n})\n\n// 5. Graceful shutdown — the AbortSignal is wired up automatically\nprocess.on('SIGTERM', () => queue.stop())\n```\n\nThat's it. No config files. No migrations to run. No registration ceremony.\n\n---\n\n## Async Iterator Interface\n\nPrefer pull-based consumption? Every queue exposes an async iterator alongside `.work()`.\n\n```ts\nfor await (const job of queue.jobs('send-email')) {\n  await sendEmail(job.payload);\n  await job.done();\n}\n```\n\nThe iterator respects backpressure and the same `AbortSignal`-based shutdown as `.work()`.\n\n---\n\n## Batteries\n\nAdvanced features are opt-in. Include only what you need.\n\n```ts\nconst queue = new Queue<Jobs>({\n  connectionString: process.env.DATABASE_URL,\n  batteries: {\n    retries: {\n      attempts: 3,\n      backoff: 'exponential', // or 'linear' | 'fixed'\n      baseDelay: 1000, // ms\n    },\n    dlq: true,\n    cron: true,\n    priority: true,\n    rateLimit: true,\n    fanout: true,\n    hooks: true,\n    dashboard: {\n      path: '/queue', // mounts REST API at this path on your existing server\n    },\n  },\n});\n```\n\nEach battery is documented below. If it's not in your config, it doesn't exist — no schema additions, no overhead.\n\n---\n\n### 🔁 `retries`\n\nAutomatic retry with exponential backoff and jitter. Configurable globally or per job type.\n\n```ts\nbatteries: {\n  retries: {\n    attempts: 5,\n    backoff: 'exponential',\n    baseDelay: 500,\n  }\n}\n```\n\nOverride per job at enqueue time:\n\n```ts\nawait queue.enqueue('send-email', payload, {\n  retries: { attempts: 10, backoff: 'linear' },\n});\n```\n\nFailed jobs that exhaust their retries are moved to the dead-letter queue if `dlq` is enabled, or discarded if not.\n\n---\n\n### ☠️ `dlq`\n\nDead-letter queue. Jobs that fail all retry attempts land here for inspection and replay.\n\n```ts\nbatteries: {\n  dlq: true,\n}\n```\n\nInspect and replay dead jobs:\n\n```ts\nconst dead = await queue.dlq.list('send-email'); // paginated\nawait queue.dlq.replay(dead[0].id); // re-enqueues with fresh retry count\nawait queue.dlq.discard(dead[0].id); // permanent delete\nawait queue.dlq.replayAll('send-email'); // bulk replay\n```\n\nRequires `retries` battery to be enabled.\n\n---\n\n### ⏰ `cron`\n\nRecurring jobs defined in code, not in a separate scheduler. Uses standard 5-field cron expressions (minute, hour, day-of-month, month, day-of-week).\n\n```ts\nbatteries: {\n  cron: true,\n}\n```\n\nRegister recurring jobs after queue creation:\n\n```ts\nqueue.cron('send-email', '0 9 * * 1-5', {\n  // 9am weekdays\n  to: 'digest@example.com',\n  subject: 'Daily digest',\n  body: '...',\n});\n```\n\nCron jobs are deduplicated across multiple workers using Postgres advisory locks — no double-firing in a scaled deployment.\n\n---\n\n### 🏆 `priority`\n\nPriority queues. Higher priority jobs are worked first within the same queue.\n\n```ts\nbatteries: {\n  priority: true,\n}\n```\n\nSet priority at enqueue time (higher number = higher priority, default 0):\n\n```ts\nawait queue.enqueue('send-email', payload, { priority: 10 });\nawait queue.enqueue('send-email', payload, { priority: 1 }); // worked after\n```\n\nWorkers are priority-aware automatically — no change to `.work()` or the iterator.\n\n---\n\n### 🚦 `rateLimit`\n\nLimit how fast a queue is consumed, without Redis. Uses Postgres timestamptz precision.\n\n```ts\nbatteries: {\n  rateLimit: true,\n}\n```\n\nSet rate limits per queue:\n\n```ts\nqueue.setRateLimit('send-email', {\n  max: 100,\n  window: '1m', // '1s' | '1m' | '1h'\n});\n```\n\nRate limits are enforced cluster-wide — safe across multiple worker processes.\n\n---\n\n### 📡 `fanout`\n\nEnqueue a single job to multiple queues at once. Useful for event-driven flows where multiple systems need to react to the same event.\n\n```ts\nbatteries: {\n  fanout: true,\n}\n```\n\nDefine fanout rules:\n\n```ts\nqueue.fanout('user-signed-up', ['send-welcome-email', 'create-billing-account', 'notify-slack']);\n```\n\nThen enqueue normally — Fabrikk fans it out for you:\n\n```ts\nawait queue.enqueue('user-signed-up', { userId: '123' });\n// → enqueues to send-welcome-email, create-billing-account, notify-slack\n```\n\n---\n\n### 🪝 `hooks`\n\nStructured event emitter for job lifecycle events. Wire into your own observability stack.\n\n```ts\nbatteries: {\n  hooks: true,\n}\n```\n\nSubscribe to events:\n\n```ts\nqueue.on('job:enqueued', (event) => logger.info(event));\nqueue.on('job:started', (event) => metrics.increment('job.started'));\nqueue.on('job:completed', (event) => metrics.histogram('job.duration', event.durationMs));\nqueue.on('job:failed', (event) => logger.error(event));\nqueue.on('job:retrying', (event) => logger.warn(event));\nqueue.on('job:dead', (event) => alerts.notify(event));\n```\n\nAll events are typed. `event.jobName` narrows the payload type in the handler.\n\n---\n\n### 📊 `dashboard`\n\nMounts a REST API on your existing HTTP server. Bring your own UI.\n\n```ts\nbatteries: {\n  dashboard: {\n    path: '/queue',\n  }\n}\n```\n\nMount on Express, Fastify, or any Node.js HTTP server:\n\n```ts\n// Express\napp.use('/queue', queue.dashboardHandler());\n\n// Fastify\nfastify.all('/queue/*', queue.dashboardHandler());\n```\n\n#### Endpoints\n\n| Method   | Path                     | Description                                            |\n| -------- | ------------------------ | ------------------------------------------------------ |\n| `GET`    | `/queue/jobs`            | List jobs, filterable by queue / status / date         |\n| `GET`    | `/queue/jobs/:id`        | Get a single job                                       |\n| `POST`   | `/queue/jobs/:id/replay` | Re-enqueue a dead job                                  |\n| `DELETE` | `/queue/jobs/:id`        | Discard a job                                          |\n| `GET`    | `/queue/queues`          | List queues with stats (depth, throughput, error rate) |\n| `GET`    | `/queue/cron`            | List cron schedules and last-run times                 |\n| `GET`    | `/queue/health`          | Liveness check — returns 200 if queue is healthy       |\n\nAll responses are JSON. Authentication is your responsibility — mount behind your existing auth middleware.\n\n---\n\n## Schema\n\nFabrikk manages all its own tables. On first startup it creates them. On subsequent startups it checks and repairs any schema drift — safe to run in a multi-instance deploy.\n\nAll tables are prefixed with `fabrikk_` and live in your existing database. No separate database needed.\n\n---\n\n## Graceful Shutdown\n\nEvery worker automatically receives an `AbortSignal`. When you call `queue.stop()`, in-flight jobs are given a grace period to complete before the process exits.\n\n```ts\nconst queue = new Queue<Jobs>({\n  connectionString: process.env.DATABASE_URL,\n  shutdown: {\n    gracePeriodMs: 30_000, // default: 30s\n  },\n});\n\n// Wire to your process signals\nprocess.on('SIGTERM', () => queue.stop());\nprocess.on('SIGINT', () => queue.stop());\n```\n\nInside your worker, respect the signal for long-running jobs:\n\n```ts\nqueue.work('resize-image', async (job, signal) => {\n  for (const chunk of chunks) {\n    if (signal.aborted) break;\n    await processChunk(chunk);\n  }\n  await job.done();\n});\n```\n\n---\n\n## TypeScript\n\nFabrikk is written in TypeScript and ships its own types. Define your job payload types once at queue creation — they propagate automatically.\n\n```ts\ntype Jobs = {\n  'send-email': { to: string; subject: string };\n  'process-payment': { orderId: string; amountCents: number };\n};\n\nconst queue = new Queue<Jobs>({ connectionString: '...' });\n\n// ✅ Payload is { to: string; subject: string } — inferred, no casting\nqueue.work('send-email', async (job) => {\n  job.payload.to; // string ✓\n  job.payload.subject; // string ✓\n  job.payload.orderId; // TS error — wrong job type ✓\n});\n\n// ✅ Enqueue is type-checked too\nawait queue.enqueue('send-email', { to: 'a@b.com', subject: 'Hi' }); // ✓\nawait queue.enqueue('send-email', { amountCents: 100 }); // TS error ✓\n```\n\n---\n\n## Configuration Reference\n\n```ts\nconst queue = new Queue<Jobs>({\n  // Connection — provide one of:\n  connectionString: string, // Postgres connection string (Fabrikk creates and owns the pool)\n  pool: Pool, // Existing pg.Pool (Fabrikk borrows it; stop() will not close it)\n\n  // Optional\n  schema: string, // Postgres schema, default: 'public'\n  poolSize: number, // PG connection pool size — only used with connectionString, default: 10\n  pollIntervalMs: number, // How often workers poll, default: 1000\n  shutdown: {\n    gracePeriodMs: number, // Grace period on stop(), default: 30000\n  },\n\n  // Batteries (all optional)\n  batteries: {\n    retries: {\n      attempts: number, // Max attempts including first, default: 3\n      backoff: 'exponential' | 'linear' | 'fixed',\n      baseDelay: number, // ms, default: 1000\n    },\n    dlq: boolean,\n    cron: boolean,\n    priority: boolean,\n    rateLimit: boolean,\n    fanout: boolean,\n    hooks: boolean,\n    dashboard: {\n      path: string, // Mount path, default: '/queue'\n    },\n  },\n});\n```\n\n---\n\n## Comparison\n\n|                   | Fabrikk               | pg-boss   | BullMQ           |\n| ----------------- | --------------------- | --------- | ---------------- |\n| Backend           | Postgres              | Postgres  | Redis            |\n| TypeScript        | First-class, inferred | Partial   | Good             |\n| API surface       | Minimal (opt-in)      | Large     | Large            |\n| Schema management | Automatic             | Automatic | N/A              |\n| Dashboard         | API endpoint (BYO UI) | None      | Paid (BullBoard) |\n| Cron              | Optional battery      | Built-in  | Built-in         |\n| Rate limiting     | Optional battery      | None      | Built-in         |\n| Graceful shutdown | Built-in              | Manual    | Manual           |\n| Zero-dep core     | ✓                     | ✗         | ✗                |\n\n---\n\n## Contributing\n\nFabrikk is open source and maintained by [The Bitfoot Company](https://bitfoot.co) as part of our labs projects. Issues, PRs, and feedback are welcome.\n\n```bash\ngit clone https://github.com/bitfootco/fabrikk\ncd Fabrikk\nnpm install\nnpm test\n```\n\nYou'll need a local Postgres instance. Copy `.env.example` to `.env` and set `DATABASE_URL`.\n\n---\n\n## License\n\nMIT\n","readmeFilename":"README.md"}