<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>The Hack Job Handbook</title>
    <description>A blog about hacking, programming, and other tech stuff.</description>
    <link>https://blog.jarv.dev</link>
    <atom:link href="https://blog.jarv.dev/feed.xml" rel="self" type="application/rss+xml" />
    <author>
      <name>nihilok</name>
      <email>mike@jarv.dev</email>
      <uri>https://blog.jarv.dev</uri>
    </author>
    
      <item>
        <title>The Silent Ping: How We Built Multi-Agent Slack Orchestration with SQLite and a &quot;Godfather&quot; Session</title>
        <description>&lt;p&gt;You kick off a large refactor in Claude Code. It’s going to run 300 tests, update 14 files across three packages, and take roughly eight minutes.&lt;/p&gt;

&lt;p&gt;You switch over to Slack or go put the kettle on.&lt;/p&gt;

&lt;p&gt;Ten minutes later you wander back to your desk. The terminal hasn’t moved. It stalled ninety seconds after you left because it wanted confirmation to run a migration.&lt;/p&gt;

&lt;p&gt;Naturally, your first instinct is to automate this: hook up a Slack webhook or a bot token so the agent pings you when it’s done or stuck.&lt;/p&gt;

&lt;p&gt;Except you immediately run into the Silent Ping.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-silent-ping-problem&quot;&gt;The Silent Ping Problem&lt;/h2&gt;

&lt;p&gt;If you use your own user token or standard incoming webhooks tied to your user profile, Slack is “helpful”: it knows &lt;em&gt;you&lt;/em&gt; sent the message.&lt;/p&gt;

&lt;p&gt;And Slack does not send desktop banners, sound chimes, or mobile push notifications for messages you send to yourself. You get a little grey text line in your “saved messages” or a private channel, utterly devoid of urgency. You’re still babysitting the terminal; you’re just doing it through Slack.&lt;/p&gt;

&lt;p&gt;The fix was obvious: my agent needed its own legal identity.&lt;/p&gt;

&lt;p&gt;Enter &lt;strong&gt;Mike’s Agent&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I created a dedicated Slack app with its own bot token, installed it into our workspace, and called it Mike’s Agent.&lt;/p&gt;

&lt;p&gt;Then came the real trick: &lt;strong&gt;I marked Mike’s Agent as a “VIP” in Slack.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you haven’t used Slack’s VIP feature, it’s a game changer for agent workflows. VIP messages bypass standard notification throttling and muting. The bot doesn’t even need to loudly &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@&lt;/code&gt; me in every message (though it can). Anything Mike’s Agent posts in our 1:1 App DM lands as an immediate, unmissable notification on my desktop, phone, and watch.&lt;/p&gt;

&lt;p&gt;And in that 1:1 App DM, there’s an important operational bonus: &lt;em&gt;every&lt;/em&gt; message I send to the bot is implicitly addressed to it. No awkward &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;@Mike&apos;s Agent&amp;gt;&lt;/code&gt; syntax required. In public channels, it listens for its mention; in our private DM, it knows I’m talking directly to it.&lt;/p&gt;

&lt;p&gt;The ping is no longer silent.&lt;/p&gt;

&lt;p&gt;Once that worked, I persuaded a couple of teammates to do the same. Soon we had Sarah’s Agent and Dave’s Agent in the workspace. But that’s when things got interesting — and slightly chaotic.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-multi-session-chaos-and-the-rate-limit-wall&quot;&gt;The Multi-Session Chaos and the Rate Limit Wall&lt;/h2&gt;

&lt;p&gt;It’s 2026; nobody runs just one agent session anymore.&lt;/p&gt;

&lt;p&gt;On any given morning, I might have three separate Claude Code or Codex sessions open in different tmux panes:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;One is running a slow integration test suite in a detached worktree.&lt;/li&gt;
  &lt;li&gt;One is refactoring a database schema.&lt;/li&gt;
  &lt;li&gt;One is doing exploratory research on an external API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If all three sessions talk to Slack directly, two disasters strike immediately:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;The Rate Limit Wall:&lt;/strong&gt; If three local sessions each start polling Slack’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;conversations.history&lt;/code&gt; or opening parallel WebSockets, you get slammed with HTTP 429 rate limit bans before lunch.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The Token Wildfire:&lt;/strong&gt; If someone in Slack threads a message: &lt;em&gt;“@Mike’s Agent can you rerun that migration with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--dry-run&lt;/code&gt; flag?”&lt;/em&gt; — which session answers? If all three are listening, three LLMs wake up simultaneously, burn thousands of context tokens generating overlapping responses, and race to reply in the same thread.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We needed an orchestrator. But I didn’t want a heavy Kafka queue, a Redis cluster, ngrok tunnels, or an over-engineered cloud broker for local terminal sessions.&lt;/p&gt;

&lt;p&gt;So I reached for the most reliable piece of infrastructure on Earth: &lt;strong&gt;SQLite&lt;/strong&gt;, backed by a single host-wide daemon.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-host-daemon-and-the-local-sqlite-engine&quot;&gt;The Host Daemon and the Local SQLite Engine&lt;/h2&gt;

&lt;p&gt;Instead of each terminal session fumbling with Slack tokens and hitting API endpoints, we decoupled the architecture into two layers:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;                  [ Slack Web API ]
                          │
         (Single host-wide polling connection)
                          │
                          ▼
            [ Local Host Daemon (launchd/systemd) ]
                          │
            (Writes diffs &amp;amp; buffers events)
                          │
                          ▼
             ┌─────────────────────────┐
             │   ~/.claude/state/      │
             │     slack-sync.db       │
             │  (SQLite Local State)   │
             └─────────────────────────┘
                          │
           (Zero-overhead local watch streams)
                          │
         ┌────────────────┴────────────────┐
         ▼                                 ▼
 [ Godfather Session ]             [ Worker Session ]
  (Lead Orchestrator)               (Focused Task)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;A Single Host Daemon:&lt;/strong&gt; Managed by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;launchd&lt;/code&gt; on macOS or a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;systemd&lt;/code&gt; user unit on Linux. It runs in the background, authenticated securely via the Slack CLI (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slack api ... --app &amp;lt;id&amp;gt;&lt;/code&gt;), keeping tokens out of plaintext environment variables. It acts as our rate-limit shield, sweeping channels and DMs on a disciplined cadence.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Local SQLite State (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.claude/state/slack-sync.db&lt;/code&gt;):&lt;/strong&gt; The daemon diffs conversation history locally. Incoming replies, reactions, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@mentions&lt;/code&gt; are written straight into normalized local tables:
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tracked_messages&lt;/code&gt;: Outbound messages waiting on human input.&lt;/li&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;seen_reactions&lt;/code&gt; &amp;amp; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;seen_replies&lt;/code&gt;: The local diff engine that isolates what is strictly &lt;em&gt;new&lt;/em&gt;.&lt;/li&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mentions&lt;/code&gt;: Inbound requests from humans or other bots.&lt;/li&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessions&lt;/code&gt;: Heartbeat tracking of active terminal sessions.&lt;/li&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;poll_events&lt;/code&gt;: An event queue drained by active sessions without touching Slack.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;outbound-approval-gates-vs-ambient-watching&quot;&gt;Outbound Approval Gates vs. Ambient Watching&lt;/h2&gt;

&lt;p&gt;When an agent needs human input, it doesn’t just spew text into a channel. It uses structured modes:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Post an approval gate to the operator&apos;s DM&lt;/span&gt;
slack_sync post &lt;span class=&quot;nt&quot;&gt;--mode&lt;/span&gt; gate &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--text&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Deploying migration 0043_orders to staging — ✅ approve, ❌ hold&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--purpose&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;approve migration 0043&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--mode gate&lt;/code&gt;, the engine explicitly parses reaction semantics:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:white_check_mark:&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:+1:&lt;/code&gt; → &lt;strong&gt;Approved&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:x:&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:-1:&lt;/code&gt; → &lt;strong&gt;Rejected&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;Thread reply → &lt;strong&gt;Replied&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent posts once, receives a unique tracking ID in SQLite, and yields.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;why-the-monitored-daemon-is-orders-of-magnitude-more-context-efficient&quot;&gt;Why the Monitored Daemon is Orders of Magnitude More Context-Efficient&lt;/h2&gt;

&lt;p&gt;The biggest silent failure mode in agentic workflows is letting the LLM manage its own polling logic.&lt;/p&gt;

&lt;p&gt;In a conventional setup (or when an agent relies on direct Slack MCP tools like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slack_read_thread&lt;/code&gt;), waiting for a human approval looks like this:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;The agent calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slack_read_thread&lt;/code&gt; → dumps 1,500 tokens of raw JSON, timestamps, Block Kit schemas, and full conversation history straight into its context window.&lt;/li&gt;
  &lt;li&gt;The agent parses the payload: &lt;em&gt;“No reaction or reply detected yet.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;The agent initiates a manual polling turn (a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sleep 60&lt;/code&gt; command or a scheduled wakeup).&lt;/li&gt;
  &lt;li&gt;The agent wakes up, executes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slack_read_thread&lt;/code&gt; again → dumps another 1,500 tokens of duplicate thread history.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you step away from your desk for ten minutes to put the kettle on and the agent checks every sixty seconds, that’s &lt;strong&gt;ten tool calls and 15,000+ tokens of redundant Slack payload sludge&lt;/strong&gt; permanently baked into your active context window before any decision has even been made.&lt;/p&gt;

&lt;p&gt;You hit context compression prematurely. Your agent starts hedging, second-guessing its plan, and losing earlier instructions. And you burn API tokens merely to watch paint dry.&lt;/p&gt;

&lt;p&gt;The monitored daemon completely inverts this paradigm.&lt;/p&gt;

&lt;p&gt;Instead of the LLM polling Slack, the agent delegates the waiting to Claude Code’s native &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Monitor&lt;/code&gt; background tool:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Monitor(command=&quot;slack_sync --session &amp;lt;SID&amp;gt; watch --ids 7&quot;,
        description=&quot;Waiting for approval on migration 0043&quot;, 
        timeout_ms=1800000)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;watch&lt;/code&gt; command sits quietly in the background outside the LLM’s conversation history, tailing the local SQLite &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;poll_events&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;There are &lt;strong&gt;zero Slack API calls&lt;/strong&gt; executed by the agent. There is &lt;strong&gt;zero manual polling logic&lt;/strong&gt; cluttering the conversation. While waiting, the context cost is literally &lt;strong&gt;zero tokens&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The moment I tap &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:white_check_mark:&lt;/code&gt; on my phone:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;The host daemon records the reaction in SQLite during its background sweep.&lt;/li&gt;
  &lt;li&gt;The local &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;watch&lt;/code&gt; process computes the diff and emits a single, surgical JSON line to stdout:
    &lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;event&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;slack-sync&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;summary&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;APPROVED by U0123 via :white_check_mark:&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;resolution&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;kind&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;approved&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Monitor&lt;/code&gt; tool catches that stdout line and triggers an immediate reactive notification into the agent’s context.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total tokens consumed across that entire ten-minute wait: &lt;strong&gt;~40 tokens&lt;/strong&gt; (just the final resolution event).&lt;/p&gt;

&lt;p&gt;Removing repeated integration calls and manual polling isn’t just a minor optimisation — it is &lt;strong&gt;orders of magnitude&lt;/strong&gt; more context-efficient.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;concurrency-and-atomic-claims&quot;&gt;Concurrency and Atomic Claims&lt;/h2&gt;

&lt;p&gt;What about inbound mentions? If someone asks a question in a public channel, how do we prevent three tmux panes from racing to answer?&lt;/p&gt;

&lt;p&gt;Mentions land in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mentions&lt;/code&gt; table in an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unclaimed&lt;/code&gt; state. When a session is ready for ambient work, it executes an atomic SQLite transaction:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;UPDATE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mentions&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;claimed&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;claimed_by&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;session_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;claimed_at&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mention_id&lt;/span&gt; 
  &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;unclaimed&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;RETURNING&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If the query returns a row, that specific terminal session owns the task. If zero rows return, another session grabbed it a microsecond earlier. Fast, atomic, zero lock contention.&lt;/p&gt;

&lt;p&gt;And if a mention goes completely unheeded? The database enforces a &lt;strong&gt;120-second grace fallback&lt;/strong&gt;: if an event sits unclaimed, it is automatically routed to the oldest live session recorded in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessions&lt;/code&gt; table.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;enter-the-godfather-session&quot;&gt;Enter the “Godfather” Session&lt;/h2&gt;

&lt;p&gt;Most of the time, individual worker sessions shouldn’t be browsing for random tasks. They should remain strictly focused on the feature branch or failing test suite they were invoked to solve.&lt;/p&gt;

&lt;p&gt;To maintain discipline, I run what I call the &lt;strong&gt;Godfather session&lt;/strong&gt; (our lead orchestrator).&lt;/p&gt;

&lt;p&gt;The Godfather is an overarching Claude/Codex session running in a persistent tmux window. It doesn’t write low-level code directly. Instead, it acts as the project manager:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;It claims unassigned ambient mentions from teammates.&lt;/li&gt;
  &lt;li&gt;It breaks large goals into bounded sub-packages and delegates them to worker sessions running in detached worktrees.&lt;/li&gt;
  &lt;li&gt;It monitors approval gates and routes responses back to the original Slack threads.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;bot-to-bot-channels-multi-day-autonomous-runs&quot;&gt;Bot-to-Bot Channels: Multi-Day Autonomous Runs&lt;/h2&gt;

&lt;p&gt;The 1:1 App DM with Mike’s Agent is my personal cockpit. But the real breakthrough happened when we put our bots into shared channels together.&lt;/p&gt;

&lt;p&gt;We created dedicated project channels where humans rarely type, but &lt;strong&gt;Mike’s Agent&lt;/strong&gt;, &lt;strong&gt;Sarah’s Agent&lt;/strong&gt;, and &lt;strong&gt;Dave’s Agent&lt;/strong&gt; live side-by-side.&lt;/p&gt;

&lt;p&gt;This unlocked multi-day autonomous workflows:&lt;/p&gt;

&lt;p&gt;Instead of babysitting a long migration, my &lt;strong&gt;Godfather session can drive work autonomously across entire weekends&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When Mike’s Agent finishes generating updated database bindings, it doesn’t wait for me to ping Sarah. It posts an update into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#bot-migrations&lt;/code&gt;:&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“Mike’s Agent: Schema migration &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0043_orders&lt;/code&gt; complete on branch &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;feat/orders-v2&lt;/code&gt;. Artifacts pushed.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sarah’s Agent—listening via her local daemon—picks up the event, claims it atomically, runs the frontend integration tests against that branch, and replies in the thread:&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“Sarah’s Agent: Contract tests passed. 2 warnings in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useOrders.ts&lt;/code&gt;. Output logged.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;All of this happens while Sarah and I are asleep, reviewing PRs, or working on entirely different projects. No human serving as a biological copy-paste relay between two terminal windows.&lt;/p&gt;

