Skip to main content

Quick Start

This walkthrough wires the queue end to end on SQLite with better-sqlite3 — the same path the 00-showcase sample proves. The Postgres and MySQL dialects are identical except for the import path and awaiting enqueue; see the API Reference.

1. Install​

npm install @nest-native/jobs
# plus your driver + transaction library (peer dependencies):
npm install drizzle-orm @nestjs-cls/transactional @nestjs-cls/transactional-adapter-drizzle-orm nestjs-cls better-sqlite3

The published package declares a single runtime dependency — croner, which does the cron math for schedules. Nest, Drizzle, and your driver are peer dependencies you already control.

Compatibility​

PeerSupported rangeNotes
Node.js>=22 (>=22.12 with NestJS 12 — see the note below the table)engines is >=22; the 12 end of the NestJS range raises the floor, the 11 end does not
@nestjs/common, @nestjs/core^11.0.0 || ^12.0.012 is ESM-only; both majors run the full suite and the samples in CI
@nestjs-cls/transactional^3.0.0on NestJS 12 you need >=3.3.0 (with nestjs-cls >=6.3.0) — earlier minors declare @nestjs/core >= 10 < 12
drizzle-orm^0.44.0 || ^0.45.0
better-sqlite3^11.0.0 || ^12.0.0 || ^13.0.0optional; 13 requires Node >=22
pg^8.0.0optional
mysql2^3.0.0optional

The Node.js floor depends on which end of the NestJS range you are on. NestJS 11 runs on any Node.js >=22. NestJS 12 is ESM-only; a CommonJS app — the usual NestJS build, and this package itself — loads it through Node's require(esm), which is behind a flag before Node.js 22.12.0, so NestJS 12 needs Node.js >=22.12. engines stays >=22 because the 11 end does not need more; Node 22.0–22.11 satisfies it and still cannot load NestJS 12. CI's NestJS 12 leg runs on a current 22.x. Both ends of the NestJS range are tested claims: the 11 floor CI leg pins 11.0.0 exactly (nothing this package uses was added by a later 11.x) and the 12 leg floats on ^12.

2. Add the jobs table to your schema​

Import the dialect's table definition and add it to your Drizzle schema alongside your business tables, then generate a migration with drizzle-kit.

schema.ts
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { jobs } from '@nest-native/jobs/sqlite';

export const users = sqliteTable('users', {
id: text('id').primaryKey(),
email: text('email').notNull(),
});

export const schema = { jobs, users };

3. Wire CLS + the module​

Register @nestjs-cls/transactional with the Drizzle adapter (this is what makes enqueue share your business transaction), then JobsModule.forRoot with the dialect store:

app.module.ts
import { Module } from '@nestjs/common';
import { ClsModule } from 'nestjs-cls';
import { ClsPluginTransactional } from '@nestjs-cls/transactional';
import { TransactionalAdapterDrizzleOrm } from '@nestjs-cls/transactional-adapter-drizzle-orm';
import { JobsModule } from '@nest-native/jobs';
import { SqliteJobStore } from '@nest-native/jobs/sqlite';
import { DRIZZLE } from './database'; // your app's Drizzle provider token

@Module({
imports: [
ClsModule.forRoot({
global: true,
plugins: [
new ClsPluginTransactional({
adapter: new TransactionalAdapterDrizzleOrm({
drizzleInstanceToken: DRIZZLE,
}),
enableTransactionProxy: true,
}),
],
}),
JobsModule.forRoot({
drizzleInstanceToken: DRIZZLE,
store: new SqliteJobStore(),
}),
],
})
export class AppModule {}

4. Enqueue inside your business transaction​

user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectTransaction, Transactional } from '@nestjs-cls/transactional';
import { JobsService } from '@nest-native/jobs';
import type { SqliteJobStore } from '@nest-native/jobs/sqlite';

interface WelcomeEmailPayload {
email: string;
}

@Injectable()
export class UserService {
constructor(
@InjectTransaction() private readonly db: AppDatabase,
private readonly jobs: JobsService<SqliteJobStore>,
) {}

@Transactional()
register(id: string, email: string) {
this.db.insert(users).values({ id, email }).run();
const payload: WelcomeEmailPayload = { email };
this.jobs.enqueue({
name: 'email.welcome',
payload,
uniqueKey: `welcome:${email}`, // dedup among active jobs
});
// both rows commit atomically; a throw rolls both back
}
}

On sqlite the body is synchronous and enqueue returns the JobRow directly; on Postgres/MySQL, await it. Scheduling options: runAt (absolute) xor delayMs (relative) — setting both throws; priority (higher first); maxAttempts (default 10).

5. Handle the job​

welcome-email.handler.ts
import { Injectable } from '@nestjs/common';
import { JobHandler, type JobContext } from '@nest-native/jobs';

@JobHandler('email.welcome')
@Injectable()
export class WelcomeEmailHandler implements JobHandler {
constructor(private readonly mailer: MailerService) {}

async handle(payload: Record<string, unknown>, ctx: JobContext) {
// Delivery is at-least-once: key side effects on ctx.jobId, or make
// them naturally idempotent.
await this.mailer.send(String(payload.email));
}
}

Register the class as a provider in any module. The explorer discovers every @JobHandler at bootstrap; two classes claiming the same name fail the app at startup.

Inside a handler, throw to steer retries:

  • throw new RetryableError('rate limited', 30_000) — retry in 30s (omit the delay for jittered exponential backoff);
  • throw new PermanentError('malformed payload') — fail now, no retries;
  • any other throw — retry with backoff until maxAttempts, then fail.

6. Run the worker​

main.ts
import { JobsClaimer, runWorkerLoop } from '@nest-native/jobs';

const app = await NestFactory.create(AppModule);
await app.listen(3000);

// Same process, or a dedicated worker process — your call.
const controller = new AbortController();
void runWorkerLoop(app.get(JobsClaimer), {
pollIntervalMs: 1_000,
signal: controller.signal,
onError: (error) => logger.error(error),
});
app.enableShutdownHooks();
process.on('SIGTERM', () => controller.abort());

The loop drains due jobs in batches (priority first, oldest due first, reclaiming jobs stuck in processing), then idles for pollIntervalMs when the queue is empty. Aborting the signal stops it cleanly.

That's the whole system: one table, your transaction, your handlers, a poll loop. See the Testing guide for drainJobs and the API Reference for every knob.