<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Samet GOKTEPE</title>
    <description>The latest articles on DEV Community by Samet GOKTEPE (@sametgoktepe).</description>
    <link>https://dev.to/sametgoktepe</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2130077%2F0151cb2c-8323-453c-b793-f2439c330a70.png</url>
      <title>DEV Community: Samet GOKTEPE</title>
      <link>https://dev.to/sametgoktepe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sametgoktepe"/>
    <language>en</language>
    <item>
      <title>Five days with eventferry — MySQL, SQL Server, MSK IAM, and a core that didn't move</title>
      <dc:creator>Samet GOKTEPE</dc:creator>
      <pubDate>Sat, 20 Jun 2026 16:32:10 +0000</pubDate>
      <link>https://dev.to/sametgoktepe/five-days-with-eventferry-mysql-sql-server-msk-iam-and-a-core-that-didnt-move-26fh</link>
      <guid>https://dev.to/sametgoktepe/five-days-with-eventferry-mysql-sql-server-msk-iam-and-a-core-that-didnt-move-26fh</guid>
      <description>&lt;p&gt;Five days ago I posted &lt;a href="https://dev.to/sametgoktepe/i-built-a-transactional-outbox-toolkit-for-nodejs-meet-eventferry-2opd"&gt;I built a transactional outbox toolkit for Node.js — meet eventferry&lt;/a&gt;. The pitch was small and specific: Postgres + Kafka, the dual-write bug, a Goldilocks alternative between Debezium and a hand-rolled outbox. The roadmap was a single line — &lt;em&gt;"MySQL, SQL Server, MongoDB are next"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is the follow-up. Same library, same problem, much wider surface — and a slightly absurd amount of code for one work-week.&lt;/p&gt;