&lt;p&gt;And if something goes sideways? Mike’s Agent posts to my 1:1 DM. Because it’s a VIP contact, my phone buzzes immediately.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-maths&quot;&gt;The Maths&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Architecture&lt;/th&gt;
      &lt;th&gt;Context Burn (10-min wait)&lt;/th&gt;
      &lt;th&gt;Slack Tool Calls in Session&lt;/th&gt;
      &lt;th&gt;Notification Reliability&lt;/th&gt;
      &lt;th&gt;Concurrency &amp;amp; Rate Limits&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Direct Slack MCP / API Polling&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;~15,000–30,000 tokens (repeating thread dumps)&lt;/td&gt;
      &lt;td&gt;10+ calls (manual sleep/wake loop)&lt;/td&gt;
      &lt;td&gt;❌ Silent or throttled&lt;/td&gt;
      &lt;td&gt;Rapid HTTP 429 rate-limit risk&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Heavy Cloud Broker&lt;/strong&gt; (Kafka / Redis)&lt;/td&gt;
      &lt;td&gt;~2,000–5,000 tokens (broker handshakes)&lt;/td&gt;
      &lt;td&gt;Multiple polling turns&lt;/td&gt;
      &lt;td&gt;⚠️ Standard (throttled unless VIP)&lt;/td&gt;
      &lt;td&gt;Complex distributed locks; cloud bill&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Host Daemon + SQLite &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Monitor&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;~40 tokens&lt;/strong&gt; (only the final resolution diff)&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;0 calls&lt;/strong&gt; (1 background watch process)&lt;/td&gt;
      &lt;td&gt;✅ Unmissable (VIP bypasses throttles)&lt;/td&gt;
      &lt;td&gt;✅ Atomic (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UPDATE ... RETURNING&lt;/code&gt;); zero 429s&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-takeaway&quot;&gt;The Takeaway&lt;/h2&gt;

&lt;p&gt;We spend so much time talking about “autonomous agent swarms” in the abstract, but the real bottlenecks are mundane developer ergonomics:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Kill the Silent Ping:&lt;/strong&gt; Give your agent its own bot identity and mark it as a Slack &lt;strong&gt;VIP&lt;/strong&gt; so priority notifications always break through without &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@mention&lt;/code&gt; fatigue.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Decouple with a Host Daemon:&lt;/strong&gt; Don’t let raw agent sessions hit Slack directly. A single host daemon protects your team from 429 rate limit bans and manages authentication cleanly.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Reactive Streaming over Polling:&lt;/strong&gt; Never let an agent sleep in a loop. Pair a local SQLite queue with background monitoring tools so events trigger zero-token wakeups.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Local Atomic State:&lt;/strong&gt; A simple local SQLite table with atomic &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UPDATE ... RETURNING&lt;/code&gt; and session heartbeats handles multi-agent concurrency without cloud dependencies.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The Godfather Pattern:&lt;/strong&gt; Use one high-level orchestrator session to triage and coordinate, while worker sessions focus on their local worktrees.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Give your agent a name. Let it ping you like a real colleague. Your context window — and your kettle — will thank you.&lt;/p&gt;
</description>
        <pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate>
        <link>https://blog.jarv.dev//the-silent-ping</link>
        <link href="https://blog.jarv.dev/the-silent-ping"/>
        <guid isPermaLink="true">https://blog.jarv.dev/the-silent-ping</guid>
      </item>
    
      <item>
        <title>The Biological Relay: How Spud and Gemini Turned Hyprland into an AI Poweruser Paradise</title>
        <description>&lt;p&gt;Hold &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Super + D&lt;/code&gt;. Say &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl get pods -n ingress-nginx&lt;/code&gt;. Release.&lt;/p&gt;

&lt;p&gt;Spotify drops to half volume, the command appears letter-by-letter at the cursor in Ghostty, and the music snaps straight back to 100%. Total time elapsed: 420 milliseconds.&lt;/p&gt;

&lt;p&gt;No typing. No pausing the music. No reaching for the mouse.&lt;/p&gt;

&lt;p&gt;If you’d told me two years ago that my daily desktop would handle push-to-talk terminal dictation, auto-duck my audio streams, talk back in a cloned copy of my own voice, and pair with an AI that writes its own window manager hotkeys on the fly, I’d have asked what unholy PPA cocktail you’d been drinking.&lt;/p&gt;

&lt;p&gt;Yet here we are. It runs on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shpadoinkle-1&lt;/code&gt; (my main workstation, named in honour of &lt;em&gt;Cannibal! The Musical&lt;/em&gt; — &lt;em&gt;“My heart’s as full as a baked potato”&lt;/em&gt;), and it came together in a single afternoon.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;1-the-distro-graveyard&quot;&gt;1. The Distro Graveyard&lt;/h2&gt;

