<?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: Rodrigo Nogueira</title>
    <description>The latest articles on DEV Community by Rodrigo Nogueira (@rodrigobnogueira).</description>
    <link>https://dev.to/rodrigobnogueira</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%2F806537%2F35f1c67a-cb9c-4bb8-8f57-314cce07df30.jpg</url>
      <title>DEV Community: Rodrigo Nogueira</title>
      <link>https://dev.to/rodrigobnogueira</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rodrigobnogueira"/>
    <language>en</language>
    <item>
      <title>The lockout bypass hiding in your login form</title>
      <dc:creator>Rodrigo Nogueira</dc:creator>
      <pubDate>Mon, 20 Jul 2026 16:39:15 +0000</pubDate>
      <link>https://dev.to/rodrigobnogueira/the-lockout-bypass-hiding-in-your-login-form-369l</link>
      <guid>https://dev.to/rodrigobnogueira/the-lockout-bypass-hiding-in-your-login-form-369l</guid>
      <description>&lt;p&gt;An application adds account lockout: five failed logins for a username, then a cooloff. It looks correct. It has a hole.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /login  { username: "Alice",  password: "..." }   // failure #1
POST /login  { username: "alice",  password: "..." }   // failure #1 (again)
POST /login  { username: "ALICE",  password: "..." }   // failure #1 (again)
POST /login  { username: "alice ", password: "..." }   // failure #1 (again)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the login is case-insensitive but the lockout keys on the raw string, each spelling is a &lt;em&gt;separate&lt;/em&gt; counter. The limit is per-spelling, so the attacker never trips it. Case and trailing whitespace alone multiply the effective budget several times over; add Unicode and it is worse.&lt;/p&gt;

&lt;p&gt;This is a small bug with a large blast radius — precisely the kind of edge a lockout implementation has to normalize away. Python has &lt;a href="https://django-axes.readthedocs.io/" rel="noopener noreferrer"&gt;django-axes&lt;/a&gt; for it. The NestJS ecosystem had no equivalent for the common case — lock on the database you already run, no Redis — so &lt;a href="https://www.npmjs.com/package/@nest-native/lockout" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/lockout&lt;/code&gt;&lt;/a&gt; (on a framework-agnostic core, &lt;a href="https://www.npmjs.com/package/@authlock/core" rel="noopener noreferrer"&gt;&lt;code&gt;@authlock/core&lt;/code&gt;&lt;/a&gt;) fills it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest constraint first
&lt;/h2&gt;

&lt;p&gt;django-axes can be nearly install-and-forget because Django emits an ambient &lt;code&gt;user_login_failed&lt;/code&gt; signal it hooks into. &lt;strong&gt;NestJS has no equivalent signal bus.&lt;/strong&gt; Any library that claims otherwise is hiding wiring somewhere. So this one doesn't claim it: the integration is explicit, and the docs lead with that.&lt;/p&gt;

&lt;p&gt;You call it from your own login handler, at the point where the outcome is known:&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;async&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LoginInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&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;identity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ip&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;gate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lockout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// pre-auth: is it locked?&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;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TooManyRequests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryAfterMs&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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lockout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reportFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// count it&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Unauthorized&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lockout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reportSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// reset on success&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three explicit calls — &lt;code&gt;check&lt;/code&gt; before the credential, &lt;code&gt;reportFailure&lt;/code&gt; / &lt;code&gt;reportSuccess&lt;/code&gt; after. There is a &lt;code&gt;LockoutGuard&lt;/code&gt; for plain HTTP, and the same service works on tRPC, GraphQL, or a WebSocket, where a guard's body-reading extractor doesn't fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the bypass
&lt;/h2&gt;

