<?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: explita</title>
    <description>The latest articles on DEV Community by explita (@explita).</description>
    <link>https://dev.to/explita</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%2F3942119%2Fc5779919-5f00-42f1-8671-d5dc87859e21.png</url>
      <title>DEV Community: explita</title>
      <link>https://dev.to/explita</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/explita"/>
    <language>en</language>
    <item>
      <title>I Built an Encrypted Ngrok Alternative — The Hardest Part Wasn't the Crypto</title>
      <dc:creator>explita</dc:creator>
      <pubDate>Mon, 28 Sep 2026 22:40:17 +0000</pubDate>
      <link>https://dev.to/explita/i-built-an-encrypted-ngrok-alternative-the-hardest-part-wasnt-the-crypto-108i</link>
      <guid>https://dev.to/explita/i-built-an-encrypted-ngrok-alternative-the-hardest-part-wasnt-the-crypto-108i</guid>
      <description>&lt;p&gt;Like pretty much every developer who has ever tested a Stripe webhook, debugged OAuth callbacks, or demoed a local Next.js app to a teammate, I’ve spent years relying on tunneling tools.&lt;/p&gt;

&lt;p&gt;And for years, the ritual looked like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fire up a tunnel tool.&lt;/li&gt;
&lt;li&gt;Get hit with an unmemorable generated URL or an account upgrade paywall.&lt;/li&gt;
&lt;li&gt;Test your webhook.&lt;/li&gt;
&lt;li&gt;Realize the tunnel relay in the cloud is sitting in the middle of your connection, terminating TLS, and technically has cleartext access to every authorization header, test credit card, and secret cookie crossing the wire.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Eventually, frustration turned into curiosity: &lt;strong&gt;What would it take to build a modern, high-throughput reverse tunnel that treats the relay server as untrusted by default?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not just a generic port forwarder, but something that gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inner session encryption (AES-256-GCM)&lt;/strong&gt; so the relay server cannot read your payloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flawless streaming &amp;amp; WebSocket support&lt;/strong&gt; (Next.js Turbopack HMR, Vite, Server-Sent Events).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A zero-dependency local inspector &amp;amp; instant replay&lt;/strong&gt; (press &lt;code&gt;r&lt;/code&gt; in the terminal to replay the last webhook without re-triggering it from Stripe).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in IP/CIDR firewalls&lt;/strong&gt; right at the edge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the story of building &lt;strong&gt;&lt;a href="https://tunnel.explita.ng" rel="noopener noreferrer"&gt;Explita Tunnel&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;eta&lt;/code&gt;), the architecture under the hood, and the hair-pulling edge cases I hit along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Does This Fit? (Ngrok, Cloudflare, Tailscale)
&lt;/h2&gt;

&lt;p&gt;Before getting into the crypto and networking guts, it’s worth addressing the elephant in the room: &lt;em&gt;why not just use an existing tool?&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ngrok:&lt;/strong&gt; Still the gold standard for developer ergonomics, but recent years brought pricing changes, random URLs on free tiers, browser interstitial warning pages, and closed-source relays that terminate TLS in cleartext.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Tunnel (&lt;code&gt;cloudflared&lt;/code&gt;):&lt;/strong&gt; Incredible for production services running on your own domain, but it requires routing your domain through Cloudflare DNS, configuring credentials, and managing daemon configs. It’s heavy when all you want is a 5-second throwaway URL to test a webhook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailscale Funnel:&lt;/strong&gt; Superb if your nodes already live on a Tailnet, but it requires Tailscale client routing and isn’t geared as a friction-free public webhook ingress for 3rd parties like Stripe or GitHub.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Setup Friction&lt;/th&gt;
&lt;th&gt;Relay Trust Model&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ngrok&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Minimal (CLI binary)&lt;/td&gt;
&lt;td&gt;Cleartext relay (TLS terminated at edge)&lt;/td&gt;
&lt;td&gt;Rapid prototyping &amp;amp; client demos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare Tunnel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High (Domain on Cloudflare DNS + config)&lt;/td&gt;
&lt;td&gt;Cleartext relay (TLS terminated at edge)&lt;/td&gt;
&lt;td&gt;Persistent production services on custom domains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tailscale Funnel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Moderate (Tailnet device auth)&lt;/td&gt;
&lt;td&gt;Cleartext relay (Tailnet ingress node)&lt;/td&gt;
&lt;td&gt;Private internal team mesh &amp;amp; node access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Explita Tunnel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Minimal (CLI binary, zero config)&lt;/td&gt;
&lt;td&gt;Untrusted relay (Inner AES-256-GCM session)&lt;/td&gt;
&lt;td&gt;Webhook testing, HMR dev, &amp;amp; privacy-conscious local work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To be clear about trade-offs: if you're deploying a high-availability production service running 24/7 on a permanent company domain, tools like Cloudflare Tunnel or an AWS ALB are still the standard choice. Explita Tunnel is purpose-engineered for the active development loop: instantaneous throwaway ingress, local webhook testing, HMR stability, and inner payload confidentiality without account friction.&lt;/p&gt;

&lt;p&gt;I wanted something with the 2-second speed of Ngrok, but with zero configuration, automatic subdomains, and an architecture where the relay server is treated as an untrusted pipe.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Trust Problem with Traditional Tunnels
&lt;/h2&gt;