&lt;p&gt;Tally as of today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;3 databases&lt;/strong&gt;: Postgres, MySQL / MariaDB, SQL Server (Azure SQL DB + Managed Instance + on-prem)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;8 packages&lt;/strong&gt; under &lt;code&gt;@eventferry/*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;A dedicated AWS MSK IAM helper&lt;/strong&gt; package&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;A CDC-driven waker&lt;/strong&gt; for SQL Server (separate package, on purpose)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;A 23-page wiki&lt;/strong&gt; + &lt;code&gt;llms.txt&lt;/code&gt; for AI assistants&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;&lt;code&gt;@eventferry/core&lt;/code&gt; did not need to change&lt;/strong&gt; for any of it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last bullet is the one I'm proudest of. I'll come back to it at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What got added (in one breath)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Two new database adapters: &lt;strong&gt;MySQL / MariaDB&lt;/strong&gt; and &lt;strong&gt;SQL Server&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A new &lt;strong&gt;AWS MSK IAM&lt;/strong&gt; helper package (&lt;code&gt;@eventferry/kafka-iam&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;CDC-driven waker&lt;/strong&gt; package (&lt;code&gt;@eventferry/mssql-cdc-relay&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Service Broker waker&lt;/strong&gt; for SQL Server (inside &lt;code&gt;@eventferry/mssql&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Kafka: producer-fenced restart, librdkafka stats hook, &lt;code&gt;publisher.admin()&lt;/code&gt;, &lt;code&gt;ensureTopics&lt;/code&gt;, &lt;code&gt;validateTopicsOnConnect&lt;/code&gt;, &lt;code&gt;publisher.healthCheck()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@eventferry/kafka/consume&lt;/code&gt; subpath — &lt;code&gt;decode()&lt;/code&gt; + &lt;code&gt;extractTraceContext()&lt;/code&gt; for consumers&lt;/li&gt;
&lt;li&gt;Schema Registry: subject naming strategies (TopicName / RecordName / TopicRecordName), basic + bearer auth, &lt;code&gt;serializeKey&lt;/code&gt;, &lt;code&gt;autoRegister&lt;/code&gt; toggle&lt;/li&gt;
&lt;li&gt;mTLS + SNI, SASL/OAUTHBEARER, full driver tuning, error classification with &lt;code&gt;errorKind&lt;/code&gt;, OpenTelemetry semconv tracing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The two new database adapters
&lt;/h2&gt;

&lt;h3&gt;
  
  
  MySQL / MariaDB
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MysqlStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createMigrationSql&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@eventferry/mysql&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createMigrationSql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MysqlStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MySQL 8.0.1+ / MariaDB 10.6+ — both engines use &lt;code&gt;SELECT ... FOR UPDATE SKIP LOCKED&lt;/code&gt; for the concurrent claim. There's also a binlog-driven streaming relay (&lt;code&gt;MysqlBinlogRelay&lt;/code&gt;) for sub-millisecond wakes on ROW-format binlogs.&lt;/p&gt;

&lt;p&gt;Shipping caught one real bug worth telling: MariaDB exposes the JSON type as a LONGTEXT alias and the mysql2 driver doesn't auto-parse it. On MySQL 8 the same column comes back as an already-parsed object. The integration suite caught it on the first MariaDB container run — three tests went red, fix was a defensive &lt;code&gt;JSON.parse&lt;/code&gt; for string payloads. The kind of bug you only find by running on the actual engine.&lt;/p&gt;

&lt;p&gt;For shops stuck on MySQL 5.7 or MariaDB &amp;lt; 10.6 (no SKIP LOCKED), the README documents an &lt;code&gt;UPDATE ... ORDER BY id LIMIT n&lt;/code&gt; + claim-token fallback. Throughput floor is lower, but race-free and shippable.&lt;/p&gt;

&lt;h3&gt;
  
  
  SQL Server
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MssqlStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createMigrationSql&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@eventferry/mssql&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createMigrationSql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dbo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MssqlStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dbo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was the bigger build. Three things in the claim path bit me during design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;READCOMMITTEDLOCK&lt;/code&gt; dodges a real RCSI gotcha. When the database has &lt;code&gt;READ_COMMITTED_SNAPSHOT = ON&lt;/code&gt;, &lt;code&gt;READPAST&lt;/code&gt; is silently rejected under default READ COMMITTED. The table hint forces locking semantics for the statement and works on both RCSI and non-RCSI databases.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ROWLOCK&lt;/code&gt; is mandatory with &lt;code&gt;READPAST&lt;/code&gt;. Without it, SQL Server's page-lock escalation turns your skip-locked queue into a serial queue. No error, just collapsed throughput.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OUTPUT inserted.* INTO @t&lt;/code&gt; is trigger-compatible. Raw &lt;code&gt;OUTPUT inserted.*&lt;/code&gt; fails with error 334 the moment any trigger is enabled on the table — even if no triggers exist today.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Schema-side: &lt;code&gt;BIGINT IDENTITY(1,1)&lt;/code&gt; clustered (not uniqueidentifier — that fragments the B-tree), &lt;code&gt;NVARCHAR(MAX) CHECK (ISJSON(value) = 1)&lt;/code&gt; for payload/headers, &lt;code&gt;DATETIME2(3)&lt;/code&gt; for every timestamp (&lt;code&gt;SYSUTCDATETIME()&lt;/code&gt; returns datetime2(7) and the comparison is direct — no &lt;code&gt;CONVERT_IMPLICIT&lt;/code&gt; penalty that would disable index seeks).&lt;/p&gt;

&lt;p&gt;Integration tests against a real SQL Server 2022 testcontainer cover strict head-of-aggregate ordering, no-double-claim under 4 concurrent claimers across 20 aggregates, BIGINT-as-JS-string round-trip (tedious returns BIGINT as a string by default), unicode + emoji payload, and &lt;code&gt;markFailed(null, 'failed')&lt;/code&gt; as a TypeError (instant re-claim hot loop guard).&lt;/p&gt;

&lt;h2&gt;
  
  
  Sub-second waking for SQL Server
&lt;/h2&gt;

&lt;p&gt;Polling works but leaves you with 250 ms–1 s of latency on idle. Two options shipped, picked by your hosting story:&lt;/p&gt;

&lt;h3&gt;
  
  
  MssqlServiceBrokerWaker (in-package, opt-in)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MssqlServiceBrokerWaker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createServiceBrokerSetupSql&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@eventferry/mssql&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createServiceBrokerSetupSql&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dbo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;waker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MssqlServiceBrokerWaker&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;poolConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// dedicated pool — NOT the main store pool&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dbo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Relay&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publisher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;waker&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The setup helper provisions queues, contracts, message types, an activation procedure that drains EndDialog messages so &lt;code&gt;sys.conversation_endpoints&lt;/code&gt; doesn't grow unbounded (the classic Rusanu leak), and an AFTER INSERT trigger that drops a wakeup per statement.&lt;/p&gt;

&lt;p&gt;It auto-detects Azure SQL Database (EngineEdition = 5) at &lt;code&gt;start()&lt;/code&gt; and refuses with a clear error. Service Broker is unsupported there per Microsoft's own migration-assessment rule — the waker won't lie to you about it.&lt;/p&gt;

&lt;p&gt;The dedicated pool is non-negotiable. &lt;code&gt;WAITFOR (RECEIVE ...)&lt;/code&gt; holds a connection slot indefinitely; sharing it with the store pool would starve everything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  @eventferry/mssql-cdc-relay (separate package)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MssqlCdcWaker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createCdcEnablementSql&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@eventferry/mssql-cdc-relay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createCdcEnablementSql&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dbo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Relay&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;publisher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;waker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MssqlCdcWaker&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dbo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CDC requires SQL Server Agent, and Agent isn't available on Azure SQL Database. So CDC went into a separate package — bundling it would have excluded the most common managed footprint from the main mssql package.&lt;/p&gt;

&lt;p&gt;The waker reads &lt;code&gt;sys.fn_cdc_get_max_lsn()&lt;/code&gt; on a small poll loop and wakes the core Relay when the LSN advances past a persisted watermark. The polling Relay then runs the same &lt;code&gt;MssqlStore.claimBatch&lt;/code&gt; — CDC is the wake signal, not a parallel publisher. Strict head-of-aggregate still flows through the claim path; I didn't try to re-implement it in CDC space.&lt;/p&gt;

&lt;p&gt;Failure modes are typed (&lt;code&gt;CdcNotEnabledError&lt;/code&gt;, &lt;code&gt;CdcRetentionExceededError&lt;/code&gt;, &lt;code&gt;WatermarkBelowMinLsnError&lt;/code&gt;, &lt;code&gt;CdcCaptureJobStoppedError&lt;/code&gt;) and surface via &lt;code&gt;onError&lt;/code&gt;. The waker never throws into the relay's claim loop — operator-facing errors stay where operators can see them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What grew on the Kafka side
&lt;/h2&gt;

&lt;p&gt;Five days ago the Kafka publisher was a thin wrapper. Now it's the layer most teams will spend the most config time on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auth and TLS
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;KafkaPublisher&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;brokers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/etc/ssl/kafka-ca.pem&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/etc/ssl/client.pem&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/etc/ssl/client-key.pem&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;servername&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;broker.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;sasl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;mechanism&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oauthbearer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;oauthBearerProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;service&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lifetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full &lt;code&gt;TlsConfig&lt;/code&gt; (CA pinning + mTLS + SNI), every SASL mechanism (PLAIN / SCRAM-SHA-256 / SCRAM-SHA-512 / OAUTHBEARER). The driver asymmetry on OAUTHBEARER is documented in the type — kafkajs reads &lt;code&gt;value&lt;/code&gt;; the confluent driver requires &lt;code&gt;value&lt;/code&gt; + &lt;code&gt;principal&lt;/code&gt; + &lt;code&gt;lifetime&lt;/code&gt; and accepts extensions. &lt;code&gt;rejectUnauthorized: false&lt;/code&gt; is not an option — TLS verification is non-negotiable; bring a ca.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS MSK IAM, as a separate package
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createMskIamSasl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@eventferry/kafka-iam&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;KafkaPublisher&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;brokers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sasl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createMskIamSasl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;us-east-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Token cache with a configurable refresh-ahead window (default 60 s on the 15-min MSK token lifetime), concurrent-refresh dedup so a thundering herd hits the SigV4 signer once, transient-failure recovery. Lives outside the main kafka package so non-AWS users don't pull aws-sdk into their tree.&lt;/p&gt;

&lt;h3&gt;
  
  
  Producer-fenced restart
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;KafkaPublisher&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;brokers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;transactional&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;transactionalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;POD_NAME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;replicaIndex&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// callable&lt;/span&gt;
  &lt;span class="na"&gt;autoRecoverFromFence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;onProducerFenced&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kafka.fenced&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PRODUCER_FENCED&lt;/code&gt; and &lt;code&gt;INVALID_PRODUCER_EPOCH&lt;/code&gt; split into a dedicated &lt;code&gt;errorKind: "fenced"&lt;/code&gt;. With &lt;code&gt;autoRecoverFromFence: true&lt;/code&gt;, the publisher does one transparent disconnect → connect → re-send cycle, with &lt;code&gt;initTransactions&lt;/code&gt; re-running as part of the reconnect. Concurrent fenced publishes share a single in-flight reconnect.&lt;/p&gt;

&lt;p&gt;For multi-instance setups the documented advice is: leave the option off, use a callable &lt;code&gt;transactionalId&lt;/code&gt; derived from per-pod runtime context, and treat a cross-instance fence as the broker telling the loser instance to stop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Admin + healthcheck
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;publisher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ensureTopics&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orders.created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="na"&gt;numPartitions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;replicationFactor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orders.created.dlq&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;numPartitions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;replicationFactor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/healthz&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;publisher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;healthCheck&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;publisher.admin()&lt;/code&gt; borrows a typed admin client; &lt;code&gt;ensureTopics&lt;/code&gt; is idempotent; &lt;code&gt;validateTopicsOnConnect&lt;/code&gt; catches "topic doesn't exist" surprises at startup on managed Kafka with &lt;code&gt;auto.create.topics.enable=false&lt;/code&gt;. &lt;code&gt;healthCheck&lt;/code&gt; is documented as "broker reachable + auth still good", not "publisher fully operational" — a fenced transactional producer would still answer healthy here, and the docs say so.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consumer side, in the same package
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;extractTraceContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@eventferry/kafka/consume&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;eachMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractTraceContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;startConsumerSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spanId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A producer-free subpath. &lt;code&gt;decode()&lt;/code&gt; normalizes the raw shape both kafkajs and confluent deliver; &lt;code&gt;extractTraceContext()&lt;/code&gt; strictly validates W3C traceparent headers (rejects all-zero IDs, &lt;code&gt;version: ff&lt;/code&gt;, malformed hex). Pair with &lt;code&gt;tracer.inject&lt;/code&gt; on the publisher and you have end-to-end W3C trace propagation.&lt;/p&gt;

&lt;h2&gt;
  
  
  @eventferry/all is now genuinely "all"
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i @eventferry/all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Last week this was 4 packages. Today it installs all 8: core + postgres + mysql + mssql + mssql-cdc-relay + kafka + kafka-iam + schema-registry. Cross-adapter name collisions resolve via &lt;code&gt;Mysql*&lt;/code&gt; / &lt;code&gt;Mssql*&lt;/code&gt; prefixes — the convention I picked up the moment a second database adapter joined.&lt;/p&gt;

&lt;p&gt;For production services that only need one database adapter, the individual packages are still the right install. &lt;code&gt;@eventferry/all&lt;/code&gt; is the prototyping convenience, not the production default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I'm proudest of (in five days)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Slice&lt;/th&gt;
&lt;th&gt;What it shipped&lt;/th&gt;
&lt;th&gt;Did &lt;code&gt;@eventferry/core&lt;/code&gt; need to change?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Phase A&lt;/td&gt;
&lt;td&gt;mTLS, OAUTHBEARER, OTel, tuning, error classifier&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phase B&lt;/td&gt;
&lt;td&gt;Admin, consume subpath, MSK IAM helper, Schema Registry enhancements&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phase C&lt;/td&gt;
&lt;td&gt;librdkafka stats, fenced restart, healthCheck, TLS docs&lt;/td&gt;
&lt;td&gt;One additive line — added "fenced" to the errorKind union&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL adapter&lt;/td&gt;
&lt;td&gt;MysqlStore, binlog relay&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server adapter&lt;/td&gt;
&lt;td&gt;MssqlStore, claim path, migrations&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service Broker + CDC waker&lt;/td&gt;
&lt;td&gt;Two OutboxWaker implementations&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Six slices of substantial work, one additive extension on the core's union type, zero breaking changes downstream.&lt;/p&gt;

&lt;p&gt;The core defines five interfaces — &lt;code&gt;OutboxStore&amp;lt;Tx&amp;gt;&lt;/code&gt;, &lt;code&gt;Publisher&lt;/code&gt;, &lt;code&gt;OutboxWaker&lt;/code&gt;, &lt;code&gt;Serializer&lt;/code&gt;, &lt;code&gt;Logger&lt;/code&gt;. Every adapter implements one of these. The relay orchestrates them without knowing what database or broker it's talking to. Dependency Inversion Principle, applied with intent at the start, paying off five days later when "add SQL Server" turned out to mean "implement &lt;code&gt;OutboxStore&amp;lt;mssql.Transaction&amp;gt;&lt;/code&gt;" rather than "redesign the core."&lt;/p&gt;

&lt;p&gt;This is the boring win nobody writes blog posts about, so here's mine: picking the right seams up-front meant a five-day expansion didn't cascade into a coordinated multi-package rewrite. Anyone still on the version of &lt;code&gt;@eventferry/core&lt;/code&gt; that shipped at launch can pull every adapter improvement without an upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;MongoDB is up next in the roadmap. After that, CockroachDB and SQLite. On the Kafka side: an &lt;code&gt;onMessageRetried&lt;/code&gt; hook, per-topic retry policy, a Prometheus metrics helper, OpenTelemetry messaging metrics once the OTel SDK stabilizes the counters.&lt;/p&gt;

&lt;p&gt;The bigger thing this week was documentation. There's now a &lt;a href="https://github.com/SametGoktepe/eventferry/wiki" rel="noopener noreferrer"&gt;23-page wiki&lt;/a&gt; covering every surface — Getting Started, Core Concepts, every adapter, Schema Registry, Authentication and TLS, AWS MSK IAM, Transactions and EOS, Admin Operations, Observability, Consuming Events, Dead-Letter Queue, Reliability and Error Handling, Operations Guide, Migrations and Upgrades, Troubleshooting, API Reference, FAQ, Roadmap. And an &lt;code&gt;llms.txt&lt;/code&gt; for AI assistants that want to recommend the right package.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i @eventferry/all                                                        &lt;span class="c"&gt;# everything&lt;/span&gt;
npm i @eventferry/core @eventferry/postgres @eventferry/kafka pg kafkajs     &lt;span class="c"&gt;# just postgres + kafka&lt;/span&gt;
npm i @eventferry/core @eventferry/mssql mssql                               &lt;span class="c"&gt;# just sql server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/SametGoktepe/eventferry" rel="noopener noreferrer"&gt;https://github.com/SametGoktepe/eventferry&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Wiki: &lt;a href="https://github.com/SametGoktepe/eventferry/wiki" rel="noopener noreferrer"&gt;https://github.com/SametGoktepe/eventferry/wiki&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm scope: &lt;a href="https://www.npmjs.com/org/eventferry" rel="noopener noreferrer"&gt;https://www.npmjs.com/org/eventferry&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Roadmap: &lt;a href="https://github.com/SametGoktepe/eventferry/blob/main/ROADMAP.md" rel="noopener noreferrer"&gt;https://github.com/SametGoktepe/eventferry/blob/main/ROADMAP.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you tried the library when it was Postgres-only and bounced because you were on MySQL or SQL Server, those doors are open now. If you're on the Kafka side and ran into auth / EOS / Schema Registry sharp edges, those got the most polish this week. And if your team has a Node.js / TypeScript service with a database write + a Kafka publish that needs to stay in sync — I'd still love to hear what's working, what's missing, and what you'd want next.&lt;/p&gt;

&lt;p&gt;Feedback, issues, and stars very welcome ⭐&lt;/p&gt;

</description>
      <category>node</category>
      <category>kafka</category>
      <category>typescript</category>
      <category>npm</category>
    </item>
    <item>
      <title>I built a transactional outbox toolkit for Node.js — meet eventferry</title>
      <dc:creator>Samet GOKTEPE</dc:creator>
      <pubDate>Wed, 17 Jun 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/sametgoktepe/i-built-a-transactional-outbox-toolkit-for-nodejs-meet-eventferry-2opd</link>
      <guid>https://dev.to/sametgoktepe/i-built-a-transactional-outbox-toolkit-for-nodejs-meet-eventferry-2opd</guid>
      <description>&lt;p&gt;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 &lt;a href="https://github.com/SametGoktepe/eventferry" rel="noopener noreferrer"&gt;eventferry&lt;/a&gt; to fix it: write your event in the &lt;em&gt;same transaction&lt;/em&gt; as your data, and a background relay reliably ships it to the broker. MIT-licensed, zero dep core, &lt;/p&gt;

&lt;p&gt;## The bug you might not know you have&lt;/p&gt;

&lt;p&gt;You write the order to your database. You commit. You send to Kafka.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INSERT INTO orders ...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;kafka&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orders.created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;dual-write problem&lt;/strong&gt;, and it's the reason event-driven Node.js services fail in ways that are very hard to debug after the fact.&lt;/p&gt;

&lt;p&gt;## The fix is the transactional outbox pattern&lt;/p&gt;

&lt;p&gt;Write the event into an &lt;strong&gt;outbox table&lt;/strong&gt; in the same transaction as your business data. A background &lt;strong&gt;relay&lt;/strong&gt; picks rows off that table and reliably publishes them to the broker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ┌─────────────┐   one TX     ┌──────────────┐   relay     ┌───────────────┐
  │  your code  │ ───────────▶ │  outbox tbl  │ ──────────▶ │ Kafka/Redpanda│
  │ (order svc) │   (atomic)   │  (Postgres)  │   publish   │     topic     │
  └─────────────┘              └──────────────┘             └───────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;## Why a new library — the honest answer&lt;/p&gt;

&lt;p&gt;There are three answers when you Google this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debezium&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pg-boss / BullMQ&lt;/strong&gt; keep getting suggested for this — they're &lt;em&gt;job queues&lt;/em&gt;, not outboxes. There's no atomic dual-write with your business transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A DIY outbox table&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;eventferry is the "I just want a small library" option.&lt;/p&gt;

&lt;p&gt;## Quick start&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Relay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PostgresStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;KafkaPublisher&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@eventferry/all&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PostgresStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;publisher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;KafkaPublisher&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kafkajs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;brokers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;localhost:19092&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;idempotent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Inside your business transaction:&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orders.created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;aggregateType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;aggregateId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;                                                                                                                                                                                                                                                           
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Background relay:&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;relay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Relay&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publisher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dlq&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orders.dlq&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;relay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SIGTERM&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;relay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole pattern.&lt;/p&gt;

&lt;p&gt;## What's inside&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Strict per-aggregate ordering&lt;/strong&gt; across N concurrent relays (&lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; + a NOT EXISTS guard)&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Crash-recovery reaper&lt;/strong&gt; — visibility timeout reclaims rows stuck in &lt;code&gt;processing&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🔁 &lt;strong&gt;Retries&lt;/strong&gt; with backoff + jitter, &lt;strong&gt;DLQ&lt;/strong&gt; routing for terminal failures&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Low-latency delivery&lt;/strong&gt;: poll, &lt;code&gt;LISTEN&lt;/code&gt;/&lt;code&gt;NOTIFY&lt;/code&gt; waker, or &lt;strong&gt;WAL streaming relay&lt;/strong&gt; (same mechanism Debezium uses)&lt;/li&gt;
&lt;li&gt;🔒 &lt;strong&gt;Type-safe event registry&lt;/strong&gt; with Standard Schema validation&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Schema Registry&lt;/strong&gt; support (Avro / Protobuf / JSON Schema, Confluent wire format)&lt;/li&gt;
&lt;li&gt;🧭 &lt;strong&gt;W3C trace propagation&lt;/strong&gt; (OpenTelemetry-ready)&lt;/li&gt;
&lt;li&gt;🪶 Zero-dependency core; pluggable store and broker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Integration tests run against real Postgres + Redpanda via Testcontainers.&lt;/p&gt;

&lt;p&gt;## Roadmap&lt;/p&gt;

&lt;p&gt;PostgreSQL ships today. &lt;strong&gt;MySQL/MariaDB, SQL Server, and MongoDB are next&lt;/strong&gt; — the relay is database-agnostic; each adapter is the &lt;code&gt;OutboxStore&lt;/code&gt; contract. CockroachDB, SQLite, Oracle, and DynamoDB are on the horizon. Full plan with architecture diagrams: &lt;a href="https://github.com/SametGoktepe/eventferry/blob/main/ROADMAP.md" rel="noopener noreferrer"&gt;ROADMAP.md&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;## Try it&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  npm i @eventferry/all pg kafkajs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;📦 &lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/@eventferry/all" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@eventferry/all&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/SametGoktepe/eventferry" rel="noopener noreferrer"&gt;https://github.com/SametGoktepe/eventferry&lt;/a&gt; — feedback, issues, and stars very welcome ⭐&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>node</category>
      <category>typescript</category>
      <category>postgres</category>
      <category>kafka</category>
    </item>
  </channel>
</rss>