&lt;p&gt;The opening bug is a one-line configuration once the library owns key derivation. Declare a per-dimension normalizer; it is applied to each value before the key is hashed, on every path (check, record, and reset), so the counter is shared and unlock works under any spelling:&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="nx"&gt;LockoutModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forRoot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;store&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;InMemoryLockoutStore&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// swap for a Drizzle store in production&lt;/span&gt;
  &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cooloffMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;parameters&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="s1"&gt;username&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
  &lt;span class="na"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&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;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// ip left verbatim&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normalization is the app's decision — only you know which dimensions your auth treats as equal — so it is opt-in, but applied on every path (check, record, reset) rather than left to each call site.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F90qgqz0wpkq62jmqw5jy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F90qgqz0wpkq62jmqw5jy.png" alt="Three separate counters labeled Alice, ALICE, and " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The security details that are easy to get wrong
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv3rw0pbdy0zs6cahqcqr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv3rw0pbdy0zs6cahqcqr.png" alt="Two counters side by side — a username counter, full and locked, and an ip counter, half full — both feeding into a single LOCKED state, illustrating that any configured key tripping locks the identity" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A lock trips if &lt;em&gt;any&lt;/em&gt; configured key trips.&lt;/strong&gt; With &lt;code&gt;[['username'], ['ip']]&lt;/code&gt;,
a username lock catches a single-target attack even from rotating IPs, and an
IP lock catches spraying across many usernames.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cooloff is anchored to the &lt;em&gt;last&lt;/em&gt; failure, not the window start.&lt;/strong&gt; Every
failed attempt during a lock re-anchors it, so an attacker can't get a free
burst of guesses between a base lock and the next tier. The counting &lt;em&gt;window&lt;/em&gt;
is still measured from the first failure, which bounds how long a persistent
attacker can keep a victim locked out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiered cooloff is validated as monotonic.&lt;/strong&gt; A schedule where failing &lt;em&gt;more&lt;/em&gt;
locks for &lt;em&gt;less&lt;/em&gt; time would let an attacker self-unlock early; the constructor
rejects it (along with duplicate or non-integer thresholds).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail-open by default.&lt;/strong&gt; If the store errors, the attempt is allowed and
logged — a database blip must not lock every user out. &lt;code&gt;failMode: 'closed'&lt;/code&gt;
is available when denying during an outage is the right trade-off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credentials never reach the store.&lt;/strong&gt; Keys are SHA-256 hashes of the
identity dimensions, so a raw username never appears in a row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-instance counting is atomic.&lt;/strong&gt; The Drizzle stores
(SQLite / Postgres / MySQL) increment through a single atomic
create-or-increment-with-window-reset, so concurrent attempts across app
instances count exactly once and can never overshoot the limit unnoticed.
(MySQL re-reads the row afterward, since it has no &lt;code&gt;RETURNING&lt;/code&gt;.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Operations
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;reset(identity)&lt;/code&gt; unlocks one identity (an admin action, an unlock-via-email link). With single-dimension parameters, &lt;code&gt;reset({ username })&lt;/code&gt; already clears that user across every IP. For a false-positive lockout wave, &lt;code&gt;resetAll()&lt;/code&gt; clears every counter at once.&lt;/p&gt;

&lt;p&gt;There is no built-in audit log: the library stores only the counters it needs. An audit trail is built from what you already have — log each attempt at your own &lt;code&gt;reportFailure&lt;/code&gt; / &lt;code&gt;reportSuccess&lt;/code&gt; call site, and alert on the &lt;code&gt;onLockout&lt;/code&gt; transition hook — recording identity dimensions only, never the credential.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is not
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not a rate limiter.&lt;/strong&gt; Request-rate limiting is
&lt;a href="https://www.npmjs.com/package/@nestjs/throttler" rel="noopener noreferrer"&gt;&lt;code&gt;@nestjs/throttler&lt;/code&gt;&lt;/a&gt;; this is
&lt;em&gt;failed-authentication&lt;/em&gt; lockout. They compose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a proxy-header parser.&lt;/strong&gt; The default extractor uses &lt;code&gt;req.ip&lt;/code&gt;, not
&lt;code&gt;X-Forwarded-For&lt;/code&gt; — trusting a spoofable header is how an attacker rotates
past an IP lock or forges a victim's IP to lock them out. Configure &lt;code&gt;trust
proxy&lt;/code&gt; for your topology; the extractor is a hook, not a guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not tied to a framework.&lt;/strong&gt; The engine is zero-dependency and runs from
Express or a bare script; &lt;code&gt;@nest-native/lockout&lt;/code&gt; is a thin DI shell over it,
built on stable NestJS primitives so the same code runs on NestJS 10, 11, and 12.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Case and whitespace aren't app-level afterthoughts — they are correctness properties of the lockout key, enforced where the library owns that key.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adapter: &lt;a href="https://www.npmjs.com/package/@nest-native/lockout" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/lockout&lt;/code&gt;&lt;/a&gt; · engine: &lt;a href="https://www.npmjs.com/package/@authlock/core" rel="noopener noreferrer"&gt;&lt;code&gt;@authlock/core&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://nest-native.dev/lockout/" rel="noopener noreferrer"&gt;nest-native.dev/lockout&lt;/a&gt; · source: &lt;a href="https://github.com/nest-native/lockout" rel="noopener noreferrer"&gt;github.com/nest-native/lockout&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not affiliated with the NestJS core team or the django-axes project.&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>security</category>
      <category>authentication</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Cache invalidation is the dual-write problem</title>
      <dc:creator>Rodrigo Nogueira</dc:creator>
      <pubDate>Mon, 20 Jul 2026 05:33:25 +0000</pubDate>
      <link>https://dev.to/rodrigobnogueira/cache-invalidation-is-the-dual-write-problem-lp6</link>
      <guid>https://dev.to/rodrigobnogueira/cache-invalidation-is-the-dual-write-problem-lp6</guid>
      <description>&lt;p&gt;Cache invalidation, in the form most backends actually meet it, is not a unique problem. It is the dual-write problem — the failure mode covered in &lt;a href="https://dev.to/rodrigobnogueira/the-dual-write-problem-in-nestjs-solved-with-drizzle-a-transactional-outbox-idempotent-inbox-462l"&gt;the transactional outbox article&lt;/a&gt; — in different clothing:&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="c1"&gt;// the dual-write problem, messaging edition&lt;/span&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;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// 1. write the row&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="s1"&gt;order.placed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;  &lt;span class="c1"&gt;// 2. publish the event&lt;/span&gt;

&lt;span class="c1"&gt;// the dual-write problem, caching edition&lt;/span&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;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;      &lt;span class="c1"&gt;// 1. write the row&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`project:&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;             &lt;span class="c1"&gt;// 2. invalidate the cache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same bug, both times. If the process crashes between step 1 and step 2, the database and the second system disagree, and nothing reconciles them. In the messaging edition, consumers silently miss an event. In the caching edition, every instance keeps serving the old value. A database and a cache cannot commit atomically, for the same reason a database and a broker cannot.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn7s4sb2m14o8vaireywr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn7s4sb2m14o8vaireywr.png" alt="Two identical flows side by side: a database write followed by " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Messaging has a proven answer: the transactional outbox — one write, inside the transaction, with the second system driven from what committed. Caching mostly relies on TTLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ecosystem today
&lt;/h2&gt;

&lt;p&gt;What a survey of the ecosystem shows, before building anything new:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@nestjs/cache-manager&lt;/code&gt;&lt;/strong&gt; (the official module, ~2M downloads/week) supports TTL, &lt;code&gt;del&lt;/code&gt;, and &lt;code&gt;clear&lt;/code&gt;. No tags, no cross-instance invalidation. Requests for richer features — an &lt;a href="https://github.com/nestjs/cache-manager/issues/948" rel="noopener noreferrer"&gt;auto-invalidation strategy&lt;/a&gt; and &lt;a href="https://github.com/nestjs/cache-manager/issues/944" rel="noopener noreferrer"&gt;hybrid multi-store caching&lt;/a&gt; — were both declined in the same month, with the same reply: &lt;em&gt;"we'd encourage you to collaborate with the community on publishing it as an open source package."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Upstream, tag-based invalidation was &lt;a href="https://github.com/jaredwray/cacheable/issues/120" rel="noopener noreferrer"&gt;requested in 2018&lt;/a&gt; and &lt;a href="https://github.com/jaredwray/cacheable/issues/1640" rel="noopener noreferrer"&gt;shipped a first version in mid-2026&lt;/a&gt; — a lazy version-check design, with no way to push an invalidation to other instances.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;bentocache&lt;/strong&gt;, the most architecturally complete Node cache (L1 memory + L2 store + a sync bus), supports Postgres as &lt;em&gt;storage&lt;/em&gt; — but its invalidation bus requires &lt;strong&gt;Redis or MQTT&lt;/strong&gt;. The coherence layer is the one piece that demands new infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rails 8&lt;/strong&gt; made a database-backed cache the default for new apps; &lt;strong&gt;Laravel 11&lt;/strong&gt; ships the database cache driver by default. Database-as-cache-infrastructure is mainstream — but neither framework pushes invalidations to in-memory tiers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The missing piece: an in-memory cache per instance, kept coherent across instances &lt;em&gt;through the database those instances already share&lt;/em&gt;, with the invalidation &lt;strong&gt;atomic with the write&lt;/strong&gt;. That now exists as &lt;a href="https://www.npmjs.com/package/@stalefree/core" rel="noopener noreferrer"&gt;&lt;code&gt;@stalefree/core&lt;/code&gt;&lt;/a&gt;, with a thin NestJS adapter, &lt;a href="https://www.npmjs.com/package/@nest-native/cache" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/cache&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same fix
&lt;/h2&gt;

&lt;p&gt;On Postgres, &lt;code&gt;pg_notify&lt;/code&gt; is transactional: a notification fired inside a transaction is delivered &lt;strong&gt;when the transaction commits&lt;/strong&gt;, and silently dropped if it rolls back. That property removes the dual write. The invalidation is not a second system written after the database — it is part of the same commit:&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;transaction&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;tx&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projects&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="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="c1"&gt;// both of these ride THIS 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;invalidateTagsInTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`project:&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// shared L2 rows die with the commit&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publishInTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`project:&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// every instance's L1 evicts on commit&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A crash before the commit means nothing happened — no write, no invalidation, no disagreement. When the commit lands, Postgres itself delivers the eviction to every instance holding a &lt;code&gt;LISTEN&lt;/code&gt; connection. There is no window where the row changed but the invalidation was lost, because there were never two writes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs7dmqdbjmtgaqn2v06aa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs7dmqdbjmtgaqn2v06aa.png" alt="One transaction containing the row update, the L2 delete, and the notify — on commit the eviction fans out to every instance's in-memory cache; on rollback everything is discarded" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the outbox mechanism — &lt;code&gt;pg_notify&lt;/code&gt; riding the business transaction — pointed at a different second system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tag-based invalidation
&lt;/h2&gt;

&lt;p&gt;Exact-key invalidation breaks as soon as one mutation affects several cached reads: a renamed project appears in &lt;code&gt;project:42&lt;/code&gt;, in &lt;code&gt;org:7:projects&lt;/code&gt;, and in list views. Reads declare &lt;strong&gt;tags&lt;/strong&gt;; mutations evict by tag:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;project&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;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`org:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:project:&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// tenancy lives IN the key&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ttlMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`org:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:projects`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`project:&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="s2"&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;span class="c1"&gt;// one call, after the mutation — evicts every carrier, on every instance&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateTags&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;`project:&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="s2"&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;wrap&lt;/code&gt; is read-through with single-flight: concurrent misses for a key share one loader run. The L1 keeps a reverse tag→keys index, so a tag eviction is O(affected entries), not O(cache size).&lt;/p&gt;

&lt;h2&gt;
  
  
  Two correctness rules
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Every entry has a TTL — the API rejects infinite ones.&lt;/strong&gt; The bus is best-effort by design; a listener mid-reconnect misses a message. The TTL is the delivery backstop: a lost invalidation means stale-until-TTL, never stale-forever. A cache whose correctness depends on a bus message arriving is a design bug, and the API makes that cache impossible to build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Take atomicity where it is available.&lt;/strong&gt; The transactional path is the strongest guarantee on offer and costs one line inside an existing transaction. Outside Postgres — or outside a transaction — &lt;code&gt;invalidateTags&lt;/code&gt; still evicts locally, deletes L2 rows, and publishes, falling back from "atomic with the write" to "fire-and-forget with the TTL backstop."&lt;/p&gt;

&lt;h2&gt;
  
  
  Coherence by deployment shape
&lt;/h2&gt;

&lt;p&gt;The bus is a seam; the implementation follows the deployment:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Deployment&lt;/th&gt;
&lt;th&gt;Bus&lt;/th&gt;
&lt;th&gt;Extra infrastructure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One process&lt;/td&gt;
&lt;td&gt;none — coherent by definition&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Several processes, one machine (the classic app + worker split)&lt;/td&gt;
&lt;td&gt;a unix-domain-socket mesh with crash re-election&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Several machines sharing Postgres&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LISTEN&lt;/code&gt;/&lt;code&gt;NOTIFY&lt;/code&gt;, transactional publish&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The middle row covers SQLite completely: processes sharing a SQLite file are on one machine by definition, so the socket bus is the entire multi-process story for that dialect. The bottom row is the summary: cross-machine cache coherence, carried by a database already in production.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fumxwitq43eb2ozq2w968.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fumxwitq43eb2ozq2w968.png" alt="Three deployment shapes: one process, several processes on one machine joined by a socket, and several machines sharing one Postgres" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Degradation: on the socket bus, a frame too large for the wire is not dropped — it degrades on the send side to a full clear on every receiver (colder, never staler). On Postgres, large invalidations are chunked across multiple &lt;code&gt;NOTIFY&lt;/code&gt; payloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Non-goals
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Not a distributed cache, and not a Redis replacement for hot-path KV at massive scale. The target is the common case: expensive reads, tagged, evicted when the data changes.&lt;/li&gt;
&lt;li&gt;Not exactly-once delivery of invalidations — the TTL backstop covers loss.&lt;/li&gt;
&lt;li&gt;Single-flight is per-process; cross-instance stampede control is out of scope.&lt;/li&gt;
&lt;li&gt;Cached values are held by reference in L1 — treat them as immutable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A running example
&lt;/h2&gt;

&lt;p&gt;The pattern runs as the newest chapter of the &lt;a href="https://github.com/nest-native/reference-app" rel="noopener noreferrer"&gt;nest-native reference app&lt;/a&gt;: &lt;code&gt;projects.*&lt;/code&gt; and &lt;code&gt;activity.list&lt;/code&gt; are cached at the API seam, mutations invalidate by tag, and the activity feed's projection invalidates its own cache at the write site. The e2e spec runs with a deliberately long &lt;strong&gt;ten-minute TTL&lt;/strong&gt; — when it asserts that a mutation is visible in the next read within seconds, only invalidation can make that pass, provably not expiry.&lt;/p&gt;

&lt;p&gt;Cache invalidation remains hard in the general case. The version most backends face — a cached read, a database write, an instance serving the old value — is the dual-write problem, and the dual-write problem has a known fix: write to one system, and let the system that can commit atomically drive the other.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;Core: &lt;a href="https://www.npmjs.com/package/@stalefree/core" rel="noopener noreferrer"&gt;&lt;code&gt;@stalefree/core&lt;/code&gt;&lt;/a&gt; · adapter: &lt;a href="https://www.npmjs.com/package/@nest-native/cache" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/cache&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://nest-native.dev/cache/" rel="noopener noreferrer"&gt;nest-native.dev/cache&lt;/a&gt; · source: &lt;a href="https://github.com/nest-native/cache" rel="noopener noreferrer"&gt;github.com/nest-native/cache&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback and issues welcome — especially for cases the pattern does not cover.&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>postgres</category>
      <category>caching</category>
      <category>typescript</category>
    </item>
    <item>
      <title>The dual-write problem in NestJS, solved with Drizzle: a transactional outbox + idempotent inbox</title>
      <dc:creator>Rodrigo Nogueira</dc:creator>
      <pubDate>Fri, 03 Jul 2026 22:00:26 +0000</pubDate>
      <link>https://dev.to/rodrigobnogueira/the-dual-write-problem-in-nestjs-solved-with-drizzle-a-transactional-outbox-idempotent-inbox-462l</link>
      <guid>https://dev.to/rodrigobnogueira/the-dual-write-problem-in-nestjs-solved-with-drizzle-a-transactional-outbox-idempotent-inbox-462l</guid>
      <description>&lt;p&gt;Somewhere in most event-driven backends, there's a method that looks like this:&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;async&lt;/span&gt; &lt;span class="nf"&gt;placeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PlaceOrderInput&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&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;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// 1. write the row&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&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="s1"&gt;order.placed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// 2. publish the event&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks fine, and it has a bug. If the process crashes between 1 and 2, the order exists but the event never happened — downstream consumers silently miss it. Swap the order and you get the opposite failure: an event for an order that was rolled back. There is no &lt;code&gt;try/catch&lt;/code&gt; arrangement that fixes this, because a database and a broker cannot commit atomically. This is the &lt;strong&gt;dual-write problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The boring, proven fix is the &lt;strong&gt;transactional outbox&lt;/strong&gt;: don't publish in step 2. Instead, write the event into an &lt;code&gt;outbox_events&lt;/code&gt; table &lt;em&gt;in the same database transaction&lt;/em&gt; as the business row. A background worker then relays committed rows to the broker. The transaction is the only atomic boundary you have — so put both writes inside it.&lt;/p&gt;

&lt;p&gt;That gives you at-least-once delivery, which means the consumer side needs the mirror-image pattern: an &lt;strong&gt;idempotent inbox&lt;/strong&gt; that deduplicates redeliveries, so the side effect runs exactly once even when Kafka delivers twice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nest-native/messaging" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/messaging&lt;/code&gt;&lt;/a&gt; — part of &lt;a href="https://github.com/nest-native" rel="noopener noreferrer"&gt;nest-native&lt;/a&gt;, a set of NestJS integrations — packages this pair as a library, after the pattern was first built by hand inside a reference application. It's the outbox/inbox pattern for the &lt;strong&gt;Drizzle ORM + NestJS&lt;/strong&gt; stack — a niche the existing NestJS outbox libraries (which target TypeORM and MikroORM — see &lt;a href="https://github.com/fullstackhouse/nestjs-outbox" rel="noopener noreferrer"&gt;nestjs-outbox&lt;/a&gt; and &lt;a href="https://github.com/Nestixis/nestjs-inbox-outbox" rel="noopener noreferrer"&gt;nestjs-inbox-outbox&lt;/a&gt;, both solid) don't cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  The producer half: enqueue inside your transaction
&lt;/h2&gt;

&lt;p&gt;The library ships the tables as Drizzle factories per dialect (SQLite, Postgres, MySQL). You add them to your schema and generate a migration like any other table:&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="c1"&gt;// schema.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;outboxEvents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inboxEvents&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="s1"&gt;@nest-native/messaging/sqlite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// or /postgres, /mysql&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transactions ride on &lt;a href="https://www.npmjs.com/package/@nestjs-cls/transactional" rel="noopener noreferrer"&gt;&lt;code&gt;@nestjs-cls/transactional&lt;/code&gt;&lt;/a&gt; with its Drizzle adapter — the same &lt;code&gt;@Transactional()&lt;/code&gt; decorator you'd use anyway:&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="nx"&gt;MessagingModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forRoot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;drizzleInstanceToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DRIZZLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// your Drizzle DI token&lt;/span&gt;
  &lt;span class="na"&gt;outboxStore&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;SqliteOutboxStore&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;   &lt;span class="c1"&gt;// or PostgresOutboxStore / MysqlOutboxStore&lt;/span&gt;
  &lt;span class="na"&gt;inboxStore&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;SqliteInboxStore&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nx"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                              &lt;span class="c1"&gt;// where the claimer relays to — below&lt;/span&gt;
&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the business code:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;InjectTransaction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppDatabase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OutboxProducer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SqliteOutboxStore&lt;/span&gt;&lt;span class="o"&gt;&amp;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;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Transactional&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;placeOrder&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&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;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&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="nx"&gt;item&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;producer&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="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="s1"&gt;order.placed&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&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="s2"&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order row and the outbox row commit together, or roll back together. A throw after &lt;code&gt;enqueue&lt;/code&gt; produces no phantom event; a crash after commit loses nothing, because the event is durably in your database.&lt;/p&gt;

&lt;p&gt;(One nuance the library handles for you: better-sqlite3 transactions are synchronous while Postgres/MySQL are async. The per-dialect stores own that difference — on SQLite &lt;code&gt;enqueue&lt;/code&gt; returns the row directly inside the sync transaction body; on Postgres you &lt;code&gt;await&lt;/code&gt; it. Same code shape either way.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Relaying: the claimer and the worker
&lt;/h2&gt;

&lt;p&gt;A claimer polls for committed rows, publishes each through a transport, and applies retry-with-backoff on failure — including reclaiming rows from a worker that died mid-flight:&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="c1"&gt;// scripts/start-worker.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&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;NestFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createApplicationContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AppModule&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;runWorkerLoop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&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="nx"&gt;OutboxClaimer&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;pollIntervalMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;shutdownSignal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// AbortSignal wired to SIGTERM&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A note on that &lt;code&gt;pollIntervalMs&lt;/code&gt;, because a sharp question in the comments called it "a quiet latency knob": the loop is &lt;strong&gt;self-clocking&lt;/strong&gt;. After a tick that claims a full batch it loops again immediately to drain the backlog; it only sleeps the interval when a tick claims &lt;em&gt;nothing&lt;/em&gt;. So 2s is the worst case for a lone event landing in an otherwise-idle outbox — not a per-event tax — and under load, throughput is never gated by it.&lt;/p&gt;

&lt;p&gt;When even that idle latency matters (something user-facing waiting behind an outbox event), lowering the interval works but has a floor and a DB-poll cost. As of 0.4.0 there's a better lever — an in-process wake:&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="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;OutboxWaker&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;runWorkerLoop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&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="nx"&gt;OutboxClaimer&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;pollIntervalMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&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;waker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// request path — right AFTER the enqueueing transaction commits:&lt;/span&gt;
&lt;span class="nx"&gt;waker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// the idle worker ticks now instead of on the next poll&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Polling stays the backstop — a missed &lt;code&gt;notify()&lt;/code&gt; only widens latency back to one interval, never drops an event — and wakes are latched, so a notify landing in the sliver between a tick and its sleep isn't lost.&lt;/p&gt;

&lt;p&gt;Running the worker as a &lt;em&gt;separate process&lt;/em&gt; on the same machine (the usual app + worker split)? The &lt;code&gt;WakeSocketServer&lt;/code&gt;/&lt;code&gt;WakeSocketClient&lt;/code&gt; pair carries the same wake over a unix domain socket — the worker feeds incoming connections into its waker, and the client's &lt;code&gt;notify()&lt;/code&gt; is fire-and-forget, never throwing into the request path:&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="c1"&gt;// worker process&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WakeSocketServer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&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;wakeSocket&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;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// app process — same path, right after the commit&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WakeSocketClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&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;wakeSocket&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Workers on &lt;em&gt;other machines&lt;/em&gt; (Postgres only, as of 0.5.0) get the wake through the one thing they already share — the database, via &lt;code&gt;LISTEN&lt;/code&gt;/&lt;code&gt;NOTIFY&lt;/code&gt;:&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="c1"&gt;// producer side — one store option; pg_notify rides the enqueue transaction,&lt;/span&gt;
&lt;span class="c1"&gt;// so Postgres delivers the wake ON COMMIT and drops it on rollback: the signal&lt;/span&gt;
&lt;span class="c1"&gt;// is atomic with the event becoming visible, no post-commit discipline needed&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PostgresOutboxStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;wakeChannel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;outbox_wake&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// worker side — a dedicated (non-pooled) LISTEN connection feeds the same waker&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listener&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;PostgresWakeListener&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;connect&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="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;keepAlive&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;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;outbox_wake&lt;/span&gt;&lt;span class="dl"&gt;'&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;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;listener&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="c1"&gt;// reconnects on drops; await listener.stop() on shutdown&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Missed notifications aren't recovered — polling remains the backstop, as with every tier.&lt;/p&gt;

&lt;p&gt;For Kafka the transport is one line, built on &lt;a href="https://github.com/nest-native/kafka" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/kafka&lt;/code&gt;&lt;/a&gt; (Confluent's official JS client underneath):&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="nx"&gt;MessagingModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forRootAsync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;drizzleInstanceToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DRIZZLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;outboxStore&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;SqliteOutboxStore&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;inboxStore&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;SqliteInboxStore&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;KafkaProducerService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;useTransport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;KafkaOutboxTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;producer&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;Don't have Kafka yet? There's an in-process transport (&lt;code&gt;@nest-native/messaging/in-process&lt;/code&gt;) — a topic→handler registry with the same at-least-once semantics — so a modular monolith can adopt the pattern today and swap the transport for a broker later without touching a line of domain code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The consumer half: exactly-once effects
&lt;/h2&gt;

&lt;p&gt;Kafka is at-least-once by contract, so redelivery is a &lt;em&gt;when&lt;/em&gt;, not an &lt;em&gt;if&lt;/em&gt;. The inbox primitive is a single method:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outcome&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;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dedupKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;source&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="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// your side effect — runs in the SAME transaction as the dedup row&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// 'processed' on the first delivery, 'duplicate' on any redelivery&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;runOnce&lt;/code&gt; inserts a &lt;code&gt;(source, message_key)&lt;/code&gt; row protected by a unique index and runs your side effect in the same transaction. A redelivery violates the index → &lt;code&gt;'duplicate'&lt;/code&gt; → the side effect is skipped. If your side effect throws, the dedup row rolls back with it, so the retry reprocesses cleanly. That composition — unique index + shared transaction — is the entire trick, and it's provable in the database.&lt;/p&gt;

&lt;p&gt;For Kafka consumers the library wraps the full delivery decision (validate → dedup → ack / dead-letter / redeliver) in an engine you delegate to from a thin &lt;code&gt;@KafkaConsumer&lt;/code&gt; shell:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;KafkaConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order.placed&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;groupId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orders-service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderConsumer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KafkaInboxConsumer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderAuditService&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;span class="nd"&gt;KafkaHandler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;KafkaMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;KafkaHeaders&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="nx"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;KafkaCtx&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KafkaContext&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;consume&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;OrderPlaced&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;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order.placed:orders-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;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ctx&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="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isOrderPlaced&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                   &lt;span class="c1"&gt;// poison message → DLQ, then ack&lt;/span&gt;
      &lt;span class="na"&gt;sideEffect&lt;/span&gt;&lt;span class="p"&gt;:&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;dedupKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&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;dedupKey&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;dlqTopic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order.placed.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="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;Poison messages (unparseable, unkeyable) go to a dead-letter topic instead of redelivering forever; transient failures rethrow so the broker redelivers; duplicates ack silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing without a broker
&lt;/h2&gt;

&lt;p&gt;Everything above runs in tests with no infrastructure: an in-memory outbox transport (&lt;code&gt;@nest-native/messaging/testing&lt;/code&gt;) for the producer half, and &lt;code&gt;@nest-native/kafka/testing&lt;/code&gt;'s in-memory broker for the full pipeline — including redelivery:&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;broker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order.placed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publishedMessage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// redeliver the same message&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;broker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;idle&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                                 &lt;span class="c1"&gt;// wait for handler pipelines to settle&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auditRows&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&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="c1"&gt;// side effect ran exactly once&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The library itself is tested at 100% coverage against real SQLite, real in-process Postgres (pglite), and the in-memory broker — plus gated round-trips against real Postgres and MySQL containers.&lt;/p&gt;

&lt;h2&gt;
  
  
  See the whole thing running
&lt;/h2&gt;

&lt;p&gt;The pattern is one chapter of a larger, runnable story: the &lt;a href="https://github.com/nest-native/reference-app" rel="noopener noreferrer"&gt;nest-native reference app&lt;/a&gt; is a multi-tenant work-tracking SaaS where every task write emits &lt;code&gt;task.created/assigned/completed&lt;/code&gt; through this outbox, a consumer builds an activity feed through this inbox, the event contracts are published as an &lt;a href="https://github.com/nest-native/asyncapi" rel="noopener noreferrer"&gt;AsyncAPI 3.0 catalog&lt;/a&gt;, and a &lt;a href="https://github.com/nest-native/ai-sdk" rel="noopener noreferrer"&gt;streaming AI assistant&lt;/a&gt; summarizes the activity — eight libraries, one coherent journey, green tests, no Docker required for the default profile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest scope
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This is the &lt;strong&gt;app-level&lt;/strong&gt; outbox. At larger scale you may prefer CDC (Debezium + Kafka Connect) tailing the WAL — different trade-offs, no app code, more infrastructure.&lt;/li&gt;
&lt;li&gt;If you're on TypeORM or MikroORM, the libraries linked above already serve you well. &lt;code&gt;@nest-native/messaging&lt;/code&gt; exists specifically because nothing covered Drizzle.&lt;/li&gt;
&lt;li&gt;Delivery is at-least-once end to end; the inbox gives you exactly-once &lt;em&gt;effects&lt;/em&gt;, which is the guarantee that actually matters — exactly-once &lt;em&gt;delivery&lt;/em&gt; across a network isn't something any tool can promise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Docs: &lt;a href="https://nest-native.dev/messaging/" rel="noopener noreferrer"&gt;nest-native.dev/messaging&lt;/a&gt; · Source: &lt;a href="https://github.com/nest-native/messaging" rel="noopener noreferrer"&gt;github.com/nest-native/messaging&lt;/a&gt;. Feedback and issues welcome — especially if you hit a case the pattern doesn't cover.&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>typescript</category>
      <category>kafka</category>
      <category>database</category>
    </item>
    <item>
      <title>A NestJS reference app that proves seven nest-native libraries under realistic backend pressure</title>
      <dc:creator>Rodrigo Nogueira</dc:creator>
      <pubDate>Mon, 25 May 2026 15:37:52 +0000</pubDate>
      <link>https://dev.to/rodrigobnogueira/a-nestjs-reference-app-that-proves-the-nest-native-stack-under-realistic-backend-pressure-5dc0</link>
      <guid>https://dev.to/rodrigobnogueira/a-nestjs-reference-app-that-proves-the-nest-native-stack-under-realistic-backend-pressure-5dc0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: &lt;a href="https://github.com/nest-native/reference-app" rel="noopener noreferrer"&gt;&lt;code&gt;nest-native/reference-app&lt;/code&gt;&lt;/a&gt; is a production-shaped &lt;strong&gt;multi-tenant work-tracking SaaS&lt;/strong&gt; — think a small Linear — that composes &lt;strong&gt;all seven &lt;a href="https://nest-native.dev/" rel="noopener noreferrer"&gt;nest-native&lt;/a&gt; libraries&lt;/strong&gt; the way a real product would: Drizzle persistence, a typed tRPC API, domain events emitted &lt;strong&gt;in the same transaction&lt;/strong&gt; as the writes, a Kafka backbone with exactly-once consumer effects, a published AsyncAPI 3.0 catalog, &lt;strong&gt;background jobs in the same database (no Redis)&lt;/strong&gt;, and a streaming AI assistant. Every guarantee has a test. It runs with zero infrastructure by default (SQLite, no broker, offline AI), and the same domain code runs against real Kafka by setting one env var.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The org, in one paragraph
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/nest-native" rel="noopener noreferrer"&gt;&lt;code&gt;nest-native&lt;/code&gt;&lt;/a&gt; publishes decorator-first NestJS integrations that should &lt;strong&gt;feel like official NestJS packages&lt;/strong&gt; — modules, DI, decorators, guards/interceptors, lifecycle hooks — while staying honest about the tool underneath. Drizzle stays SQL-first; tRPC stays tRPC; the outbox is a real table, not magic. Every package ships with &lt;code&gt;"dependencies": {}&lt;/code&gt; (you install only the peers you use), 100% test coverage as a hard CI gate, runnable samples, and documented non-goals.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem this app exists to solve
&lt;/h2&gt;

&lt;p&gt;Library docs cover their slice; nobody covers the seams. And the seams are where backends actually get built:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How does "current user / current organization" reach a tRPC procedure, a guard, and a repository three calls deep — through one request-scoped mechanism?&lt;/li&gt;
&lt;li&gt;How do I write a row &lt;strong&gt;and&lt;/strong&gt; publish an event without a crash window between them?&lt;/li&gt;
&lt;li&gt;How do consumers survive Kafka's at-least-once redelivery without double-applying effects?&lt;/li&gt;
&lt;li&gt;How do I schedule deferred work — reminders, follow-ups — &lt;strong&gt;atomically with the write that caused it&lt;/strong&gt;, without adding Redis to my stack just to run it?&lt;/li&gt;
&lt;li&gt;Where do other teams find my event contracts — and how do I keep those contracts from drifting apart across producer, consumer, and docs?&lt;/li&gt;
&lt;li&gt;How do I ship a streaming AI endpoint that tests &lt;strong&gt;offline&lt;/strong&gt;, deterministically, in CI?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reference app answers all of these in one codebase, as a coherent product rather than a pile of demos.&lt;/p&gt;

&lt;h2&gt;
  
  
  The story: one journey through seven libraries
&lt;/h2&gt;

&lt;p&gt;The product is a multi-tenant work-tracking SaaS. Follow one journey and every library shows up exactly where a real system would reach for it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;An org invites a teammate&lt;/strong&gt; — one &lt;code&gt;@Transactional()&lt;/code&gt; method writes the user, membership, project, and audit rows &lt;em&gt;and&lt;/em&gt; enqueues a &lt;code&gt;user.invited&lt;/code&gt; event, atomically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They work tasks&lt;/strong&gt; — create → assign → complete; each write emits &lt;code&gt;task.created&lt;/code&gt; / &lt;code&gt;task.assigned&lt;/code&gt; / &lt;code&gt;task.completed&lt;/code&gt; in the same transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Events flow over the backbone&lt;/strong&gt; — a background claimer relays committed events (in-process by default; Kafka in production), and consumers build an &lt;strong&gt;activity feed&lt;/strong&gt; read-model, deduplicated against redelivery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assignment schedules deferred work&lt;/strong&gt; — the same delivery that projects &lt;code&gt;task.assigned&lt;/code&gt; into the feed also enqueues a delayed &lt;strong&gt;assignment-reminder job&lt;/strong&gt;, in the same transaction, keyed by the same dedup key. A worker fires it exactly once when due. The queue is a table in the same database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The contracts are published&lt;/strong&gt; — an AsyncAPI 3.0 catalog at &lt;code&gt;/asyncapi&lt;/code&gt; documents every event, generated from the same Zod schemas the code validates with.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI reads the activity&lt;/strong&gt; — a streaming assistant turns a project's recent activity into a status update, token by token.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi4xe2yz1b6buwan6okws.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi4xe2yz1b6buwan6okws.png" alt="reference-app as an architecture city: a central reference-app building wires the feature modules (db, auth, users, projects, tasks, activity, onboarding, audit, reminders, outbox, inbox, events-catalog, assistant, trpc) via glowing teal paths; two red arcs trace the cross-cutting concerns — transactions and auth request context" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Its job in the story&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/nest-native/drizzle" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/drizzle&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Persistence: repositories (&lt;code&gt;@DrizzleRepository&lt;/code&gt;, &lt;code&gt;@InjectTransaction&lt;/code&gt;), tenant-scoped queries, migrations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/nest-native/trpc" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/trpc&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The typed API: &lt;code&gt;@Router&lt;/code&gt;/&lt;code&gt;@Query&lt;/code&gt;/&lt;code&gt;@Mutation&lt;/code&gt;, generated &lt;code&gt;AppRouter&lt;/code&gt;, superjson, guards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/nest-native/messaging" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/messaging&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Reliable events: transactional outbox + idempotent inbox on Drizzle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/nest-native/kafka" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/kafka&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The backbone: &lt;code&gt;KafkaOutboxTransport&lt;/code&gt; + &lt;code&gt;@KafkaConsumer&lt;/code&gt; read-models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/nest-native/jobs" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/jobs&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Deferred work: the assignment reminder — enqueued transactionally, deduped, executed exactly once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/nest-native/asyncapi" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/asyncapi&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The event catalog other teams integrate against&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/nest-native/ai-sdk" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/ai-sdk&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The streaming assistant (&lt;code&gt;@AiStream&lt;/code&gt;), offline-testable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The central proof: five writes, one transaction, zero lost events
&lt;/h2&gt;

&lt;p&gt;The onboarding workflow is the crown jewel. &lt;code&gt;inviteUser()&lt;/code&gt; writes five rows — user, membership, project link, audit event, &lt;strong&gt;and the outbox event&lt;/strong&gt; — inside a single &lt;code&gt;@Transactional()&lt;/code&gt; method:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flk42qo03pb26up435azr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flk42qo03pb26up435azr.png" alt="inviteUser as a pipeline: five numbered steps — users, memberships, projects, audit_events, outbox_events — flow through a transaction; a commit valve releases the queued outbox event to the worker" width="800" height="450"&gt;&lt;/a&gt;&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Transactional&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;inviteUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;InviteUserInput&lt;/span&gt;&lt;span class="p"&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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;memberships&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
  &lt;span class="c1"&gt;// ...audit row...&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outbox&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="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="s1"&gt;user.invited&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;// a plain typed interface — no casts&lt;/span&gt;
    &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`user.invited:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orgId&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="nx"&gt;user&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="s2"&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;A throw anywhere rolls back &lt;em&gt;everything&lt;/em&gt; — no phantom event. A crash after commit loses nothing — the event is durably in the database. The tests prove both directions (happy path, rollback safety, crash recovery), because "trust me" is not a delivery guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  No lost events, no double effects
&lt;/h2&gt;

&lt;p&gt;The dual-write problem ("write the row, then publish — and pray nothing crashes in between") is solved by &lt;a href="https://github.com/nest-native/messaging" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/messaging&lt;/code&gt;&lt;/a&gt;: the enqueue above is just an insert into an &lt;code&gt;outbox_events&lt;/code&gt; table, and a background &lt;strong&gt;claimer&lt;/strong&gt; relays committed rows with retry/backoff and stuck-claim recovery:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F94q8nbluhqndu03ys992.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F94q8nbluhqndu03ys992.png" alt="The delivery lifecycle shared by the outbox and the jobs queue: a worker picks up envelopes from a " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The consumer half is the mirror image: delivery is at-least-once by contract, so the &lt;strong&gt;idempotent inbox&lt;/strong&gt; dedups redeliveries via a unique &lt;code&gt;(source, message_key)&lt;/code&gt; row written &lt;em&gt;in the same transaction as the side effect&lt;/em&gt; — exactly-once effects, provable in the database. The app's activity feed is built exactly this way, and the test suite redelivers messages on purpose and asserts the side effect ran once.&lt;/p&gt;

&lt;p&gt;Two profiles, one codebase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default&lt;/strong&gt; — no broker: the outbox dispatches through an in-process topic→handler registry (&lt;code&gt;@nest-native/messaging/in-process&lt;/code&gt;). SQLite in a file. This is what CI runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kafka&lt;/strong&gt; — set &lt;code&gt;KAFKA_BROKERS&lt;/code&gt; and the same domain code relays through &lt;code&gt;KafkaOutboxTransport&lt;/code&gt;, with &lt;code&gt;@KafkaConsumer&lt;/code&gt;s (via &lt;a href="https://github.com/nest-native/kafka" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/kafka&lt;/code&gt;&lt;/a&gt;, on Confluent's official client) on the other side. Same event bodies, same dedup keys, same wire headers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deferred work in the same database (no Redis)
&lt;/h2&gt;

&lt;p&gt;The newest library closes a gap the rest of the story kept pointing at: &lt;em&gt;"…and then, later, do something."&lt;/em&gt; In NestJS the standard answer to reminders, follow-ups, and cleanups is BullMQ — which means running Redis. &lt;a href="https://github.com/nest-native/jobs" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/jobs&lt;/code&gt;&lt;/a&gt; puts the job queue &lt;strong&gt;in the database you already have&lt;/strong&gt; (SQLite/Postgres/MySQL, on Drizzle), with &lt;code&gt;@JobHandler&lt;/code&gt; classes, retry with backoff, scheduling, and unique-key dedup. If the lifecycle picture above looks like a job queue — that's because it &lt;em&gt;is&lt;/em&gt; one: jobs runs the same hardened claim/retry engine the outbox relay proved in this app.&lt;/p&gt;

&lt;p&gt;The app's use case: &lt;strong&gt;when a task is assigned, send a reminder later.&lt;/strong&gt; The interesting part is &lt;em&gt;where&lt;/em&gt; the job is created — inside the same transaction as the activity-feed projection of the &lt;code&gt;task.assigned&lt;/code&gt; event:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Transactional&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// under Kafka this JOINS the inbox's dedup transaction&lt;/span&gt;
&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TaskAssignedPayload&lt;/span&gt;&lt;span class="p"&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;activity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;taskAssignedActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&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;inserted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// (org_id, dedup_key) unique&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;inserted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jobs&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;task.assignment-reminder&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;// the event payload IS the job payload — one Zod contract&lt;/span&gt;
      &lt;span class="na"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reminderDelayMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// TASK_REMINDER_DELAY_MS&lt;/span&gt;
      &lt;span class="na"&gt;uniqueKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dedupKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// the SAME key that dedups the event&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;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkp96ea9yze6mhh42wsqp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkp96ea9yze6mhh42wsqp.png" alt="One delivery, one transaction, one deferred reminder" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One delivery ⇒ one feed row ⇒ one scheduled reminder — atomic, because the dedup row, the feed row, and the job row commit together. And redelivery can never schedule a second reminder, through &lt;strong&gt;two independent layers&lt;/strong&gt;: the feed's &lt;code&gt;(org_id, dedup_key)&lt;/code&gt; unique index skips the enqueue for an already-projected event, and the job's &lt;code&gt;uniqueKey&lt;/code&gt; (the same dedup key) turns a concurrent duplicate into a no-op that returns the existing row.&lt;/p&gt;

&lt;p&gt;When the reminder comes due, a handler class picks it up. A payload that can never succeed fails immediately — no retries burned:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;JobHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;task.assignment-reminder&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AssignmentReminderHandler&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;JobHandler&lt;/span&gt; &lt;span class="p"&gt;{&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;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JobContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&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="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isTaskAssignedPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PermanentError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;task.assignment-reminder: malformed payload&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="c1"&gt;// record the notification (audit entry with ctx.jobId / ctx.attempt)&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;The integration test redelivers the event on purpose, enqueues a duplicate on purpose, drains the queue twice, and asserts: one job, one reminder, unique key released on completion. And the worker didn't become a second process — &lt;code&gt;npm run start:worker&lt;/code&gt; drains the outbox &lt;strong&gt;and&lt;/strong&gt; runs due jobs: two library-provided loops, one shared graceful shutdown.&lt;/p&gt;

&lt;h2&gt;
  
  
  A typed API that doesn't lie
&lt;/h2&gt;

&lt;p&gt;The tRPC layer (&lt;a href="https://github.com/nest-native/trpc" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/trpc&lt;/code&gt;&lt;/a&gt;) is decorator-first (&lt;code&gt;@Router('tasks')&lt;/code&gt;, &lt;code&gt;@Query&lt;/code&gt;, &lt;code&gt;@Mutation&lt;/code&gt;, Zod I/O) with a generated &lt;code&gt;AppRouter&lt;/code&gt; consumed by a typed client in CI. Three details worth stealing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;superjson end-to-end&lt;/strong&gt; — &lt;code&gt;activity.list&lt;/code&gt; returns real &lt;code&gt;Date&lt;/code&gt; objects, and the generated router carries a type-level transformer marker: a client that forgets to configure superjson &lt;strong&gt;fails to compile&lt;/strong&gt;. Both directions are CI steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation errors clients can use&lt;/strong&gt; — an &lt;code&gt;errorFormatter&lt;/code&gt; flattens &lt;code&gt;ZodError&lt;/code&gt;s so a failing mutation surfaces &lt;code&gt;error.data.zodError.fieldErrors.title&lt;/code&gt; on the typed client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache headers where they're safe&lt;/strong&gt; — &lt;code&gt;responseMeta&lt;/code&gt; sets &lt;code&gt;cache-control&lt;/code&gt; only on the public &lt;code&gt;ping&lt;/code&gt; query; everything tenant-scoped stays uncached, and tests assert both presence and absence.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Your events, documented
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/nest-native/asyncapi" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/asyncapi&lt;/code&gt;&lt;/a&gt; publishes an &lt;strong&gt;AsyncAPI 3.0&lt;/strong&gt; document at &lt;code&gt;/asyncapi&lt;/code&gt; (plus &lt;code&gt;-json&lt;/code&gt;/&lt;code&gt;-yaml&lt;/code&gt;) describing all four domain events. The payload schemas are &lt;strong&gt;defined once&lt;/strong&gt; as Zod objects — the same definitions derive the TypeScript types (&lt;code&gt;z.infer&lt;/code&gt;), the runtime guards (&lt;code&gt;safeParse&lt;/code&gt;), and the catalog schemas. Producer, consumer, and documentation cannot drift apart, and the document validates against &lt;code&gt;@asyncapi/parser&lt;/code&gt; in the test suite. (The reminder job reuses the &lt;code&gt;task.assigned&lt;/code&gt; guard — the job payload can't drift from the event contract either.)&lt;/p&gt;

&lt;h2&gt;
  
  
  AI over your data, tested offline
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;POST /projects/:id/assistant&lt;/code&gt; streams a status update summarizing the project's recent activity, via &lt;a href="https://github.com/nest-native/ai-sdk" rel="noopener noreferrer"&gt;&lt;code&gt;@nest-native/ai-sdk&lt;/code&gt;&lt;/a&gt;'s &lt;code&gt;@AiStream&lt;/code&gt; (SSE, abort on client disconnect). By default it streams from an &lt;strong&gt;offline mock model&lt;/strong&gt; built from the real activity digest — deterministic, no API key, CI-safe. Set &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; and a real provider swaps in with no code change. (Adopting the AI SDK is also why the app requires &lt;strong&gt;Node ≥ 22&lt;/strong&gt; — &lt;code&gt;ai@7&lt;/code&gt; demands it.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in 30 seconds
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/nest-native/reference-app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;reference-app
nvm use            &lt;span class="c"&gt;# Node &amp;gt;= 22&lt;/span&gt;
npm &lt;span class="nb"&gt;install
&lt;/span&gt;&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./reference-app.db npm run db:migrate
&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./reference-app.db npm run seed
&lt;span class="nv"&gt;AUTH_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev-secret-must-be-at-least-32-characters-xxxxx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./reference-app.db npm run start:dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;tRPC at &lt;code&gt;/trpc&lt;/code&gt;, health at &lt;code&gt;/health&lt;/code&gt;, the AsyncAPI catalog at &lt;code&gt;/asyncapi&lt;/code&gt;, the AI assistant at &lt;code&gt;POST /projects/1/assistant&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Seed login: &lt;code&gt;admin@acme.test&lt;/code&gt; / &lt;code&gt;admin123!&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The background worker runs as its own process: &lt;code&gt;npm run start:worker&lt;/code&gt; — it relays the outbox &lt;strong&gt;and&lt;/strong&gt; executes due jobs&lt;/li&gt;
&lt;li&gt;No Docker, no broker, no Redis, no API keys required for any of it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The loop that makes it honest
&lt;/h2&gt;

&lt;p&gt;The app isn't just a showcase — it's the &lt;strong&gt;feedback loop&lt;/strong&gt; for the libraries, and it has teeth. Recent examples, all shipped upstream because building this app surfaced them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The in-process outbox transport was hand-rolled here first, proved generic, and was &lt;strong&gt;extracted into&lt;/strong&gt; &lt;code&gt;@nest-native/messaging/in-process&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@nest-native/jobs&lt;/code&gt; exists because of this loop&lt;/strong&gt;: the claim/retry/backoff engine hardened here as the outbox relay was generalized into a job queue — and when this app adopted it back for the assignment reminder, the dogfood PR found zero API warts. Porting a battle-tested engine is what that buys.&lt;/li&gt;
&lt;li&gt;The app's hand-written AI mock became &lt;code&gt;@nest-native/ai-sdk/testing&lt;/code&gt; (&lt;code&gt;createMockLanguageModel&lt;/code&gt;) — and deleted every copy-pasted mock in the family.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;enqueue()&lt;/code&gt; becoming generic (killing &lt;code&gt;as unknown as Record&amp;lt;string, unknown&amp;gt;&lt;/code&gt; casts), the in-memory Kafka broker gaining an awaitable &lt;code&gt;idle()&lt;/code&gt; (killing &lt;code&gt;sleep(50)&lt;/code&gt; in tests), and AsyncAPI accepting Zod schemas directly — all of it started as friction in this codebase.&lt;/li&gt;
&lt;li&gt;The app's install even caught a broken peer-dependency range in a fresh release before any user hit it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If something feels awkward here, that's treated as a library bug — fixed upstream, in its own PR, with its own tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it deliberately is not
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not a starter kit or boilerplate&lt;/strong&gt; — it optimizes for being read, not forked. Copy patterns, not the repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not 100%-covered&lt;/strong&gt; — the 100% bar belongs to the libraries; the app's suite is pragmatic and targets the guarantees (rollback, crash recovery, dedup, exactly-once jobs, catalog validity, stream framing).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a CDC system&lt;/strong&gt; — the outbox is the app-level answer; Debezium-style log tailing is a different trade-off and an explicit non-goal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a BullMQ replacement&lt;/strong&gt; — if you already run Redis and need its throughput, BullMQ is the right tool; &lt;code&gt;@nest-native/jobs&lt;/code&gt; is for apps that want deferred work in the database they already operate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not affiliated with the NestJS core team.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to find it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The app: &lt;a href="https://github.com/nest-native/reference-app" rel="noopener noreferrer"&gt;github.com/nest-native/reference-app&lt;/a&gt; — start with the README's story, then the integration tests (they read like the spec of every guarantee above)&lt;/li&gt;
&lt;li&gt;The org + all seven packages: &lt;a href="https://github.com/nest-native" rel="noopener noreferrer"&gt;github.com/nest-native&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs hub: &lt;a href="https://nest-native.dev/" rel="noopener noreferrer"&gt;nest-native.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback, issues, and "this seam is still awkward" reports are the most useful thing you can send — that's literally the mechanism this whole stack improves by.&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>typescript</category>
      <category>drizzle</category>
      <category>trpc</category>
    </item>
    <item>
      <title>Introducing @nest-native/drizzle: A Nest-native Drizzle ORM integration</title>
      <dc:creator>Rodrigo Nogueira</dc:creator>
      <pubDate>Sun, 17 May 2026 05:11:11 +0000</pubDate>
      <link>https://dev.to/rodrigobnogueira/introducing-nest-drizzle-native-a-nest-native-drizzle-orm-integration-21pe</link>
      <guid>https://dev.to/rodrigobnogueira/introducing-nest-drizzle-native-a-nest-native-drizzle-orm-integration-21pe</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Filhaomftbws398167mnz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Filhaomftbws398167mnz.png" alt="![nest-drizzle-native announcement image](...)" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Renamed:&lt;/strong&gt; this project was originally published as &lt;code&gt;nest-drizzle-native&lt;/code&gt; and is now the scoped &lt;strong&gt;&lt;code&gt;@nest-native/drizzle&lt;/code&gt;&lt;/strong&gt; (repo: &lt;a href="https://github.com/nest-native/drizzle" rel="noopener noreferrer"&gt;&lt;code&gt;nest-native/drizzle&lt;/code&gt;&lt;/a&gt;). Same library, same public API — only the package name changed. To migrate: &lt;code&gt;npm uninstall nest-drizzle-native &amp;amp;&amp;amp; npm install @nest-native/drizzle&lt;/code&gt;, then update imports to &lt;code&gt;@nest-native/drizzle&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;@nest-native/drizzle&lt;/strong&gt; is a community package designed to make Drizzle ORM feel entirely natural inside NestJS applications.&lt;/p&gt;

&lt;p&gt;The core concept is straightforward: preserve Drizzle's explicit, SQL-first philosophy while giving NestJS projects the module, dependency injection, repository, testing, and transaction ergonomics they expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Motivation
&lt;/h2&gt;

&lt;p&gt;Drizzle is excellent because it stays close to SQL and avoids heavy, opaque ORM magic.&lt;/p&gt;

&lt;p&gt;NestJS, on the other hand, provides applications with a strong architectural shape built around modules, providers, decorators, testing utilities, lifecycle hooks, and clear dependency boundaries.&lt;/p&gt;

&lt;p&gt;A bridge between those two worlds allows teams to build scalable enterprise architectures without turning Drizzle into something it is not. Existing solutions often act as thin connection wrappers, leaving developers to manually solve complex enterprise patterns like cross-service transactions or DTO co-generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it supports today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DrizzleModule.forRoot()&lt;/code&gt;, &lt;code&gt;forRootAsync()&lt;/code&gt;, and &lt;code&gt;forFeature()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@InjectDrizzle()&lt;/code&gt; for direct Drizzle client injection&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@DrizzleRepository()&lt;/code&gt; for query-focused provider classes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@Transactional()&lt;/code&gt; / &lt;code&gt;@InjectTransaction()&lt;/code&gt; powered natively via &lt;code&gt;@nestjs-cls/transactional&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Named connections for multi-database applications&lt;/li&gt;
&lt;li&gt;Testing utilities for clean module and repository testing&lt;/li&gt;
&lt;li&gt;Zero runtime dependencies in the published library package&lt;/li&gt;
&lt;li&gt;Runnable samples covering transactions, DTO validation, optional &lt;code&gt;drizzle-zod&lt;/code&gt; validation, Swagger/OpenAPI integration, raw SQL execution, and various driver setups&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Let's look at the code
&lt;/h2&gt;

&lt;p&gt;The biggest pain point of using Drizzle in a structured backend is transaction management. Passing a &lt;code&gt;tx&lt;/code&gt; object through five layers of injected services completely breaks Dependency Injection.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;@nest-native/drizzle&lt;/code&gt;, transactions are propagated through the CLS transaction context used by &lt;code&gt;@nestjs-cls/transactional&lt;/code&gt;, allowing them to flow seamlessly across your services just by using a decorator:&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;Injectable&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="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Transactional&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="s1"&gt;@nest-native/drizzle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AuditService&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="s1"&gt;./audit.service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UsersRepository&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="s1"&gt;./users.repository&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CreateUserDto&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="s1"&gt;./create-user.dto&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="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UsersService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;usersRepository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;auditService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AuditService&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;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Transactional&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateUserDto&lt;/span&gt;&lt;span class="p"&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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usersRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auditService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USER_CREATED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;user&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;The repository itself is a clean Nest provider that keeps Drizzle's query builder visible instead of hiding SQL behind an Active Record layer:&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;eq&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="s1"&gt;drizzle-orm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;DrizzleRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;InjectDrizzle&lt;/span&gt;&lt;span class="p"&gt;,&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="s1"&gt;@nest-native/drizzle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppDatabase&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="s1"&gt;./database&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;users&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="s1"&gt;./schema&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CreateUserDto&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="s1"&gt;./create-user.dto&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="nd"&gt;DrizzleRepository&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UsersRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;InjectDrizzle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppDatabase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateUserDto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&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;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;returning&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;findById&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&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;select&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&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="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;user&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;h2&gt;
  
  
  What it does not try to do
&lt;/h2&gt;

&lt;p&gt;The goal is strictly to avoid re-creating TypeORM or an Active Record layer.&lt;/p&gt;

&lt;p&gt;Developers still write real, functional Drizzle queries. The library simply provides those queries with a native NestJS structure through decorators and context management, eliminating the need to pass transaction callbacks (&lt;code&gt;tx&lt;/code&gt;) down through multiple layers of services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://nest-native.dev/drizzle/" rel="noopener noreferrer"&gt;https://nest-native.dev/drizzle/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/nest-native/drizzle" rel="noopener noreferrer"&gt;https://github.com/nest-native/drizzle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/@nest-native/drizzle" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@nest-native/drizzle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Releases:&lt;/strong&gt; &lt;a href="https://github.com/nest-native/drizzle/releases/latest" rel="noopener noreferrer"&gt;https://github.com/nest-native/drizzle/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Community feedback from NestJS and Drizzle users is highly encourage — especially regarding real-world transaction patterns, testing strategies, multi-database setups, and production adoption. Feel free to open issues or contribute to the repository!&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>drizzle</category>
      <category>typescript</category>
      <category>node</category>
    </item>
    <item>
      <title>nest-trpc-native: Full NestJS Power + tRPC Type Safety with Zero Runtime Overhead</title>
      <dc:creator>Rodrigo Nogueira</dc:creator>
      <pubDate>Tue, 17 Mar 2026 17:24:47 +0000</pubDate>
      <link>https://dev.to/rodrigobnogueira/nest-trpc-native-full-nestjs-power-trpc-type-safety-with-zero-runtime-overhead-4f15</link>
      <guid>https://dev.to/rodrigobnogueira/nest-trpc-native-full-nestjs-power-trpc-type-safety-with-zero-runtime-overhead-4f15</guid>
      <description>&lt;p&gt;Hey NestJS + tRPC community! 👋&lt;/p&gt;

&lt;p&gt;I’m excited to release &lt;strong&gt;nest-trpc-native v0.3.0&lt;/strong&gt; — a decorator-first, native-feeling tRPC integration for NestJS.&lt;/p&gt;

&lt;p&gt;No adapter glue code in your app layer. No compromise on NestJS features.&lt;/p&gt;

&lt;p&gt;You write tRPC routers like Nest classes, with full support for dependency injection, guards, interceptors, pipes, filters, request-scoped providers, and end-to-end type safety.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmqc6nbb01gt8h8em6u3d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmqc6nbb01gt8h8em6u3d.png" alt="nest-trpc-native router sample" width="800" height="585"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters
&lt;/h3&gt;

&lt;p&gt;Most tRPC + NestJS approaches push you toward one side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep all NestJS lifecycle features, but lose clean tRPC developer experience, or&lt;/li&gt;
&lt;li&gt;Keep pure tRPC style, but lose NestJS enhancers and request-scoped DI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;nest-trpc-native&lt;/code&gt; is designed so you can have both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@Router('users')&lt;/code&gt; + &lt;code&gt;@Query()&lt;/code&gt;, &lt;code&gt;@Mutation()&lt;/code&gt;, &lt;code&gt;@Subscription()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Full support for &lt;code&gt;@UseGuards()&lt;/code&gt;, &lt;code&gt;@UseInterceptors()&lt;/code&gt;, &lt;code&gt;@UsePipes(ValidationPipe)&lt;/code&gt;, &lt;code&gt;@UseFilters()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@Input()&lt;/code&gt; and &lt;code&gt;@TrpcContext()&lt;/code&gt; for parameter extraction&lt;/li&gt;
&lt;li&gt;Auto-generated router types (&lt;code&gt;autoSchemaFile: 'src/@generated/server.ts'&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Works with &lt;strong&gt;Express&lt;/strong&gt; and &lt;strong&gt;Fastify&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Zod &lt;strong&gt;or&lt;/strong&gt; classic &lt;code&gt;class-validator&lt;/code&gt; validation&lt;/li&gt;
&lt;li&gt;Zero runtime dependencies in &lt;code&gt;nest-trpc-native&lt;/code&gt; itself (host app controls Nest/tRPC peers)&lt;/li&gt;
&lt;li&gt;Monorepo-friendly sample layout&lt;/li&gt;
&lt;li&gt;Microservice transport pattern demonstrated in a focused sample&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1xn1o5rsdflf70y2yrrr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1xn1o5rsdflf70y2yrrr.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Router Example (NestJS style)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users&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="nd"&gt;UseGuards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AuthGuard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UsersRouter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersService&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;span class="nd"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FindOneSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nullable&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;span class="nd"&gt;UsePipes&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;ValidationPipe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;transform&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="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;TrpcContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;requestId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&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="p"&gt;}&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Mutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateUserSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&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;Client side stays fully type-safe:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&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;trpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findOne&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// autocomplete + compile-time safety&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;nest-trpc-native @trpc/server
npm &lt;span class="nb"&gt;install&lt;/span&gt; @nestjs/common @nestjs/core reflect-metadata rxjs
&lt;span class="c"&gt;# optional:&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;zod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then register the module:&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="nx"&gt;TrpcModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forRoot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/trpc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;autoSchemaFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/@generated/server.ts&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;h3&gt;
  
  
  Samples You Can Run Now
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sample/00-showcase&lt;/code&gt;: full integration baseline (guards, interceptors, pipes, filters, request scope, Express/Fastify, typed clients, subscriptions)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sample/11-microservice-transport&lt;/code&gt;: tRPC edge gateway + Nest microservice transport (TCP)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From repo root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run showcase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Docs, Repo, and NPM
&lt;/h3&gt;

&lt;p&gt;Docs: &lt;a href="https://rodrigobnogueira.github.io/nest-trpc-native/docs/introduction" rel="noopener noreferrer"&gt;https://rodrigobnogueira.github.io/nest-trpc-native/docs/introduction&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Repo: &lt;a href="https://github.com/rodrigobnogueira/nest-trpc-native" rel="noopener noreferrer"&gt;https://github.com/rodrigobnogueira/nest-trpc-native&lt;/a&gt;&lt;br&gt;&lt;br&gt;
NPM: &lt;a href="https://www.npmjs.com/package/nest-trpc-native" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/nest-trpc-native&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try it, break it, open issues, and share feedback.&lt;/p&gt;

&lt;p&gt;Thanks!&lt;/p&gt;

&lt;h1&gt;
  
  
  nestjs #trpc #typescript
&lt;/h1&gt;

</description>
      <category>nestjs</category>
      <category>trpc</category>
      <category>typescript</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