&lt;p&gt;Before &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shpadoinkle-1&lt;/code&gt; found peace, my NVMe drive was a revolving graveyard of operating systems I genuinely tried to daily-drive:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Debian&lt;/strong&gt; — Solid as granite, and roughly three geological epochs behind on Wayland compositors.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Ubuntu&lt;/strong&gt; — Fine until release upgrades shattered custom PPAs and Snaps hijacked startup times.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Linux Mint&lt;/strong&gt; — Comfortable like slippers, but never quite the bleeding-edge sandbox I wanted.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Zorin OS&lt;/strong&gt; — Slick out of the box, but too curated for low-level plumbing.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Kubuntu &amp;amp; Lubuntu&lt;/strong&gt; — Useful desktop experiments, but the underlying Debian/Ubuntu upgrade friction remained.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Vanilla OS&lt;/strong&gt; — Clever immutable root architecture, but fighting immutability when hacking system internals is an acquired taste.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Pop!_OS&lt;/strong&gt; — Admirable tiling window manager work, but tied to an external release cadence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;(And then there was the side-quest tier: &lt;strong&gt;Kali&lt;/strong&gt;, &lt;strong&gt;Parrot Security&lt;/strong&gt;, and &lt;strong&gt;Tails&lt;/strong&gt;. Let’s be completely honest—those were never serious daily-driver contenders. They were strictly for my occasional script-kiddy experiments, Wi-Fi auditing, and amnesic live-USB tinkering where keeping persistent dotfiles is impossible by design.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two years ago, I installed &lt;strong&gt;EndeavourOS&lt;/strong&gt;. And the hopping stopped for good.&lt;/p&gt;

&lt;p&gt;EndeavourOS succeeded where the others stumbled because it got out of the way: a clean, bloat-free Arch Linux foundation, rolling packages the minute upstream tags them, and the unfettered reach of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pacman&lt;/code&gt; and the AUR. For two solid years, it ran comfortably with &lt;strong&gt;KDE Plasma 6&lt;/strong&gt;. Reliable, snappy, and uncomplaining.&lt;/p&gt;

&lt;p&gt;Yet for over a year, I’d had an itch: I kept watching Hyprland demos from afar. The fluid Wayland shaders, the dynamic workspaces that spin up when you need them and vanish when empty, the pure keyboard-driven focus. But you know how it goes when you have a daily driver that works—you hesitate to burn a weekend ripping out your desktop environment.&lt;/p&gt;

&lt;p&gt;Then, just a few days ago, the catalyst hit.&lt;/p&gt;

&lt;p&gt;I stumbled across an article dismissively claiming that “Omarchy” Linux was nothing more than an “overhyped set of config files.”&lt;/p&gt;

&lt;p&gt;That was the lightbulb moment. I realised: &lt;em&gt;hang on, if all the fuss is literally just a sharp set of Hyprland dotfiles on top of Arch, why am I still sitting here in Plasma? Why wait for someone else to package it?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That evening, I finally made the leap. I shifted from Plasma 6 into Hyprland, started crafting my native Lua config, and watched my workstation transform into a featherweight rocket ship.&lt;/p&gt;

&lt;p&gt;The operating system was dialed in. The window manager was finally right.&lt;/p&gt;

&lt;p&gt;The remaining bottleneck was me.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;2-the-biological-relay&quot;&gt;2. The Biological Relay&lt;/h2&gt;

&lt;p&gt;In 2026, we have screaming-fast local GPUs and sub-second frontier LLMs. Yet developers are still acting as biological copper wire between their tools.&lt;/p&gt;

&lt;p&gt;You know the routine:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;You’re listening to music.&lt;/li&gt;
  &lt;li&gt;You hit an idea or need to run a command.&lt;/li&gt;
  &lt;li&gt;You fumble for media keys to mute Spotify.&lt;/li&gt;
  &lt;li&gt;You alt-tab to a browser or an AI window.&lt;/li&gt;
  &lt;li&gt;You type a prompt.&lt;/li&gt;
  &lt;li&gt;You copy the snippet.&lt;/li&gt;
  &lt;li&gt;You alt-tab back.&lt;/li&gt;
  &lt;li&gt;You paste it into Ghostty.&lt;/li&gt;
  &lt;li&gt;You unmute Spotify.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then you hit a daily rate limit twenty minutes into your flow state, and the entire train of thought evaporates while you wait for a reset timer.&lt;/p&gt;

&lt;p&gt;That is &lt;strong&gt;The Biological Relay&lt;/strong&gt;: human beings burning mental context to manually shuttle text and state between the microphone, the browser, and the terminal.&lt;/p&gt;

&lt;p&gt;The fix was obvious: speech had to type directly into the active cursor, audio ducking had to happen automatically in the sound server, and the AI copilot had to live natively in the terminal without quota anxiety.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;3-the-giants-and-the-missing-edge&quot;&gt;3. The Giants and the Missing Edge&lt;/h2&gt;

&lt;p&gt;Existing speech and agent tooling solved pieces of this puzzle, but left glaring papercuts:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Wispr Flow&lt;/strong&gt;: I use Wispr Flow daily on my MacBooks for work—it’s slick, responsive, and genuinely brilliant in macOS. Naturally, my first thought was to track down a Linux wrapper and bring that exact workflow across to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shpadoinkle-1&lt;/code&gt;. But under Hyprland, it was clunky at best. Wayland virtual keyboard friction, focus-stealing wrapper windows, zero integration with PipeWire audio streams, and that unmistakable feeling of an app fighting the compositor rather than living inside it.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Talon Voice&lt;/strong&gt; (~3k stars): Astonishingly capable for full hands-free accessibility. But constructing complex Python grammar trees just to dictate a quick bash one-liner into a terminal is like hiring an architect to hang a picture hook.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Whisper.cpp&lt;/strong&gt; (~38k stars): An exquisite C++ port. But batch-transcribing recorded WAV files from the command line is a world away from interactive, low-latency push-to-talk typing across Wayland panes.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Browser AI interfaces&lt;/strong&gt;: Great until you hit quota ceilings, lose terminal working-directory context, or spend your afternoon copying and pasting diffs back and forth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We needed three things working in unison:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Push-to-talk transcription fast enough to feel instantaneous.&lt;/li&gt;
  &lt;li&gt;Intelligent PipeWire stream ducking so voice capture is pristine without touching volume knobs.&lt;/li&gt;
  &lt;li&gt;An AI terminal pairing workflow with generous quota and rapid-fire reasoning.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Enter &lt;strong&gt;Gemini in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agy&lt;/code&gt; CLI&lt;/strong&gt; — and &lt;strong&gt;Spud&lt;/strong&gt;.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;4-building-spud-from-zero-to-voice-clone-in-25-hours&quot;&gt;4. Building Spud: From Zero to Voice Clone in 2.5 Hours&lt;/h2&gt;

&lt;p&gt;I paired with Gemini 3.8 Flash using Google’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agy&lt;/code&gt; CLI to build &lt;a href=&quot;https://github.com/nihilok/spud&quot;&gt;Spud&lt;/a&gt;: a local-first voice orchestrator written in Rust.&lt;/p&gt;

&lt;p&gt;And when I say Gemini in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agy&lt;/code&gt; has been a revelation, I mean it:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Generous Quota:&lt;/strong&gt; My usage never seems to run out. No quota countdowns. No artificial throttle.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The Personality:&lt;/strong&gt; Direct, sharp, slightly wry, zero corporate fluff.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Gemini 3.8 Flash:&lt;/strong&gt; Blazingly quick. It digests complex multi-crate Rust errors, PipeWire wireplumber node graphs, and Wayland virtual seat protocols without breaking sweat.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agy&lt;/code&gt; operates directly inside the repository with native tool execution, we didn’t just write code — we built, tested, and debugged live.&lt;/p&gt;

&lt;p&gt;Here is the git log from that single afternoon session:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;13:46:04 | Initial commit: Spud architecture and roadmap
13:50:14 | feat(stt): implement warm faster-whisper daemon &amp;amp; systemd unit
13:52:03 | feat(cli): compile native Rust CLI with STT client
13:55:11 | fix(injection): Hyprland signature fallback &amp;amp; wtype virtual typing
14:14:19 | feat(agent): Agent Command Mode with Herdr &amp;amp; LM Studio dispatch
14:37:13 | feat(ptt): Push-to-Talk Hold-and-Release with tap-filtering
15:13:51 | fix(dictation): route dictation via Wayland virtual keyboard
15:43:07 | feat(audio): automatic PipeWire audio ducking during dictation
15:54:09 | feat(tts): Phase 3 zero-shot cloned voice synthesis daemon
15:58:30 | fix(audio): duck per-stream media players (keep voice at 100%)
16:02:42 | feat(tts): vocal mastering with soft-knee compression &amp;amp; +10dB
16:11:16 | perf(tts): pipelined streaming, x-vector fast mode &amp;amp; audio earcons
16:13:46 | feat(agent): inject live date/time into system prompt
19:14:04 | feat(dict): custom dictionary and phonetic jargon reinforcement
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Two hours and twenty-seven minutes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In that window, we went from an empty directory to:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Push-to-Talk Terminal Dictation (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Super + D&lt;/code&gt;)&lt;/strong&gt;: Captures audio with micro-tap filtering, ducks background media streams to 50% via PipeWire (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wpctl&lt;/code&gt;), feeds a warm &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;faster-whisper&lt;/code&gt; CUDA daemon (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/tmp/spud-stt.sock&lt;/code&gt;), and types into the focused window using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wtype&lt;/code&gt; (Wayland virtual keyboard) at sub-200ms latency.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Jargon Reinforcement (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/dict.rs&lt;/code&gt;)&lt;/strong&gt;: Pre-conditions Whisper’s vocabulary with terms like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shpadoinkle-1&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Hyprland&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Herdr&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wtype&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PipeWire&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl&lt;/code&gt;, coupled with deterministic phonetic replacement rules so technical jargon is never misheard.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Agent Mode &amp;amp; Cloned Voice Egress (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Super + Shift + D&lt;/code&gt;)&lt;/strong&gt;: Dispatches commands to local agent panes, queries local LLMs, and synthesizes verbal responses through a warm &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Qwen3-TTS&lt;/code&gt; daemon on CUDA using cached speaker embeddings and soft-knee audio compression.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Self-Improving Desktop Skills&lt;/strong&gt;: Antigravity skills (like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;add-shortcut&lt;/code&gt;) allow the agent to inspect Hyprland configurations, validate keybindings, and trigger live &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hyprctl&lt;/code&gt; reloads without touching a config file by hand.&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;5-the-maths&quot;&gt;5. The Maths&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Metric&lt;/th&gt;
      &lt;th&gt;The Biological Relay&lt;/th&gt;
      &lt;th&gt;Spud + Gemini (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agy&lt;/code&gt;)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Dictation Latency&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;3–6s (cloud API / batch STT)&lt;/td&gt;
      &lt;td&gt;&amp;lt;200ms (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;faster-whisper&lt;/code&gt; on CUDA)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Audio Ducking&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Manual keyboard faffing&lt;/td&gt;
      &lt;td&gt;Automatic PipeWire stream attenuation (50%)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Cursor Typing&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Clipboard copy-pasting&lt;/td&gt;
      &lt;td&gt;Native Wayland &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wtype&lt;/code&gt; at focused cursor&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Tech Jargon Accuracy&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Fails on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wtype&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Deterministic phonetic dictionary&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Wayland &amp;amp; Hyprland Fit&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Clunky wrappers, focus loss, XWayland hacks&lt;/td&gt;
      &lt;td&gt;Zero-overhead native Wayland daemon (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wtype&lt;/code&gt;)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Agent Egress&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Reading terminal blocks&lt;/td&gt;
      &lt;td&gt;Zero-shot cloned voice TTS audio debrief&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Pairing Quota&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Daily exhaustion anxiety&lt;/td&gt;
      &lt;td&gt;Endless practical quota, zero interruptions&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Initial Implementation&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;2–3 weeks of weekend hacking&lt;/td&gt;
      &lt;td&gt;2 hours 27 minutes&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-receipt&quot;&gt;The Receipt&lt;/h2&gt;

&lt;p&gt;The biological relay is retired.&lt;/p&gt;

&lt;p&gt;Give your terminal a voice. Let your music duck itself. Let your desktop configure its own shortcuts.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Clone and build Spud&lt;/span&gt;
git clone git@github.com:nihilok/spud.git ~/Code/spud
&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ~/Code/spud &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; cargo build &lt;span class=&quot;nt&quot;&gt;--release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/nihilok/spud&quot;&gt;Spud on GitHub&lt;/a&gt;&lt;/p&gt;
</description>
        <pubDate>Wed, 23 Sep 2026 00:00:00 +0000</pubDate>
        <link>https://blog.jarv.dev//spud-and-the-hyprland-paradise</link>
        <link href="https://blog.jarv.dev/spud-and-the-hyprland-paradise"/>
        <guid isPermaLink="true">https://blog.jarv.dev/spud-and-the-hyprland-paradise</guid>
      </item>
    
      <item>
        <title>The Discovery Tax</title>
        <description>&lt;p&gt;Every Claude Code session starts the same way. You say “run the tests.” The agent says “I’ll look for how to run tests in this project.”&lt;/p&gt;

&lt;p&gt;Then it reads your README. Then your Makefile. Then your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt;. Then it tries &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;make test&lt;/code&gt;. Wrong. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm test&lt;/code&gt;. Wrong project. It &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cat&lt;/code&gt;s the Makefile again, picks a different target, runs it with the wrong arguments. Eventually it gets there.&lt;/p&gt;

&lt;p&gt;Six tool calls. 2,500 tokens of context. Gone — before any real work begins.&lt;/p&gt;

&lt;p&gt;That’s the discovery tax. You pay it every session. The agent forgets everything and starts over. Every time.&lt;/p&gt;

&lt;h2 id=&quot;what-it-actually-costs&quot;&gt;What it actually costs&lt;/h2&gt;

&lt;p&gt;It’s not just the wasted tokens. It’s the compound effect:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Context pressure.&lt;/strong&gt; On long sessions you’re already fighting the context window. Burning 2,500 tokens on discovery means you hit compression sooner.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Confidence degradation.&lt;/strong&gt; Each failed attempt leaves error messages in the context. The agent second-guesses itself. It starts hedging. “Let me try another approach…” You’ve seen this — the agent starts confidently, hits two errors, and suddenly every response begins with “I apologise for the confusion.”&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Your time.&lt;/strong&gt; You’re sitting there watching it fumble through files you could have pointed it at in seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;the-refund&quot;&gt;The refund&lt;/h2&gt;

&lt;p&gt;What if the agent already knew your project’s tools before it started?&lt;/p&gt;

&lt;p&gt;That’s what &lt;a href=&quot;https://modelcontextprotocol.io&quot;&gt;MCP&lt;/a&gt; is for. Instead of discovering tools by reading files, the agent gets a structured registry: tool names, descriptions, typed parameters, defaults. No reading. No guessing. No retries.&lt;/p&gt;

&lt;p&gt;I built a task runner called &lt;a href=&quot;https://runtool.dev&quot;&gt;run&lt;/a&gt; with a built-in MCP server. You define your tasks in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Runfile&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# @desc Run the test suite&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# @arg filter Optional test name filter&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;filter &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    cargo &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--workspace&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--no-fail-fast&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$filter&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# @desc Deploy to an environment&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# @arg env Target environment (staging|prod)&lt;/span&gt;
deploy&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; ./scripts/deploy.sh &lt;span class=&quot;nv&quot;&gt;$env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Add it to your Claude Code config:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;mcpServers&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;runtool&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;run&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;args&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;--serve-mcp&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now the same interaction looks like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;You:    run the tests
Agent:  [tool] mcp:runtool test → success
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;One tool call. ~100 tokens. Same result next session.&lt;/p&gt;

&lt;h2 id=&quot;the-maths&quot;&gt;The maths&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt;Without MCP&lt;/th&gt;
      &lt;th&gt;With MCP&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Tool calls to run tests&lt;/td&gt;
      &lt;td&gt;~6&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Context consumed&lt;/td&gt;
      &lt;td&gt;~2,500 tokens&lt;/td&gt;
      &lt;td&gt;~100 tokens&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Next session&lt;/td&gt;
      &lt;td&gt;Starts from scratch&lt;/td&gt;
      &lt;td&gt;Same registry&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Reliability&lt;/td&gt;
      &lt;td&gt;Varies by session&lt;/td&gt;
      &lt;td&gt;Deterministic&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Over a day of coding sessions, that’s thousands of tokens and dozens of tool calls you’re not wasting on rediscovery.&lt;/p&gt;

&lt;p&gt;The discovery tax is zero.&lt;/p&gt;

&lt;p&gt;For the full picture — auto-truncation, security sandboxing, the deterministic skills layer — see &lt;a href=&quot;/deterministic-toolbox-for-claude-code&quot;&gt;the deep dive&lt;/a&gt;.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;nihilok/tap/runtool
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/nihilok/run&quot;&gt;GitHub&lt;/a&gt; · &lt;a href=&quot;https://runtool.dev/docs&quot;&gt;Docs&lt;/a&gt;&lt;/p&gt;
</description>
        <pubDate>Sat, 19 Sep 2026 00:00:00 +0000</pubDate>
        <link>https://blog.jarv.dev//the-discovery-tax</link>
        <link href="https://blog.jarv.dev/the-discovery-tax"/>
        <guid isPermaLink="true">https://blog.jarv.dev/the-discovery-tax</guid>
      </item>
    
      <item>
        <title>Your Agent Doesn&apos;t Need a README</title>
        <description>&lt;p&gt;Your AI agent is reading your README right now. It’s on paragraph three of the “Getting Started” section, trying to figure out whether &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm run dev&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;make dev&lt;/code&gt; is the current way to start the project. It will get there eventually. That’s the problem.&lt;/p&gt;

&lt;p&gt;We write READMEs for humans. Structured headings, prose explanations, code blocks with context. A human reads “To run the tests, use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cargo test --workspace&lt;/code&gt;” and knows what to do. But an AI agent reads the same sentence and has to:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Find the README&lt;/li&gt;
  &lt;li&gt;Parse the natural language&lt;/li&gt;
  &lt;li&gt;Extract the command from the surrounding prose&lt;/li&gt;
  &lt;li&gt;Hope the README is up to date&lt;/li&gt;
  &lt;li&gt;Hope there aren’t flags mentioned three paragraphs later that are also required&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It works. Eventually. But it’s the wrong interface for the job.&lt;/p&gt;

&lt;h2 id=&quot;structured-beats-unstructured&quot;&gt;Structured beats unstructured&lt;/h2&gt;

&lt;p&gt;READMEs are documentation. They’re great at explaining &lt;em&gt;why&lt;/em&gt;. They’re terrible at telling a machine &lt;em&gt;what&lt;/em&gt;, because the machine has to do natural language parsing on a document designed for humans, and extract structured information from an unstructured format.&lt;/p&gt;

&lt;p&gt;What agents actually need is a schema: a tool name, a description, typed parameters, and defaults. That’s what &lt;a href=&quot;https://modelcontextprotocol.io&quot;&gt;MCP&lt;/a&gt; provides.&lt;/p&gt;

&lt;p&gt;A &lt;a href=&quot;https://runtool.dev/docs&quot;&gt;Runfile&lt;/a&gt; gives your project’s tools exactly that schema:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# @desc Run the test suite&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# @arg filter Optional test name filter&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;filter &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    cargo &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--workspace&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--no-fail-fast&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$filter&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The agent doesn’t read this file. It receives a structured tool definition over MCP: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt; is a tool, it takes an optional string parameter called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter&lt;/code&gt;, and here’s what it does. No parsing. No ambiguity.&lt;/p&gt;

&lt;h2 id=&quot;what-the-agent-sees&quot;&gt;What the agent sees&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;From a README:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A blob of Markdown. It has to figure out which parts are commands, which parts are commentary, and which parts are outdated. It might get it right. It might try the example from the “Legacy Setup” section that nobody deleted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From an MCP tool registry:&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Run the test suite&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;parameters&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Optional test name filter&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;No ambiguity. No staleness risk. The schema &lt;em&gt;is&lt;/em&gt; the interface.&lt;/p&gt;

&lt;h2 id=&quot;what-the-agent-doesnt-see&quot;&gt;What the agent doesn’t see&lt;/h2&gt;

&lt;p&gt;Here’s the other thing: when the agent reads your README to discover commands, it also reads everything else in there. Internal URLs, architecture decisions, deployment details, service names. All of it goes into the context window.&lt;/p&gt;

&lt;p&gt;An MCP tool registry exposes &lt;em&gt;only&lt;/em&gt; the tool interface: name, description, parameters. The implementation stays behind the wall. Your agent gets powerful, well-defined tools without getting a map of your internals.&lt;/p&gt;

&lt;h2 id=&quot;deterministic-beats-probabilistic&quot;&gt;Deterministic beats probabilistic&lt;/h2&gt;

&lt;p&gt;There’s a deeper point here. When an agent reads a README and extracts a command, the result is probabilistic. It &lt;em&gt;probably&lt;/em&gt; gets the right command. It &lt;em&gt;probably&lt;/em&gt; passes the right flags. But “probably” compounds badly across a session. Each probably-correct step increases the chance that one of them isn’t.&lt;/p&gt;

&lt;p&gt;A Runfile is deterministic. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run test&lt;/code&gt; runs the test suite, with the right flags, every time. The agent doesn’t interpret instructions, it calls a tool. There’s no gap between what you intended and what executes.&lt;/p&gt;

&lt;p&gt;This matters even more when you combine it with Claude Code’s &lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/skills&quot;&gt;skills&lt;/a&gt;. You can write a skill that says “before committing, always run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ci&lt;/code&gt;” — and because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ci&lt;/code&gt; is a deterministic MCP tool, not a natural language instruction, the behaviour is predictable and auditable. Skills tell the agent &lt;em&gt;when&lt;/em&gt; to act. The Runfile tells it &lt;em&gt;what&lt;/em&gt; to do. Keeping those concerns separate makes both more reliable.&lt;/p&gt;

&lt;h2 id=&quot;the-readme-is-still-useful&quot;&gt;The README is still useful&lt;/h2&gt;

&lt;p&gt;This isn’t an argument against READMEs. Write them for your human teammates. Explain the &lt;em&gt;why&lt;/em&gt;, the architecture, the gotchas.&lt;/p&gt;

&lt;p&gt;You can even document your Runfile commands in the README. They’re just as useful on the command line as they are via MCP: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run test&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run deploy staging&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run ci&lt;/code&gt;. The README explains when and why to use them. The Runfile is the executable source of truth.&lt;/p&gt;

&lt;p&gt;But stop expecting your agent to use the README as an API. Give it a real one.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;nihilok/tap/runtool
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/nihilok/run&quot;&gt;GitHub&lt;/a&gt; · &lt;a href=&quot;https://runtool.dev/docs&quot;&gt;Docs&lt;/a&gt;&lt;/p&gt;
</description>
        <pubDate>Sun, 08 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://blog.jarv.dev//your-agent-doesn-t-need-a-readme</link>
        <link href="https://blog.jarv.dev/your-agent-doesn-t-need-a-readme"/>
        <guid isPermaLink="true">https://blog.jarv.dev/your-agent-doesn-t-need-a-readme</guid>
      </item>
    
      <item>
        <title>How I Gave Claude Code a Deterministic Toolbox (and Stopped It Guessing How to Run My Project)</title>
        <description>&lt;p&gt;If you’ve spent any real time pairing with Claude Code, you’ve seen the dance.&lt;/p&gt;

&lt;p&gt;You ask it to run the tests. It reads your README. Maybe it finds a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Makefile&lt;/code&gt;, maybe a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt;, maybe a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scripts/&lt;/code&gt; directory. It tries &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;make test&lt;/code&gt;. That fails. It tries &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm test&lt;/code&gt;. Wrong project. It &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cat&lt;/code&gt;s your Makefile, parses the targets, picks one, runs it with the wrong arguments. Three tool calls and 2,000 tokens later, it’s running the right command — until next session, when it’s forgotten everything and does the dance again.&lt;/p&gt;

&lt;p&gt;This is the discovery tax. Every Claude Code session pays it, and it compounds: each failed attempt burns context window, each retry eats tokens, and the agent’s confidence degrades as it accumulates error messages. On a long session where you’re already bumping up against context limits, those wasted tokens matter.&lt;/p&gt;

&lt;p&gt;I built a tool called &lt;a href=&quot;https://runtool.dev&quot;&gt;run&lt;/a&gt; that fixes this. Here’s how.&lt;/p&gt;

&lt;h2 id=&quot;the-problem-isnt-intelligence-its-discovery&quot;&gt;The problem isn’t intelligence, it’s discovery&lt;/h2&gt;

&lt;p&gt;Claude Code is remarkably good at executing tasks once it knows what to do. The bottleneck is the gap between “I need to run the tests” and “the command is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cargo test --workspace --no-fail-fast&lt;/code&gt;”. Bridging that gap currently requires the agent to:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Read files&lt;/strong&gt; to discover what’s available (README, Makefile, package.json, scripts/)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Infer intent&lt;/strong&gt; from naming conventions and comments&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Guess at arguments&lt;/strong&gt; — is it &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;make deploy ENV=staging&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;make deploy-staging&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;./scripts/deploy.sh staging&lt;/code&gt;?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Fail and retry&lt;/strong&gt; when the guess is wrong&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Forget everything&lt;/strong&gt; next session and repeat from step 1&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of these steps consumes tool calls and context. And because there’s no structured metadata — just files full of text — the agent is doing natural language parsing of shell scripts, which is exactly the kind of task where LLMs are most likely to make subtle mistakes.&lt;/p&gt;

&lt;h2 id=&quot;what-if-the-agent-already-knew&quot;&gt;What if the agent already knew?&lt;/h2&gt;

&lt;p&gt;MCP (Model Context Protocol) lets you expose tools to AI agents with structured schemas — name, description, typed parameters, defaults. The agent doesn’t discover tools by reading files; it discovers them through a protocol designed for exactly this purpose.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://runtool.dev&quot;&gt;RunTool&lt;/a&gt; is a task runner with a built-in MCP server. You define your project’s tasks in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Runfile&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# @desc Run the full test suite
# @arg filter Optional test name filter
test(filter = &quot;&quot;) {
    cargo test --workspace --no-fail-fast $filter
}

# @desc Deploy to the specified environment
# @arg environment Target environment (staging|prod)
deploy(environment) {
    ./scripts/deploy.sh $environment
}

# @desc Resize an image using Python
# @arg file Path to the image file
# @arg width Target width in pixels
# @arg height Target height in pixels
resize(file, width: int, height: int) {
    #!/usr/bin/env python
    from PIL import Image
    img = Image.open(file)
    img.resize((width, height)).save(file)
    print(f&quot;Resized to {width}x{height}&quot;)
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run&lt;/code&gt; as an MCP server in your Claude Code config:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;mcpServers&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;runtool&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;run&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;args&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;--serve-mcp&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now when Claude Code starts a session, it immediately sees a tool registry: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt; takes an optional string &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter&lt;/code&gt;; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deploy&lt;/code&gt; takes a required string &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;environment&lt;/code&gt;; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resize&lt;/code&gt; takes a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;file&lt;/code&gt; string plus &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;width&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;height&lt;/code&gt; integers. No file reading. No guessing. No retry loops. Zero discovery tax.&lt;/p&gt;

&lt;h2 id=&quot;before-and-after&quot;&gt;Before and after&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before (no MCP, typical Claude Code session):&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Human: run the tests

Claude Code: I&apos;ll look for how to run tests in this project.
  [tool] cat README.md                           → 800 tokens
  [tool] ls scripts/                              → 50 tokens
  [tool] cat Makefile                             → 600 tokens
  [tool] make test                                → fails (wrong target name)
  [tool] make tests                               → fails (missing argument)
  [tool] make tests FILTER=&quot;&quot;                     → success

  Total: 6 tool calls, ~2,500 tokens of context consumed
  Next session: starts from scratch
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;After (RunTool MCP):&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Human: run the tests

Claude Code: I&apos;ll run the test suite.
  [tool] mcp:runtool test                          → success

  Total: 1 tool call, ~100 tokens of context consumed
  Next session: same tool registry, same result
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s not a marginal improvement. It’s the difference between an agent that fumbles through your project and one that operates like a team member who already knows the codebase.&lt;/p&gt;

&lt;h2 id=&quot;auto-truncation-solving-the-output-problem&quot;&gt;Auto-truncation: solving the output problem&lt;/h2&gt;

&lt;p&gt;There’s a second problem that anyone using MCP tools with Claude Code or Codex has run into: output truncation. Claude Code and Codex both impose limits on tool output — and when your test suite dumps 500 lines of results, the agent gets a chopped-up view with the middle missing, which is often exactly where the useful information is.&lt;/p&gt;

&lt;p&gt;RunTool handles this at the MCP server level. By default, tool output is capped at approximately 300 tokens (~1KB) — enough for the agent to see whether something passed or failed and get the key details, without blowing up the context window. Critically, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run&lt;/code&gt; keeps the &lt;em&gt;tail&lt;/em&gt; of the output, not the head. That’s where the useful information almost always is: the test results summary, the final error message, the exit status. When output exceeds the limit, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run&lt;/code&gt; truncates from the top and saves the full output to a file. The agent gets the actionable ending plus a file path it can read selectively if it needs the full details. The threshold is configurable in the environment, so you can tune it up or down to match your context budget.&lt;/p&gt;

&lt;p&gt;Compare this to what happens without it: Claude Code and Codex both impose their own truncation on tool output — Codex chops at 256 lines or 10KB using a head+tail strategy that drops the middle, which is often exactly where the useful information is. RunTool’s approach is smarter because it happens at the source, before the agent’s own limits kick in, it prioritises the part of the output you actually care about, and the full output is always recoverable.&lt;/p&gt;

&lt;h2 id=&quot;the-security-argument-sandboxing-through-metadata&quot;&gt;The security argument: sandboxing through metadata&lt;/h2&gt;

&lt;p&gt;Here’s something that’s easy to overlook but has real implications for how you think about agent access.&lt;/p&gt;

&lt;p&gt;When Claude Code reads your Makefile or scripts to discover tasks, it sees &lt;em&gt;everything&lt;/em&gt;: implementation details, file paths, secrets files being sourced, internal service URLs, database connection strings referenced in scripts. All of that goes into the context window.&lt;/p&gt;

&lt;p&gt;RunTool’s MCP server exposes &lt;em&gt;only&lt;/em&gt; the annotations — function name, description, argument signatures. The implementation stays hidden. The agent knows that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deploy&lt;/code&gt; takes an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;environment&lt;/code&gt; argument of type string; it doesn’t know that internally it sources &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.secrets/prod.env&lt;/code&gt; and shells out to an internal deployment service at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deploy.internal.corp:8443&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is sandboxing through metadata. You’re giving the agent powerful, well-defined tools without giving it a map of your internals. If the agent wants to read the Runfile itself it can (it’s just a file), but that’s an explicit read operation that you could restrict — not something the MCP server hands over automatically.&lt;/p&gt;

&lt;p&gt;For teams that are cautious about what their AI agents can see — and you should be — this is a meaningful property.&lt;/p&gt;

&lt;h2 id=&quot;the-deterministic-layer-encoding-skills-in-your-runfile&quot;&gt;The deterministic layer: encoding skills in your Runfile&lt;/h2&gt;

&lt;p&gt;Claude Code has a concept of “skills” — instructions stored in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/&lt;/code&gt; that tell the agent how to handle specific tasks. Skills are useful, but they’re prompts: natural language instructions that the agent interprets probabilistically. They can drift, be misinterpreted, or interact unpredictably with other context.&lt;/p&gt;

&lt;p&gt;A Runfile is a deterministic skill layer. When you define:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# @desc Run linting with auto-fix
lint() cargo clippy --fix --allow-dirty

# @desc Format all source files
fmt() cargo fmt --all

# @desc Run the full CI pipeline locally
ci() {
    run lint
    run fmt
    run test
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;There’s no interpretation involved. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ci&lt;/code&gt; runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lint&lt;/code&gt;, then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fmt&lt;/code&gt;, then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt;, in that order, every time. The agent doesn’t need to figure out your CI pipeline from scattered config files — it has a single, composable tool that does exactly what you’ve defined.&lt;/p&gt;

&lt;p&gt;This is particularly powerful when combined with skills. You can write a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/&lt;/code&gt; skill that says “before committing, always run the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ci&lt;/code&gt; tool” — and because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ci&lt;/code&gt; is a deterministic MCP tool rather than a set of natural language instructions, the behaviour is predictable and auditable.&lt;/p&gt;

&lt;p&gt;Skills tell the agent &lt;em&gt;when&lt;/em&gt; to act. The Runfile tells it &lt;em&gt;what&lt;/em&gt; to do. Keeping those concerns separate makes both more reliable.&lt;/p&gt;

&lt;h2 id=&quot;getting-started&quot;&gt;Getting started&lt;/h2&gt;

&lt;p&gt;Install &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;nihilok/tap/runtool
&lt;span class=&quot;c&quot;&gt;# or&lt;/span&gt;
cargo &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;run
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Create a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Runfile&lt;/code&gt; in your project root with your common tasks. Add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@desc&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@arg&lt;/code&gt; annotations for anything you want the agent to discover. Add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run&lt;/code&gt; as an MCP server in your Claude Code config.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://runtool.dev/docs&quot;&gt;documentation&lt;/a&gt; covers the full syntax, including polyglot scripting (Python, Node, Ruby, PowerShell in the same file), cross-platform &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@os&lt;/code&gt; variants, and command composition.&lt;/p&gt;

&lt;p&gt;The code is on &lt;a href=&quot;https://github.com/nihilok/run&quot;&gt;GitHub&lt;/a&gt;. If you’re working with Claude Code daily and you’re tired of watching it guess how to run your project, give it a try.&lt;/p&gt;
</description>
        <pubDate>Sat, 07 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://blog.jarv.dev//deterministic-toolbox-for-claude-code</link>
        <link href="https://blog.jarv.dev/deterministic-toolbox-for-claude-code"/>
        <guid isPermaLink="true">https://blog.jarv.dev/deterministic-toolbox-for-claude-code</guid>
      </item>
    
      <item>
        <title>Integration Test Isolation in a Next.js App with Drizzle ORM</title>
        <description>&lt;p&gt;&lt;img src=&quot;./assets/testing-strategies.png&quot; alt=&quot;test-isolation&quot; style=&quot;width: 100%;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;1-introduction-the-complexity-of-modern-integration-testing&quot;&gt;1. Introduction: The Complexity of Modern Integration Testing&lt;/h2&gt;

&lt;p&gt;The evolution of web application architecture has precipitously increased the complexity of ensuring software quality. In the era of the monolithic, server-rendered application—typified by frameworks such as Ruby on Rails or early ASP.NET—testing strategies were relatively straightforward. The application ran in a single process, connected to a single relational database, and served HTML directly. Integration testing in such an environment was a solved problem: wrap the test in a database transaction, execute the logic, and roll back the transaction at the end of the test. This approach, often referred to as the “transactional rollback” strategy, provided a clean slate for every test case, ensuring isolation and determinism with minimal overhead.&lt;/p&gt;

&lt;p&gt;However, the modern full-stack landscape, dominated by React meta-frameworks like Next.js and diverse data-access layers such as Drizzle ORM, has fundamentally altered this equation. The shift towards serverless compute models, the introduction of React Server Components (RSC), and the decoupling of the frontend from the backend logic via mechanisms like Server Actions have introduced new boundaries and constraints. In this environment, the database is no longer just a passive store of state; it is a shared mutable resource accessed by highly concurrent, asynchronous processes that may not share the same memory space or execution context as the test runner.&lt;/p&gt;

&lt;p&gt;This report provides an exhaustive analysis of the strategies available for achieving rigorous integration test isolation in a Next.js application using Drizzle ORM. It explores the theoretical underpinnings of database isolation, the specific architectural challenges posed by the Next.js App Router, and the practical implementation of four distinct testing methodologies: Transactional Rollbacks, Database Truncation, Containerised Isolation (Testcontainers), and In-Memory WebAssembly Databases (PGlite). By examining the mechanisms, performance characteristics, and trade-offs of each approach, this document aims to equip engineering teams with the knowledge required to architect robust, scalable, and non-flaky test suites for mission-critical applications.&lt;/p&gt;

&lt;h3 id=&quot;11-the-imperative-of-isolation&quot;&gt;1.1 The Imperative of Isolation&lt;/h3&gt;

&lt;p&gt;Integration testing sits at the precarious intersection of the testing pyramid. Unlike unit tests, which verify discrete logic in isolation by mocking external dependencies, integration tests must validate the interaction between the application code and its infrastructure—specifically, the database. The fidelity of these tests is paramount. If a test mocks the database driver, it ceases to be an integration test; it merely tests the developer’s assumptions about how the database should behave, rather than how it actually behaves.&lt;/p&gt;

&lt;p&gt;The central challenge in integration testing is &lt;strong&gt;Shared Mutable State&lt;/strong&gt;. When a test suite runs, multiple test cases execute against the database. If these tests are not perfectly isolated, they interfere with one another, leading to a class of failures known as “flaky tests.” Flakiness manifests in several ways:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Data Pollution&lt;/strong&gt;: One test inserts a record (e.g., a user with email &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test@example.com&lt;/code&gt;) that persists after the test finishes. A subsequent test, expecting an empty user table or attempting to create a user with the same unique email, fails.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Phantom Reads&lt;/strong&gt;: In a concurrent test environment, a query in Test A might inadvertently retrieve data inserted by Test B, leading to assertion failures that are impossible to reproduce when running the test in isolation.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Race Conditions&lt;/strong&gt;: Tests that modify global configuration or shared singleton resources can create non-deterministic outcomes based on the precise timing of execution by the CPU scheduler.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To mitigate these risks, the testing environment must guarantee that every test runs in a pristine environment. The database state at the beginning of a test must be known, deterministic, and unaffected by any previous or concurrent test execution.&lt;/p&gt;

&lt;h3 id=&quot;12-the-nextjs-effect-architectural-constraints&quot;&gt;1.2 The “Next.js Effect”: Architectural Constraints&lt;/h3&gt;

&lt;p&gt;Next.js, particularly with its App Router architecture introduced in version 13, imposes specific constraints that complicate traditional isolation strategies.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Server Actions as Remote Procedure Calls (RPC)&lt;/strong&gt;: Server Actions in Next.js allows functions to be executed on the server, triggered directly from client-side components. From a testing perspective, these are not simple function calls; they are asynchronous operations that often run in a separate context. Testing them requires an environment that can simulate the Next.js server runtime, including access to environment variables, headers, and cookies.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;The Singleton Pattern &amp;amp; Module Caching&lt;/strong&gt;: In a typical Next.js application, the database client (Drizzle instance) is instantiated as a global singleton. This is necessary to prevent connection exhaustion during development (hot reloading) and to manage connection pooling in production. However, this global singleton makes Dependency Injection (DI)—a primary technique for test isolation—significantly more difficult. If a Server Action imports the database client directly from a static module path (e.g., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import { db } from &apos;@/db&apos;&lt;/code&gt;), the test runner must intervene at the module loading level to inject a test-specific database instance.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Server-Only Boundaries&lt;/strong&gt;: Next.js enforces strict boundaries between server and client code using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;server-only&lt;/code&gt; package. Integration tests that import server-side logic must execute in a Node.js-compatible environment (like vitest with a node environment) rather than a browser-like environment (like jsdom), or they will trigger build-time errors. This constrains the choice of test runners and mocking strategies.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;13-drizzle-orm-a-typescript-first-approach&quot;&gt;1.3 Drizzle ORM: A TypeScript-First Approach&lt;/h3&gt;

&lt;p&gt;Drizzle ORM represents a modern approach to database interaction, favoring type safety and SQL-like syntax over the heavy abstraction layers of traditional ORMs. It separates the query builder (the TypeScript API) from the driver (the mechanism that talks to the database). This separation is crucial for testing because it allows the underlying driver to be swapped—for example, replacing a network-based Postgres client with an in-memory PGlite client—without changing the application logic. However, Drizzle’s reliance on specific driver features (like prepared statements or specific transaction APIs) means that the test environment must closely mirror the production environment’s capabilities to avoid false positives.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;2-theoretical-framework-acid-properties-and-test-concurrency&quot;&gt;2. Theoretical Framework: ACID Properties and Test Concurrency&lt;/h2&gt;

&lt;p&gt;To understand why integration test isolation is difficult, one must delve into the fundamental properties of relational databases: ACID (Atomicity, Consistency, Isolation, Durability). Integration testing strategies essentially manipulate these properties to achieve their goals.&lt;/p&gt;

&lt;h3 id=&quot;21-atomicity-and-test-boundaries&quot;&gt;2.1 Atomicity and Test Boundaries&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Atomicity&lt;/strong&gt; guarantees that a transaction is treated as a single “unit of work,” which either completely succeeds or completely fails. The Transactional Rollback strategy leverages this property. By wrapping a test case in a transaction, the test runner ensures that all database writes are provisional. When the test concludes, the runner issues a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ROLLBACK&lt;/code&gt; command, which atomically undoes all changes. This relies on the database’s Write-Ahead Log (WAL) to revert the state efficiently, without the overhead of deleting records from the disk.&lt;/p&gt;

&lt;p&gt;However, Atomicity interacts complexly with modern application logic. If the application code under test itself uses transactions (e.g., a signup flow that creates a user and an organisation in a single transaction), the test runner must support &lt;strong&gt;Nested Transactions&lt;/strong&gt;. In PostgreSQL, true nested transactions do not exist; they are simulated using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SAVEPOINT&lt;/code&gt;. Drizzle ORM supports this via its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tx&lt;/code&gt; API, allowing a test to start a transaction (Level 1) and the application to start a nested transaction (Level 2). A rollback of Level 2 reverts to the savepoint, while a rollback of Level 1 reverts everything.&lt;/p&gt;

&lt;h3 id=&quot;22-database-isolation-levels&quot;&gt;2.2 Database Isolation Levels&lt;/h3&gt;

&lt;p&gt;The “I” in ACID refers to &lt;strong&gt;Isolation&lt;/strong&gt;—the degree to which a transaction is protected from the effects of other concurrent transactions. PostgreSQL offers several isolation levels:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Read Committed (Default)&lt;/strong&gt;: A query sees only data committed before the query began.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Repeatable Read&lt;/strong&gt;: A query sees a snapshot of the database as of the start of the transaction.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Serialisable&lt;/strong&gt;: The strictest level, ensuring that the result of concurrent transactions is the same as if they had executed serially.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For integration tests running in parallel against a shared database, Read Committed is often insufficient. If Test A (running in a transaction) inserts a record, and Test B (also in a transaction) runs a query that counts records, Test B generally won’t see Test A’s uncommitted data. However, if Test A commits (which might happen if the application logic forces a commit), Test B’s state is polluted. This creates a “race condition” where tests pass or fail depending on which one finishes first. Achieving true isolation usually requires ensuring that tests running in parallel never share the same database instance, or use strict row-level locking which degrades performance.&lt;/p&gt;

&lt;h3 id=&quot;23-the-throughput-vs-latency-trade-off-in-testing&quot;&gt;2.3 The Throughput vs. Latency Trade-off in Testing&lt;/h3&gt;

&lt;p&gt;Architecting a test suite involves a fundamental trade-off between &lt;strong&gt;Latency&lt;/strong&gt; (how fast a single test runs) and &lt;strong&gt;Throughput&lt;/strong&gt; (how fast the entire suite runs).&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Serial Execution&lt;/strong&gt;: Running tests one by one (latency focused) is the easiest way to ensure isolation. You can use a single database and truncate it between tests. However, as the suite grows to thousands of tests, the total execution time (throughput) becomes unacceptable, leading to slow CI pipelines.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Parallel Execution&lt;/strong&gt;: Running tests concurrently (throughput focused) drastically reduces total time but requires advanced isolation strategies. Each parallel worker needs its own isolated environment (e.g., its own database schema or container) to prevent collisions. This increases the resource cost (CPU/RAM) of the test runner.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following sections analyze four strategies that navigate this trade-off space differently.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;3-strategy-i-transactional-rollbacks-the-rails-pattern&quot;&gt;3. Strategy I: Transactional Rollbacks (The “Rails” Pattern)&lt;/h2&gt;

&lt;p&gt;The Transactional Rollback strategy is often considered the “gold standard” in frameworks like Ruby on Rails and Django. It promises the best of both worlds: speed (no need to recreate schemas) and isolation (changes are never committed).&lt;/p&gt;

&lt;h3 id=&quot;31-mechanism-of-action&quot;&gt;3.1 Mechanism of Action&lt;/h3&gt;

&lt;p&gt;In this model, the test runner performs the following sequence for each test case:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Connect&lt;/strong&gt;: Establish a connection to the database.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Begin&lt;/strong&gt;: Execute &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BEGIN&lt;/code&gt; to start a transaction.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Run&lt;/strong&gt;: Execute the test logic. The application performs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INSERT&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UPDATE&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELETE&lt;/code&gt; operations. These changes are visible to the connection but not to the outside world.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Rollback&lt;/strong&gt;: Execute &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ROLLBACK&lt;/code&gt; in the teardown or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;afterEach&lt;/code&gt; hook. The database discards the changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach is highly efficient because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ROLLBACK&lt;/code&gt; is an inexpensive operation for the database engine compared to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TRUNCATE&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELETE&lt;/code&gt;, which require scanning tables and updating indexes.&lt;/p&gt;

&lt;h3 id=&quot;32-implementation-challenges-in-nextjs&quot;&gt;3.2 Implementation Challenges in Next.js&lt;/h3&gt;

&lt;p&gt;While conceptually simple, implementing this in a Next.js/Drizzle application is fraught with architectural difficulties, primarily due to the &lt;strong&gt;Connection Context Problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In a typical Next.js app, the database client is a global singleton imported by Server Actions:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// src/lib/db.ts&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;drizzle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;postgres&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;DATABASE_URL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When a test runs, it typically initiates a transaction on its own connection instance. However, when the test calls a Server Action, that action imports the global &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;db&lt;/code&gt; instance, which usually maintains its own connection pool. Consequently, the Server Action executes its queries outside the test’s transaction. The changes are committed to the database, polluting the state for subsequent tests and rendering the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ROLLBACK&lt;/code&gt; ineffective.&lt;/p&gt;

&lt;h3 id=&quot;33-the-proxy--asynclocalstorage-solution&quot;&gt;3.3 The Proxy &amp;amp; AsyncLocalStorage Solution&lt;/h3&gt;

&lt;p&gt;To make Transactional Rollbacks work in Node.js/Next.js, one must force the application code to use the test’s transaction connection instead of the global pool. This requires a form of “Context Propagation.”&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AsyncLocalStorage&lt;/code&gt; (ALS) from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node:async_hooks&lt;/code&gt; module provides a mechanism to store data that is unique to the current asynchronous execution context (similar to Thread-Local Storage in multi-threaded languages).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation Architecture:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Wrapper&lt;/strong&gt;: Create a database accessor that checks the ALS store.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AsyncLocalStorage&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;node:async_hooks&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;txStorage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AsyncLocalStorage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getDb&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;txStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tx&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tx&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;globalDb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The Test Hook&lt;/strong&gt;: In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;beforeEach&lt;/code&gt;, start a transaction and enter the ALS context.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;example test&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;globalDb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;transaction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;txStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// Inside this block, getDb() returns &apos;tx&apos;&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;myServerAction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; 
      &lt;span class=&quot;nx&quot;&gt;tx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;rollback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Force rollback at end&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Refactoring&lt;/strong&gt;: The application must be refactored to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getDb()&lt;/code&gt; instead of importing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;db&lt;/code&gt; directly, or the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;db&lt;/code&gt; export must be a Proxy object that handles this logic internally.&lt;/p&gt;

&lt;h3 id=&quot;34-pros-and-cons&quot;&gt;3.4 Pros and Cons&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Feature&lt;/th&gt;
      &lt;th&gt;Analysis&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Speed&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;High. Rollbacks are nearly instantaneous. No file I/O or schema rebuilding is required between tests.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Parallelism&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Low/Complex. Since multiple tests effectively share the same underlying database instance (even if wrapped in transactions), running them in parallel requires careful management of connection limits and locking. It essentially forces serial execution within a single database.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Complexity&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;High. Requires invasive changes to the application architecture (using ALS or Proxies) or strict Dependency Injection. Testing “transactional” logic within the app becomes confusing (nesting transactions inside test transactions).&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Fidelity&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Medium. Logic that relies on “after commit” hooks or side effects visible to other connections cannot be tested easily because the transaction is never actually committed.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: While powerful, the Transactional Rollback strategy fights against the grain of the Next.js App Router’s module system. It is best suited for applications that already use heavy Dependency Injection patterns (like NestJS) rather than standard Next.js patterns.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;4-strategy-ii-database-truncation-the-clean-slate-approach&quot;&gt;4. Strategy II: Database Truncation (The “Clean Slate” Approach)&lt;/h2&gt;

&lt;p&gt;The Truncation strategy is the brute-force alternative to rollbacks. Instead of preventing writes from persisting, we allow them to persist and then aggressively clean the database after every test.&lt;/p&gt;

&lt;h3 id=&quot;41-mechanism-of-action&quot;&gt;4.1 Mechanism of Action&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Global Setup&lt;/strong&gt;: A real database is spun up (usually via Docker) and the schema is migrated once at the start of the test suite run.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Test Execution&lt;/strong&gt;: The test runs against this real database. Data is committed.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Teardown (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;afterEach&lt;/code&gt;)&lt;/strong&gt;: A utility function queries the database for all table names and executes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TRUNCATE&lt;/code&gt; commands to wipe all rows.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;42-handling-foreign-keys-and-sequences&quot;&gt;4.2 Handling Foreign Keys and Sequences&lt;/h3&gt;

&lt;p&gt;A naive &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELETE FROM table&lt;/code&gt; is insufficient and slow. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TRUNCATE&lt;/code&gt; is faster as it deallocates data pages. However, relational integrity constraints (Foreign Keys) pose a challenge. You cannot truncate the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;users&lt;/code&gt; table if the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;posts&lt;/code&gt; table references it.&lt;/p&gt;

&lt;p&gt;To solve this, PostgreSQL provides the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CASCADE&lt;/code&gt; option:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;TRUNCATE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;posts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;comments&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;CASCADE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Additionally, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TRUNCATE&lt;/code&gt; does not reset auto-incrementing primary keys (IDENTITY columns) by default. If Test A creates User ID 1, Test B will create User ID 2. If Test B asserts “User ID should be 1”, it will fail. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RESTART IDENTITY&lt;/code&gt; clause is required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optimised Drizzle Implementation:&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sql&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;drizzle-orm&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;resetDatabase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Disable triggers if necessary or use CASCADE&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sql&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`
    SELECT table_name
    FROM information_schema.tables
    WHERE table_schema = &apos;public&apos; AND table_type = &apos;BASE TABLE&apos;
  `&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tables&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Construct a single query to truncate all tables at once&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tableNames&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;`&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;table_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;, &lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  
  &lt;span class=&quot;c1&quot;&gt;// RESTART IDENTITY resets sequences&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// CASCADE handles foreign keys&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sql&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;raw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`TRUNCATE TABLE &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tableNames&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; RESTART IDENTITY CASCADE`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This script is robust because it dynamically discovers tables, ensuring that newly added tables are automatically cleaned up without updating the test helper.&lt;/p&gt;

&lt;h3 id=&quot;43-the-concurrency-bottleneck&quot;&gt;4.3 The Concurrency Bottleneck&lt;/h3&gt;

&lt;p&gt;The fatal flaw of the Truncation strategy is its incompatibility with parallel testing. Since there is only one shared database instance, you cannot run Test A and Test B simultaneously. If Test A truncates the database while Test B is in the middle of an operation, Test B will fail catastrophically.&lt;/p&gt;

&lt;p&gt;This forces the test runner (e.g., Vitest or Jest) to run in Serial Mode (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--runInBand&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxWorkers=1&lt;/code&gt;). As the application grows to hundreds of integration tests, the feedback loop extends from seconds to minutes, severely hampering developer productivity.&lt;/p&gt;

&lt;h3 id=&quot;44-pros-and-cons&quot;&gt;4.4 Pros and Cons&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Feature&lt;/th&gt;
      &lt;th&gt;Analysis&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Speed (Per Test)&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Medium. Truncate is faster than DROP/CREATE but slower than ROLLBACK. The database must physically clean pages on disk.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Speed (Total)&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Low. Forces serial execution. Scalability is linear with the number of tests.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Simplicity&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;High. Very easy to understand and implement. No “magic” proxies or complex context switching. The database behaves exactly as production.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Reliability&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;High. Cleans everything. Less prone to subtle state leaks than complex transaction nesting.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Database Truncation is a viable strategy for smaller projects or CI pipelines where parallelism is not a priority. It is robust and simple but hits a hard ceiling on scalability.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;5-strategy-iii-containerised-isolation-testcontainers&quot;&gt;5. Strategy III: Containerised Isolation (Testcontainers)&lt;/h2&gt;

&lt;p&gt;To solve the concurrency problem of the Truncation strategy without sacrificing the fidelity of a real database, many teams turn to Testcontainers. This library allows the test runner to programmatically spin up disposable Docker containers for dependencies.&lt;/p&gt;

&lt;h3 id=&quot;51-the-architecture-of-ephemeral-infrastructure&quot;&gt;5.1 The Architecture of Ephemeral Infrastructure&lt;/h3&gt;

&lt;p&gt;Testcontainers for Node.js wraps the Docker API. It allows a test to:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Request a PostgreSQL image (e.g., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postgres:16-alpine&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;Start the container on a random, available port.&lt;/li&gt;
  &lt;li&gt;Wait for the database to be ready (using log strategies or health checks).&lt;/li&gt;
  &lt;li&gt;Return the connection string to the application.&lt;/li&gt;
  &lt;li&gt;Destroy the container automatically when the test finishes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The “Ryuk” sidecar container is a special component of Testcontainers that ensures cleanup. It monitors the connection to the test runner; if the runner process dies (even unexpectedly), Ryuk kills the spawned containers, preventing “zombie” processes from consuming system resources.&lt;/p&gt;

&lt;h3 id=&quot;52-implementation-modes&quot;&gt;5.2 Implementation Modes&lt;/h3&gt;

&lt;p&gt;There are two ways to deploy this for Next.js testing:&lt;/p&gt;

&lt;h4 id=&quot;521-singleton-container-with-logical-isolation&quot;&gt;5.2.1 Singleton Container with Logical Isolation&lt;/h4&gt;

&lt;p&gt;Spinning up a Docker container takes time (500ms - 2s). Doing this for every test file is often too slow. A common pattern is to start one container for the entire test suite (global setup). Then, for each test worker, create a unique logical database inside that container.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Setup&lt;/strong&gt;: Container starts at port 5432.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Worker 1&lt;/strong&gt;: Connects, runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CREATE DATABASE test_db_1&lt;/code&gt;, migrates, runs tests.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Worker 2&lt;/strong&gt;: Connects, runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CREATE DATABASE test_db_2&lt;/code&gt;, migrates, runs tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows parallelism. However, it requires complex orchestration code to manage unique database names and ensure migrations are run for every new logical database created.&lt;/p&gt;

&lt;h4 id=&quot;522-container-per-test-file&quot;&gt;5.2.2 Container Per Test File&lt;/h4&gt;

&lt;p&gt;The most isolated approach is to give every test file its own container.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Perfect isolation. No shared state.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Cons&lt;/strong&gt;: Heavy Resource Usage. Running 10 parallel test files means running 10 PostgreSQL containers simultaneously. This requires significant RAM and CPU. On a typical CI agent (with 2 vCPUs and 4GB RAM), this will likely cause crashes or extreme slowness due to context switching.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;53-drizzle-integration&quot;&gt;5.3 Drizzle Integration&lt;/h3&gt;

&lt;p&gt;Connecting Drizzle to a Testcontainer is straightforward since Drizzle accepts any standard connection string.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;PostgreSqlContainer&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@testcontainers/postgresql&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;drizzle&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;drizzle-orm/node-postgres&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// In global setup&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;container&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;PostgreSqlContainer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;postgres:16&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;client&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;connectionString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;container&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getConnectionUri&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;connect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;drizzle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;54-pros-and-cons&quot;&gt;5.4 Pros and Cons&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Feature&lt;/th&gt;
      &lt;th&gt;Analysis&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Fidelity&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Maximum. You are testing against the exact binary version used in production. Extensions (PostGIS, pgvector) work perfectly.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Isolation&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;High. Containers provide process-level isolation.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Low to Medium. Docker startup overhead is the main bottleneck. High resource consumption limits the degree of parallelism possible on standard hardware.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Complex. Requires a Docker daemon availability. This can be challenging in certain CI environments (e.g., Docker-in-Docker setups) or restricted corporate laptops.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Testcontainers is the industry standard for End-to-End (E2E) tests where accuracy is more important than speed. For integration tests that run frequently (e.g., on every file save), it is often too heavy.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;6-strategy-iv-in-memory-webassembly-databases-pglite&quot;&gt;6. Strategy IV: In-Memory WebAssembly Databases (PGlite)&lt;/h2&gt;

&lt;p&gt;The most recent and transformative development in this space is the emergence of PGlite. This technology fundamentally shifts the “Latency vs. Throughput” curve by offering the isolation of containers with the speed of in-memory execution.&lt;/p&gt;

&lt;h3 id=&quot;61-what-is-pglite&quot;&gt;6.1 What is PGlite?&lt;/h3&gt;

&lt;p&gt;PGlite is a build of PostgreSQL compiled to WebAssembly (WASM). Unlike mocks (which fake the database) or SQLite (which is a different database entirely), PGlite runs the actual Postgres query engine code. It executes within the Node.js process memory. It does not require a daemon, a network port, or Docker.&lt;/p&gt;

&lt;p&gt;Because it is just a JavaScript object consuming memory, starting a PGlite instance takes milliseconds (~10-50ms), compared to seconds for a Docker container.&lt;/p&gt;

&lt;h3 id=&quot;62-architecture-for-massive-parallelism&quot;&gt;6.2 Architecture for Massive Parallelism&lt;/h3&gt;

&lt;p&gt;PGlite enables a “Database per Test File” architecture without the resource penalty of Docker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integration with Vitest:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Vitest is a modern test runner that supports multi-threading. By combining Vitest’s parallelism with PGlite, we can achieve perfect isolation.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Vitest Config&lt;/strong&gt;: Configure the test pool to use threads or forks. This ensures each test file runs in its own worker.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Mocking the Database&lt;/strong&gt;: We use Vitest’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vi.mock&lt;/code&gt; to intercept imports to the application’s database module.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Setup File&lt;/strong&gt;: In the setup for each test file, we instantiate a new PGlite database.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// vitest.setup.ts&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;vi&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;vitest&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;PGlite&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@electric-sql/pglite&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;drizzle&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;drizzle-orm/pglite&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;migrate&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;drizzle-orm/pglite/migrator&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Mock the module that exports the singleton &apos;db&apos;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;vi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@/lib/db&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// 1. Create a fresh in-memory Postgres instance&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;client&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;PGlite&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; 
  
  &lt;span class=&quot;c1&quot;&gt;// 2. Connect Drizzle&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;drizzle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// 3. Apply Schema (using Drizzle&apos;s push or migrate)&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Note: PGlite is fast enough to run migrations per test file&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;migrate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;migrationsFolder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./drizzle&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;63-the-memory-filesystem-mechanism&quot;&gt;6.3 The “Memory Filesystem” Mechanism&lt;/h3&gt;

&lt;p&gt;PGlite can persist data to a virtual filesystem in memory (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memory://&lt;/code&gt;). This means that when the test file finishes and the worker process terminates, the memory is reclaimed, and the database effectively vanishes. There is no cleanup required—no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DROP DATABASE&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TRUNCATE&lt;/code&gt; calls are needed. This “fire and forget” model drastically simplifies the test lifecycle.&lt;/p&gt;

&lt;h3 id=&quot;64-handling-server-actions-with-pglite&quot;&gt;6.4 Handling Server Actions with PGlite&lt;/h3&gt;

&lt;p&gt;Since Next.js Server Actions are just functions imported into the test, they will transparently use the mocked &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;db&lt;/code&gt; module. When &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;myServerAction()&lt;/code&gt; calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;db.insert(...)&lt;/code&gt;, it is inserting into the local PGlite instance running in the test thread. Because each test thread has its own mocked instance, they can run fully in parallel without any risk of data collision.&lt;/p&gt;

&lt;h3 id=&quot;65-performance-benchmarks&quot;&gt;6.5 Performance Benchmarks&lt;/h3&gt;

&lt;p&gt;Comparative benchmarks reveal the efficiency of this approach:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Startup Time&lt;/strong&gt;: PGlite starts in ~20ms vs Testcontainers ~1500ms.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Throughput&lt;/strong&gt;: In a suite of 100 test files, PGlite allows running as many workers as CPU cores (e.g., 8 or 16). Testcontainers might be limited to 2-3 concurrent containers before saturating RAM.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Execution&lt;/strong&gt;: CRUD operations in PGlite are slightly slower than native Postgres due to WASM overhead, but the elimination of network latency (localhost TCP loopback) often compensates for this in integration test scenarios.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;66-pros-and-cons&quot;&gt;6.6 Pros and Cons&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Feature&lt;/th&gt;
      &lt;th&gt;Analysis&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Speed&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Very High. Instant startup enables a “Database per Test File” strategy.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Isolation&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Perfect. Every test file gets a completely independent, shared-nothing database instance.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Parallelism&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Excellent. Scaling is limited only by CPU cores, not RAM or Docker limits.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Compatibility&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;High. Supports most Postgres features (JSONB, Triggers). However, as a WASM build, it is single-process (no multi-connection testing) and may lack support for specific native extensions like PostGIS unless custom-built.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Dev Experience&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Superior. No need to install Docker. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; is all that is needed to run the test suite.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: For 95% of Next.js applications using Drizzle, PGlite is the optimal strategy. It provides the best balance of speed, isolation, and developer experience.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;7-data-management-seeding-and-determinism&quot;&gt;7. Data Management: Seeding and Determinism&lt;/h2&gt;

&lt;p&gt;Regardless of the isolation strategy chosen, integration tests require data. “Seeding” is the process of populating the database with the necessary state (users, organisations, items) before a test runs.&lt;/p&gt;

&lt;h3 id=&quot;71-the-drizzle-seed-library&quot;&gt;7.1 The Drizzle Seed Library&lt;/h3&gt;

&lt;p&gt;The Drizzle team recently introduced &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;drizzle-seed&lt;/code&gt;, a library designed to solve the problem of generating realistic, relational data. Unlike traditional faker libraries which are purely random, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;drizzle-seed&lt;/code&gt; uses a seedable pseudo-random number generator (pRNG).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Determinism Matters:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In testing, you want randomness to cover edge cases, but you also want reproducibility. If a test fails because a random name contained an emoji that broke your validation, you need to be able to reproduce that exact failure. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;drizzle-seed&lt;/code&gt; allows you to set a specific seed (e.g., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;seed: 12345&lt;/code&gt;). Every time the test runs with that seed, it generates the exact same set of “random” data.&lt;/p&gt;

&lt;h3 id=&quot;72-relational-integrity&quot;&gt;7.2 Relational Integrity&lt;/h3&gt;

&lt;p&gt;Generating data for relational databases is hard because of foreign keys. You can’t create a Post without a User. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;drizzle-seed&lt;/code&gt; introspects your Drizzle schema relationships.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;seed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;refine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;funcs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;columns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;funcs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// deterministic emails&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;posts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Distributed among the 5 users&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This automatically handles the foreign key dependency, ensuring that every post created is linked to a valid user ID from the users generation step.&lt;/p&gt;

&lt;h3 id=&quot;73-factories-vs-global-seeds&quot;&gt;7.3 Factories vs. Global Seeds&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Global Seeding&lt;/strong&gt;: Populating the DB with a massive set of “standard” data at the start of the suite. This is good for read-heavy tests but bad for isolation (tests share the seed state).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Factories (Recommended)&lt;/strong&gt;: Using helper functions within each test to create only the data needed for that specific test.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// test/factories.ts&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;createUser&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;overrides&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{})&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;defaultData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;overrides&lt;/span&gt; 
  &lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;returning&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Combined with PGlite, this is extremely fast. Since the DB is empty at the start of the test file, the test creates 1 user, asserts against it, and finishes.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;8-comparative-analysis--decision-matrix&quot;&gt;8. Comparative Analysis &amp;amp; Decision Matrix&lt;/h2&gt;

&lt;p&gt;To guide the selection of the appropriate strategy, we synthesise the findings into a decision matrix.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Criterion&lt;/th&gt;
      &lt;th&gt;Transactional Rollback&lt;/th&gt;
      &lt;th&gt;Database Truncation&lt;/th&gt;
      &lt;th&gt;Testcontainers&lt;/th&gt;
      &lt;th&gt;PGlite (WASM)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Isolation Quality&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Medium (Connection sharing risks)&lt;/td&gt;
      &lt;td&gt;High (If serial) / Low (If parallel)&lt;/td&gt;
      &lt;td&gt;High (Process Isolation)&lt;/td&gt;
      &lt;td&gt;High (Memory Isolation)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Parallelism&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Low (Difficult to implement)&lt;/td&gt;
      &lt;td&gt;None (Must be Serial)&lt;/td&gt;
      &lt;td&gt;Medium (Resource constraints)&lt;/td&gt;
      &lt;td&gt;High (CPU bound)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Startup Overhead&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Fast&lt;/td&gt;
      &lt;td&gt;Fast&lt;/td&gt;
      &lt;td&gt;Slow (Docker spin-up)&lt;/td&gt;
      &lt;td&gt;Instant&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;CI Infrastructure&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Minimal&lt;/td&gt;
      &lt;td&gt;Minimal&lt;/td&gt;
      &lt;td&gt;Heavy (Needs Docker)&lt;/td&gt;
      &lt;td&gt;Minimal (Node only)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Production Parity&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;High&lt;/td&gt;
      &lt;td&gt;High&lt;/td&gt;
      &lt;td&gt;Exact&lt;/td&gt;
      &lt;td&gt;High (High fidelity simulation)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Implementation&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Complex (Proxies/ALS)&lt;/td&gt;
      &lt;td&gt;Simple&lt;/td&gt;
      &lt;td&gt;Moderate&lt;/td&gt;
      &lt;td&gt;Moderate (Mocking)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;81-recommendations&quot;&gt;8.1 Recommendations&lt;/h3&gt;

&lt;h4 id=&quot;for-new-greenfield-nextjs-applications&quot;&gt;For New (Greenfield) Next.js Applications&lt;/h4&gt;

&lt;p&gt;Adopt the &lt;strong&gt;PGlite Strategy&lt;/strong&gt;. The benefits of parallel execution and instant startup significantly outweigh the minor complexity of setting up the mock. It allows your test suite to grow to thousands of tests without slowing down your CI pipeline. It aligns perfectly with the isolated module structure of Next.js Server Actions.&lt;/p&gt;

&lt;h4 id=&quot;for-applications-with-specific-postgres-extensions-eg-postgis&quot;&gt;For Applications with Specific Postgres Extensions (e.g., PostGIS)&lt;/h4&gt;

&lt;p&gt;Use &lt;strong&gt;Testcontainers&lt;/strong&gt;. PGlite currently has limited support for complex C-based extensions. If your app relies heavily on geospatial queries or vector similarity search (pgvector), the fidelity of a real Dockerised Postgres instance is non-negotiable. To mitigate the slowness, consider sharding your tests in CI (running different test files on different machines).&lt;/p&gt;

&lt;h4 id=&quot;for-legacy-applications-or-small-suites&quot;&gt;For Legacy Applications or Small Suites&lt;/h4&gt;

&lt;p&gt;Use &lt;strong&gt;Database Truncation&lt;/strong&gt;. If you already have a working Postgres setup and your test suite takes less than 2 minutes to run serially, the effort to migrate to PGlite may not yield immediate ROI. Truncation is simple, reliable, and “good enough” for small scales.&lt;/p&gt;

&lt;h4 id=&quot;avoid-transactional-rollbacks-in-nextjs&quot;&gt;Avoid Transactional Rollbacks in Next.js&lt;/h4&gt;

&lt;p&gt;The impedance mismatch between the “Stateless Request” model of Next.js Server Actions and the “Stateful Connection” model of SQL transactions makes this strategy brittle. It typically leads to “magic” code (Proxies, ALS) that is hard to debug and hard to onboard new developers to.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;9-conclusion&quot;&gt;9. Conclusion&lt;/h2&gt;

&lt;p&gt;The landscape of integration testing in the Next.js and Drizzle ecosystem has shifted from managing shared resources to provisioning ephemeral ones. The “old way” of managing a single database and carefully cleaning it up is being superseded by the “new way” of creating disposables.&lt;/p&gt;

&lt;p&gt;Technological advancements—specifically containerisation and WebAssembly—have given us new tools. Testcontainers allows us to treat a database server as an ephemeral object, while PGlite allows us to treat the entire database concept as a disposable JavaScript variable.&lt;/p&gt;

&lt;p&gt;For the modern Next.js developer, the PGlite approach represents a sweet spot. It respects the boundaries of the framework, enables the blazing speed of Drizzle’s query generation, and provides the isolation necessary to banish flaky tests forever. By combining Vitest’s threading, PGlite’s in-memory speed, and Drizzle’s schema management, teams can build test suites that are not just safety nets, but accelerators of development velocity.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Next.js and Drizzle Integration&lt;/li&gt;
  &lt;li&gt;PGlite Architecture and Benchmarks&lt;/li&gt;
  &lt;li&gt;Testcontainers Usage and Comparison&lt;/li&gt;
  &lt;li&gt;Truncation and Seeding Logic&lt;/li&gt;
  &lt;li&gt;Transaction Management in Drizzle&lt;/li&gt;
  &lt;li&gt;Server-Only Constraints in Next.js&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sun, 25 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://blog.jarv.dev//test-isolation-for-a-nextjs-app-with-drizzle-orm</link>
        <link href="https://blog.jarv.dev/test-isolation-for-a-nextjs-app-with-drizzle-orm"/>
        <guid isPermaLink="true">https://blog.jarv.dev/test-isolation-for-a-nextjs-app-with-drizzle-orm</guid>
      </item>
    
      <item>
        <title>Why I Built Another Task Runner</title>
        <description>&lt;p&gt;Yes, I know. Another task runner. In 2026. Let me explain why.&lt;/p&gt;

&lt;h2 id=&quot;the-problem&quot;&gt;The Problem&lt;/h2&gt;

&lt;p&gt;I’m not a Make expert. But somehow I became the person people ask when they need to add a task to our Makefile.&lt;/p&gt;

&lt;p&gt;Last week it was “How do I pass an environment variable to this target?” The week before: “Why does this only work if I add a tab character?” And the week before that it was me forgetting to add a task to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.PHONY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every time, I’m Googling the same things they could be Googling. Our build process has things like this:&lt;/p&gt;

&lt;div class=&quot;language-makefile highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nl&quot;&gt;deploy-%&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;guard-% check-git-clean&lt;/span&gt;
	&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;eval&lt;/span&gt; ENV :&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;word&lt;/span&gt; 2,&lt;span class=&quot;nf&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;subst&lt;/span&gt; -, ,&lt;span class=&quot;nv&quot;&gt;$@&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;)))&lt;/span&gt;
	./scripts/deploy.sh &lt;span class=&quot;nv&quot;&gt;$(ENV)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you understand what &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$(word 2,$(subst -, ,$@))&lt;/code&gt; does without looking it up, congratulations — you’re in the 1% of developers who’ve memorised Make’s arcane variable substitution syntax.&lt;/p&gt;

&lt;h2 id=&quot;the-alternatives-werent-much-better&quot;&gt;The Alternatives Weren’t Much Better&lt;/h2&gt;

&lt;p&gt;Other repos in the company use npm scripts. Some have custom Python CLI packages run with uv. Every project has its own approach.&lt;/p&gt;

&lt;p&gt;The npm scripts repos hit the same cross-platform issues. Shell commands that work fine on Mac and Linux fail on Windows. You end up with:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;clean&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;rm -rf dist || rmdir /s /q dist&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;build&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;NODE_ENV=production webpack || set NODE_ENV=production &amp;amp;&amp;amp; webpack&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Ugly. Fragile. And it still broke in random edge cases.&lt;/p&gt;

&lt;p&gt;So I looked at the established alternatives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Just&lt;/strong&gt; (~22k stars) is the closest thing to what I wanted: a clean &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;justfile&lt;/code&gt; with Make-inspired syntax, recipe parameters, shell completions. But parameters have no type annotations — they’re positional strings and that’s it. Handy for humans, but AI agents can’t reliably introspect them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Task (go-task)&lt;/strong&gt; (~15k stars) impressed me with its built-in POSIX shell interpreter, which gives genuinely consistent cross-platform behaviour without relying on the system shell. But it’s shell-only — no inline Python or Node.js. And MCP support is still just a GitHub issue with no implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mise&lt;/strong&gt; (~25k stars) is arguably the most feature-rich tool in this space, combining dev tool version management (replacing asdf/nvm/pyenv), environment management, and task running in a single binary. It does have an experimental &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mise mcp&lt;/code&gt; server, which is genuinely interesting — though it exposes the broader dev environment surface rather than being focused specifically on task execution. For teams already using mise for version management, that breadth is a strength.&lt;/p&gt;

&lt;p&gt;The Markdown-based runners — &lt;strong&gt;Mask&lt;/strong&gt;, &lt;strong&gt;xc&lt;/strong&gt;, &lt;strong&gt;Maid&lt;/strong&gt; — are a compelling idea (documentation and automation in one file), but none of them have any MCP story, and Maid hasn’t been meaningfully maintained in years.&lt;/p&gt;

&lt;h2 id=&quot;what-i-actually-wanted&quot;&gt;What I Actually Wanted&lt;/h2&gt;

&lt;p&gt;I wanted to write tasks the way I think about them:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Just deploy the thing&lt;/span&gt;
deploy&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;environment&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    ./scripts/deploy.sh &lt;span class=&quot;nv&quot;&gt;$environment&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But I also wanted:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Python when I needed real logic (not bash’s string manipulation nightmare)&lt;/li&gt;
  &lt;li&gt;Node when working with JSON or async operations&lt;/li&gt;
  &lt;li&gt;Cross-platform support without conditional hell&lt;/li&gt;
  &lt;li&gt;Something AI agents could actually use — not as an afterthought, but as a first-class feature&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point turned out to be the gap nobody had filled properly.&lt;/p&gt;

&lt;h2 id=&quot;so-i-built-run&quot;&gt;So I Built &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;So I built &lt;a href=&quot;https://runtool.dev&quot;&gt;run&lt;/a&gt;. A Runfile looks like this:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Shell for simple stuff&lt;/span&gt;
build&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; cargo build &lt;span class=&quot;nt&quot;&gt;--release&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Python when you need it&lt;/span&gt;
analyze&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;file&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;#!/usr/bin/env python&lt;/span&gt;
    import json
    with open&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;file&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; as f:
        data &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; json.load&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;f&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        print&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;f&lt;span class=&quot;s2&quot;&gt;&quot;Processed {len(data)} records&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Node works too&lt;/span&gt;
process&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;#!/usr/bin/env node&lt;/span&gt;
    const fs &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; require&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;fs&apos;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    console.log&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Processing...&apos;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Platform-specific versions&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# @os windows&lt;/span&gt;
deploy&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;environment&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    .&lt;span class=&quot;se&quot;&gt;\s&lt;/span&gt;cripts&lt;span class=&quot;se&quot;&gt;\d&lt;/span&gt;eploy.ps1 &lt;span class=&quot;nv&quot;&gt;$environment&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# @os linux darwin&lt;/span&gt;
deploy&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;environment&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    ./scripts/deploy.sh &lt;span class=&quot;nv&quot;&gt;$environment&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;No YAML. No TOML. No &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$(word 2,$(subst -, ,$@))&lt;/code&gt;. Just functions with named parameters.&lt;/p&gt;

&lt;h3 id=&quot;polyglot-argument-passing&quot;&gt;Polyglot Argument Passing&lt;/h3&gt;

&lt;p&gt;The polyglot model is worth dwelling on — even if you never touch the AI features, this alone is a meaningful improvement over existing tools.&lt;/p&gt;

&lt;p&gt;Notice how the Python function above gets &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;file&lt;/code&gt; as a native Python variable — no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sys.argv[1]&lt;/code&gt;, no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;argparse&lt;/code&gt;, no parsing. And it goes further than just strings. If you add a type annotation:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;resize&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;file, width: int, height: int&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;#!/usr/bin/env python&lt;/span&gt;
    from PIL import Image
    img &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; Image.open&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;file&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    img.resize&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;width, height&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;.save&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;file&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    print&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;f&lt;span class=&quot;s2&quot;&gt;&quot;Resized to {width}x{height}&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;width&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;height&lt;/code&gt; arrive as actual Python &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int&lt;/code&gt;s. You can multiply them, pass them to APIs that expect numbers, use them in range expressions — no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int(sys.argv[2])&lt;/code&gt; boilerplate. The same applies to Node.js, Ruby, and PowerShell: RunTool generates the appropriate variable declarations for each interpreter, with type-correct values.&lt;/p&gt;

&lt;p&gt;Compare this to the typical polyglot task runner experience: you write a shebang script, and everything arrives as a string. You spend the first few lines of every script parsing and converting arguments. It’s not a lot of code, but it’s friction — and it’s the kind of friction that makes people reach for a full CLI framework instead of a quick task.&lt;/p&gt;

&lt;p&gt;Just supports multiple languages too, via shebangs — but each shebang recipe has to be a separate named recipe. RunTool lets you mix languages within a single file using function-scoped shebangs. And the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@os&lt;/code&gt; attribute lets you define the &lt;em&gt;same function name&lt;/em&gt; with different implementations per platform, which is more ergonomic than Task’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;platforms&lt;/code&gt; filter or mise’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run_windows&lt;/code&gt; property.&lt;/p&gt;

&lt;h2 id=&quot;the-ai-integration&quot;&gt;The AI Integration&lt;/h2&gt;

&lt;p&gt;Here’s where it gets interesting: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run&lt;/code&gt; has a built-in MCP (Model Context Protocol) server.&lt;/p&gt;

&lt;p&gt;I was pairing with Claude Code on some refactoring (&lt;em&gt;it&lt;/em&gt; was driving 🙈) when I realised: it would be helpful if Claude could just run our tests or deployment checks directly. With MCP support, AI agents can discover and execute your project’s tools automatically, and use the exact same tools that you use.&lt;/p&gt;

&lt;p&gt;Add some metadata:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# @desc Deploy to specified environment&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# @arg environment Target environment (staging|prod)&lt;/span&gt;
deploy&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;environment&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    ./scripts/deploy.sh &lt;span class=&quot;nv&quot;&gt;$environment&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The function signature &lt;em&gt;is&lt;/em&gt; the schema. Claude (or any MCP-compatible agent) sees a tool called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deploy&lt;/code&gt; with a required &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;environment&lt;/code&gt; parameter — no guessing, no multi-step discovery, no verbose Markdown explanations needed. Type annotations and defaults work too:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# @desc Scale a service&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# @arg service The service name&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# @arg replicas Number of instances&lt;/span&gt;
scale&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;service, replicas: int &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 1&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    docker compose scale &lt;span class=&quot;nv&quot;&gt;$service&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$replicas&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@desc&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@arg&lt;/code&gt; annotations serve dual purposes: they generate human-readable help text &lt;em&gt;and&lt;/em&gt; provide structured metadata that MCP clients use for tool discovery and invocation. That’s architecturally different from Just’s approach, where three independent community-built MCP servers have to parse the justfile format externally without any structured argument metadata to work with.&lt;/p&gt;

&lt;p&gt;There’s a useful security property here too: the MCP server only exposes the annotations — the function name, description, and argument signatures. The implementation is not surfaced. An agent knows that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deploy&lt;/code&gt; takes an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;environment&lt;/code&gt; argument of type string; it doesn’t know (or need to know) that internally it shells out to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;./scripts/deploy.sh&lt;/code&gt;, or that there’s a secrets file being sourced, or any other implementation detail. If the agent wants to read the Runfile itself it can, but that’s an explicit read operation — not something the MCP server hands over automatically. You can give agents access to your tooling without giving them a map of your internals.&lt;/p&gt;

&lt;p&gt;As of early 2026, the landscape looks like this:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Tool&lt;/th&gt;
      &lt;th&gt;Polyglot scripting&lt;/th&gt;
      &lt;th&gt;Typed arguments&lt;/th&gt;
      &lt;th&gt;MCP integration&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;RunTool&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;✅ Shell/Python/Node/Ruby/PS&lt;/td&gt;
      &lt;td&gt;✅ Signature types + defaults&lt;/td&gt;
      &lt;td&gt;✅ Built-in&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Just&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;✅ Via shebangs&lt;/td&gt;
      &lt;td&gt;⚠️ Params, no types&lt;/td&gt;
      &lt;td&gt;⚠️ 3 third-party servers&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Task&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;❌ Shell-only&lt;/td&gt;
      &lt;td&gt;⚠️ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requires&lt;/code&gt; + enum&lt;/td&gt;
      &lt;td&gt;❌ Requested only&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Mise&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;✅ Via shebangs&lt;/td&gt;
      &lt;td&gt;✅ usage spec&lt;/td&gt;
      &lt;td&gt;✅ Built-in (experimental)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Mask&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;✅ Code block langs&lt;/td&gt;
      &lt;td&gt;⚠️ Flags with types&lt;/td&gt;
      &lt;td&gt;❌ None&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;xc&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;❌ Shell-only&lt;/td&gt;
      &lt;td&gt;⚠️ Inputs attribute&lt;/td&gt;
      &lt;td&gt;❌ None&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Make&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;❌ Shell (with quirks)&lt;/td&gt;
      &lt;td&gt;❌ None&lt;/td&gt;
      &lt;td&gt;⚠️ Third-party only&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;RunTool and mise are the only tools with built-in MCP servers. The difference is scope: mise’s MCP server exposes its full environment management surface (tools, tasks, versions, env vars), which is powerful if you want an agent that can manage your entire dev environment. RunTool’s is narrower by design — purpose-built around task execution and structured argument metadata — which means less surface area to audit and a simpler mental model for what the agent can do.&lt;/p&gt;

&lt;h2 id=&quot;zero-dependencies-instant-startup&quot;&gt;Zero Dependencies, Instant Startup&lt;/h2&gt;

&lt;p&gt;It’s a single Rust binary. No runtime. No package.json with 500 dependencies. You run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run deploy staging&lt;/code&gt; and it just works.&lt;/p&gt;

&lt;p&gt;And it comes with shell completions out of the box — bash, zsh, fish, and PowerShell. Tab completion for all your tasks, no extra setup required.&lt;/p&gt;

&lt;h2 id=&quot;honestly-the-tradeoffs&quot;&gt;Honestly, the Tradeoffs&lt;/h2&gt;

&lt;p&gt;I’d be doing you a disservice if I didn’t acknowledge what the alternatives have that RunTool doesn’t — yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Just&lt;/strong&gt; has years of battle-testing, a backwards-compatibility guarantee (“there will never be a just 2.0”), and extensive editor support across VS Code, JetBrains, Helix, Kakoune, and Zed. Its community has built three independent MCP servers. That ecosystem trust takes years to build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Task&lt;/strong&gt; has something technically impressive that’s easy to overlook: its built-in shell interpreter (mvdan/sh) plus built-in Unix utilities means it’s genuinely cross-platform without platform-specific shims. If your team has Windows developers and you’ve suffered through shell compatibility issues, that’s a compelling argument.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mise&lt;/strong&gt; bundles tool version management, environment management, and task running into a single binary — reducing the number of tools a project depends on. If you’re already using mise for version management, its experimental MCP support and rich task runner make it the most comprehensive single-binary solution. And its broader MCP surface area is genuinely useful if you want agents that can manage your full dev environment, not just run tasks.&lt;/p&gt;

&lt;p&gt;RunTool is a new entrant betting that AI-agent interoperability will become a first-class concern for developer tooling. That bet might be right or wrong. What I can say is: when I’m working alongside Claude Code on a project and it can discover and run the project’s tasks through the same interface I do, that workflow genuinely changes how I work.&lt;/p&gt;

&lt;h2 id=&quot;is-this-useful-to-anyone-else&quot;&gt;Is This Useful to Anyone Else?&lt;/h2&gt;

&lt;p&gt;I don’t know. Maybe you’re happy with Make. Maybe Just works perfectly for you. Maybe mise already does everything you need. Maybe you don’t care whether your AI agent can introspect your task arguments.&lt;/p&gt;

&lt;p&gt;But if you’ve ever thought “there has to be a simpler way to do this” — whether that’s passing typed arguments to a Python task without &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;argparse&lt;/code&gt;, writing cross-platform tasks without conditional hell, or giving your AI agent access to your project’s tools — maybe give it a try:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;nihilok/tap/runtool
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;or&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cargo &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;run
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The code is on &lt;a href=&quot;https://github.com/nihilok/run&quot;&gt;GitHub&lt;/a&gt;. It solves my problems. Maybe it’ll solve yours too.&lt;/p&gt;
</description>
        <pubDate>Sat, 17 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://blog.jarv.dev//why-i-built-another-task-runner</link>
        <link href="https://blog.jarv.dev/why-i-built-another-task-runner"/>
        <guid isPermaLink="true">https://blog.jarv.dev/why-i-built-another-task-runner</guid>
      </item>
    
      <item>
        <title>Offline Encryption Using WebAuthn</title>
        <description>&lt;p&gt;I’ve been diving deep into browser-based cryptography lately, and implementing truly secure offline encryption is frustratingly complex. The core problem? Getting consistent key material across sessions without storing anything sensitive.&lt;/p&gt;

&lt;p&gt;After countless late nights of cursing at my keyboard, I stumbled upon an intriguing solution: using WebAuthn (e.g. those little USB security keys like YubiKey) not just for authentication, but as a source of encryption key material.&lt;/p&gt;

&lt;h2 id=&quot;the-encryption-challenge&quot;&gt;The Encryption Challenge&lt;/h2&gt;

&lt;p&gt;Creating secure encryption that works completely offline in a browser environment is difficult for several reasons:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Your key material needs to be consistent across browser sessions&lt;/li&gt;
  &lt;li&gt;You can’t just derive keys from code (that’s basically security through obscurity)&lt;/li&gt;
  &lt;li&gt;Browser storage isn’t secure enough for sensitive key material&lt;/li&gt;
  &lt;li&gt;And of course, the whole thing needs to work offline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After banging my head against various approaches (including some truly questionable experiments with IndexedDB), I realised WebAuthn might be the perfect solution. Why? Because those little hardware authenticators never expose their private keys — they’re basically tiny HSMs that live in your USB port!&lt;/p&gt;

&lt;h2 id=&quot;webauthn-not-just-for-logins-anymore&quot;&gt;WebAuthn: Not Just for Logins Anymore&lt;/h2&gt;

&lt;p&gt;WebAuthn wasn’t designed to be a “key extractor” - it’s an authentication standard. But with a bit of creative thinking (and some cryptographic sleight of hand), we can use it to generate consistent encryption material.&lt;/p&gt;

&lt;p&gt;Here’s how it works in a nutshell:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Register a credential&lt;/strong&gt; (one-time setup)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Store the credential ID&lt;/strong&gt; (not secret, so browser storage is fine)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Get an assertion&lt;/strong&gt; using a fixed challenge whenever you need key material&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Use the signature&lt;/strong&gt; as input for your encryption key derivation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The real magic here is that for a given credential and challenge, the hardware authenticator will produce a signature that we can use as key material. The private key never leaves the device!&lt;/p&gt;

&lt;h2 id=&quot;implementation&quot;&gt;Implementation&lt;/h2&gt;

&lt;p&gt;Here’s how to implement the registration phase:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// A randomly generated challenge (only used during registration)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;challenge&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getRandomValues&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Configure your publicKeyCredentialCreationOptions&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;publicKeyOptions&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;challenge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;challenge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;rp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Offline Demo RP&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;unique-user-id&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;charCodeAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)),&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;demo-user&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;displayName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Demo User&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;pubKeyCredParams&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;public-key&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;alg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}],&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// ECDSA with SHA-256&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;60000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;attestation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;none&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;credentials&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;publicKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;publicKeyOptions&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cred&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Save the credential ID for future assertions&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;credId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cred&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;rawId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Persist credId (e.g., in IndexedDB)&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Registration done, credentialId:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;bufferToBase64Url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;credId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;bufferToBase64Url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;buffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;str&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fromCharCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(...&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;buffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;btoa&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\+&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\/&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/=+$/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And here’s the really interesting part - getting key material later:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Retrieve the previously saved credentialId&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;savedCredId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;cm&quot;&gt;/* retrieve your stored credentialId */&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;credIdBuffer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;base64UrlToBuffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;savedCredId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Use a deterministically derived challenge&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fixedChallenge&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt; &lt;span class=&quot;cm&quot;&gt;/* 32 bytes of fixed challenge data */&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;publicKeyRequestOptions&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;challenge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fixedChallenge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;allowCredentials&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;credIdBuffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;public-key&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}],&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;userVerification&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;discouraged&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;60000&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;credentials&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;publicKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;publicKeyRequestOptions&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;assertion&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Extract the signature&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signature&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;assertion&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;signature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// Combine with additional secret entropy&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// const combinedMaterial = concat(signature, additionalEntropy);&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// const rawKeyMaterial = await deriveRawKeyMaterial(combinedMaterial);&lt;/span&gt;
    
    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Obtained signature for key derivation:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Continue with key derivation...&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;base64UrlToBuffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;base64UrlString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;base64&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;base64UrlString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/-/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/_/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;pad&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;base64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;padded&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;pad&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;base64&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;pad&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;base64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;binary&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;atob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;padded&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buffer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;binary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;binary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;char&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;char&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;charCodeAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;but-wait-theres-a-catch&quot;&gt;But Wait, There’s a Catch!&lt;/h2&gt;

&lt;p&gt;Of course there is. When is crypto ever straightforward?&lt;/p&gt;

&lt;p&gt;Some authenticators don’t produce deterministic signatures for the same input. They might:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Use non-deterministic signature algorithms (looking at you, ECDSA)&lt;/li&gt;
  &lt;li&gt;Embed signature counters in the output&lt;/li&gt;
  &lt;li&gt;Do other vendor-specific weird stuff&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means that your key material might change slightly between uses. Not ideal for encryption!&lt;/p&gt;

&lt;p&gt;The solution is a &lt;strong&gt;fuzzy extractor&lt;/strong&gt; — a cryptographic primitive that takes “noisy” input and reliably produces consistent output.&lt;/p&gt;

&lt;p&gt;A typical fuzzy extractor has two phases:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Generate&lt;/strong&gt;: Takes initial input, produces a key and a “helper”&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Reproduce&lt;/strong&gt;: Uses the helper to recover the same key from slightly different input&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;security-considerations-the-javascript-memory-problem&quot;&gt;Security Considerations: The JavaScript Memory Problem&lt;/h2&gt;

&lt;p&gt;One important consideration: JavaScript memory.&lt;/p&gt;

&lt;p&gt;Since all your variables in JavaScript are potentially accessible to attackers with debugging capabilities, your signature values could be exposed if someone can run a debugger or inject code through XSS.&lt;/p&gt;

&lt;p&gt;Some mitigation strategies:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Keep sensitive variables around for as short a time as possible&lt;/li&gt;
  &lt;li&gt;Use Web Crypto APIs which operate outside typical JavaScript memory&lt;/li&gt;
  &lt;li&gt;Implement strict Content Security Policies&lt;/li&gt;
  &lt;li&gt;Clear sensitive data ASAP&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And for extra security, consider:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Adding user-provided entropy (like a password) to your key derivation&lt;/li&gt;
  &lt;li&gt;Never, ever, ever storing sensitive key material in browser storage&lt;/li&gt;
  &lt;li&gt;Considering the security of the device itself in your threat model&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h2&gt;

&lt;p&gt;So there you have it - WebAuthn hardware authenticators can be cleverly repurposed as key material providers for offline encryption. The private key stays safely locked away in hardware, your encryption keys become consistently derivable without network connectivity, and you get to feel like a crypto wizard in the process.&lt;/p&gt;

&lt;p&gt;I’ve been using this technique successfully in a few personal projects, and while it’s definitely not the most conventional approach, it’s been surprisingly robust in practice. The combination of hardware security and fully offline operation makes it a winner for certain types of applications.&lt;/p&gt;

&lt;p&gt;Give it a try in your next offline encryption project, and let me know if you come up with any clever improvements to the technique! Just remember - no crypto system is perfect, so always consider your specific threat model and use case.&lt;/p&gt;

&lt;p&gt;P.S. Huge thanks to the WebAuthn standard creators - I’m pretty sure this wasn’t what they had in mind, but that’s the beauty of well-designed crypto primitives: they enable use cases the designers never even imagined!&lt;/p&gt;
</description>
        <pubDate>Sat, 08 Mar 2025 00:00:00 +0000</pubDate>
        <link>https://blog.jarv.dev//offline-only-encryption</link>
        <link href="https://blog.jarv.dev/offline-only-encryption"/>
        <guid isPermaLink="true">https://blog.jarv.dev/offline-only-encryption</guid>
      </item>
    
      <item>
        <title>Loading tmux when loading shell</title>
        <description>&lt;p&gt;This is something I’ve had mixed success with in the past. I’ve had things that worked but not quite in the way I wanted them to, and other things that just didn’t work! (Try sticking an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exec&lt;/code&gt; before an error in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.zshrc&lt;/code&gt;, and you’ve just nerfed your shell startup!)&lt;/p&gt;

&lt;p&gt;There are a few things we need to consider, e.g. Do we want the same session as last time? What if there is no session? Can we create a new session but attach to an existing one if one by the same name exists? Do we want the same session when on SSH? Or when running a terminal emulator inside an IDE for example?&lt;/p&gt;

&lt;p&gt;I’ve finally nailed it down to the following:&lt;/p&gt;

&lt;div class=&quot;language-zsh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/usr/bin/env zsh&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$TERMINAL_EMULATOR&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;JetBrains-JediTerm&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$TMUX&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
        if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$SSH_CONNECTION&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
            &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;TMUX_SESSION_NAME&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;tmux_&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SSH_CONNECTION&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;md5sum&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos; &apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; 1 | &lt;span class=&quot;nb&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; 1-6&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else
            &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;TMUX_SESSION_NAME&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;tmux&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;fi
        if &lt;/span&gt;tmux has-session &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$TMUX_SESSION_NAME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
            &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;tmux attach &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$TMUX_SESSION_NAME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else
            &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;tmux new-session &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$TMUX_SESSION_NAME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;fi
    fi
fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This script will start a new tmux session if one doesn’t exist, or attach to an existing one if it does. It will also create a new session if you’re on SSH, and it will not run if you’re using the JetBrains terminal emulator (as it doesn’t play nicely with tmux).&lt;/p&gt;

&lt;p&gt;I have this in a separate script, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.load_tmux&lt;/code&gt; which I source from my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.zshrc&lt;/code&gt;, right at the top of the file (so that it is the first thing that is executed):&lt;/p&gt;

&lt;div class=&quot;language-zsh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;source&lt;/span&gt; ~/.load_tmux
&lt;span class=&quot;c&quot;&gt;# rest of .zshrc...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
        <pubDate>Wed, 22 Jan 2025 00:00:00 +0000</pubDate>
        <link>https://blog.jarv.dev//loading-tmux-when-loading-shell</link>
        <link href="https://blog.jarv.dev/loading-tmux-when-loading-shell"/>
        <guid isPermaLink="true">https://blog.jarv.dev/loading-tmux-when-loading-shell</guid>
      </item>
    
      <item>
        <title>Server status monitor bash script</title>
        <description>&lt;p&gt;When managing multiple servers, keeping track of their availability is crucial. Building on an example from “Cybersecurity Ops with bash” by Paul Troncone, I’ve developed a bash script that provides a simple yet effective way to monitor server status across multiple hosts. Let’s break down how this script works and why it can be a valuable tool for system administrators.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# For a given list of servers, this script will check the status of the server and notify the user if the server is down.&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Usage: ./server_status.sh [-i &amp;lt;interval&amp;gt;] &amp;lt; server_list.txt&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;declare&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;INTERVAL&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;30 &lt;span class=&quot;c&quot;&gt;# Default interval is 30 seconds&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; /var/log/server_status.log &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then&lt;/span&gt;
        &lt;span class=&quot;c&quot;&gt;# Check if user has permission to write to /var/log&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-w&lt;/span&gt; /var/log &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
                &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Cannot write to /var/log/server_status.log; hint: use sudo when running this script for the first time&quot;&lt;/span&gt; 1&amp;gt;&amp;amp;2
                &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1
        &lt;span class=&quot;k&quot;&gt;fi
        &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch&lt;/span&gt; /var/log/server_status.log
        &lt;span class=&quot;nb&quot;&gt;chown&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$USER&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; /var/log/server_status.log
        &lt;span class=&quot;nb&quot;&gt;chmod &lt;/span&gt;644 /var/log/server_status.log
&lt;span class=&quot;k&quot;&gt;fi

while &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;getopts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;:i:&quot;&lt;/span&gt; opt&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
	case&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;opt&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in
	&lt;/span&gt;i&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
		&lt;span class=&quot;nv&quot;&gt;INTERVAL&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$OPTARG&lt;/span&gt;
		&lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt;
	&lt;span class=&quot;se&quot;&gt;\?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
		&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Invalid option: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$OPTARG&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; 1&amp;gt;&amp;amp;2
		&lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt;
	:&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
		&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Invalid option: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$OPTARG&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; requires an argument&quot;&lt;/span&gt; 1&amp;gt;&amp;amp;2
		&lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;esac&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;shift&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;OPTIND &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;done

while &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
	&lt;/span&gt;clear
	&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Server Status Monitor&apos;&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Status: Scanning ...&apos;&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;---------------------------------&apos;&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;while &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; server_hostname&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
		&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;server_hostname&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$server_hostname&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;tr&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;\r&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$server_hostname&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; http&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
            &lt;/span&gt;curl &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; /dev/null &lt;span class=&quot;nt&quot;&gt;-w&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;%{http_code}&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$server_hostname&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;(000|404)&apos;&lt;/span&gt; &amp;amp;&amp;gt;/dev/null
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$?&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-eq&lt;/span&gt; 0 &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
                &lt;/span&gt;tput setaf 1
                &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Server: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$server_hostname&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; is down - &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;tee&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-a&lt;/span&gt; /var/log/server_status.log
                tput setaf 7
            &lt;span class=&quot;k&quot;&gt;fi
            continue
        fi

		&lt;/span&gt;ping &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; 1 &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$server_hostname&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;(Destination Host Unreachable|100% packet loss)&apos;&lt;/span&gt; &amp;amp;&amp;gt;/dev/null
		&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$?&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-eq&lt;/span&gt; 0 &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
			&lt;/span&gt;tput setaf 1
			&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Server: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$server_hostname&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; is down - &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;tee&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-a&lt;/span&gt; /var/log/server_status.log
			tput setaf 7
		&lt;span class=&quot;k&quot;&gt;fi
	done&lt;/span&gt; &amp;lt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;:-&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;/dev/stdin&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
	&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Press [CTRL+C] to stop...&quot;&lt;/span&gt;

	&lt;span class=&quot;nb&quot;&gt;declare&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; i
	&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;i &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$INTERVAL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; 0&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; i--&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
		&lt;/span&gt;tput cup 1 0
		&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Status: Next scan in &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$i&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; seconds      &quot;&lt;/span&gt;
		&lt;span class=&quot;nb&quot;&gt;sleep &lt;/span&gt;1
	&lt;span class=&quot;k&quot;&gt;done
done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;script-overview&quot;&gt;Script Overview&lt;/h2&gt;

&lt;p&gt;The script allows you to monitor the status of a list of servers, whether they’re URLs or hostnames, and provides real-time notifications when a server goes down. Here’s how it functions:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;./server_status.sh &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; &amp;lt;interval&amp;gt;] &amp;lt; server_list.txt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;key-features&quot;&gt;Key Features&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Flexible Input&lt;/strong&gt;: The script can read server names from a file or standard input.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Configurable Scan Interval&lt;/strong&gt;: You can specify a custom scan interval (default is 30 seconds).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Logging&lt;/strong&gt;: Automatically logs server down events to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/var/log/server_status.log&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Support for Both Hostnames and URLs&lt;/strong&gt;: Can ping traditional servers or check HTTP status codes for web servers.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;implementation&quot;&gt;Implementation&lt;/h2&gt;

&lt;h3 id=&quot;logging-setup&quot;&gt;Logging Setup&lt;/h3&gt;

&lt;p&gt;The script first checks if it can create and write to the log file at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/var/log/server_status.log&lt;/code&gt;. If the directory isn’t writable, it provides a helpful hint to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo&lt;/code&gt; when running the script for the first time. This ensures proper permissions are set for logging.&lt;/p&gt;

&lt;h3 id=&quot;server-status-checking&quot;&gt;Server Status Checking&lt;/h3&gt;

&lt;p&gt;The script supports two types of server checks:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# For traditional servers (hostnames)&lt;/span&gt;
ping &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; 1 &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$server_hostname&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;(Destination Host Unreachable|100% packet loss)&apos;&lt;/span&gt; &amp;amp;&amp;gt;/dev/null

&lt;span class=&quot;c&quot;&gt;# For web servers (URLs)&lt;/span&gt;
curl &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; /dev/null &lt;span class=&quot;nt&quot;&gt;-w&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;%{http_code}&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$server_hostname&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;(000|404)&apos;&lt;/span&gt; &amp;amp;&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This dual approach allows monitoring of both network-level connectivity for traditional servers and HTTP availability for web services.&lt;/p&gt;

&lt;h3 id=&quot;user-experience&quot;&gt;User Experience&lt;/h3&gt;

&lt;p&gt;The script provides a clean, interactive terminal interface:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Colorized output (red for down servers)&lt;/li&gt;
  &lt;li&gt;Countdown timer to next scan&lt;/li&gt;
  &lt;li&gt;Option to stop monitoring with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CTRL+C&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;usage-example&quot;&gt;Usage Example&lt;/h3&gt;

&lt;p&gt;Create a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;server_list.txt&lt;/code&gt; with your servers:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;example.com
https://mywebsite.com
192.168.1.100
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then run the script:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;./server_status.sh &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; 60 &amp;lt; server_list.txt  &lt;span class=&quot;c&quot;&gt;# Scan every 60 seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;considerations-and-warnings&quot;&gt;Considerations and Warnings&lt;/h2&gt;

&lt;p&gt;As with any system monitoring script, be cautious:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Ensure you have permission to monitor the servers&lt;/li&gt;
  &lt;li&gt;Be mindful of network load, especially with frequent scans&lt;/li&gt;
  &lt;li&gt;The script is best used in controlled, internal network environments&lt;/li&gt;
  &lt;li&gt;Unless the hostname starts with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http&lt;/code&gt;, the script assumes it’s a traditional server and uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ping&lt;/code&gt; for monitoring; therefore, it may not work for all types of servers if they don’t respond to ICMP requests (I had to add a custom rule to an AWS security group to allow ICMP traffic for this script to work for my EC2 instances)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;This bash script offers a lightweight, flexible solution for monitoring server availability. It demonstrates the power of bash scripting in system administration tasks, providing a simple yet effective tool for keeping an eye on your infrastructure.&lt;/p&gt;

&lt;p&gt;Remember to always test scripts in a staging environment before deploying to production, and customize the script to fit your specific monitoring needs.&lt;/p&gt;

&lt;p&gt;Happy scripting!&lt;/p&gt;
</description>
        <pubDate>Mon, 16 Dec 2024 00:00:00 +0000</pubDate>
        <link>https://blog.jarv.dev//server-status-monitor-bash-script</link>
        <link href="https://blog.jarv.dev/server-status-monitor-bash-script"/>
        <guid isPermaLink="true">https://blog.jarv.dev/server-status-monitor-bash-script</guid>
      </item>
    
  </channel>
</rss>
