Transactional outbox + idempotent inbox for NestJS — persisted with Drizzle ORM (SQLite, Postgres & MySQL), delivered in-process, over Kafka, or over RabbitMQ.
Note
v0.x — early but stable. The public API (the producer, claimer, inbox, transport seam, and the Drizzle stores) is implemented and tested at 100% coverage. SQLite, Postgres, and MySQL are supported, with in-process (no broker), Kafka, and RabbitMQ transports.
"Write rows and publish an event" is a dual write — two systems that can't be updated atomically. If the process crashes between the DB commit and the broker publish, the event is lost; if it publishes then fails to commit, you emit a phantom event.
@nest-native/messaging closes that gap with the two halves of the reliable-messaging pattern:
- Transactional outbox (producer):
enqueue()writes the event into anoutbox_eventsrow inside your business transaction (via@nestjs-cls/transactional). A background claimer then relays committed rows to the broker — at-least-once, with retry/backoff. - Idempotent inbox (consumer):
runOnce()dedups redeliveries via a unique(source, message_key)row written in the same transaction as the side effect, yielding effective exactly-once processing.
It is not a generic multi-broker abstraction — it is the outbox/inbox pattern, done natively for the Drizzle + NestJS stack, delivered over Kafka or RabbitMQ.
npm install @nest-native/messaging
# plus your driver + transport (peers):
npm install drizzle-orm @nestjs-cls/transactional better-sqlite3 # or pg / mysql2
npm install @nest-native/kafka # only for the Kafka transport
npm install amqplib # only for the RabbitMQ transport| Import | Contents |
|---|---|
@nest-native/messaging |
core engine — OutboxProducer, OutboxClaimer + worker loop, InboxService, the OutboxTransport/OutboxStore/InboxStore seams, the wire contract, MessagingModule |
@nest-native/messaging/in-process |
the no-broker default transport — OutboxRegistry (topic → handler) + InProcessOutboxTransport |
@nest-native/messaging/sqlite |
better-sqlite3 (synchronous) stores + outbox_events/inbox_events table factories |
@nest-native/messaging/postgres |
node-postgres (async) stores + table factories |
@nest-native/messaging/mysql |
mysql2 (async) stores + table factories |
@nest-native/messaging/kafka |
KafkaOutboxTransport + the idempotent @KafkaConsumer base, over @nest-native/kafka |
@nest-native/messaging/rabbitmq |
RabbitOutboxTransport (confirm channel, mandatory publishes) + RabbitInboxConsumer, over your amqplib connection |
@nest-native/messaging/testing |
in-memory transport + harness for broker-free tests |
- Drivers: SQLite (better-sqlite3, sync), Postgres (
pg, async), and MySQL (mysql2, async) via per-dialect stores. - Transports: in-process (default,
@nest-native/messaging/in-process— no broker, at-least-once via the claimer), Kafka (@nest-native/kafka), and RabbitMQ (amqplib) — see RabbitMQ. - Roadmap: additional transports. CDC (Debezium) is an intentional non-goal — this is the app-level outbox.
| Runtime | Supported line |
|---|---|
| Node.js | >=22 (>=22.12 with NestJS 12 — see the note below the table) |
| NestJS | ^11.0.0 || ^12.0.0 |
| Drizzle ORM | ^0.44.0 || ^0.45.0 |
@nestjs-cls/transactional |
^3.0.0 — on NestJS 12, 3.3+ (with nestjs-cls 6.3+): the first releases whose own peer ranges admit 12 |
better-sqlite3 |
^11.0.0 || ^12.0.0 || ^13.0.0 |
@nest-native/kafka |
^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 — on NestJS 12, 0.5.1+: the first release whose peer range admits 12 |
amqplib |
^2.0.0 (RabbitMQ 4, optional) |
Both ends of the NestJS range are tested, not assumed: the default lockfile
keeps the suite on an 11.x in the middle of the range, and the nestjs-compat
CI matrix resolves the tree against each end in every workspace — 11.0.0
pinned exactly (nothing this package uses was added by a later 11.x), and
^12 — proves every workspace resolves exactly that and every peer range in
the NestJS ecosystem is satisfied, and reruns the suite, the package build, and
every sample (the RabbitMQ ones against a real broker). NestJS 11 runs on any Node.js >=22. NestJS 12 is ESM-only;
loading it from CommonJS (this package, and every sample) goes through Node's
require(esm), which is behind a flag before Node.js 22.12.0, so the 12 end of
the range needs Node.js >=22.12 — a current Node 22, or 24. engines stays
>=22 because the 11 end does not need more; Node 22.0–22.11 satisfies it and
still cannot load NestJS 12.
Every PR runs the full gate — build, typecheck, coverage with c8 enforced at
100% for statements, branches, functions, and lines, cognitive complexity
enforcement (SonarJS threshold 15), tarball validation, sample version sync,
and a supply-chain audit:
npm run ciCI adds compatibility legs on top of that gate for the ends of every published
peer range the default lockfile does not install: better-sqlite3 13, and the
nestjs-compat matrix for NestJS 11.0.0 (pinned exactly) and ^12 (the
lockfile dropped and the tree resolved with --no-save against that end in
every workspace, proven from inside the package and each sample to resolve
exactly it with every NestJS-ecosystem peer range satisfied, then the suite,
the build, and the sample matrix). Both ends of every published peer range are
tested claims. The NestJS install is a fresh-checkout recipe — it only resolves from an empty
node_modules with no lockfile, which is what a CI runner has. On top of an
existing install, the hidden node_modules/.package-lock.json replays the
ERESOLVE that dropping the lockfile avoids, and every workspace stays on 11;
to rerun that leg locally, start from a clean worktree or
rm -rf node_modules package-lock.json first (the exact command is in
.github/workflows/ci.yml).
A dedicated integration job runs the gated real-backend specs — the MySQL
and PostgreSQL round-trips and the RabbitMQ transport and inbox — against
service containers on every PR, and fails if any of them skipped, so a
missing backend can never pass as green.
Two optional, local-only layers sit on top (forks work without them):
- Full mode —
npm run infra:up && npm run test:fullruns those same gated specs against disposable Docker containers (compose.yaml) on your machine;npm run infra:downcleans up. - Mutation testing —
npm run test:mutation(incremental Stryker run;test:mutation:fullre-tests everything). Scope withSTRYKER_MUTATE, include the gated I/O specs withSTRYKER_WITH_INFRA=1.
Details — including the pre-PR ritual and agent instructions — in GUIDELINES_NEST_MESSAGING.md.
See the documentation for the full guide. Part of the nest-native family. Not affiliated with the NestJS core team.