Skip to content

Installation

1. Install

bash
npm install @nestarc/outbox @nestjs/schedule @prisma/client

@nestjs/schedule and @prisma/client are peer dependencies.

2. Run the SQL Migration

The outbox_events table is not managed through schema.prisma. It uses raw SQL shipped with the package:

bash
# Apply with psql
psql "$DATABASE_URL" -f "$(node -e "console.log(require.resolve('@nestarc/outbox/src/sql/create-outbox-table.sql'))")"

The migration creates the table and three partial indexes (PENDING, PROCESSING, FAILED). It is safe to run multiple times (IF NOT EXISTS).

View the full SQL
sql
CREATE TABLE IF NOT EXISTS outbox_events (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  event_type    VARCHAR(255) NOT NULL,
  payload       JSONB NOT NULL,
  status        VARCHAR(20) NOT NULL DEFAULT 'PENDING',
  created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  processed_at  TIMESTAMPTZ,
  retry_count   INT NOT NULL DEFAULT 0,
  max_retries   INT NOT NULL DEFAULT 5,
  last_error    TEXT,
  tenant_id     VARCHAR(255),

  CONSTRAINT chk_status CHECK (status IN ('PENDING', 'PROCESSING', 'SENT', 'FAILED'))
);

CREATE INDEX IF NOT EXISTS idx_outbox_pending
  ON outbox_events (created_at ASC) WHERE status = 'PENDING';

CREATE INDEX IF NOT EXISTS idx_outbox_processing
  ON outbox_events (updated_at ASC) WHERE status = 'PROCESSING';

CREATE INDEX IF NOT EXISTS idx_outbox_failed
  ON outbox_events (created_at DESC) WHERE status = 'FAILED';

3. Register the Module

typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { OutboxModule } from '@nestarc/outbox';

@Module({
  imports: [
    OutboxModule.forRoot({
      prisma: PrismaService, // class reference — must be in a @Global() module
    }),
  ],
})
export class AppModule {}

WARNING

When passing a class reference to prisma in forRoot(), the class must be provided by a @Global() module (e.g. PrismaModule) so NestJS can resolve it across module boundaries.

typescript
import { ConfigModule, ConfigService } from '@nestjs/config';
import { OutboxModule } from '@nestarc/outbox';

@Module({
  imports: [
    OutboxModule.forRootAsync({
      imports: [PrismaModule, ConfigModule],
      inject: [PrismaService, ConfigService],
      useFactory: (prisma: PrismaService, config: ConfigService) => ({
        prisma,
        polling: {
          interval: config.get('OUTBOX_POLL_INTERVAL', 5000),
          batchSize: config.get('OUTBOX_BATCH_SIZE', 100),
        },
        retry: {
          maxRetries: config.get('OUTBOX_MAX_RETRIES', 5),
          backoff: 'exponential',
          initialDelay: 1000,
        },
      }),
    }),
  ],
})
export class AppModule {}

4. Emit Your First Event

typescript
import { OutboxEvent } from '@nestarc/outbox';

// Define an event class
export class OrderCreatedEvent extends OutboxEvent {
  static readonly eventType = 'order.created';

  constructor(
    public readonly orderId: string,
    public readonly total: number,
  ) {
    super();
  }
}
typescript
import { Injectable } from '@nestjs/common';
import { OutboxEmitter } from '@nestarc/outbox';

@Injectable()
export class OrdersService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly outbox: OutboxEmitter,
  ) {}

  async createOrder(dto: CreateOrderDto) {
    return this.prisma.$transaction(async (tx) => {
      const order = await tx.order.create({ data: dto });
      await this.outbox.emit(tx, new OrderCreatedEvent(order.id, dto.total));
      return order;
    });
  }
}

Module Options

OptionTypeDefaultDescription
prismaclass ref / instancerequiredPrismaService class reference (forRoot) or resolved instance (forRootAsync)
polling.enabledbooleantrueEnable or disable the polling scheduler
polling.intervalnumber5000Milliseconds between polling cycles
polling.batchSizenumber100Max events processed per polling cycle
retry.maxRetriesnumber5Max delivery attempts before marking FAILED
retry.backoff'fixed' | 'exponential''exponential'Backoff strategy between retries
retry.initialDelaynumber1000Initial delay in ms (base for exponential, constant for fixed)
transportTypeLocalTransportCustom transport class implementing OutboxTransport
isGlobalbooleantrueRegister module globally so OutboxEmitter is available everywhere
stuckThresholdnumber300000Events stuck in PROCESSING longer than this (ms) are reset to PENDING

Released under the MIT License.