&lt;p&gt;Most tunneling setups work like standard reverse proxies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Browser / Webhook] ---&amp;gt; (HTTPS/TLS) ---&amp;gt; [Tunnel Cloud Server] ---&amp;gt; (WebSocket/TCP) ---&amp;gt; [Your CLI Agent] ---&amp;gt; [Localhost:3000]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what happens at the &lt;strong&gt;Tunnel Cloud Server&lt;/strong&gt;: TLS terminates there.&lt;/p&gt;

&lt;p&gt;If the tunnel server is compromised, or if you're using a shared multi-tenant cluster, whoever operates that gateway has full visibility into your raw plaintext HTTP headers, authorization bearer tokens, customer webhook payloads, and environment secrets.&lt;/p&gt;

&lt;p&gt;To fix this, I implemented an &lt;strong&gt;Untrusted Relay Architecture&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Untrusted Relay Architecture (ECDH + AES-256-GCM)
&lt;/h2&gt;

&lt;p&gt;I designed the wire protocol so that even if an attacker completely controls the tunnel server, they see nothing but encrypted noise.&lt;/p&gt;

&lt;p&gt;Here’s how the handshake works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When your CLI (&lt;code&gt;eta 3000&lt;/code&gt;) establishes its control WebSocket with the server, it sends a &lt;code&gt;hello&lt;/code&gt; packet containing an &lt;strong&gt;ephemeral ECDH public key&lt;/strong&gt; (using the NIST P-256 / &lt;code&gt;prime256v1&lt;/code&gt; curve).&lt;/li&gt;
&lt;li&gt;The server responds with its own ephemeral public key inside a &lt;code&gt;ready&lt;/code&gt; frame.&lt;/li&gt;
&lt;li&gt;Both sides perform Elliptic Curve Diffie-Hellman to compute a shared secret, and derive a 256-bit symmetric key using &lt;strong&gt;HKDF-SHA256&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Every subsequent frame—incoming HTTP requests, response chunks, headers, and WebSocket data—is enveloped into an authenticated &lt;strong&gt;AES-256-GCM&lt;/strong&gt; ciphertext:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"encrypted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iv"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0H/rhkojjBR1sEgL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2DAsewK7mcu2/EWxbs..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lZczO9B..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s the actual Node.js crypto derivation under the hood:&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;createECDH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hkdfSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:crypto&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;function&lt;/span&gt; &lt;span class="nf"&gt;generateEcdhKeyPair&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;ecdh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createECDH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prime256v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ecdh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateKeys&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;publicKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ecdh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPublicKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;computeSharedKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;peerPublicKey&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;Buffer&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ecdh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;computeSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;peerPublicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// Derive a 256-bit (32-byte) symmetric key via HKDF&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;derived&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hkdfSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;rawSecret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;explita-tunnel-e2ee&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;32&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="nx"&gt;Buffer&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;derived&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;h3&gt;
  
  
  Session Fingerprints &amp;amp; Verifying the Handshake
&lt;/h3&gt;

&lt;p&gt;To confirm that the key exchange succeeded and give each connection a unique cryptographic identity, the agent computes and displays a truncated SHA-256 session fingerprint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ Key exchange complete • ECDH P-256 + AES-GCM (256-bit)
Fingerprint: SHA256:2c:2a:07:7b:a7:c4:7d:71:a1:fc:5a:99:85:69:18:67
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fingerprint deterministically binds the client's public key, the server's public key, and the derived session key. It gives you immediate visual confirmation that the tunnel is running with inner encryption active rather than in plaintext. Whenever your connection reconnects or rotates, a fresh keypair is negotiated and the fingerprint changes.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Looking ahead: because ephemeral ECDH alone does not authenticate the server's identity, I plan to add server-identity key signing in a future release so the agent can cryptographically verify the gateway out-of-the-box).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And what about performance? On modern hardware with AES-NI instructions, AES-256-GCM throughput impact is negligible (&amp;lt;1-2% CPU). The &lt;code&gt;--no-encrypt&lt;/code&gt; flag remains available for low-power embedded microcontrollers or local testing when you want to eliminate cryptographic compute completely.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Happens on Reconnect &amp;amp; Key Rotation?
&lt;/h3&gt;

&lt;p&gt;If your laptop sleeps or your Wi-Fi drops, the client enters an exponential backoff reconnect loop. The server reserves your subdomain with a 60-second grace lock so nobody snatches it while you're offline.&lt;/p&gt;

&lt;p&gt;Upon reconnecting, the agent and server negotiate a &lt;strong&gt;brand-new ephemeral ECDH keypair&lt;/strong&gt;, rotating the session key automatically. In-flight requests during the drop fail cleanly, and subsequent traffic immediately resumes over the fresh cipher.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Nightmare of Streaming &amp;amp; WebSockets (Gotchas from the Trenches)
&lt;/h2&gt;

&lt;p&gt;Building basic request-response forwarding is easy. Anyone can write a 50-line Node.js prototype that fetches a URL and sends it over a socket.&lt;/p&gt;

&lt;p&gt;What's &lt;em&gt;hard&lt;/em&gt; is real-world web traffic.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Next.js / Vite HMR Problem
&lt;/h3&gt;

