If your Node.js service writes to Postgres and publishes events to Kafka or Redpanda, you probably have a silent dual-write bug. I built eventferry to fix it: write your event in the same transaction as your data, and a background relay reliably ships it to the broker. MIT-licensed, zero dep core,
## The bug you might not know you have
You write the order to your database. You commit. You send to Kafka.
await db.query("INSERT INTO orders ...");
await kafka.send({ topic: "orders.created", ... });
Looks fine. Until a crash or a Kafka outage hits between those two calls. The DB thinks the order happened; downstream services never hear about it. Quietly broken β until production breaks.
This is the dual-write problem, and it's the reason event-driven Node.js services fail in ways that are very hard to debug after the fact.
## The fix is the transactional outbox pattern
Write the event into an outbox table in the same transaction as your business data. A background relay picks rows off that table and reliably publishes them to the broker.
βββββββββββββββ one TX ββββββββββββββββ relay βββββββββββββββββ
β your code β ββββββββββββΆ β outbox tbl β βββββββββββΆ β Kafka/Redpandaβ
β (order svc) β (atomic) β (Postgres) β publish β topic β
βββββββββββββββ ββββββββββββββββ βββββββββββββββββ
It's a well-known pattern, but most Node.js implementations get the corners wrong: strict per-aggregate ordering under concurrent relays, the crash-recovery reaper, retry/backoff math, DLQ routing, Schema Registry serialization.
## Why a new library β the honest answer
There are three answers when you Google this:
- Debezium is the obvious one. Great, but it's a JVM cluster + Kafka Connect to operate, and events are row-level (not domain-level). For a Node.js team that just wants a library, that's heavy.
- pg-boss / BullMQ keep getting suggested for this β they're job queues, not outboxes. There's no atomic dual-write with your business transaction.
- A DIY outbox table is what most teams roll. It works until it doesn't; the parts that bite you are exactly the ones you haven't written yet β per-aggregate ordering, the reaper, retry/backoff, DLQ.
eventferry is the "I just want a small library" option.
## Quick start
import { Relay, PostgresStore, KafkaPublisher } from "@eventferry/all";
const store = new PostgresStore({ pool });
const publisher = new KafkaPublisher({
driver: "kafkajs",
brokers: ["localhost:19092"],
idempotent: true,
});
// Inside your business transaction:
await store.enqueue(client, {
topic: "orders.created",
aggregateType: "order",
aggregateId: order.id,
payload: { orderId: order.id, total: order.total },
});
// Background relay:
const relay = new Relay({ store, publisher, dlq: { topic: "orders.dlq" } });
await relay.start();
process.on("SIGTERM", () => relay.stop());
That's the whole pattern.
## What's inside
- β
Strict per-aggregate ordering across N concurrent relays (
FOR UPDATE SKIP LOCKED+ a NOT EXISTS guard) - π Crash-recovery reaper β visibility timeout reclaims rows stuck in
processing - π Retries with backoff + jitter, DLQ routing for terminal failures
- β‘ Low-latency delivery: poll,
LISTEN/NOTIFYwaker, or WAL streaming relay (same mechanism Debezium uses) - π Type-safe event registry with Standard Schema validation
- π¦ Schema Registry support (Avro / Protobuf / JSON Schema, Confluent wire format)
- π§ W3C trace propagation (OpenTelemetry-ready)
- πͺΆ Zero-dependency core; pluggable store and broker
Integration tests run against real Postgres + Redpanda via Testcontainers.
## Roadmap
PostgreSQL ships today. MySQL/MariaDB, SQL Server, and MongoDB are next β the relay is database-agnostic; each adapter is the OutboxStore contract. CockroachDB, SQLite, Oracle, and DynamoDB are on the horizon. Full plan with architecture diagrams: ROADMAP.md.
## Try it
npm i @eventferry/all pg kafkajs
- π¦ npm: https://www.npmjs.com/package/@eventferry/all
- π» GitHub: https://github.com/SametGoktepe/eventferry β feedback, issues, and stars very welcome β
If you've tried Debezium or pg-boss or a DIY outbox for this and either landed somewhere good or got bitten, I'd love to hear about it in the comments.