&lt;p&gt;When you run a modern frontend framework through a tunnel, it opens a persistent WebSocket connection for Hot Module Replacement (e.g. &lt;code&gt;/_next/hmr&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;During early testing with Next.js 15 + Turbopack, my browser console kept spamming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[HMR] connected
[HMR] disconnected
[HMR] connected
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tracking this down took hours of packet tracing. It boiled down to two subtle bugs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lost &lt;code&gt;this&lt;/code&gt; in Fastify's WebSocket Handler:&lt;/strong&gt;&lt;br&gt;
Fastify’s &lt;code&gt;@fastify/websocket&lt;/code&gt; calls route handlers with &lt;code&gt;wsHandler.call(this, socket, request)&lt;/code&gt; where &lt;code&gt;this&lt;/code&gt; is bound to the Fastify instance instead of my router class. A standard class method &lt;code&gt;routeWs(socket, req)&lt;/code&gt; lost its scope, crashed on a property lookup, and triggered an abnormal closure (&lt;code&gt;1006&lt;/code&gt;), kicking off an endless reconnect loop in the browser. Changing it to an arrow property (&lt;code&gt;routeWs = (socket, req) =&amp;gt;&lt;/code&gt;) pinned the class context and fixed it instantly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Server-Sent Events (SSE) vs Compression:&lt;/strong&gt;&lt;br&gt;
When you enable Brotli/Gzip compression on your edge proxy with &lt;code&gt;@fastify/compress&lt;/code&gt;, it loves to buffer text streams until it hits a threshold (usually 1KB). But Server-Sent Events (&lt;code&gt;text/event-stream&lt;/code&gt;) require immediate flushes. If your compression middleware buffers chunks, your live stream hangs. I tuned the compression regex to explicitly bypass SSE streams:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;   &lt;span class="c1"&gt;// Compress text and JSON, but exclude streaming event-streams&lt;/span&gt;
   &lt;span class="nx"&gt;customTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;/^&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;(?&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;json$&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;xml$&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once those were solved, full-duplex WebSockets and HMR ran buttery smooth with zero drops.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Developer Experience: Instant Replay &amp;amp; Live Wire
&lt;/h2&gt;

&lt;p&gt;Most CLI tools either show you nothing (just a static URL) or dump an overwhelming firehose of debug logs into your terminal.&lt;/p&gt;

&lt;p&gt;I wanted a better middle ground:&lt;/p&gt;

&lt;h3&gt;
  
  
  1-Key Request Replay (&lt;code&gt;r&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Ever spent 10 minutes setting up a checkout in Stripe Test Mode just to trigger a single webhook event to your local app?&lt;/p&gt;

&lt;p&gt;When your local code throws a 500 error because of a typo, you normally have to go back to Stripe and trigger the event again.&lt;/p&gt;

&lt;p&gt;With Explita Tunnel, you don't. Just hit &lt;strong&gt;&lt;code&gt;r&lt;/code&gt;&lt;/strong&gt; in your terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚡ Replaying: POST /api/webhooks/stripe...
200 OK POST /api/webhooks/stripe 14ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent stores recent requests in an in-memory ring buffer and replays the exact headers and payload directly against your local port without making round-trips to the internet.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Security note on replay:&lt;/em&gt; Because webhooks carry sensitive authentication tokens (like &lt;code&gt;Stripe-Signature&lt;/code&gt;), the ring buffer is stored strictly in volatile RAM (never written to disk), capped at 50 requests (with a 2MB payload ceiling), and wiped from memory the moment the process terminates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Live Wire Inspector (&lt;code&gt;w&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Instead of flooding the console with raw frames by default, the terminal stays clean. But whenever you want to see what's happening on the wire, just tap &lt;strong&gt;&lt;code&gt;w&lt;/code&gt;&lt;/strong&gt; to toggle the live wire inspector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[WIRE &amp;lt;-] 🔒 CIPHERTEXT {"type":"encrypted","iv":"...","tag":"...","data":"..."}
[WIRE -&amp;gt;] 🔒 CIPHERTEXT {"type":"encrypted","iv":"...","tag":"...","data":"..."}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tap &lt;strong&gt;&lt;code&gt;w&lt;/code&gt;&lt;/strong&gt; again, and you're back to the clean request stream.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Pro-tip I learned:&lt;/em&gt; When implementing a wire inspector, &lt;strong&gt;filter out internal keepalive &lt;code&gt;ping&lt;/code&gt; and &lt;code&gt;pong&lt;/code&gt; frames!&lt;/strong&gt; Heartbeats tick every 15 seconds; nothing ruins a debug session faster than keepalive noise drowning out the API payload you were trying to read.&lt;/p&gt;

&lt;h3&gt;
  
  
  Built-in Local Web Dashboard
&lt;/h3&gt;

&lt;p&gt;If you prefer a browser UI, the agent spins up a local inspector on &lt;code&gt;http://127.0.0.1:39755&lt;/code&gt; with live SSE updates, payload formatting, header inspection, and image previews. No external SaaS dashboard required.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Edge Security: IP &amp;amp; CIDR Firewalls
&lt;/h2&gt;

&lt;p&gt;When you expose a local server to the internet, it’s exposed to &lt;em&gt;everyone&lt;/em&gt;. Bots and port scanners will find it within minutes.&lt;/p&gt;

&lt;p&gt;Instead of writing custom middleware in your local development server, you can tell the tunnel agent to enforce an IP allowlist right at the gateway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;eta 3000 &lt;span class="nt"&gt;--allow-ip&lt;/span&gt; &lt;span class="s2"&gt;"102.89.42.10, 192.168.1.0/24"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any unauthorized caller is rejected at the edge with an HTTP &lt;code&gt;403 Forbidden&lt;/code&gt; before the request ever touches your machine.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Trying It Out
&lt;/h2&gt;

&lt;p&gt;Explita Tunnel (&lt;code&gt;eta&lt;/code&gt;) is currently in early release.&lt;/p&gt;

&lt;p&gt;You can install the CLI globally:&lt;br&gt;
&lt;/p&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; &lt;span class="nt"&gt;-g&lt;/span&gt; @explita/cli
&lt;span class="c"&gt;# or with pnpm&lt;/span&gt;
pnpm add &lt;span class="nt"&gt;-g&lt;/span&gt; @explita/cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expose your local app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Expose port 3000&lt;/span&gt;
eta 3000

&lt;span class="c"&gt;# Expose with a custom subdomain, auth, and IP restriction&lt;/span&gt;
eta 3000 &lt;span class="nt"&gt;-s&lt;/span&gt; myapp &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="s2"&gt;"dev:secret"&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"1.2.3.4"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can learn more and get access at:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Website &amp;amp; Documentation:&lt;/strong&gt; &lt;a href="https://tunnel.explita.ng" rel="noopener noreferrer"&gt;https://tunnel.explita.ng&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Building networking tools from scratch is humbling. You think the hard part is cryptography or concurrency, but in reality, the hardest parts are the edge cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Socket close codes (&lt;code&gt;1000&lt;/code&gt; vs &lt;code&gt;1006&lt;/code&gt; vs &lt;code&gt;1008&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Chunk encoding across binary and utf-8 streams&lt;/li&gt;
&lt;li&gt;Keeping terminal interfaces responsive in raw TTY mode&lt;/li&gt;
&lt;li&gt;Ensuring your relay never buffers what should be streaming&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re building developer tools or experimenting with WebSockets and cryptography in Node.js, I hope this breakdown gives you some useful ideas for your own architecture. And if you have any questions or feedback, let's chat in the comments!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>devtools</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Generate Zod schemas &amp; guard your DB automatically.</title>
      <dc:creator>explita</dc:creator>
      <pubDate>Fri, 22 May 2026 08:08:21 +0000</pubDate>
      <link>https://dev.to/explita/stop-manually-stripping-prisma-inputs-generate-zod-schemas-guard-your-db-automatically-56mj</link>
      <guid>https://dev.to/explita/stop-manually-stripping-prisma-inputs-generate-zod-schemas-guard-your-db-automatically-56mj</guid>
      <description>&lt;p&gt;If you use Prisma, you already know it's a fantastic ORM with excellent developer experience. But there's a common friction point almost all of us hit when building real-world APIs: &lt;strong&gt;input validation and sanitization&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Prisma is strict. If a user passes an unknown field in a &lt;code&gt;.create()&lt;/code&gt; or &lt;code&gt;.update()&lt;/code&gt; payload (like an injected &lt;code&gt;isAdmin: true&lt;/code&gt;), Prisma throws an error. So what do you do? You end up writing tedious mapping functions, picking fields manually, or writing massive Zod schemas by hand to validate and strip out the junk before it reaches Prisma.&lt;/p&gt;

&lt;p&gt;What if your Prisma schema could just... do it all for you?&lt;/p&gt;

&lt;p&gt;Enter &lt;a href="https://github.com/explita/prisma-guard" rel="noopener noreferrer"&gt;&lt;strong&gt;Prisma Guard&lt;/strong&gt;&lt;/a&gt; 🛡️.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Prisma Guard?
&lt;/h2&gt;

&lt;p&gt;Prisma Guard is the ultimate Prisma companion designed to bridge the gap between database models and API validation. It does two main things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Runtime Protection (The Guard):&lt;/strong&gt; A Prisma Client extension that silently strips unknown fields from your queries at runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema Generation (The Magic):&lt;/strong&gt; Automatically transforms your &lt;code&gt;.prisma&lt;/code&gt; models into robust, decorated Zod schemas with complete type-safety.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's completely zero-config out of the box (it even manages your &lt;code&gt;.gitignore&lt;/code&gt; and Prettier formatting for generated files), but offers immense power for those who need custom validation logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✌️ Two Ways to Use It
&lt;/h2&gt;

&lt;p&gt;Depending on your needs, you can use Prisma Guard in two modes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mode 1: Runtime Protection Only (The Quick Fix)
&lt;/h3&gt;

&lt;p&gt;If you just want to stop Prisma from throwing errors when random fields are passed from the frontend, just add the extension to your client:&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;PrismaClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@prisma/client&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;prismaGuard&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@explita/prisma-guard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prisma&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;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;$extends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;prismaGuard&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// Any extra fields passed to 'data' will be silently stripped&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&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="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;poisonField&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;will be removed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Poof! Gone.&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;
  
  
  Mode 2: Full Validation &amp;amp; Schema Generation (Recommended)
&lt;/h3&gt;

&lt;p&gt;This is where the real magic happens. By running &lt;code&gt;npx prisma-guard&lt;/code&gt;, it reads your Prisma schema and generates perfect Zod schemas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your Prisma Schema:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model User {
  id        String   @id @default(cuid())
  email     String   @unique
  password  String
  createdAt DateTime @default(now())
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserCreateSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This field is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&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;min&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;// id and createdAt are managed by Prisma defaults or omitted entirely depending on your config!&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💎 Advanced Type Ergonomics &amp;amp; Dual Schemas
&lt;/h2&gt;

&lt;p&gt;One of the biggest headaches with schema generation is that a "public" API schema shouldn't include sensitive internal database fields (like &lt;code&gt;secretApiKey&lt;/code&gt; or &lt;code&gt;tenantId&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Prisma Guard handles this beautifully by generating &lt;strong&gt;Dual Schemas&lt;/strong&gt; for every model:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Public Schemas&lt;/strong&gt; (&lt;code&gt;UserCreateSchema&lt;/code&gt;): Respects your omission rules. Perfect for API request validation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalar Schemas&lt;/strong&gt; (&lt;code&gt;UserCreateScalarSchema&lt;/code&gt;): Includes &lt;em&gt;every&lt;/em&gt; database field. Perfect for internal services and database operations.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It also exports a suite of ergonomic TypeScript types (&lt;code&gt;UserCreate&lt;/code&gt;, &lt;code&gt;UserUpdate&lt;/code&gt;, &lt;code&gt;UserCreateRequired&lt;/code&gt;) so your service layers are always perfectly typed.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎨 The Power of Decorators (and the &lt;code&gt;v&lt;/code&gt; proxy)
&lt;/h2&gt;

&lt;p&gt;Sometimes &lt;code&gt;z.string()&lt;/code&gt; isn't enough. You need &lt;code&gt;.email()&lt;/code&gt;, &lt;code&gt;.min(8)&lt;/code&gt;, or custom refinement logic. Prisma Guard lets you define this cleanly using triple-slash comments (&lt;code&gt;///&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Instead of polluting your Prisma schema with massive strings of Zod code, Prisma Guard introduces the &lt;strong&gt;&lt;code&gt;v&lt;/code&gt; proxy&lt;/strong&gt; in a &lt;code&gt;prisma-guard.config.js&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// prisma-guard.config.js&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;defineConfig&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@explita/prisma-guard&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="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;decorators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Chain validations onto the inferred Prisma type&lt;/span&gt;
    &lt;span class="na"&gt;email&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="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&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="c1"&gt;// Completely override a type&lt;/span&gt;
    &lt;span class="na"&gt;strongPassword&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="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Z&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&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;Then, you just use them in your Prisma schema!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model User {
  id       String @id

  /// @zod.use(email)
  email    String

  /// @zod.use(strongPassword)
  password String
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  IDE Superpowers 🪄
&lt;/h3&gt;

&lt;p&gt;No one likes typing out decorators from memory. Prisma Guard includes an incredible &lt;code&gt;npx prisma-guard metadata --vscode&lt;/code&gt; command that automatically installs VS Code snippets for every Zod method. You literally get autocompletion inside your &lt;code&gt;.prisma&lt;/code&gt; files!&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ Blazing Fast Performance
&lt;/h2&gt;

&lt;p&gt;You might be thinking: &lt;em&gt;"Doesn't a runtime guard slow down my queries?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The answer is no. The runtime guard is designed for ultra-low latency. It uses strict $O(n)$ complexity (looping only over predefined schema fields instead of arbitrary user input), memoized whitelists, and hot-path early exits.&lt;/p&gt;

&lt;p&gt;In benchmarks of 100,000 recursive sanitization iterations on a complex payload:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total duration&lt;/strong&gt;: &lt;code&gt;221.26ms&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Average per query&lt;/strong&gt;: &lt;strong&gt;&lt;code&gt;2.21 microseconds&lt;/code&gt; (&lt;code&gt;0.0022ms&lt;/code&gt;)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your database query takes 10-50ms, adding 0.0022ms introduces &lt;strong&gt;virtually 0% overhead&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Get Started in 2 Minutes
&lt;/h2&gt;

&lt;p&gt;Stop wasting time manually maintaining Zod schemas that drift out of sync with your database.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install the package:
&lt;/li&gt;
&lt;/ol&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; @explita/prisma-guard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Initialize your config:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx prisma-guard init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Generate the magic:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx prisma-guard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check out the full documentation, advanced relation handling, and custom schema generation features on our &lt;a href="https://github.com/explita/prisma-guard" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If Prisma Guard saves you time, I'd deeply appreciate a ⭐ on GitHub! Let me know what you think in the comments below. Happy coding! 🚀&lt;/p&gt;

</description>
      <category>prisma</category>
      <category>typescript</category>
      <category>zod</category>
      <category>node</category>
    </item>
    <item>
      <title>Automatic Audit Logging in Prisma</title>
      <dc:creator>explita</dc:creator>
      <pubDate>Thu, 21 May 2026 12:25:08 +0000</pubDate>
      <link>https://dev.to/explita/stop-writing-manual-database-triggers-automatic-audit-logging-in-prisma-4l83</link>
      <guid>https://dev.to/explita/stop-writing-manual-database-triggers-automatic-audit-logging-in-prisma-4l83</guid>
      <description>&lt;p&gt;&lt;strong&gt;Picture this:&lt;/strong&gt; A critical database record is modified or deleted in production. Customer support is flooded, and your manager asks, &lt;em&gt;"Who did this, what was changed, and what did it look like before?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you don't have audit logging set up, you're stuck digging through database transaction logs, parsing raw server outputs, or doing forensic database recovery. It's stressful, slow, and completely avoidable.&lt;/p&gt;

&lt;p&gt;But building audit logging is notoriously tedious:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database Triggers&lt;/strong&gt; are hard to maintain, slow, and don't know anything about your application context (like the HTTP Request IP or the active User Session).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual Service Logging&lt;/strong&gt; requires you to write boilerplate code around every database update, mutation, and delete in your entire codebase. One missing line, and your audit log is incomplete.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To solve this once and for all, I built &lt;strong&gt;Prisma Audit Log&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;It’s a native Prisma Client extension that automatically intercepts your queries (&lt;code&gt;create&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;, &lt;code&gt;upsert&lt;/code&gt;, and batch operations) to log changes. Because it runs directly inside your application code, it captures full request context and user details seamlessly.&lt;/p&gt;

&lt;p&gt;Here is how to set it up and what it gives you.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. The Audit Log Schema
&lt;/h3&gt;

&lt;p&gt;First, add an &lt;code&gt;AuditLog&lt;/code&gt; model to your &lt;code&gt;schema.prisma&lt;/code&gt; file. This is where your audit records will be stored:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model AuditLog {
  id            String   @id @default(cuid())
  userId        String?  @map("user_id")
  recordId      String   @map("record_id")
  action        String   // "create" | "update" | "delete"
  model         String   // E.g. "User", "Post"
  oldData       Json?    @map("old_data")
  newData       Json?    @map("new_data")
  changedFields String[] @map("changed_fields")
  ipAddress     String?  @map("ip_address")
  userAgent     String?  @map("user_agent")
  metadata      Json?
  createdAt     DateTime @default(now()) @map("created_at")

  @@map("audit_logs")
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2. Basic Extension Setup
&lt;/h3&gt;

&lt;p&gt;You can register the audit log extension on your existing &lt;code&gt;PrismaClient&lt;/code&gt; in seconds:&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;PrismaClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@prisma/client&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;auditLogExtension&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@explita/prisma-audit-log&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;getAuthContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Your auth/request storage&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prisma&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;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;$extends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;auditLogExtension&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Context-Aware Logging: Automatically capture user/network context&lt;/span&gt;
    &lt;span class="na"&gt;getContext&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAuthContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anonymous&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;ipAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&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="na"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&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="c1"&gt;// 2. Data Sanitization: Mask passwords or security tokens globally&lt;/span&gt;
    &lt;span class="na"&gt;maskFields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;creditCard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;maskValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[REDACTED]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Fine-grained filtering per model&lt;/span&gt;
    &lt;span class="na"&gt;fieldFilters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refreshToken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;passwordSalt&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;Payment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;amount&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;createdAt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// Whitelist only&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. Skip unnecessary logging (e.g. high-frequency sessions)&lt;/span&gt;
    &lt;span class="na"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;operation&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Session&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;operation&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;create&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;h3&gt;
  
  
  3. What Happens Under the Hood?
&lt;/h3&gt;

&lt;p&gt;Once registered, you write your normal database queries, and Prisma Audit Log handles the rest behind the scenes.&lt;/p&gt;

&lt;p&gt;For example, when you perform a normal Prisma update:&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;prisma&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="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_abc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;newemail@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ADMIN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extension intercepts this, calculates the diff between the database state before and after, and creates a record that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"log_12345"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin_user_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recordId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_abc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"oldData"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_abc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"oldemail@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USER"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"newData"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_abc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"newemail@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ADMIN"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"changedFields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ipAddress"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"192.168.1.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userAgent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tenantId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tenant_1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-21T13:15:34Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  4. Key Developer-First Features
&lt;/h3&gt;

&lt;p&gt;Here are some details I built in to keep database logs clean, performant, and safe:&lt;/p&gt;

&lt;h4&gt;
  
  
  🔇 Noise-Free Update Logs
&lt;/h4&gt;

&lt;p&gt;Have you ever seen audit logs filled with thousands of rows where nothing actually changed? Prisma updates often touch a model's &lt;code&gt;updatedAt&lt;/code&gt; field or re-save unmodified fields. Prisma Audit Log automatically detects if &lt;strong&gt;only&lt;/strong&gt; the timestamp fields changed, and quietly skips creating an audit entry. Your logs stay clean and noise-free.&lt;/p&gt;

&lt;h4&gt;
  
  
  ⚡ Efficient Batch Processing
&lt;/h4&gt;

&lt;p&gt;When you run bulk operations like &lt;code&gt;createMany&lt;/code&gt;, &lt;code&gt;updateMany&lt;/code&gt;, or &lt;code&gt;deleteMany&lt;/code&gt;, inserting audit logs one-by-one is a performance killer. The extension groups and batches these operations, falling back gracefully to single inserts only when necessary.&lt;/p&gt;

&lt;h4&gt;
  
  
  🔌 Send Logs Anywhere
&lt;/h4&gt;

&lt;p&gt;By default, the extension saves audit logs directly to your &lt;code&gt;AuditLog&lt;/code&gt; table using the Prisma schema above. But if you want to stream logs to an external service (like Datadog, Axiom, or a Kafka queue), you can override the target using a custom &lt;code&gt;logger&lt;/code&gt; callback:&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="nf"&gt;auditLogExtension&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;logger&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;logs&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;// Send logs to Elasticsearch, Datadog, or cloud storage&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;datadogClient&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="nx"&gt;logs&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;h3&gt;
  
  
  Give it a try! 🚀
&lt;/h3&gt;

&lt;p&gt;Prisma Audit Log is open-source (MIT licensed) and ready for use. If you're building systems that require database compliance, transparency, or debugging reassurance, give it a spin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NPM&lt;/strong&gt;: &lt;code&gt;npm i @explita/prisma-audit-log&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/explita/prisma-audit-log" rel="noopener noreferrer"&gt;github.com/explita/prisma-audit-log&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;If this helps you sleep a bit better at night knowing your database history is safe, I'd love a GitHub ⭐! Let me know in the comments if you have any questions or feature suggestions.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>prisma</category>
      <category>audit</category>
      <category>logging</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Why I Built My Own TypeScript RPC Framework (And Why You Might Actually Want It)</title>
      <dc:creator>explita</dc:creator>
      <pubDate>Thu, 21 May 2026 12:11:49 +0000</pubDate>
      <link>https://dev.to/explita/why-i-built-my-own-typescript-rpc-framework-and-why-you-might-actually-want-it-32b9</link>
      <guid>https://dev.to/explita/why-i-built-my-own-typescript-rpc-framework-and-why-you-might-actually-want-it-32b9</guid>
      <description>&lt;p&gt;I have a problem.&lt;/p&gt;

&lt;p&gt;Every time I start building a Next.js application, I find myself fighting the exact same set of friction points:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fragile Type Safety&lt;/strong&gt;: Type safety that works beautifully on the server but quietly disintegrates or requires manual alignment across the network boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev-Loop Bloat&lt;/strong&gt;: Codegen tools and compilation steps that add 15 to 30 seconds of waiting time to my dev loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Imprecise Error Handling&lt;/strong&gt;: Catching errors that is either overly verbose (try-catch blocks wrapping every handler) or non-existent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual Plumbing&lt;/strong&gt;: Wiring up Redis caching, rate limiting, and circuit breakers manually, procedure by procedure, project by project.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So, I did the classic, slightly unhealthy developer thing: &lt;strong&gt;I built my own solution.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s called &lt;strong&gt;Actyx RPC&lt;/strong&gt;. And before you roll your eyes at the mention of another "typesafe RPC framework" (trust me, I know there are like seventeen of these now) — &lt;strong&gt;hear me out&lt;/strong&gt;. Actyx isn't just about sharing types; it's about solving the practical, everyday headaches of production backend development in TypeScript.&lt;/p&gt;

&lt;p&gt;Here is how it works and why it’s become my go-to stack.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Foundation: A Composable Base Procedure
&lt;/h2&gt;

&lt;p&gt;Instead of setting up boilerplate for every route, Actyx uses a builder pattern. You define a base procedure instance that handles authentication, error mapping, telemetry, and input enrichment globally. Every procedure you create from this base builder automatically inherits these behaviors.&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;createProcedure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RedisCache&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@explita/actyx-rpc&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;redirect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/navigation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Optional: Spin up Redis for cross-serverless caching and rate limiting&lt;/span&gt;
&lt;span class="c1"&gt;// const redisClient = new Redis("redis://localhost:6379");&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;procedure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createProcedure&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// cache: new RedisCache({ redis: redisClient, defaultTTL: "5m" }),&lt;/span&gt;

  &lt;span class="c1"&gt;// 1. Establish the request context (Auth, session, user roles)&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;createContext&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;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getSession&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;session&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INVALID_SESSION&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;ok&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;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&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;role&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="c1"&gt;// 2. Automatically handle context failures (like redirects or custom errors)&lt;/span&gt;
  &lt;span class="c1"&gt;// This is only called when the createContext returns an object with ok:false&lt;/span&gt;
  &lt;span class="na"&gt;onContextError&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;reason&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loginPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INVALID_SESSION&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;_redirect&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="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loginPath&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="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UNAUTHORIZED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unauthorized access&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="c1"&gt;// 3. Centralized error mapping — no try/catch pyramids in your handlers!&lt;/span&gt;
  &lt;span class="nf"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;error&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[Error in &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;handlerName&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;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Map database constraints (like Prisma unique key violations) globally&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;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;P2002&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This record already exists&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CONFLICT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;409&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="c1"&gt;// 4. Enrich every incoming handler input with context variables automatically&lt;/span&gt;
  &lt;span class="nf"&gt;enrichInput&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&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;userId&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;Now, every procedure created from this &lt;code&gt;procedure&lt;/code&gt; builder gets auth context checks, global error transforms, and user ID injection with &lt;strong&gt;zero&lt;/strong&gt; additional lines of code.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Defining and Calling Procedures
&lt;/h3&gt;

&lt;p&gt;Actyx RPC keeps procedures simple. You define inputs using your validator of choice (Zod, Valibot, ArkType, Joi, Yup, or custom resolvers) and resolve them as either &lt;code&gt;.query()&lt;/code&gt; (for reads) or &lt;code&gt;.mutation()&lt;/code&gt; (for writes).&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;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&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;zodResolver&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@explita/actyx-rpc/resolvers/zod&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;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/db&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;procedure&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./base-procedure&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Create a mutation procedure&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createPost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;procedure&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;createPost&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="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;zodResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mutation&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;ctx&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ctx.userId is fully typed and available&lt;/span&gt;
    &lt;span class="c1"&gt;// input.title and input.body are validated, typed, and sanitized&lt;/span&gt;
    &lt;span class="c1"&gt;// input.userId is injected from enrichInput!&lt;/span&gt;
    &lt;span class="k"&gt;return&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="nx"&gt;post&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="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;title&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;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;body&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;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;authorId&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;userId&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;When you call this procedure, Actyx returns a &lt;strong&gt;Go-style &lt;code&gt;[data, error]&lt;/code&gt; tuple&lt;/strong&gt;. No try/catch blocks needed:&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createPost&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A Brand New Post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This is a body longer than ten characters.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&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;// 'post' is fully typed!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  3. The Features That Surprised Me (DX Wins)
&lt;/h3&gt;

&lt;p&gt;While building Actyx, I added a few features to solve my own development fatigue, and they turned out to be absolute game-changers:&lt;/p&gt;

&lt;h4&gt;
  
  
  💡 The &lt;code&gt;.mock()&lt;/code&gt; Method (For Parallel Development)
&lt;/h4&gt;

&lt;p&gt;Seeding and maintaining local databases just to test a frontend feature is exhausting. Actyx introduces first-class mocking directly in the builder chain:&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;procedure&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;getUser&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="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_mock_123&lt;/span&gt;&lt;span class="dl"&gt;"&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="s2"&gt;Jane Doe (Mocked)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jane@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="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="k"&gt;async &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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// This expensive DB query is skipped when MOCKing is active!&lt;/span&gt;
    &lt;span class="k"&gt;return&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="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;ACTYX_MOCK="true"&lt;/code&gt; in your environment variables, and the backend handlers are bypassed. The middleware, session validation, and inputs are still processed, but your database is never touched. Frontend devs can code against a "perfect" mock state while the backend is still being designed.&lt;/p&gt;

&lt;h4&gt;
  
  
  🧠 Caching with a Brain (SWR Built-in)
&lt;/h4&gt;

&lt;p&gt;Actyx comes with Stale-While-revalidate (SWR) caching out of the box. You don't need Redis to get started; an in-memory cache adapter works by default, but you can plug in Redis as you scale:&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getPost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;procedure&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;5m&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Fresh for 5 minutes&lt;/span&gt;
    &lt;span class="na"&gt;staleTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1m&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Becomes stale after 1 minute&lt;/span&gt;
    &lt;span class="na"&gt;staleWhileRevalidate&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="c1"&gt;// Serve stale data, refresh in the background&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`post:&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="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="nf"&gt;query&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;input&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;return&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="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  🛡️ Serverless-Friendly Rate Limiting
&lt;/h4&gt;

&lt;p&gt;Because Actyx can plug directly into your Redis cache adapter, rate limiting persists across serverless function invocations (like Vercel Serverless/AWS Lambda instances).&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;procedure&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;({&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;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1m&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="o"&gt;=&amp;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;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Limit requests per user&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You're sending messages too fast!&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="nf"&gt;mutation&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;input&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;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;messageService&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="nx"&gt;input&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;h4&gt;
  
  
  🔗 Inter-Procedure Calling (Zero Network Hops)
&lt;/h4&gt;

&lt;p&gt;When procedures call other procedures, network overhead and duplicated auth logic usually slow things down. Actyx uses &lt;code&gt;AsyncLocalStorage&lt;/code&gt; under the hood. When one procedure calls another, it bypasses context re-creation and authentication entirely while keeping the context secure:&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;getAuditLog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;procedure&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="k"&gt;async &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="o"&gt;=&amp;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;await&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;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&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;userId&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getUserDashboard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;procedure&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="k"&gt;async &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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;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;db&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;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// This nested call automatically inherits the parent's auth context!&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;logs&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="nf"&gt;getAuditLog&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&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;logs&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;
  
  
  4. What Else is in the Box?
&lt;/h3&gt;

&lt;p&gt;To make Actyx a production-ready choice, I added several advanced features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenTelemetry&lt;/strong&gt;: Native instrumentation so you can trace why a specific procedure is running slow at 3 AM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic OpenAPI Specs&lt;/strong&gt;: Auto-generate standard OpenAPI JSON files to keep documentation from rotting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Circuit Breakers&lt;/strong&gt;: Prevent cascading failures when third-party microservices go down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adaptive Compression&lt;/strong&gt;: Native support for gzip, deflate, and brotli.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeouts&lt;/strong&gt;: Safe defaults to prevent hanging server processes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React Hooks&lt;/strong&gt;: Built-in hooks (&lt;code&gt;useQuery&lt;/code&gt;, &lt;code&gt;useMutation&lt;/code&gt;, &lt;code&gt;useInfiniteQuery&lt;/code&gt;) for frontend state synchronization.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  What I Haven't Figured Out Yet
&lt;/h3&gt;

&lt;p&gt;Actyx is working great for my production projects, but it’s still in active development. Here’s what’s currently on my drawing board:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bidirectional WebSockets&lt;/strong&gt;: SSE (Server-Sent Events) streaming works perfectly for one-way flows (like LLM stream responses), but full bidirectional WS subscriptions are still being refined.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance Benchmarks&lt;/strong&gt;: It feels lightning-fast under typical workloads, but I want to perform strict load/torture tests to publish concrete numbers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you have experience building scalable RPC networks or websocket adapters, I'd love to hear how you approached those challenges!&lt;/p&gt;




&lt;h3&gt;
  
  
  Give it a Spin 🚀
&lt;/h3&gt;

&lt;p&gt;Actyx RPC is entirely open-source (MIT licensed). Break it, inspect it, and tell me what you think.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NPM&lt;/strong&gt;: &lt;code&gt;npm i @explita/actyx-rpc&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/explita/actyx-rpc" rel="noopener noreferrer"&gt;github.com/explita/actyx-rpc&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;P.S. — The name "Actyx" comes from "action" + "tyx" (as in type safety). Naming is hard. If you have a better suggestion, I'm all ears!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>nextjs</category>
      <category>showdev</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
