<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: M Hossein</title>
    <description>The latest articles on DEV Community by M Hossein (@_mh).</description>
    <link>https://dev.to/_mh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F169516%2Fb3bc3f9a-6dfa-45b1-8e15-83d3afc57ba1.jpeg</url>
      <title>DEV Community: M Hossein</title>
      <link>https://dev.to/_mh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_mh"/>
    <language>en</language>
    <item>
      <title>I switched on production evals for my LLM app — and they scored nothing</title>
      <dc:creator>M Hossein</dc:creator>
      <pubDate>Tue, 23 Jun 2026 12:46:23 +0000</pubDate>
      <link>https://dev.to/_mh/i-switched-on-production-evals-for-my-llm-app-and-they-scored-nothing-4po2</link>
      <guid>https://dev.to/_mh/i-switched-on-production-evals-for-my-llm-app-and-they-scored-nothing-4po2</guid>
      <description>&lt;p&gt;&lt;strong&gt;What data privacy taught me about online evals, and why I stopped treating LLM prompts like magic and started treating them like hostile user input.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Context &amp;amp; The Constraint
&lt;/h3&gt;

&lt;p&gt;I am building a meeting assistant that fact-checks claims in real time. Because it processes meeting audio in the EU, it is bound by strict data residency rules: personal data and transcripts cannot arbitrarily leave our European infrastructure.&lt;/p&gt;

&lt;p&gt;This introduces a fundamental distributed systems headache when we introduce &lt;strong&gt;Online Evaluations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Online evals run after you ship, on live traffic. There is no answer key. Instead, you score live outputs for qualitative properties: &lt;em&gt;Is this answer actually grounded in its sources? Is the model hallucinating?&lt;/em&gt; To do this, an evaluator needs to look at the inputs and outputs.&lt;/p&gt;

&lt;p&gt;I wired up tracing, wrote an LLM judge, enabled LangSmith’s online evaluator, and waited for the scores to roll in. Nothing came back. Not a failure, not an error—just an endless stream of empty dashboards. The reason is a trap that is trivial to fall into, and it requires rethinking how we handle telemetry at the edge.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Naive Approach (The MVP)
&lt;/h3&gt;

&lt;p&gt;The standard, happy-path implementation for online evals assumes a globally unified system where data flows freely. You pipe your inputs, outputs, and intermediate states to an observability platform, and a cloud-hosted LLM judge scores them asynchronously.&lt;/p&gt;

&lt;p&gt;Because of EU data rules, I had aggressively masked my traces. I configured my LangSmith client to strip all inputs and outputs before they left my server. LangSmith kept the metadata (latency, tokens) and nothing else.&lt;/p&gt;

&lt;p&gt;Great for privacy. Fatal for evaluations. The evaluator opened the trace, found an empty object, and scored nothing. It failed silently because it was doing exactly what I configured it to do. But fixing this by simply "turning off masking" wasn't a legal option, and running the judge synchronously in the application code is an operational death sentence.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Architectural Evolution (Iterative Refinement)
&lt;/h3&gt;

&lt;p&gt;Building a robust evaluation pipeline at the edge is not about just wiring APIs together; it is about respecting the physical limits of your compute environment and the failure modes of generative models.&lt;/p&gt;

&lt;h4&gt;
  
  
  Subsystem 1: Tracing Without Leaking
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Pitfall:&lt;/strong&gt; Treating data masking as a boolean operation—send everything or send nothing. Sending nothing blinds your telemetry; sending everything violates data residency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Fix:&lt;/strong&gt; The masking hook isn't a toggle; it’s a transformation function. We can store a derived, safe projection of the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Hidden Pitfall:&lt;/strong&gt; If you simply hash the text, you lose all semantic value for your downstream judge. If you extract entities, you risk accidentally leaking PII inside those entities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Definitive Fix:&lt;/strong&gt; We emit structurally safe telemetry. No transcripts, no raw claims. We emit hashes for correlation, array lengths for shape validation, and enums for state.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;hideInputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({}),&lt;/span&gt; 
  &lt;span class="c1"&gt;// Store a cryptographically safe, structural projection&lt;/span&gt;
  &lt;span class="na"&gt;hideOutputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;cards&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;projectForEval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt; 
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;projectForEval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;verdicts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;// Strict enum (SUPPORTED, CONTRADICTED)&lt;/span&gt;
      &lt;span class="na"&gt;claimHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;         &lt;span class="c1"&gt;// Correlation without exposure&lt;/span&gt;
      &lt;span class="na"&gt;sourceDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// FQDNs only, no paths&lt;/span&gt;
      &lt;span class="na"&gt;evidenceLen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// Shape validation&lt;/span&gt;
    &lt;span class="p"&gt;})),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows our online evaluators to run cheap, deterministic checks (e.g., &lt;em&gt;Did every card cite a valid domain?&lt;/em&gt;) without exposing a single sensitive byte.&lt;/p&gt;

&lt;h4&gt;
  
  
  Subsystem 2: The LLM Judge &amp;amp; Edge Constraints
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Pitfall:&lt;/strong&gt; To check if evidence is actually faithful to a transcript, an LLM judge &lt;em&gt;must&lt;/em&gt; see the text. To keep the text in the EU, I initially ran the judge synchronously inside my Cloudflare Worker: &lt;code&gt;await judge(card, sources);&lt;/code&gt;. This instantly triggered CPU and wall-clock timeouts. Cloudflare Workers are built for fast I/O, not blocking for 4 seconds while an LLM grades homework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Fix:&lt;/strong&gt; Decouple the evaluation from the critical path using Cloudflare's &lt;code&gt;ctx.waitUntil()&lt;/code&gt;, allowing the worker to return the user's response immediately while the judge runs in the background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Hidden Pitfall:&lt;/strong&gt; &lt;strong&gt;The Poison Pill.&lt;/strong&gt; LLMs are non-deterministic. If your background judge hallucinates malformed JSON or markdown backticks, &lt;code&gt;const { score } = JSON.parse(llmOutput)&lt;/code&gt; will throw a runtime exception. Because this is happening in &lt;code&gt;waitUntil()&lt;/code&gt;, the error is swallowed, and your telemetry pipeline silently drops the trace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Definitive Fix:&lt;/strong&gt; Shift to an asynchronous queue with strict parsing and Dead Letter Queues (DLQ). The edge worker drops the task onto a Cloudflare Queue. A separate background consumer processes the LLM judgment with defensive validation (e.g., Zod). If the LLM returns garbage, it fails the parse and is routed to a DLQ, preserving the pipeline's integrity.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Edge Worker: Fire and forget. Never block the user.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handleMeeting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Offload evaluation to a background queue&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EVAL_QUEUE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentRunId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sources&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;span class="c1"&gt;// Queue Consumer: Defensive parsing.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
         &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawLLMOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runLocalEUJudge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

         &lt;span class="c1"&gt;// DEFENSIVE: Never trust LLM output structure&lt;/span&gt;
         &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;EvalSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawLLMOutput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
         &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
             &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DLQ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rawLLMOutput&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
             &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
         &lt;span class="p"&gt;}&lt;/span&gt;

         &lt;span class="c1"&gt;// DEFENSIVE: Do not block main edge traffic on third-party API limits&lt;/span&gt;
         &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LANGSMITH&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createFeedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
             &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;faithfulness&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
             &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; 
         &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
         &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Eval pipeline failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Subsystem 3: The Prompt Versioning Illusion
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Pitfall:&lt;/strong&gt; The rubric that dictates whether a claim is "supported" or "contradicted" lives in the prompt. Prompts are usually treated as disposable strings. LangSmith's Prompt Hub offers a neat solution: a UI to edit prompts and a &lt;code&gt;:production&lt;/code&gt; label to pull them dynamically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Fix:&lt;/strong&gt; Fetch the &lt;code&gt;:production&lt;/code&gt; prompt at runtime so the application always uses the latest logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Hidden Pitfall:&lt;/strong&gt; Fetching a prompt mid-request on a stateless edge node is a blocking network call on the hot path. It adds 100ms+ of latency to every interaction and creates a single point of failure. If the Hub goes down, your app goes down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Definitive Fix:&lt;/strong&gt; Treat prompts strictly as immutable source code. You cannot rely on a third-party UI state to dictate edge execution reality. CI/CD pulls the specific, version-pinned prompt at &lt;em&gt;build time&lt;/em&gt;, writes it to a constant, and bundles it.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;"One-click rollback" via a UI label is a dangerous illusion in distributed systems. If the prompt is baked into the build, changing the label does nothing until the CDN finishes propagating the new deployment. Architecture must respect the physical reality of the deployment pipeline.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The Takeaway
&lt;/h3&gt;

&lt;p&gt;Whiteboard architectures assume networks are perfectly reliable, external APIs never degrade, and LLMs always return beautifully formatted JSON. Production environments laugh at these assumptions.&lt;/p&gt;

&lt;p&gt;Online evaluations are not free. They cost compute, they cost latency, and they introduce entirely new failure domains into your infrastructure. Building an LLM app requires epistemic humility—accepting that the model &lt;em&gt;will&lt;/em&gt; fail, the judge &lt;em&gt;will&lt;/em&gt; hallucinate, and the network &lt;em&gt;will&lt;/em&gt; stall.&lt;/p&gt;

&lt;p&gt;By pushing heavy evaluations to asynchronous queues, defensively parsing every output like it's hostile user input, and binding prompts to immutable build artifacts, we turn a fragile "happy path" demo into a hardened, mechanically sound system. It’s boring plumbing, but boring plumbing is the only thing that survives production.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>monitoring</category>
      <category>privacy</category>
      <category>testing</category>
    </item>
    <item>
      <title>Stop Shipping Bloat: 7 Steps to 15x Faster, 90% Smaller Docker Builds</title>
      <dc:creator>M Hossein</dc:creator>
      <pubDate>Thu, 11 Jun 2026 06:01:00 +0000</pubDate>
      <link>https://dev.to/_mh/stop-shipping-bloat-7-steps-to-15x-faster-90-smaller-docker-builds-4161</link>
      <guid>https://dev.to/_mh/stop-shipping-bloat-7-steps-to-15x-faster-90-smaller-docker-builds-4161</guid>
      <description>&lt;p&gt;We’ve all seen it: a simple service wrapped in a Docker image that tips the scales at &lt;strong&gt;1.2GB&lt;/strong&gt; and drags down CI/CD pipelines with a &lt;strong&gt;4-minute build time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It’s a massive waste of storage, bandwidth, and engineering time. It doesn't have to be this way. With a few intentional tweaks to your &lt;code&gt;Dockerfile&lt;/code&gt;, you can drop that image down to under &lt;strong&gt;80MB&lt;/strong&gt; and cut build times to less than &lt;strong&gt;20 seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is a practical checklist to optimize your container builds, moving from a bloated, slow configuration to a lean, production-ready image.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Leverage Multi-Stage Builds
&lt;/h2&gt;

&lt;p&gt;Your compiler, development dependencies, and local build caches have no business being in your production environment. Multi-stage builds allow you to install tools and compile your application in an initial heavy stage, then copy &lt;em&gt;only&lt;/em&gt; the compiled artifacts into a completely fresh, minimal final runtime stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Pick a Smaller Base &amp;amp; Pin Your Versions
&lt;/h2&gt;

&lt;p&gt;Using a generic tag like &lt;code&gt;node:20&lt;/code&gt; pulls in a massive Debian image (~1.1GB) packed with build utilities you rarely need in production. Moving to &lt;code&gt;node:20-slim&lt;/code&gt; (~240MB) or &lt;code&gt;node:20-alpine&lt;/code&gt; (~135MB) dramatically shrinks your baseline.&lt;/p&gt;

&lt;p&gt;Additionally, &lt;strong&gt;never use floating tags&lt;/strong&gt; like &lt;code&gt;latest&lt;/code&gt; or &lt;code&gt;20-alpine&lt;/code&gt;. If the underlying image updates overnight, your builds can break with zero code changes. Always pin explicit versions for both the language runtime and the OS base.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Order Layers by Invalidation Frequency
&lt;/h2&gt;

&lt;p&gt;Docker caches layers sequentially. If a layer changes, &lt;strong&gt;every subsequent layer is invalidated&lt;/strong&gt; and forced to rebuild from scratch.&lt;/p&gt;

&lt;p&gt;Since your source code changes on every single commit, but your package dependencies don't, you should copy your dependency manifests and install packages &lt;em&gt;before&lt;/em&gt; introducing your actual application code. This is the single biggest win for CI/CD performance.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Antipattern vs. The Optimized Pattern
&lt;/h2&gt;

&lt;p&gt;Let's look at a typical, inefficient &lt;code&gt;Dockerfile&lt;/code&gt; versus an optimized version incorporating these rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Bloated Approach (What to Avoid)
&lt;/h3&gt;

&lt;p&gt;This single-stage approach copies everything at once, runs as &lt;code&gt;root&lt;/code&gt;, uses a massive floating base image, and busts the cache on every minor code tweak.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1.1GB Floating Base&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20&lt;/span&gt;

&lt;span class="c"&gt;# Everything is copied early - any code change busts the cache for dependency installs&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# Massive node_modules and build tools stay in the final image&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="c"&gt;# Runs as root by default&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/main.js"]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Optimized Approach (Production-Ready)
&lt;/h3&gt;

&lt;p&gt;Here is the exact same application rewritten using multi-stage builds, an Alpine base, pinned versions, correct layer ordering, non-root execution, and chained commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ==========================================&lt;/span&gt;
&lt;span class="c"&gt;# STAGE 1: Build &amp;amp; Compilation&lt;/span&gt;
&lt;span class="c"&gt;# ==========================================&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20.11.1-alpine3.19&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Copy dependency files first to utilize cached layers&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;

&lt;span class="c"&gt;# Install all dependencies (including devDeps for build)&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci

&lt;span class="c"&gt;# Copy the rest of the application source code&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# Build the production artifact&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="c"&gt;# ==========================================&lt;/span&gt;
&lt;span class="c"&gt;# STAGE 2: Lightweight Production Runtime&lt;/span&gt;
&lt;span class="c"&gt;# ==========================================&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20.11.1-alpine3.19&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runner&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Set production environment flags&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NODE_ENV=production&lt;/span&gt;

&lt;span class="c"&gt;# Copy only the necessary runtime configuration and compiled artifacts&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;

&lt;span class="c"&gt;# Chained RUN: Install production-only dependencies and clean npm cache in one layer&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--only&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm cache clean &lt;span class="nt"&gt;--force&lt;/span&gt;

&lt;span class="c"&gt;# Copy compiled JavaScript from the builder stage&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/dist ./dist&lt;/span&gt;

&lt;span class="c"&gt;# Run as a non-privileged system user instead of root&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; node&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/main.js"]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Don't Skip the &lt;code&gt;.dockerignore&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If you don't explicitly ignore files, your local build context copies everything in your directory directly into the Docker daemon. This means your massive local &lt;code&gt;node_modules&lt;/code&gt;, heavy &lt;code&gt;.git&lt;/code&gt; history, temporary logs, and local environment variables are sent to the build context, needlessly breaking your cache.&lt;/p&gt;

&lt;p&gt;Spend 10 seconds creating a &lt;code&gt;.dockerignore&lt;/code&gt; file in your root directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node_modules
.git
.github
.env
*.log
npm-debug.log*
dist
build
.coverage

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Chain Your &lt;code&gt;RUN&lt;/code&gt; Commands
&lt;/h2&gt;

&lt;p&gt;Every single &lt;code&gt;RUN&lt;/code&gt;, &lt;code&gt;COPY&lt;/code&gt;, and &lt;code&gt;ADD&lt;/code&gt; instruction in a &lt;code&gt;Dockerfile&lt;/code&gt; creates a new read-only layer. If you install an OS package on one line and delete its cache on the next line, that deleted data &lt;strong&gt;is still stored&lt;/strong&gt; in the underlying layer history.&lt;/p&gt;

&lt;p&gt;To actually shrink your image size, you must install, utilize, and clean up within a single chained &lt;code&gt;RUN&lt;/code&gt; instruction using &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# DO NOT DO THIS: The apt cache lives forever in layer history&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="c"&gt;# DO THIS: Cleanup happens in the exact same layer mutation&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary Checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Primary Benefit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-Stage Builds&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Separate compiling from running via &lt;code&gt;AS builder&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;Drops image size by omitting build-only tooling.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Slim Pinned Bases&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Use tags like &lt;code&gt;node:20.11.1-alpine3.19&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;Drops baseline footprint; prevents floating breakages.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Smart Layer Ordering&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Copy manifests and run installs &lt;em&gt;before&lt;/em&gt; copying source.&lt;/td&gt;
&lt;td&gt;Keeps expensive installation steps cached in CI/CD.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Add `.dockerignore&lt;/strong&gt;`&lt;/td&gt;
&lt;td&gt;Strip out local tooling, hidden files, and &lt;code&gt;node_modules&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;Speeds up build context initialization; protects cache.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Chain Commands&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Combine commands and cleanups (&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;) in single &lt;code&gt;RUN&lt;/code&gt; lines.&lt;/td&gt;
&lt;td&gt;Minimizes unnecessary filesystem layer overhead.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Drop Privileges&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Switch context via &lt;code&gt;USER node&lt;/code&gt; or an equivalent non-root UID.&lt;/td&gt;
&lt;td&gt;Mitigates severe container-escape security vulnerabilities.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A Note on ML and Heavy Pipelines:&lt;/strong&gt; While the examples above target Node.js, these exact structural patterns apply identically to Python or Go. Poor layer ordering and careless copying of dataset files or massive build-time dependencies (like heavy C++ bindings or CUDA build-essential wrappers) are often the silent killers of CI/CD performance in machine learning and data pipelines. Fix your layer sequences, isolate your dependencies, and keep your containers lean.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
    </item>
    <item>
      <title>Multi-VPS Distributed n8n Cluster on Ubuntu 24.04 with an IPIP Tunnel</title>
      <dc:creator>M Hossein</dc:creator>
      <pubDate>Wed, 10 Jun 2026 06:07:00 +0000</pubDate>
      <link>https://dev.to/_mh/multi-vps-distributed-n8n-cluster-on-ubuntu-2404-with-an-ipip-tunnel-1i4i</link>
      <guid>https://dev.to/_mh/multi-vps-distributed-n8n-cluster-on-ubuntu-2404-with-an-ipip-tunnel-1i4i</guid>
      <description>&lt;p&gt;When running automation tools like &lt;strong&gt;n8n&lt;/strong&gt; for personal or production workflows, you quickly run into resource walls if you stick to the default configuration. A standalone SQLite setup can experience performance drops under load, and heavy processing inside JavaScript/Python nodes can easily stall the primary web service or exhaust single-server resources.&lt;/p&gt;

&lt;p&gt;In this guide, we will step through how to architecture a decentralized, enterprise-grade n8n cluster split across &lt;strong&gt;two separate Ubuntu 24.04 VPS instances&lt;/strong&gt; using the open-source community edition.&lt;/p&gt;

&lt;p&gt;We will completely isolate the execution engine from the control plane using a lightweight &lt;strong&gt;IPIP (IP-in-IP) tunnel&lt;/strong&gt;, proxying incoming traffic via &lt;strong&gt;Nginx&lt;/strong&gt;, and managing tasks asynchronously through a &lt;strong&gt;PostgreSQL 16&lt;/strong&gt; and &lt;strong&gt;Redis 7&lt;/strong&gt; backend backbone.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Target Architecture
&lt;/h2&gt;

&lt;p&gt;Instead of over-engineering the infrastructure with complex orchestrators, we split our environment into two lightweight planes over a private network connection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VPS 1: The Control Plane (Main Node)&lt;/strong&gt; – Handles the web UI, external webhook endpoints via Nginx SSL, the PostgreSQL database, and the Redis message broker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPS 2: The Execution Plane (Worker Node)&lt;/strong&gt; – A stateless compute node dedicated purely to listening to the Redis queue, crunching complex data, and reporting back via the private tunnel network.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: Base Server Prep &amp;amp; Official Docker Installation
&lt;/h2&gt;

&lt;p&gt;We begin by prepping both &lt;strong&gt;Ubuntu 24.04 LTS&lt;/strong&gt; machines with baseline utilities, Python dependencies, and the official Docker engine repository.&lt;/p&gt;

&lt;p&gt;Run these commands on &lt;strong&gt;both servers&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Update base system packages&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;span class="c"&gt;# Install essential dependencies&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl wget unzip git vim build-essential net-tools

&lt;span class="c"&gt;# Set up Python runtimes&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; python3 python3-pip python3-dev

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Installing Docker using the Modern Apt Sources Method
&lt;/h3&gt;

&lt;p&gt;Ubuntu 24.04 drops legacy &lt;code&gt;apt-key&lt;/code&gt; procedures. We install the official engine using the modern &lt;code&gt;.sources&lt;/code&gt; system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add Docker's official GPG key&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ca-certificates curl
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/apt/keyrings
&lt;span class="nb"&gt;sudo &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/ubuntu/gpg &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/docker.asc
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;a+r /etc/apt/keyrings/docker.asc

&lt;span class="c"&gt;# Register the Docker repository configuration using DEB822 format&lt;/span&gt;
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/docker.sources &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="sh"&gt;(. /etc/os-release &amp;amp;&amp;amp; echo "&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="sh"&gt;{UBUNTU_CODENAME:-&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="sh"&gt;VERSION_CODENAME}")
Components: stable
Architectures: &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="sh"&gt;(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Install the engine components and the compose plugin&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2: Provisioning a Persistent Private IPIP Tunnel
&lt;/h2&gt;

&lt;p&gt;To safely expose PostgreSQL and Redis across our servers without leaving open holes to the public internet, we build a private point-to-point network using an kernel-level IPIP tunnel.&lt;/p&gt;

&lt;p&gt;We want &lt;code&gt;VPS 1 (Main)&lt;/code&gt; to live on private IP &lt;code&gt;10.0.0.1&lt;/code&gt; and &lt;code&gt;VPS 2 (Worker)&lt;/code&gt; on &lt;code&gt;10.0.0.2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Create the following script named &lt;code&gt;setup-tunnel.sh&lt;/code&gt; on &lt;strong&gt;both nodes&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Usage: sudo ./setup-tunnel.sh &amp;lt;MAIN_PUBLIC_IP&amp;gt; &amp;lt;WORKER_PUBLIC_IP&amp;gt; --install&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EUID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"❌ Please run as root (sudo)."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;MAIN_IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nv"&gt;WORKER_IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;
&lt;span class="nv"&gt;INSTALL_PERSISTENCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false

&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"--install"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;INSTALL_PERSISTENCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="k"&gt;fi

&lt;/span&gt;configure_tunnel&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"⚙️ Initializing IPIP Tunnel Configuration..."&lt;/span&gt;
  modprobe ipip

  &lt;span class="k"&gt;if &lt;/span&gt;ip &lt;span class="nb"&gt;link &lt;/span&gt;show tun0 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;ip &lt;span class="nb"&gt;link set &lt;/span&gt;tun0 down
    ip tunnel del tun0
  &lt;span class="k"&gt;fi

  if &lt;/span&gt;ip addr | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MAIN_IP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"🖥️  Configuring: MAIN VPS"&lt;/span&gt;
    ip tunnel add tun0 mode ipip remote &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WORKER_IP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nb"&gt;local&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MAIN_IP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; ttl 255
    ip addr add 10.0.0.1/30 dev tun0
    ip &lt;span class="nb"&gt;link set &lt;/span&gt;tun0 up
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✅ Tunnel 'tun0' is UP. Local private IP: 10.0.0.1"&lt;/span&gt;
  &lt;span class="k"&gt;elif &lt;/span&gt;ip addr | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WORKER_IP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"👷 Configuring: WORKER VPS"&lt;/span&gt;
    ip tunnel add tun0 mode ipip remote &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MAIN_IP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nb"&gt;local&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WORKER_IP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; ttl 255
    ip addr add 10.0.0.2/30 dev tun0
    ip &lt;span class="nb"&gt;link set &lt;/span&gt;tun0 up
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✅ Tunnel 'tun0' is UP. Local private IP: 10.0.0.2"&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"❌ Error: Public IP does not match local interfaces."&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

configure_tunnel

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSTALL_PERSISTENCE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;SCRIPT_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/usr/local/bin/ipip-tunnel-setup.sh"&lt;/span&gt;
  &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;chmod&lt;/span&gt; +x &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; &amp;gt; /etc/systemd/system/ipip-tunnel.service
[Unit]
Description=Maintain Persistent IPIP Tunnel between n8n Nodes
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_PATH&lt;/span&gt;&lt;span class="sh"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$MAIN_IP&lt;/span&gt;&lt;span class="sh"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$WORKER_IP&lt;/span&gt;&lt;span class="sh"&gt;

[Install]
WantedBy=multi-user.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;  systemctl daemon-reload
  systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;ipip-tunnel.service
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"🌟 Systemd persistence initialized."&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Execute this script on &lt;strong&gt;both servers&lt;/strong&gt; using your public IPs to establish the link permanently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x setup-tunnel.sh
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./setup-tunnel.sh &amp;lt;MAIN_PUBLIC_IP&amp;gt; &amp;lt;WORKER_PUBLIC_IP&amp;gt; &lt;span class="nt"&gt;--install&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the interface link by runnning a quick check from your main node: &lt;code&gt;ping -c 3 10.0.0.2&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Deploying the Control Plane (VPS 1)
&lt;/h2&gt;

&lt;p&gt;With our private tunnel channel open, we configure the orchestration layer on the Main server. Create a directory at &lt;code&gt;/home/n8n/&lt;/code&gt; to hold your configurations.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Environment Setup (&lt;code&gt;/home/n8n/.env&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;N8N_HOST=n8n.your-domain.com
N8N_PORT=5678

GENERIC_TIMEZONE=Europe/Vienna
N8N_ENCRYPTION_KEY=generate_a_random_long_string_here

POSTGRES_USER=n8n_admin
POSTGRES_PASSWORD=choose_a_secure_db_password_here
POSTGRES_DB=n8n_storage

REDIS_PASSWORD=choose_a_secure_redis_password_here

N8N_DIAGNOSTICS_ENABLED=false
N8N_PERSONALIZATION_ENABLED=false

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. The Infrastructure Deployment (&lt;code&gt;/home/n8n/docker-compose.yml&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Note that modern Docker Compose specs omit the obsolete &lt;code&gt;version&lt;/code&gt; string attribute. We explicitly map database and queue services strictly onto the secure tunnel interface IP (&lt;code&gt;10.0.0.1&lt;/code&gt;), ensuring zero external exposures.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n_postgres&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.0.0.1:5432:5432"&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_DB}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1G&lt;/span&gt;

  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n_redis&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-server --requirepass ${REDIS_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.0.0.1:6379:6379"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis_data:/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-cli"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-a"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${REDIS_PASSWORD}"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;n8n_main&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.n8n.io/n8nio/n8n:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n_core&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:${N8N_PORT}:5678"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EXECUTIONS_MODE=queue&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QUEUE_BULL_REDIS_HOST=redis&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QUEUE_BULL_REDIS_PORT=6379&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QUEUE_BULL_REDIS_PASSWORD=${REDIS_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_TYPE=postgresdb&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_HOST=postgres&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_PORT=5432&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_DATABASE=${POSTGRES_DB}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_USER=${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GENERIC_TIMEZONE=${GENERIC_TIMEZONE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TZ=${GENERIC_TIMEZONE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_HOST=${N8N_HOST}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PROTOCOL=https&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEBHOOK_URL=https://${N8N_HOST}/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EXECUTIONS_DATA_PRUNE=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EXECUTIONS_DATA_MAX_AGE=168&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_DIAGNOSTICS_ENABLED=false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PERSONALIZATION_ENABLED=false&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;n8n_data:/home/node/.n8n&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;n8n_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Launch the core stack on VPS 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: Configure Nginx Reverse Proxy with WebSockets
&lt;/h2&gt;

&lt;p&gt;n8n uses persistent WebSocket connections to update progress inside your browser canvas UI. If Nginx proxy buffering isn't optimized, your UI will become unreadably laggy or constantly drop connections.&lt;/p&gt;

&lt;p&gt;Create an Nginx server block at &lt;code&gt;/etc/nginx/sites-available/n8n&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="s"&gt;[::]:80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;n8n.your-domain.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;client_max_body_size&lt;/span&gt; &lt;span class="mi"&gt;50M&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:5678&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;# WebSocket support configurations&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;"upgrade"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;# Disable buffering to avoid streaming timeouts&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_buffering&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_read_timeout&lt;/span&gt; &lt;span class="s"&gt;24h&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_send_timeout&lt;/span&gt; &lt;span class="s"&gt;24h&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable the configuration block and use Certbot to register a Let's Encrypt SSL certificate automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx

&lt;span class="c"&gt;# Run Certbot to handle automatic HTTP -&amp;gt; HTTPS redirects&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot &lt;span class="nt"&gt;--nginx&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; n8n.your-domain.com

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 5: Deploying the Execution Worker (VPS 2)
&lt;/h2&gt;

&lt;p&gt;The execution worker node is completely stateless, meaning it needs no persistent storage databases. It only requires a minimal Compose setup pointing back across the tunnel to VPS 1.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create &lt;code&gt;/home/n8n/.env&lt;/code&gt; on VPS 2
&lt;/h3&gt;

&lt;p&gt;Copy your environment details from VPS 1 to ensure that credential cryptographic keys and database definitions match exactly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GENERIC_TIMEZONE=Europe/Vienna
N8N_ENCRYPTION_KEY=6uwhZTfJM80Hysuow5ge27r8xLtkFNWq
POSTGRES_USER=n8n_admin
POSTGRES_PASSWORD=WfVKtYp37bMDT6daPaTDSUYucBhfUw32
POSTGRES_DB=n8n_storage
REDIS_PASSWORD=8sHkQV3lpEc0yD4emUSn0oka9PrpdJ1h

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Create &lt;code&gt;/home/n8n/docker-compose.yml&lt;/code&gt; on VPS 2
&lt;/h3&gt;

&lt;p&gt;We append &lt;code&gt;command: worker&lt;/code&gt; to shift the container's operational blueprint into an isolated worker state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;n8n_worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.n8n.io/n8nio/n8n:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n_worker_remote&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worker&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EXECUTIONS_MODE=queue&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QUEUE_BULL_REDIS_HOST=10.0.0.1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QUEUE_BULL_REDIS_PORT=6379&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QUEUE_BULL_REDIS_PASSWORD=${REDIS_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_TYPE=postgresdb&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_HOST=10.0.0.1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_PORT=5432&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_DATABASE=${POSTGRES_DB}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_USER=${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GENERIC_TIMEZONE=${GENERIC_TIMEZONE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TZ=${GENERIC_TIMEZONE}&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2G&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Launch the worker node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 6: Server Hardening &amp;amp; Security Policies
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;The Golden Rule of SSH Configuration:&lt;/strong&gt; Always test your new configurations in an entirely separate terminal window before closing your current active shell connection so you don't accidentally lock yourself out.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  1. Restrict Network Interfaces via UFW
&lt;/h3&gt;

&lt;p&gt;Configure standard server firewalls to lock out random external pings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On VPS 1 (Main Node):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default deny incoming
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default allow outgoing
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 22/tcp  &lt;span class="c"&gt;# Change if using a custom SSH port&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow &lt;span class="k"&gt;in &lt;/span&gt;on tun0
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;On VPS 2 (Worker Node):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default deny incoming
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default allow outgoing
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 22/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow &lt;span class="k"&gt;in &lt;/span&gt;on tun0
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Prevent Docker from Bypassing Your Firewall
&lt;/h3&gt;

&lt;p&gt;Docker manipulates system &lt;code&gt;iptables&lt;/code&gt; directly. To guarantee it respects your interface limits, explicitly verify your Docker routing daemon defaults:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/docker/daemon.json

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ensure &lt;code&gt;"iptables": true&lt;/code&gt; is explicitly present, then apply a restart using &lt;code&gt;sudo systemctl restart docker&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Polish the SSH Configuration
&lt;/h3&gt;

&lt;p&gt;Enforce cryptographic authentication over standard passwords:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/ssh/sshd_config

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update these properties to ensure safety:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PermitEmptyPasswords no
PermitRootLogin prohibit-password
PasswordAuthentication no
MaxAuthTries 3

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test using &lt;code&gt;sudo sshd -t&lt;/code&gt; and trigger a refresh via &lt;code&gt;sudo systemctl restart ssh&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verification &amp;amp; Monitoring
&lt;/h2&gt;

&lt;p&gt;Because the multi-node worker management interface is part of n8n's enterprise plan, you can easily verify that your cluster is healthy from the command line using your Redis backend.&lt;/p&gt;

&lt;p&gt;Execute a client list inspection inside your Redis service on &lt;strong&gt;VPS 1&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; n8n_redis redis-cli &lt;span class="nt"&gt;-a&lt;/span&gt; &amp;lt;YOUR_REDIS_PASSWORD&amp;gt; client list

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for the worker's network mapping signature in the connection stream output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id=51 addr=10.0.0.2:35510 ... name=bull:am9icw== ... cmd=brpoplpush

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The presence of the &lt;code&gt;brpoplpush&lt;/code&gt; command originating from your tunnel gateway IP &lt;code&gt;10.0.0.2&lt;/code&gt; means your stateless execution worker is successfully listening to your queue broker. Your secure, distributed n8n cluster is now fully configured and running smoothly.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>distributedsystems</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Stop Guessing, Start Profiling: A Dev's Guide to Go Mechanics</title>
      <dc:creator>M Hossein</dc:creator>
      <pubDate>Tue, 09 Jun 2026 11:40:00 +0000</pubDate>
      <link>https://dev.to/_mh/stop-guessing-start-profiling-mechanical-sympathy-in-go-systems-49d0</link>
      <guid>https://dev.to/_mh/stop-guessing-start-profiling-mechanical-sympathy-in-go-systems-49d0</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: The Mysterious 2-Second Freeze
&lt;/h2&gt;

&lt;p&gt;Imagine your Go microservice is a chef in a busy kitchen. It processes orders (JSON payloads) super fast. Life is good, until every 15 minutes, the chef completely freezes for 2 seconds. Orders pile up. &lt;/p&gt;

&lt;p&gt;Why did the chef freeze? &lt;strong&gt;The Trash.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you parse JSON using the standard &lt;code&gt;encoding/json&lt;/code&gt; library, you create a lot of garbage (heap allocations). Go has a Garbage Collector (GC) that acts like a janitor. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚠️ The Myth:&lt;/strong&gt; Many engineers think the GC yells "Stop-The-World," making the chef freeze while the janitor sweeps. Modern Go has a &lt;em&gt;concurrent&lt;/em&gt; GC. Real STW pauses are sub-millisecond.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;✅ The Reality: Mark Assist.&lt;/strong&gt; &lt;br&gt;
If the chef makes trash faster than the janitor can sweep it, the Go runtime triggers &lt;em&gt;Mark Assist&lt;/em&gt;. The runtime literally hands the chef a broom and forces them to sweep instead of cooking. The chef isn't frozen; the chef is doing janitor work.&lt;/p&gt;
&lt;h3&gt;
  
  
  📊 Diagram 1: The Mark Assist Traffic Jam
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Normal:  Chef Cooks ➔ Makes Trash ➔ Janitor sweeps concurrently ➔ Fast! (20ms)

Spike:   Chef Cooks ➔ Makes TOO MUCH Trash ➔ Janitor falls behind
                |
         💥 RUNTIME: "Chef, drop the spatula, grab a broom!" (Mark Assist)
         💥 Chef spends 2 seconds sweeping instead of cooking 💥
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  The Right Way: Mechanical Sympathy
&lt;/h2&gt;

&lt;p&gt;We need to stop making trash so the chef never has to sweep. Here are the 3 big mechanical fixes you need to know.&lt;/p&gt;


&lt;h3&gt;
  
  
  Fix 1: Stop Making Trash (Size-Classed Pools)
&lt;/h3&gt;

&lt;p&gt;The standard &lt;code&gt;encoding/json&lt;/code&gt; library creates trash because it uses "reflection." &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; Use fast parsers (like &lt;code&gt;easyjson&lt;/code&gt;) and &lt;strong&gt;&lt;code&gt;sync.Pool&lt;/code&gt;&lt;/strong&gt;. A &lt;code&gt;sync.Pool&lt;/code&gt; is like a shared toolbox. You take a wrench out, use it, wipe it clean, and put it back. No trash!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚠️ Trap 1: Megamorphic Bloat&lt;/strong&gt;&lt;br&gt;
If you put a 4KB wrench in the toolbox, but someone stretches it to 60KB and puts it back, your toolbox permanently bloats. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚠️ Trap 2: The Black Hole &amp;amp; Allocation Roulette&lt;/strong&gt;&lt;br&gt;
To fix bloat, you might create separate toolboxes for Small and Large jobs. But what happens to a 4KB wrench that stretches to 5KB? &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you put it in the 64K pool, you play &lt;strong&gt;Allocation Roulette&lt;/strong&gt;: The next chef asks for a 60K wrench and gets your 5K one. It snaps, triggering a heap allocation.&lt;/li&gt;
&lt;li&gt;If you use exact equality checks (&lt;code&gt;if cap == 4096&lt;/code&gt;), you create a &lt;strong&gt;Black Hole&lt;/strong&gt;: A third-party library slices your buffer down to 3KB. It's still perfectly capable of fulfilling a 2KB request, but your code throws it in the trash. Your pool empties, forcing heap allocations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;✅ The Solution: Utility Ranges (The Half-Broken Tool)&lt;/strong&gt;&lt;br&gt;
Toolboxes need a "good enough" range. If a 4KB wrench gets filed down to 3KB, it’s still pretty useful. Put it back. But if it gets filed down to 500 bytes, it's useless for a 4KB job. Let the Garbage Collector throw it away.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pool4K&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pool64K&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;getBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;4096&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pool4K&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;65536&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pool64K&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="c"&gt;// Poison pill: Too big, don't pool.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;putBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c"&gt;// Wipe the wrench clean!&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// The 4K Toolbox: Keep it if it's between 2KB and 4KB&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;2048&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;4096&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;pool4K&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// The 64K Toolbox: Keep it if it's between 32KB and 64KB&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;32768&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;65536&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;pool64K&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// If it falls in the dead-zones (like 5KB), it doesn't fit either box. &lt;/span&gt;
    &lt;span class="c"&gt;// Let the Garbage Collector eat it.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(Note: We pool pointers `&lt;/em&gt;[]byte&lt;code&gt; because passing a slice &lt;/code&gt;[]byte&lt;code&gt; to an &lt;/code&gt;interface{}` creates a heap wrapper—defeating the whole purpose!)*&lt;/p&gt;




&lt;h3&gt;
  
  
  Fix 2: Protect the Database (The Cancellation Storm)
&lt;/h3&gt;

&lt;p&gt;When your app gets hit by Mark Assist, database queries slow down. You add timeouts (e.g., &lt;code&gt;context.WithTimeout(ctx, 500ms)&lt;/code&gt;). &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚠️ The Myth:&lt;/strong&gt; A context timeout "violently drops" the database connection. &lt;br&gt;
&lt;strong&gt;✅ The Reality:&lt;/strong&gt; When a context expires, &lt;code&gt;database/sql&lt;/code&gt; drivers (like Postgres) must send an Out-Of-Band (OOB) cancellation request. To do this, the driver opens a &lt;strong&gt;brand new TCP connection&lt;/strong&gt; just to say "cancel my previous request!"&lt;/p&gt;
&lt;h3&gt;
  
  
  📊 Diagram 2: The Self-Inflicted DDoS
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. APP GETS MARK ASSIST (Slows down)
   App ➔ 500ms Timeout hits! Query must be cancelled.

2. THE CANCEL STORM:
   App ➔ Opens NEW TCP Connection ➔ "Hey DB, Cancel Query 1"
   App ➔ Opens NEW TCP Connection ➔ "Hey DB, Cancel Query 2"
   ... (5,000 new TLS handshakes attack the Database CPU) 💥
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;✅ The Solution:&lt;/strong&gt; Put a centralized proxy (like PgBouncer) in the middle. The proxy absorbs the violent cancellation storms from your app, but keeps a calm, steady set of warm connections open to the database. &lt;em&gt;(Don't use a sidecar per pod—200 sidecars still attack the DB!)&lt;/em&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  Fix 3: Don't Break the Chain (Failing Closed)
&lt;/h3&gt;

&lt;p&gt;If a user sends a 50MB "Poison Pill" JSON, the standard answer is: "Send it to a Dead Letter Queue (DLQ) and keep processing." &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚠️ The Trap:&lt;/strong&gt; This destroys ordered message queues (like Kafka CDC). If you skip "Step 1: Create Account" because it was a Poison Pill, and process "Step 2: Update Account", your database is corrupted. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;✅ The Solution: Entity Quarantine&lt;/strong&gt;&lt;br&gt;
Halt processing for &lt;em&gt;just that specific user ID&lt;/em&gt;, put that ID in a quarantine list (like Redis), and skip future messages for that user until a human fixes it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚠️ The Distributed State Fallacy (The Radio Check):&lt;/strong&gt; What happens when the quarantine list (Redis) goes down?&lt;br&gt;
You might think: &lt;em&gt;"Just make the app sleep or return an error until Redis comes back!"&lt;/em&gt; &lt;br&gt;
&lt;strong&gt;This is a fatal mistake.&lt;/strong&gt; Kafka uses a "Radio Check" (a heartbeat). If your app goes completely silent because it's sleeping or frozen, the Kafka broker assumes your app died. It violently kicks your app out of the group and gives the broken messages to another pod... which also freezes and dies. You will crash your entire cluster in a Rebalance Storm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;✅ The Definitive Fix: Put them on Hold&lt;/strong&gt;&lt;br&gt;
When dealing with strict order, you cannot guess. If Redis is down, you must &lt;strong&gt;Pause&lt;/strong&gt; the queue, but keep the radio heartbeat alive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is entity in Quarantine?
 ➔ Yes: Skip message.
 ➔ No: Process message.
 ➔ Redis Down: Tell Kafka to "Pause()" this specific partition. 
               Keep polling the radio to say "I'm alive, just on hold." 
               Alert humans.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Fix 4: See the Ghost (Continuous Profiling)
&lt;/h3&gt;

&lt;p&gt;You can't fix what you can't see. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CI Guardrails:&lt;/strong&gt; Add &lt;code&gt;go test -benchmem&lt;/code&gt; to your pipeline. If a PR adds memory allocations on the critical path, it fails. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuous Profiling:&lt;/strong&gt; Use tools like Parca or Pyroscope to constantly take &lt;code&gt;pprof&lt;/code&gt; snapshots in production, and link them to your traces (OpenTelemetry).&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;The Pro Move:&lt;/strong&gt; When an alert fires, you click the exact trace that was slow, and it shows you the exact line of code that forced your app into Mark Assist. No guessing required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Mark Assist, not STW:&lt;/strong&gt; The GC forces your app to help clean. Stop making trash.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Utility Ranges:&lt;/strong&gt; A 3KB buffer is still a useful half-broken tool for a 2KB job. Use ranges, not exact equality.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Timeouts cause Storms:&lt;/strong&gt; Context cancellations open &lt;em&gt;new&lt;/em&gt; TCP connections. Use a centralized proxy to absorb the DDoS.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Keep the Radio Check:&lt;/strong&gt; If your quarantine check goes down, pause the partition but keep polling to prevent a cluster crash.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When you understand how the machine &lt;em&gt;actually&lt;/em&gt; works—Mark Assist, utility ranges, OOB cancellations, and heartbeats—you stop guessing and start engineering systems that survive production.&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>go</category>
      <category>performance</category>
      <category>sre</category>
    </item>
    <item>
      <title>The Blast Radius: Mentoring Senior Engineers into Systems Thinkers</title>
      <dc:creator>M Hossein</dc:creator>
      <pubDate>Mon, 08 Jun 2026 05:42:00 +0000</pubDate>
      <link>https://dev.to/_mh/the-blast-radius-mentoring-senior-engineers-into-systems-thinkers-56nd</link>
      <guid>https://dev.to/_mh/the-blast-radius-mentoring-senior-engineers-into-systems-thinkers-56nd</guid>
      <description>&lt;p&gt;The transition from Senior Engineer to Staff Engineer isn't just about writing more complex code. It is about expanding your field of vision. A Senior Engineer owns a service; a Staff Engineer owns the spaces &lt;em&gt;between&lt;/em&gt; the services.&lt;/p&gt;

&lt;p&gt;But how do you teach that? How do you take an incredibly talented coder who operates in a silo and turn them into a "Systems Thinker"?&lt;/p&gt;

&lt;p&gt;Let’s look at a classic engineering management scenario, the standard naive reaction, and the Staff-level approach to building a resilient engineering culture.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Scenario: The Siloed Senior Engineer
&lt;/h3&gt;

&lt;p&gt;Let's say you have a Senior Engineer on one of your product squads named David. David is fantastic at writing Go. He is the technical owner of the Receipt PDF Generation service. His code is spotless, his test coverage is 90%+, and he ships features on time.&lt;/p&gt;

&lt;p&gt;Last week, David pushed an optimization. He realized the PDF generator was making sequential database calls, so he rewrote the logic to aggressively parallelize the data fetching using Goroutines. He made the service 30% faster. He was thrilled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Consequence:&lt;/strong&gt; He didn't realize that by aggressively parallelizing the fetches, he effectively DDOS'd a shared internal database with concurrent reads. He exhausted the connection pool, which temporarily degraded the performance of a critical, Tier-1 compliance API that shares the same database.&lt;/p&gt;

&lt;p&gt;He simply didn't think about his downstream blast radius.&lt;/p&gt;




&lt;h3&gt;
  
  
  Phase 1: The Standard Management Reaction
&lt;/h3&gt;

&lt;p&gt;When incidents like this happen, the standard reaction in immature engineering organizations is to treat it as a behavioral problem:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Scolding:&lt;/strong&gt; A manager pulls David aside, tells him to "be more careful," and demands he test better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Vague Mandate:&lt;/strong&gt; David is told to "act more like a consultant" and "talk to other teams" to get a better image of what's going on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The Pitfall: The Culture of Fear &amp;amp; Friction&lt;/strong&gt;&lt;br&gt;
Scolding an engineer for an optimization creates a culture of fear. The next time David sees a way to improve the system, he will keep his head down to avoid getting yelled at. Furthermore, telling a squad engineer to vaguely "act as a consultant" creates massive friction with their Product Manager, who expects them to be burning down Jira tickets, not wandering around Slack asking other teams what they are doing.&lt;/p&gt;

&lt;p&gt;Worst of all, this reaction completely ignores the architectural failure.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The Staff-Level Insight:&lt;/strong&gt; Human error is never the root cause; it is merely the symptom of a fragile system. If a single engineer optimizing a PDF generator can bring down a Tier-1 API, your architecture is broken.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Phase 2: The Force Multiplier Framework
&lt;/h3&gt;

&lt;p&gt;As a Staff Engineer, your job is to use this incident as a Force Multiplier. You need to fix the architecture &lt;em&gt;and&lt;/em&gt; mentor the engineer. &lt;/p&gt;

&lt;p&gt;But first, you must reframe the failure for David before any public process begins. In your 1-on-1, you don't scold him. You validate his intent (making things faster) but challenge his context. You tell him: &lt;em&gt;"The optimization was great engineering; the lack of defensive consumption was a systems failure. Let's fix the system together."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;From there, here is the exact, actionable 4-step strategy to turn David from a Service Owner into a Systems Thinker over the next six months.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. The Blameless Post-Mortem (Reframing the Incident)
&lt;/h4&gt;

&lt;p&gt;You do not initiate a post-mortem to ask, "Why did David break the database?" You initiate a blameless post-mortem with David and the affected teams to ask a purely mechanical question:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Why did our architecture allow a single service to DDOS a shared database without rate limits or circuit breakers tripping?"&lt;/em&gt; This immediately shifts the focus from human guilt to system resilience. &lt;/p&gt;

&lt;h4&gt;
  
  
  2. Vulnerability as Leadership (The Tribe Meeting)
&lt;/h4&gt;

&lt;p&gt;We want David to present his work at the monthly engineering all-hands, but &lt;em&gt;not&lt;/em&gt; just to show off his cool parallelization code. That sends the message: "Look at my optimization, ignore the outage."&lt;/p&gt;

&lt;p&gt;Instead, you ask David to present the &lt;em&gt;entire incident&lt;/em&gt;. He walks through his optimization, explains the unexpected downstream database locks, and shares the system-level fixes. When a highly respected Senior Engineer stands up in front of the tribe and says, &lt;em&gt;"I brought down the API because I didn't think about the DB locks, and here is what I learned,"&lt;/em&gt; it creates massive psychological safety. It teaches the entire organization that it is okay to take risks, fail, and learn.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Structured Cross-Pollination (The RFC Process)
&lt;/h4&gt;

&lt;p&gt;You cannot just tell an engineer to "go see what other teams are doing." You must bake it into the engineering lifecycle—and you must protect their time to do it.&lt;/p&gt;

&lt;p&gt;Instead of a vague consultant role, bring David into the formal &lt;strong&gt;RFC (Request for Comments) / Design Doc process&lt;/strong&gt;. For the next three months, make David a mandatory reviewer on architecture design docs for the squads immediately upstream and downstream from him.&lt;/p&gt;

&lt;p&gt;This forces him to read about other systems &lt;em&gt;before&lt;/em&gt; they are built. He learns to spot API contract changes, database coupling, and capacity limits outside of his Go routines. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crucially, as the Staff Engineer, you must act as an umbrella.&lt;/strong&gt; When David’s PM complains that he is reviewing RFCs instead of burning down the sprint backlog, you step in. You translate the systems work into product terms: &lt;em&gt;"David isn't doing admin work; he's preventing the next P1 outage that will wipe out our sprint velocity for a week."&lt;/em&gt; This shields David from organizational friction and proves that systems thinking is valued over ticket-churning.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Operational Empathy ("You Build It, You Run It")
&lt;/h4&gt;

&lt;p&gt;Writing code in a silo is easy. Operating it in production is hard.&lt;/p&gt;

&lt;p&gt;If your developers are isolated from the operational consequences of their code, they will never become Systems Thinkers. You must advocate for putting Senior Developers on the PagerDuty incident rotation alongside your SREs.&lt;/p&gt;

&lt;p&gt;Nothing builds system-level empathy faster than waking up at 3:00 AM because another team's runaway retry-loop just exhausted the connections on your database. Shared operational pain is the ultimate catalyst for distributed systems thinking.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Payoff: Six Months Later
&lt;/h3&gt;

&lt;p&gt;If you execute this strategy, you don't just fix an architectural flaw; you fundamentally alter how an engineer views the world. &lt;/p&gt;

&lt;p&gt;Six months from now, a product manager will ask David to add a new feature to the PDF service that requires fetching user data from a newly acquired third-party API. &lt;/p&gt;

&lt;p&gt;The old David would have just written the integration, wrapped it in a Goroutine, and shipped it. &lt;/p&gt;

&lt;p&gt;The new David—the Systems Thinker—will pause. He'll check the API's rate limits. He'll add a semaphore to bound his concurrency, and a circuit breaker to fail fast. He'll ping the upstream squad to ensure his new polling pattern won't overflow their message queue. He will look beyond his service and own the space &lt;em&gt;between&lt;/em&gt; the services.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Takeaway
&lt;/h3&gt;

&lt;p&gt;Mentoring isn't just scheduling 1-on-1s and giving code review feedback. It is about intentionally designing the environment around the engineer.&lt;/p&gt;

&lt;p&gt;By leveraging blameless post-mortems, encouraging vulnerable leadership, injecting engineers into cross-team RFCs (while shielding them from PM friction), and exposing them to production reality through PagerDuty, you stop fighting human nature. Instead, you build an engineering culture where "Systems Thinking" is simply the path of least resistance.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>career</category>
      <category>leadership</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Architecting Strict Sequential Ordering in a Concurrent World</title>
      <dc:creator>M Hossein</dc:creator>
      <pubDate>Sun, 07 Jun 2026 06:42:00 +0000</pubDate>
      <link>https://dev.to/_mh/the-cryptographic-chain-reaction-enforcing-strict-ordering-in-a-concurrent-world-57kj</link>
      <guid>https://dev.to/_mh/the-cryptographic-chain-reaction-enforcing-strict-ordering-in-a-concurrent-world-57kj</guid>
      <description>&lt;p&gt;Imagine you are building a cloud-native backend for a high-frequency trading platform or a core banking ledger. To ensure mathematical immutability and prevent silent data tampering, compliance mandates that every transaction for a specific financial account must be cryptographically chained.&lt;/p&gt;

&lt;p&gt;This means the signature of Transaction #50 must explicitly include the cryptographic hash of Transaction #49. &lt;strong&gt;You cannot sign them out of order, and the backend is strictly responsible for generating and validating this chain.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This introduces a massive distributed systems headache: How do you enforce strict, sequential ordering while maintaining the concurrency required to scale a modern cloud architecture? Let's walk through the evolution of this system, deconstruct exactly how the standard event-driven approach fails in production, and examine the Staff-level architecture required to fix it.&lt;/p&gt;




&lt;h3&gt;
  
  
  Phase 1: The MVP &amp;amp; The Database Bottleneck
&lt;/h3&gt;

&lt;p&gt;In the early days, traffic is low. An account might see one transaction every few minutes. The "Happy Path" is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The API receives a deposit request for Account A.&lt;/li&gt;
&lt;li&gt;The API queries Postgres for the &lt;code&gt;last_signature_hash&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The API computes the new hash in memory: &lt;code&gt;SHA(last_hash + new_transaction_data)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The API writes the new transaction and updates the state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The Pitfall: The Thundering Herd&lt;/strong&gt;&lt;br&gt;
To prevent two concurrent requests from reading the same previous hash, you wrap the database operation in a pessimistic lock: &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;. This forces the database to serialize requests at the row level.&lt;br&gt;
When a massive partner bank initiates a bulk sync, dumping 5,000 transactions for a single corporate account onto the API in two seconds, 4,999 concurrent threads immediately hit the &lt;code&gt;FOR UPDATE&lt;/code&gt; lock and block. The database connection pool is instantly exhausted, latency spikes platform-wide, and the MVP dies.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The Insight:&lt;/strong&gt; The database must be your last line of defense, not your primary queueing mechanism. Contention must be solved upstream in memory.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h3&gt;
  
  
  Phase 2: The Event-Driven Reality (Step-by-Step Fixes)
&lt;/h3&gt;

&lt;p&gt;To protect the database, we introduce Kafka and Golang. We place a &lt;code&gt;ledger-events&lt;/code&gt; Kafka topic between the API and the database. By using &lt;code&gt;account_id&lt;/code&gt; as the Kafka message key, Kafka routes all traffic for a specific account to a single partition, ensuring it is processed by exactly one Go worker pod.&lt;/p&gt;

&lt;p&gt;This looks great on a whiteboard. But under the microscope of production reality, it is riddled with architectural gaps. Here is how we systematically uncover and solve them.&lt;/p&gt;
&lt;h4&gt;
  
  
  Step 1: The Ingestion Illusion &amp;amp; Operational Memory
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;The Pitfall: The Overdraft Race Condition&lt;/strong&gt;&lt;br&gt;
Keying messages by &lt;code&gt;account_id&lt;/code&gt; does not guarantee chronological order; Kafka only guarantees the order in which the broker &lt;em&gt;receives&lt;/em&gt; the messages. If a client issues a $50 deposit and a $120 withdrawal in the same millisecond, network latency might cause the withdrawal to hit Kafka first. If the backend accepts Kafka's order as absolute, the account overdrafts and the withdrawal is incorrectly rejected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix: Client-Dictated Sequence &amp;amp; In-Memory Buffering&lt;/strong&gt;&lt;br&gt;
Chronological order in finance is about business logic, not just hashing. The client &lt;em&gt;must&lt;/em&gt; provide a sequence ID (e.g., &lt;code&gt;seq_1&lt;/code&gt;, &lt;code&gt;seq_2&lt;/code&gt;). The Go worker enforces this sequence. If Kafka delivers &lt;code&gt;seq_3&lt;/code&gt; before &lt;code&gt;seq_2&lt;/code&gt;, the Goroutine cannot process it. It must buffer &lt;code&gt;seq_3&lt;/code&gt; in memory and wait for &lt;code&gt;seq_2&lt;/code&gt; to arrive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Hidden Pitfall: The Auto-Commit Catastrophe&lt;/strong&gt;&lt;br&gt;
By buffering out-of-order messages in memory, we introduce a severe operational risk. If Kafka is set to auto-commit offsets, it will tell the broker "I have successfully processed up to &lt;code&gt;seq_3&lt;/code&gt;" simply because it read it off the partition. If the pod crashes while &lt;code&gt;seq_3&lt;/code&gt; is sitting in the memory buffer waiting for &lt;code&gt;seq_2&lt;/code&gt;, &lt;code&gt;seq_3&lt;/code&gt; is permanently lost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Final Fix:&lt;/strong&gt; You must disable auto-commit. The Go worker must implement meticulous manual offset management, committing Kafka offsets &lt;em&gt;only&lt;/em&gt; after the expected sequence is successfully flushed to the database ledger. Furthermore, to prevent memory leaks from permanently stalled sequences (e.g., a client bug where &lt;code&gt;seq_2&lt;/code&gt; is never sent), the in-memory buffer must have a strict TTL. If the gap isn't filled within 60 seconds, the chain halts and triggers an SRE alert.&lt;/p&gt;
&lt;h4&gt;
  
  
  Step 2: The Database Constraint &amp;amp; The Infinite Ledger
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;The Pitfall: The "Missing Tail" and the UNIQUE Loophole&lt;/strong&gt;&lt;br&gt;
To process a transaction, the worker needs the last hash. Querying the ledger directly (&lt;code&gt;SELECT ... ORDER BY created_at DESC LIMIT 1&lt;/code&gt;) creates an $O(N)$ index scan bottleneck on high-volume accounts. If we fix this by keeping an &lt;code&gt;account_state&lt;/code&gt; table as an $O(1)$ cache, we face a new problem: If a pod crashes and a Kafka rebalance occurs, two workers might read the cached state concurrently and attempt to build off the same hash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix: The Dual-Table Pattern &amp;amp; Fork Prevention&lt;/strong&gt;&lt;br&gt;
We use two tables updated in a single atomic transaction. &lt;code&gt;account_state&lt;/code&gt; acts as our high-speed cache, and &lt;code&gt;ledger&lt;/code&gt; acts as our immutable history. We add a safety net to the ledger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;unique_chain_link&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;previous_hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Nuance:&lt;/em&gt; This &lt;code&gt;UNIQUE&lt;/code&gt; constraint prevents &lt;em&gt;forks&lt;/em&gt; (two transactions claiming the same parent), but it doesn't guarantee &lt;em&gt;continuity&lt;/em&gt; (ensuring the new hash actually links to the true tail). Continuity is guaranteed by Optimistic Concurrency Control (OCC) in Go. The worker reads the $O(1)$ cache, computes the hash, and attempts an &lt;code&gt;INSERT&lt;/code&gt; into the &lt;code&gt;ledger&lt;/code&gt;. If a rogue worker also read the stale state, the &lt;code&gt;UNIQUE&lt;/code&gt; constraint causes the second &lt;code&gt;INSERT&lt;/code&gt; to fail instantly—before touching the &lt;code&gt;account_state&lt;/code&gt; cache. The Go worker catches this error, fetches the fresh cache state, computes a new hash, and retries cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Hidden Pitfall: The Index Bloat&lt;/strong&gt;&lt;br&gt;
An immutable &lt;code&gt;ledger&lt;/code&gt; table will grow infinitely. The &lt;code&gt;UNIQUE (account_id, previous_hash)&lt;/code&gt; constraint requires a B-Tree index. On a high-frequency ledger with billions of rows, this index swells beyond available RAM, causing &lt;code&gt;INSERT&lt;/code&gt; performance to degrade exponentially due to disk I/O.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Final Fix:&lt;/strong&gt; Implement Postgres Table Partitioning. Partition the ledger by date (e.g., &lt;code&gt;ledger_2026_06&lt;/code&gt;) or account hash range. This keeps the active index sizes small and strictly in memory, preserving sub-millisecond insert times.&lt;/p&gt;
&lt;h4&gt;
  
  
  Step 3: Concurrency Anti-Patterns
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;The Pitfall: The Goroutine Memory Leak&lt;/strong&gt;&lt;br&gt;
To isolate processing per account, the Go worker uses dynamic Goroutines fed by a &lt;code&gt;sync.Map&lt;/code&gt;. To clean up idle Goroutines, engineers often use a &lt;code&gt;select&lt;/code&gt; loop with a &lt;code&gt;time.After(15 * time.Minute)&lt;/code&gt; timeout. &lt;code&gt;time.After&lt;/code&gt; allocates a new timer channel on the heap &lt;em&gt;every single iteration&lt;/em&gt; of the loop, causing massive Garbage Collection (GC) pressure. Furthermore, &lt;code&gt;sync.Map&lt;/code&gt; degrades under high-frequency writes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix: RWMutex &amp;amp; The Timer Reset Pattern&lt;/strong&gt;&lt;br&gt;
Use a standard Go map protected by a &lt;code&gt;sync.RWMutex&lt;/code&gt;. Inside the Goroutine, instantiate exactly &lt;em&gt;one&lt;/em&gt; timer and defer its stop, resetting it on every loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"sync"&lt;/span&gt;

&lt;span class="c"&gt;// Dispatcher safely manages the dynamic worker channels for each account.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Dispatcher&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mu&lt;/span&gt;       &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RWMutex&lt;/span&gt;
    &lt;span class="n"&gt;channels&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Transaction&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewDispatcher&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Dispatcher&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Dispatcher&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;processAccountChain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dispatcher&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Dispatcher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Idiomatic Go: create ONE timer, clean up on exit&lt;/span&gt;
    &lt;span class="n"&gt;idleTimer&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewTimer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Minute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;idleTimer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;idleTimer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Minute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// Reuse the same object&lt;/span&gt;

        &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="c"&gt;// Process transaction sequentially...&lt;/span&gt;

        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;idleTimer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="c"&gt;// Lock map, delete channel, safely exit&lt;/span&gt;
            &lt;span class="n"&gt;dispatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dispatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accountID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;dispatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; 
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 4: The Edge Case Reality
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;The Pitfall: The Poison Pill under Strict Sequence&lt;/strong&gt;&lt;br&gt;
Transaction &lt;code&gt;seq_2&lt;/code&gt; in a batch of 500 contains a corrupted, unparseable JSON payload. Because we have firmly established that the client dictates the sequence, the backend &lt;em&gt;cannot&lt;/em&gt; simply drop &lt;code&gt;seq_2&lt;/code&gt; and chain &lt;code&gt;seq_3&lt;/code&gt; to &lt;code&gt;seq_1&lt;/code&gt;. Doing so legally invalidates the client's intended sequence and corrupts the financial state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix: The State-Backed DLQ &amp;amp; Chain Halt&lt;/strong&gt;&lt;br&gt;
You cannot fix a broken link in a strict sequence, nor can you skip it. The Go worker must:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Route the bad payload to a Dead Letter Queue (DLQ).&lt;/li&gt;
&lt;li&gt;Update the &lt;code&gt;account_state&lt;/code&gt; table in Postgres: &lt;code&gt;UPDATE account_state SET status = 'BLOCKED'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Drop all subsequent messages (like &lt;code&gt;seq_3&lt;/code&gt; and &lt;code&gt;seq_4&lt;/code&gt;) into a holding topic or reject them directly at the API layer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The chain halts immediately. An SRE or automated reconciliation process must investigate, notify the client of the specific sequence failure, and force the client to re-issue the transactions from the exact point of failure.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Takeaway
&lt;/h3&gt;

&lt;p&gt;When business logic requires strict, sequential cryptographic chaining, high-concurrency event-driven architectures naturally fight against the requirement.&lt;/p&gt;

&lt;p&gt;You cannot solve this by throwing more threads at the problem or locking down your database rows. You must sequence deterministically via client IDs, buffer appropriately at the ingestion layer, manage Kafka offsets manually, isolate processing via memory dispatchers, and leverage your partitioned database as a high-speed cache backed by an immutable, constraint-driven ledger. Architecture at this level is about anticipating the mechanical realities of the system, not just the happy path.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>distributedsystems</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Polyglot Monorepo Magic: TypeScript, Python, and Go in One Repo</title>
      <dc:creator>M Hossein</dc:creator>
      <pubDate>Sat, 06 Jun 2026 07:22:00 +0000</pubDate>
      <link>https://dev.to/_mh/polyglot-monorepo-magic-typescript-python-and-go-in-one-repo-298n</link>
      <guid>https://dev.to/_mh/polyglot-monorepo-magic-typescript-python-and-go-in-one-repo-298n</guid>
      <description>&lt;h2&gt;
  
  
  What is a Polyglot Monorepo?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;polyglot monorepo&lt;/strong&gt; is a single Git repository containing services and packages written in multiple programming languages. The alternative — a polyrepo — means one repo per service, per team, per language. When your frontend team, your Go API team, and your Python ML team all operate in separate repos, sharing contracts between them becomes a coordination problem.&lt;/p&gt;

&lt;p&gt;This repo is &lt;code&gt;beacon-monorepo&lt;/code&gt;. It's a real-time analytics platform. It contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;beacon-monorepo/
├── frontend/
│   └── web/           ← Next.js dashboard (TypeScript)
├── services/
│   ├── api/           ← REST API (Go)
│   ├── ingest/        ← Data ingestion service (Go)
│   ├── ml/            ← ML inference API (Python / FastAPI)
│   └── worker/        ← Background jobs (Python / Celery)
├── packages/
│   ├── ui/            ← Shared React components (TypeScript)
│   └── sdk/           ← TypeScript client SDK
├── pkg/
│   ├── shared/        ← Go shared utilities
│   └── store/         ← Go data access layer
├── libs/
│   └── shared/        ← Python shared models + Pydantic schemas
├── proto/             ← Protobuf definitions (source of truth)
├── gen/
│   ├── go/            ← Generated Go stubs
│   ├── python/        ← Generated Python stubs
│   └── ts/            ← Generated TypeScript stubs
├── Taskfile.yml       ← Root task orchestration (cross-language)
├── pnpm-workspace.yaml
├── package.json       ← Root (biome, lefthook, turbo)
├── pyproject.toml     ← uv workspace root
├── uv.lock
├── biome.json
├── .golangci.yml
└── go.work            ← (gitignored — local dev only)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The core idea:&lt;/strong&gt; services are owned by different teams writing different languages, but they share contracts defined in &lt;code&gt;proto/&lt;/code&gt;, config defined at the repo root, and a single task runner (&lt;code&gt;Taskfile.yml&lt;/code&gt;) that orchestrates everything with one set of commands.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 1: Three Workspace Managers — The Parallel Foundation
&lt;/h2&gt;

&lt;p&gt;There is no single package manager that handles TypeScript, Python, and Go. Instead, three workspace managers coexist in the same repo — each governing its own language, each completely ignorant of the others.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Manager&lt;/th&gt;
&lt;th&gt;Config file&lt;/th&gt;
&lt;th&gt;Dep linking mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;pnpm workspaces&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;workspace:*&lt;/code&gt; symlinks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;uv workspaces&lt;/td&gt;
&lt;td&gt;root &lt;code&gt;pyproject.toml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{ workspace = true }&lt;/code&gt; editable installs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;go workspaces&lt;/td&gt;
&lt;td&gt;&lt;code&gt;go.work&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;local module overlay&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They coexist peacefully because they key on different file extensions. pnpm reads &lt;code&gt;package.json&lt;/code&gt;. uv reads &lt;code&gt;pyproject.toml&lt;/code&gt;. Go reads &lt;code&gt;go.mod&lt;/code&gt;. A directory like &lt;code&gt;services/ml/&lt;/code&gt; that contains a &lt;code&gt;pyproject.toml&lt;/code&gt; is invisible to pnpm. A directory with only a &lt;code&gt;package.json&lt;/code&gt; is invisible to uv.&lt;/p&gt;

&lt;h3&gt;
  
  
  TypeScript: &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;frontend/*'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;packages/*'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gen/ts'&lt;/span&gt;          &lt;span class="c1"&gt;# generated TypeScript stubs — must be a workspace member&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gen/ts/&lt;/code&gt; &lt;strong&gt;must&lt;/strong&gt; be a workspace member, not a &lt;code&gt;file:&lt;/code&gt; dependency. A &lt;code&gt;file:&lt;/code&gt; reference copies the package into &lt;code&gt;node_modules&lt;/code&gt; at install time — after &lt;code&gt;buf generate&lt;/code&gt; regenerates the stubs, the copies are stale until you re-run &lt;code&gt;pnpm install&lt;/code&gt;. A &lt;code&gt;workspace:*&lt;/code&gt; reference is a symlink — always live.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;packages/sdk/package.json&lt;/code&gt; depends on the shared UI and the generated TypeScript stubs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@beacon/sdk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@beacon/ui"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"workspace:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@beacon/proto-ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"workspace:*"&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;symlinked&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;reflects&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;buf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;generate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;immediately&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Python: root &lt;code&gt;pyproject.toml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"beacon-root"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;requires-python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.12&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;
&lt;span class="nn"&gt;[tool.uv.workspace]&lt;/span&gt;
&lt;span class="c"&gt;# List Python members explicitly — globs must not match dirs without pyproject.toml&lt;/span&gt;
&lt;span class="py"&gt;members&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"services/ml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"services/worker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"libs/shared"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"gen/python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c"&gt;# generated proto stubs — must be listed so uv can resolve them&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[tool.uv.sources]&lt;/span&gt;
&lt;span class="py"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;workspace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c"&gt;# the workspace:* equivalent for Python&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;services/ml/pyproject.toml&lt;/code&gt; declares its internal dep:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ml"&lt;/span&gt;
&lt;span class="py"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"shared"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c"&gt;# internal — resolves via workspace&lt;/span&gt;
    &lt;span class="py"&gt;"fastapi&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.115&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"torch&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.3&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[tool.uv.sources]&lt;/span&gt;
&lt;span class="py"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;workspace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why explicit members, not globs?&lt;/strong&gt; uv errors if a glob matches a directory without a &lt;code&gt;pyproject.toml&lt;/code&gt;. &lt;code&gt;frontend/web/&lt;/code&gt; and &lt;code&gt;packages/ui/&lt;/code&gt; have &lt;code&gt;package.json&lt;/code&gt; but no &lt;code&gt;pyproject.toml&lt;/code&gt; — a glob like &lt;code&gt;packages/*&lt;/code&gt; would break uv. Be explicit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Go: &lt;code&gt;go.work&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="m"&gt;1.23&lt;/span&gt;

&lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;
    &lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ingest&lt;/span&gt;
    &lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;
    &lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;
    &lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;gen&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;          &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="n"&gt;generated&lt;/span&gt; &lt;span class="n"&gt;proto&lt;/span&gt; &lt;span class="n"&gt;stubs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;own&lt;/span&gt; &lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Critical:&lt;/strong&gt; add &lt;code&gt;go.work&lt;/code&gt; and &lt;code&gt;go.work.sum&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;. CI must validate each Go module against its own &lt;code&gt;go.mod&lt;/code&gt; independently. The workspace is a local dev convenience — not a CI mechanism.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;go.mod&lt;/code&gt; plumbing — how CI works without &lt;code&gt;go.work&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the part most polyglot monorepo guides skip. When CI runs &lt;code&gt;GOWORK=off&lt;/code&gt;, each Go module must declare its in-repo dependencies in its own &lt;code&gt;go.mod&lt;/code&gt; using &lt;code&gt;replace&lt;/code&gt; directives — otherwise the module can't find &lt;code&gt;gen/go&lt;/code&gt; or &lt;code&gt;pkg/shared&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;services/api/go.mod&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;your&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;beacon&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;

&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="m"&gt;1.23&lt;/span&gt;

&lt;span class="n"&gt;require&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;your&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;beacon&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;gen&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;    &lt;span class="n"&gt;v0&lt;/span&gt;&lt;span class="m"&gt;.0.0&lt;/span&gt;
    &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;your&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;beacon&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;v0&lt;/span&gt;&lt;span class="m"&gt;.0.0&lt;/span&gt;
    &lt;span class="c"&gt;// external deps...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// replace directives make GOWORK=off CI work:&lt;/span&gt;
&lt;span class="c"&gt;// each module explicitly maps its in-repo deps to disk paths&lt;/span&gt;
&lt;span class="n"&gt;replace&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;your&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;beacon&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;gen&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;../../&lt;/span&gt;&lt;span class="n"&gt;gen&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;
    &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;your&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;beacon&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;../../&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;go work sync&lt;/code&gt; propagates the &lt;code&gt;require&lt;/code&gt; versions from each &lt;code&gt;go.mod&lt;/code&gt;. The &lt;code&gt;replace&lt;/code&gt; directives work with or without &lt;code&gt;go.work&lt;/code&gt; — so &lt;code&gt;GOWORK=off&lt;/code&gt; CI and &lt;code&gt;go.work&lt;/code&gt; local dev both resolve to the same local disk paths.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;go mod tidy&lt;/code&gt; in each module after adding the &lt;code&gt;replace&lt;/code&gt; directives. The &lt;code&gt;v0.0.0&lt;/code&gt; version is a placeholder that &lt;code&gt;go mod tidy&lt;/code&gt; fills in as a pseudo-version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# .gitignore
&lt;/span&gt;&lt;span class="n"&gt;go&lt;/span&gt;.&lt;span class="n"&gt;work&lt;/span&gt;
&lt;span class="n"&gt;go&lt;/span&gt;.&lt;span class="n"&gt;work&lt;/span&gt;.&lt;span class="n"&gt;sum&lt;/span&gt;
.&lt;span class="n"&gt;task&lt;/span&gt;/
.&lt;span class="n"&gt;venv&lt;/span&gt;/
&lt;span class="n"&gt;node_modules&lt;/span&gt;/
.&lt;span class="n"&gt;turbo&lt;/span&gt;/
.&lt;span class="n"&gt;next&lt;/span&gt;/
&lt;span class="err"&gt;__&lt;/span&gt;&lt;span class="n"&gt;pycache__&lt;/span&gt;/
*.&lt;span class="n"&gt;pyc&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;/*/&lt;span class="n"&gt;bin&lt;/span&gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What &lt;code&gt;uv sync&lt;/code&gt; and &lt;code&gt;pnpm install&lt;/code&gt; and &lt;code&gt;go work sync&lt;/code&gt; each do
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nb"&gt;install&lt;/span&gt;          &lt;span class="c"&gt;# links TypeScript packages via symlinks in node_modules&lt;/span&gt;
uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--group&lt;/span&gt; dev   &lt;span class="c"&gt;# installs all Python workspace members as editable installs&lt;/span&gt;
go work init          &lt;span class="c"&gt;# creates go.work on a fresh clone (only needed once — it's gitignored)&lt;/span&gt;
go work use ./services/api ./services/ingest ./pkg/shared ./pkg/store ./gen/go
go work &lt;span class="nb"&gt;sync&lt;/span&gt;          &lt;span class="c"&gt;# syncs go.mod files across Go modules (run after adding new deps)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fresh clone problem:&lt;/strong&gt; &lt;code&gt;go.work&lt;/code&gt; is gitignored, so a fresh clone has no workspace file. The &lt;code&gt;go work init&lt;/code&gt; + &lt;code&gt;go work use&lt;/code&gt; steps only run once per machine. The Go Taskfile handles this automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key insight:&lt;/strong&gt; these three commands are completely independent. Running &lt;code&gt;pnpm install&lt;/code&gt; does not affect Python deps. Running &lt;code&gt;uv sync&lt;/code&gt; does not touch &lt;code&gt;node_modules&lt;/code&gt;. None of them touch each other. The three workspace managers are parallel foundations — they don't compose, they coexist.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 2: Task (Taskfile.yml) — The Cross-Language Orchestrator
&lt;/h2&gt;

&lt;p&gt;Three workspace managers handle &lt;em&gt;dependencies&lt;/em&gt;. &lt;strong&gt;Task&lt;/strong&gt; handles &lt;em&gt;tasks&lt;/em&gt; (build, test, lint, generate, dev). It's the single entry point for everything in the repo, regardless of language.&lt;/p&gt;

&lt;p&gt;Task is a language-agnostic binary. Install it once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;go-task
&lt;span class="c"&gt;# or: sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The root &lt;code&gt;Taskfile.yml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;

&lt;span class="na"&gt;includes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;taskfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./Taskfile.ts.yml&lt;/span&gt;
    &lt;span class="na"&gt;optional&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;py&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;taskfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./Taskfile.py.yml&lt;/span&gt;
    &lt;span class="na"&gt;optional&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;go&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;taskfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./Taskfile.go.yml&lt;/span&gt;
    &lt;span class="na"&gt;optional&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;proto&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;taskfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./Taskfile.proto.yml&lt;/span&gt;
    &lt;span class="na"&gt;optional&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;doctor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify all required tools are installed&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;command -v node  || (echo "node not found — https://nodejs.org"  &amp;amp;&amp;amp; exit 1)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;command -v pnpm  || (echo "pnpm not found — npm install -g pnpm" &amp;amp;&amp;amp; exit 1)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;command -v uv    || (echo "uv not found — https://docs.astral.sh/uv" &amp;amp;&amp;amp; exit 1)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;command -v go    || (echo "go not found — https://go.dev/dl" &amp;amp;&amp;amp; exit 1)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;command -v buf   || (echo "buf not found — https://buf.build/docs/cli" &amp;amp;&amp;amp; exit 1)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;command -v jq    || (echo "jq not found — brew install jq" &amp;amp;&amp;amp; exit 1)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "All tools present"&lt;/span&gt;

  &lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install all dependencies (run task doctor first)&lt;/span&gt;
    &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;doctor&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pnpm install&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;uv sync --group dev&lt;/span&gt;   &lt;span class="c1"&gt;# single .venv at repo root; --group dev includes pytest/mypy/ruff&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go:init&lt;/span&gt;         &lt;span class="c1"&gt;# creates go.work if missing (idempotent)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go work sync&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proto:generate&lt;/span&gt;

  &lt;span class="na"&gt;build:all&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build everything in dependency order&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proto:generate&lt;/span&gt;       &lt;span class="c1"&gt;# proto first — all languages depend on it&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ts:build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go:build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;py:build&lt;/span&gt;

  &lt;span class="na"&gt;test:all&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test all languages in parallel&lt;/span&gt;
    &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ts:test&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go:test&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;py:test&lt;/span&gt;

  &lt;span class="na"&gt;lint:all&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lint all languages in parallel&lt;/span&gt;
    &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ts:lint&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go:lint&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;py:lint&lt;/span&gt;

  &lt;span class="na"&gt;dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Start all dev servers&lt;/span&gt;
    &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ts:dev&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go:dev&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;py:dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Language-specific Taskfiles
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Taskfile.ts.yml&lt;/code&gt; — TypeScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;

&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;frontend/**/*.ts'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;frontend/**/*.tsx'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;packages/**/*.ts'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/package.json'&lt;/span&gt;     &lt;span class="c1"&gt;# dep changes should bust the cache&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pnpm-lock.yaml'&lt;/span&gt;
    &lt;span class="na"&gt;generates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;frontend/web/.next/**'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pnpm turbo run build&lt;/span&gt;

  &lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;frontend/**/*.ts'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;packages/**/*.ts'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/package.json'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;generates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.task/ts-lint.stamp'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pnpm biome check .&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;touch .task/ts-lint.stamp&lt;/span&gt;

  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;frontend/**/*.ts'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;packages/**/*.ts'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;frontend/**/*.test.ts'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pnpm-lock.yaml'&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pnpm turbo run test&lt;/span&gt;

  &lt;span class="na"&gt;dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pnpm turbo run dev --filter=@beacon/web&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Taskfile.go.yml&lt;/code&gt; — Go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;

&lt;span class="c1"&gt;# Prerequisite: jq must be installed (brew install jq / apt install jq)&lt;/span&gt;

&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create go.work for local development (gitignored; run once per clone)&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test -f go.work&lt;/span&gt;    &lt;span class="c1"&gt;# skip if go.work already exists&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go work init&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go work use ./services/api ./services/ingest ./pkg/shared ./pkg/store ./gen/go&lt;/span&gt;

  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;init&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;           &lt;span class="c1"&gt;# ensures go.work exists before building&lt;/span&gt;
    &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;services/api/**/*.go'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;services/ingest/**/*.go'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pkg/**/*.go'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/go.mod'&lt;/span&gt;        &lt;span class="c1"&gt;# dep changes should bust the cache&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/go.sum'&lt;/span&gt;
    &lt;span class="na"&gt;generates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;services/api/bin/api'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;services/ingest/bin/ingest'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cd services/api &amp;amp;&amp;amp; go build -o bin/api ./cmd/api&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cd services/ingest &amp;amp;&amp;amp; go build -o bin/ingest ./cmd/ingest&lt;/span&gt;

  &lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;init&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;services/**/*.go'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pkg/**/*.go'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;generates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.task/go-lint.stamp'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go work edit -json | jq -r '.Use[].DiskPath' |&lt;/span&gt;
          &lt;span class="s"&gt;xargs -P4 -I{} sh -c 'cd {} &amp;amp;&amp;amp; golangci-lint run ./...'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;touch .task/go-lint.stamp&lt;/span&gt;

  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;init&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;services/**/*.go'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pkg/**/*.go'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go work edit -json | jq -r '.Use[].DiskPath' |&lt;/span&gt;
          &lt;span class="s"&gt;xargs -P4 -I{} sh -c 'cd {} &amp;amp;&amp;amp; go test ./... -race'&lt;/span&gt;

  &lt;span class="na"&gt;dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;init&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cd services/api &amp;amp;&amp;amp; go run ./cmd/api&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Taskfile.py.yml&lt;/code&gt; — Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;

&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;services/ml/**/*.py'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;services/worker/**/*.py'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;libs/**/*.py'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/pyproject.toml'&lt;/span&gt;  &lt;span class="c1"&gt;# dep changes should bust the cache&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;uv.lock'&lt;/span&gt;
    &lt;span class="na"&gt;generates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.task/py-build.stamp'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;uv sync --group dev&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;touch .task/py-build.stamp&lt;/span&gt;

  &lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;services/ml/**/*.py'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;services/worker/**/*.py'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;libs/**/*.py'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;uv.lock'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;generates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.task/py-lint.stamp'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;uv run ruff check services/ml services/worker libs&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;uv run ruff format --check services/ml services/worker libs&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;uv run mypy services/ml/ services/worker/ libs/&lt;/span&gt;   &lt;span class="c1"&gt;# from repo root — not per-service&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;touch .task/py-lint.stamp&lt;/span&gt;

  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;services/ml/**/*.py'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;services/worker/**/*.py'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;libs/**/*.py'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;uv run pytest services/ml services/worker libs -x&lt;/span&gt;

  &lt;span class="na"&gt;dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;uv run fastapi dev services/ml/src/ml/main.py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key concepts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;deps:&lt;/code&gt; runs tasks in &lt;strong&gt;parallel&lt;/strong&gt; — &lt;code&gt;test:all&lt;/code&gt; runs all three test suites simultaneously&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cmds:&lt;/code&gt; runs &lt;strong&gt;sequentially&lt;/strong&gt; — &lt;code&gt;build:all&lt;/code&gt; runs proto first, then the rest&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sources&lt;/code&gt;/&lt;code&gt;generates&lt;/code&gt; is Task's caching: if source files haven't changed since the last run (hash stored in &lt;code&gt;.task/&lt;/code&gt;), the task is skipped entirely&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;.task/&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;task build:all&lt;/code&gt; requires &lt;strong&gt;all three toolchains&lt;/strong&gt; installed (node + pnpm, uv, go, buf, jq). Frontend-only or backend-only developers who run it without the full stack will get a "task not found" error. Document this in your repo README — or split &lt;code&gt;task build:ts&lt;/code&gt;, &lt;code&gt;task build:go&lt;/code&gt;, &lt;code&gt;task build:py&lt;/code&gt; as the normal developer interface.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  CI: affected-only runs
&lt;/h3&gt;

&lt;p&gt;Task has no built-in &lt;code&gt;--affected&lt;/code&gt;. In CI, detect changed language areas with git diff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ci/affected.sh&lt;/span&gt;
&lt;span class="nv"&gt;changed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; origin/main...HEAD&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$changed&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s1"&gt;'^(frontend|packages)/'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; task ts:test
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$changed&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s1"&gt;'^(services/(api|ingest)|pkg)/'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; task go:test
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$changed&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s1"&gt;'^(services/(ml|worker)|libs)/'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; task py:test
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$changed&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s1"&gt;'^proto/'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  task proto:generate
  task ts:test &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; task go:test &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; task py:test  &lt;span class="c"&gt;# proto change affects all&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A PR touching only &lt;code&gt;services/ml/&lt;/code&gt; won't re-run Go tests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 3: buf + Protobuf — The Contract Layer
&lt;/h2&gt;

&lt;p&gt;This is the tool that makes a polyglot monorepo genuinely worth the complexity. A single &lt;code&gt;.proto&lt;/code&gt; file becomes the source of truth for how your TypeScript frontend, your Go API, and your Python ML service all communicate. Change the proto once — regenerate — all three languages are in sync.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;buf.yaml&lt;/code&gt; — workspace config at repo root
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# buf.yaml&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v2&lt;/span&gt;

&lt;span class="na"&gt;modules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proto/analytics&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buf.build/your-org/analytics&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proto/events&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buf.build/your-org/events&lt;/span&gt;

&lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;buf.build/googleapis/googleapis&lt;/span&gt;

&lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;STANDARD&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;except&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;PACKAGE_DIRECTORY_MATCH&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;breaking&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;FILE&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;buf.gen.yaml&lt;/code&gt; — one command, three languages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# buf.gen.yaml&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v2&lt;/span&gt;

&lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proto/analytics&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proto/events&lt;/span&gt;

&lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Go: message stubs&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;remote&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buf.build/protocolbuffers/go&lt;/span&gt;
    &lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gen/go&lt;/span&gt;
    &lt;span class="na"&gt;opt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paths=source_relative&lt;/span&gt;

  &lt;span class="c1"&gt;# Go: gRPC service stubs&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;remote&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buf.build/grpc/go&lt;/span&gt;
    &lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gen/go&lt;/span&gt;
    &lt;span class="na"&gt;opt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paths=source_relative&lt;/span&gt;

  &lt;span class="c1"&gt;# Python: message stubs&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;remote&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buf.build/protocolbuffers/python&lt;/span&gt;
    &lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gen/python&lt;/span&gt;

  &lt;span class="c1"&gt;# Python: gRPC service stubs&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;remote&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buf.build/grpc/python&lt;/span&gt;
    &lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gen/python&lt;/span&gt;

  &lt;span class="c1"&gt;# Python: mypy type stubs (community plugin)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;remote&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buf.build/community/nipunn1313-mypy&lt;/span&gt;
    &lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gen/python&lt;/span&gt;

  &lt;span class="c1"&gt;# TypeScript: Protobuf-ES (modern, works with Connect-RPC)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;remote&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buf.build/bufbuild/es&lt;/span&gt;
    &lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gen/ts&lt;/span&gt;
    &lt;span class="na"&gt;opt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;target=ts&lt;/span&gt;

&lt;span class="na"&gt;managed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;override&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;file_option&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go_package_prefix&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.com/your-org/beacon/gen/go&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;buf generate&lt;/code&gt; to regenerate all three at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generated code as first-class modules
&lt;/h3&gt;

&lt;p&gt;Each generated directory is its own module with its own manifest — so the three workspace managers can find it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gen/
├── go/
│   └── go.mod          ← module github.com/your-org/beacon/gen/go
│                          each consumer's go.mod has: replace ... =&amp;gt; ../../gen/go
├── python/
│   └── pyproject.toml  ← listed in uv workspace members; consumers use { workspace = true }
└── ts/
    └── package.json    ← listed in pnpm-workspace.yaml; consumers use workspace:*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three use the same pattern as other internal packages — workspace linking, not &lt;code&gt;file:&lt;/code&gt; copies. &lt;code&gt;gen/go/&lt;/code&gt; is also listed in &lt;code&gt;go.work&lt;/code&gt; for local dev, and each consumer's &lt;code&gt;go.mod&lt;/code&gt; has a &lt;code&gt;replace&lt;/code&gt; directive for CI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Taskfile.proto.yml&lt;/span&gt;
&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;proto/**/*.proto&lt;/span&gt;
    &lt;span class="na"&gt;generates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gen/go/**/*.go&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gen/python/**/*_pb2.py&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gen/ts/**/*_pb.ts&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;buf lint&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;buf generate&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;buf breaking --against '.git#branch=main'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; the breaking change check (&lt;code&gt;buf breaking&lt;/code&gt;) is the safety net. If a Go developer renames a proto field, &lt;code&gt;buf breaking&lt;/code&gt; fails the build before the Python and TypeScript consumers break. The contract is enforced at the definition layer, not the consumer layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 4: Per-Language Root Config — Three Files, One Repo
&lt;/h2&gt;

&lt;p&gt;Each language has one config file at the repo root. No duplication across services, no "why is lint disabled in this one service" surprises.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;biome.json&lt;/code&gt; — TypeScript
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://biomejs.dev/schemas/1.9.0/schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"linter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"correctness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"noUnusedVariables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"noUnusedImports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"style"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"useConst"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"formatter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"indentStyle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"space"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"indentWidth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lineWidth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ignore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"gen/ts/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"**/node_modules/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"**/.next/**"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  root &lt;code&gt;pyproject.toml&lt;/code&gt; — Python lint + type config
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.ruff]&lt;/span&gt;
&lt;span class="py"&gt;line-length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;88&lt;/span&gt;
&lt;span class="py"&gt;target-version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"py312"&lt;/span&gt;
&lt;span class="py"&gt;src&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;exclude&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"gen/python"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[tool.ruff.lint]&lt;/span&gt;
&lt;span class="py"&gt;select&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"E"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"W"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"F"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"I"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"UP"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;ignore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"E501"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;fixable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"ALL"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[tool.ruff.lint.isort]&lt;/span&gt;
&lt;span class="py"&gt;known-first-party&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"shared"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"worker"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[tool.mypy]&lt;/span&gt;
&lt;span class="py"&gt;python_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"3.12"&lt;/span&gt;
&lt;span class="py"&gt;strict&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;mypy_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"$MYPY_CONFIG_FILE_DIR/libs/shared/src"&lt;/span&gt;
&lt;span class="py"&gt;exclude&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"gen/python"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[[tool.mypy.overrides]]&lt;/span&gt;
&lt;span class="py"&gt;module&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"*_pb2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"*_pb2_grpc"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;ignore_errors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="c"&gt;# suppress errors in generated proto stubs&lt;/span&gt;

&lt;span class="nn"&gt;[dependency-groups]&lt;/span&gt;
&lt;span class="py"&gt;dev&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="py"&gt;"pytest&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"pytest-asyncio&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"mypy&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.15&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"ruff&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Critical — mypy invocation:&lt;/strong&gt; mypy reads config from the directory it's &lt;em&gt;invoked from&lt;/em&gt;, not from the file being checked. Always run &lt;code&gt;uv run mypy services/ml/ services/worker/ libs/&lt;/code&gt; from the repo root. Running from inside &lt;code&gt;services/ml/&lt;/code&gt; silently ignores &lt;code&gt;strict = true&lt;/code&gt; and falls back to defaults.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;.golangci.yml&lt;/code&gt; — Go
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2"&lt;/span&gt;

&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
  &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;linters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;
  &lt;span class="na"&gt;enable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;errcheck&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;staticcheck&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unused&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ineffassign&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;govet&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;revive&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gocritic&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;misspell&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gosec&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bodyclose&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;copyloopvar&lt;/span&gt;

  &lt;span class="na"&gt;exclusions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;generated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;strict&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen/go/.*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;.pb&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;.go$"&lt;/span&gt;
        &lt;span class="na"&gt;linters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;revive&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;gocritic&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;godot&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;errcheck&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_test&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;.go$"&lt;/span&gt;
        &lt;span class="na"&gt;linters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;gosec&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;errcheck&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;formatters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gofumpt&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;goimports&lt;/span&gt;
  &lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;goimports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;local-prefixes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;github.com/your-org/beacon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;.editorconfig&lt;/code&gt; — baseline cross-language consistency
&lt;/h3&gt;

&lt;p&gt;Biome, Ruff, and golangci-lint enforce formatting within each language, but editors need &lt;code&gt;.editorconfig&lt;/code&gt; for baseline consistency before any tool runs — charset, line endings, trailing newlines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# .editorconfig
&lt;/span&gt;&lt;span class="py"&gt;root&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[*]&lt;/span&gt;
&lt;span class="py"&gt;charset&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;utf-8&lt;/span&gt;
&lt;span class="py"&gt;end_of_line&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;lf&lt;/span&gt;
&lt;span class="py"&gt;insert_final_newline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;trim_trailing_whitespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[*.go]&lt;/span&gt;
&lt;span class="py"&gt;indent_style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;tab&lt;/span&gt;
&lt;span class="py"&gt;indent_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;4&lt;/span&gt;

&lt;span class="nn"&gt;[*.{ts,tsx,js,json,yaml,yml,toml}]&lt;/span&gt;
&lt;span class="py"&gt;indent_style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;space&lt;/span&gt;
&lt;span class="py"&gt;indent_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;2&lt;/span&gt;

&lt;span class="nn"&gt;[*.py]&lt;/span&gt;
&lt;span class="py"&gt;indent_style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;space&lt;/span&gt;
&lt;span class="py"&gt;indent_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;4&lt;/span&gt;

&lt;span class="nn"&gt;[*.proto]&lt;/span&gt;
&lt;span class="py"&gt;indent_style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;space&lt;/span&gt;
&lt;span class="py"&gt;indent_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; three config files, all at the repo root. Each tool finds its own by walking up the directory tree. You can change the Go linting rules for every Go service in one edit, the Python formatting rules for every Python service in one edit, and the TypeScript rules for every TypeScript package in one edit. No per-service drift.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 5: Three Linters, One &lt;code&gt;task lint&lt;/code&gt; Command
&lt;/h2&gt;

&lt;p&gt;Each language uses its own best-in-class linter. Task unifies them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;task lint:all   &lt;span class="c"&gt;# runs all three in parallel (deps: is parallel)&lt;/span&gt;
task ts:lint    &lt;span class="c"&gt;# TypeScript only&lt;/span&gt;
task go:lint    &lt;span class="c"&gt;# Go only&lt;/span&gt;
task py:lint    &lt;span class="c"&gt;# Python only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why not one linter for everything?
&lt;/h3&gt;

&lt;p&gt;There is no cross-language linter that handles TypeScript + Python + Go well. The best tools are language-specific:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Biome&lt;/strong&gt; — TypeScript: sub-millisecond, replaces ESLint + Prettier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ruff&lt;/strong&gt; — Python: 10–100x faster than Flake8, replaces isort + Black&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;golangci-lint&lt;/strong&gt; — Go: aggregates 50+ linters behind one binary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Task makes them feel like one: &lt;code&gt;task lint:all&lt;/code&gt; exits non-zero if any of them fails, regardless of language.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build graph — how proto flows into all three
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;proto/analytics/*.proto
proto/events/*.proto
    ↓ buf generate
    ↓
gen/go/    gen/python/    gen/ts/
    ↓             ↓              ↓
pkg/shared   libs/shared    packages/sdk
    ↓             ↓              ↓
services/api  services/ml   frontend/web
services/ingest  services/worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A change to &lt;code&gt;proto/&lt;/code&gt; forces all three build chains to run. Everything else is independent — a change to &lt;code&gt;services/api/&lt;/code&gt; doesn't rerun Python tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI: three parallel language jobs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/ci.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;detect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;proto&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.changes.outputs.proto }}&lt;/span&gt;
      &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.changes.outputs.ts }}&lt;/span&gt;
      &lt;span class="na"&gt;go&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.changes.outputs.go }}&lt;/span&gt;
      &lt;span class="na"&gt;py&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.changes.outputs.py }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dorny/paths-filter@v3&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;changes&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;proto:&lt;/span&gt;
              &lt;span class="s"&gt;- 'proto/**'&lt;/span&gt;
            &lt;span class="s"&gt;ts:&lt;/span&gt;
              &lt;span class="s"&gt;- 'frontend/**'&lt;/span&gt;
              &lt;span class="s"&gt;- 'packages/**'&lt;/span&gt;
              &lt;span class="s"&gt;- 'gen/ts/**'&lt;/span&gt;
              &lt;span class="s"&gt;- 'proto/**'   # proto change triggers TS too&lt;/span&gt;
            &lt;span class="s"&gt;go:&lt;/span&gt;
              &lt;span class="s"&gt;- 'services/api/**'&lt;/span&gt;
              &lt;span class="s"&gt;- 'services/ingest/**'&lt;/span&gt;
              &lt;span class="s"&gt;- 'pkg/**'&lt;/span&gt;
              &lt;span class="s"&gt;- 'gen/go/**'&lt;/span&gt;
              &lt;span class="s"&gt;- 'proto/**'&lt;/span&gt;
            &lt;span class="s"&gt;py:&lt;/span&gt;
              &lt;span class="s"&gt;- 'services/ml/**'&lt;/span&gt;
              &lt;span class="s"&gt;- 'services/worker/**'&lt;/span&gt;
              &lt;span class="s"&gt;- 'libs/**'&lt;/span&gt;
              &lt;span class="s"&gt;- 'gen/python/**'&lt;/span&gt;
              &lt;span class="s"&gt;- 'proto/**'&lt;/span&gt;

  &lt;span class="na"&gt;proto&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detect&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.detect.outputs.proto == 'true'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;npm install -g @bufbuild/buf&lt;/span&gt;
          &lt;span class="s"&gt;buf lint &amp;amp;&amp;amp; buf generate&lt;/span&gt;
          &lt;span class="s"&gt;buf breaking --against '.git#branch=main'&lt;/span&gt;

  &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detect&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.detect.outputs.ts == 'true'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;   &lt;span class="c1"&gt;# required for turbo --affected&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm/action-setup@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm install --frozen-lockfile&lt;/span&gt;   &lt;span class="c1"&gt;# equivalent of uv sync --frozen&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm turbo run typecheck lint test --affected&lt;/span&gt;

  &lt;span class="na"&gt;go&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detect&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.detect.outputs.go == 'true'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;services/api&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;services/ingest&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pkg/shared&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pkg/store&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-go@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;go-version-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.module }}/go.mod&lt;/span&gt;
          &lt;span class="na"&gt;cache-dependency-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.module }}/go.sum&lt;/span&gt;
      &lt;span class="c1"&gt;# GOWORK=off is explicit — not relying on go.work being gitignored&lt;/span&gt;
      &lt;span class="c1"&gt;# This forces each module to prove it has complete go.mod declarations&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cd ${{ matrix.module }} &amp;amp;&amp;amp; GOWORK=off golangci-lint run ./...&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cd ${{ matrix.module }} &amp;amp;&amp;amp; GOWORK=off go test ./... -race&lt;/span&gt;

  &lt;span class="na"&gt;py&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detect&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.detect.outputs.py == 'true'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;astral-sh/setup-uv@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;enable-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv sync --frozen --group dev&lt;/span&gt;
        &lt;span class="c1"&gt;# --frozen: fail if uv.lock is out of date (never silently update in CI)&lt;/span&gt;
        &lt;span class="c1"&gt;# --group dev: includes pytest, mypy, ruff&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv run ruff check services/ml/ services/worker/ libs/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv run mypy services/ml/ services/worker/ libs/&lt;/span&gt;
        &lt;span class="c1"&gt;# always from repo root — mypy does not discover config per-file like Ruff&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv run pytest services/ml/ services/worker/ libs/ -x&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv cache prune --ci&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A PR touching only &lt;code&gt;services/ml/&lt;/code&gt; skips the &lt;code&gt;proto&lt;/code&gt;, &lt;code&gt;ts&lt;/code&gt;, and &lt;code&gt;go&lt;/code&gt; jobs entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 6: lefthook — One Hook to Rule All Three Languages
&lt;/h2&gt;

&lt;p&gt;Each language article recommended its own hook tool (custom &lt;code&gt;.githooks/&lt;/code&gt; for TypeScript, pre-commit for Python, lefthook for Go). In a polyglot repo, you pick one. &lt;strong&gt;lefthook&lt;/strong&gt; wins: it's a single binary, works with all languages, and runs hooks in parallel by default.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;

&lt;p&gt;lefthook is managed as a root-level dev dependency so everyone on the team gets it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;package.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(root)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@biomejs/biome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^1.9.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lefthook"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^1.11.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"turbo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^2.0.0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prepare"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lefthook install"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pnpm install&lt;/code&gt; runs &lt;code&gt;prepare&lt;/code&gt; automatically — lefthook hooks are wired up on first install.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;lefthook.yml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lefthook.yml&lt;/span&gt;
&lt;span class="na"&gt;glob_matcher&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;doublestar&lt;/span&gt;  &lt;span class="c1"&gt;# ** matches 0 or more dirs (required for nested files)&lt;/span&gt;

&lt;span class="na"&gt;pre-commit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;parallel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ts-lint&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.{ts,tsx}"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm biome check --no-errors-on-unmatched {staged_files}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;py-lint&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.py"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv run ruff check {staged_files}&lt;/span&gt;
      &lt;span class="na"&gt;stage_fixed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;py-format&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.py"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv run ruff format {staged_files}&lt;/span&gt;
      &lt;span class="na"&gt;stage_fixed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proto-lint&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.proto"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buf lint&lt;/span&gt;

&lt;span class="na"&gt;pre-push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;parallel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go-lint&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.go"&lt;/span&gt;
      &lt;span class="c1"&gt;# golangci-lint cannot lint files across multiple packages (named files must all&lt;/span&gt;
      &lt;span class="c1"&gt;# be in one directory — see golangci-lint#3715). Run per-module instead:&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go work edit -json | jq -r '.Use[].DiskPath' |&lt;/span&gt;
             &lt;span class="s"&gt;xargs -P4 -I{} sh -c 'cd {} &amp;amp;&amp;amp; GOWORK=off golangci-lint run ./...'&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go-mod-tidy&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;go.{mod,sum}"&lt;/span&gt;
      &lt;span class="c1"&gt;# go mod tidy is per-module in a multi-module workspace; iterate over all modules:&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go work edit -json | jq -r '.Use[].DiskPath' |&lt;/span&gt;
             &lt;span class="s"&gt;xargs -I{} sh -c 'cd {} &amp;amp;&amp;amp; GOWORK=off go mod tidy &amp;amp;&amp;amp; git diff --exit-code go.mod go.sum'&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proto-breaking&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.proto"&lt;/span&gt;
      &lt;span class="c1"&gt;# Guard against first push / no origin/main (new repos, shallow clones)&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;if git rev-parse origin/main &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then&lt;/span&gt;
          &lt;span class="s"&gt;buf breaking --against '.git#branch=main'&lt;/span&gt;
        &lt;span class="s"&gt;else&lt;/span&gt;
          &lt;span class="s"&gt;echo "No origin/main — skipping breaking change check"&lt;/span&gt;
        &lt;span class="s"&gt;fi&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;py-typecheck&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.py"&lt;/span&gt;
      &lt;span class="c1"&gt;# mypy is 10-30s on real codebases — too slow for pre-commit, right for pre-push&lt;/span&gt;
      &lt;span class="c1"&gt;# pass whole dirs, not {staged_files}: mypy needs package-level context&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv run mypy services/ml/ services/worker/ libs/ --config-file pyproject.toml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key concepts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;glob_matcher: doublestar&lt;/code&gt; — without this, &lt;code&gt;**/*.py&lt;/code&gt; won't match &lt;code&gt;services/ml/src/ml/main.py&lt;/code&gt; (two directories deep). This single line makes nested file matching work correctly.&lt;/li&gt;
&lt;li&gt;Each job is skipped entirely if no staged files match its glob — committing only TypeScript changes doesn't run &lt;code&gt;go-lint&lt;/code&gt; at all.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stage_fixed: true&lt;/code&gt; — after Ruff auto-fixes a Python file, lefthook re-stages it so the fix is included in the commit.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;parallel: true&lt;/code&gt; — all four language checkers run simultaneously. On a fast machine, a commit touching files in all three languages still completes in the time of the slowest single checker.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pre-push&lt;/code&gt; runs &lt;code&gt;buf breaking&lt;/code&gt; — the breaking change check is too slow for pre-commit but important before pushing to a shared branch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The key insight:&lt;/strong&gt; you don't need different hook tools for different languages. lefthook routes by file glob. The Python and Go developers on your team never need to know that Biome exists; the TypeScript developers never need to know that Ruff exists. They all run &lt;code&gt;pnpm install&lt;/code&gt; once and get the right checks for their files automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Picture: How These Tools Interact
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer commits
    ↓
lefthook pre-commit (all four checks in parallel)
    ↓ Biome on staged .ts/.tsx files
    ↓ Ruff on staged .py files (+ auto-fix + re-stage)
    ↓ golangci-lint on staged .go files
    ↓ buf lint on staged .proto files
    ↓ (any failure → abort commit; all pass → continue)
    ↓
Three workspace managers resolve internal deps in parallel:
    ↓ pnpm: workspace:* symlinks for TypeScript
    ↓ uv: { workspace = true } editable installs for Python
    ↓ go work: local module overlay for Go (gitignored)
    ↓
task build:all
    ↓ proto:generate (buf → gen/go/ + gen/python/ + gen/ts/)
    ↓ ts:build (pnpm turbo — dep-ordered, cached in .turbo/)
    ↓ go:build (per-module, cached in .task/)
    ↓ py:build (uv sync, cached in .task/)
    ↓
CI: detect-changes → three parallel language jobs
    ↓ proto change → all three jobs + breaking check
    ↓ TypeScript: pnpm turbo --affected (fetch-depth: 0)
    ↓ Go: GOWORK=off → matrix per module → go test -race
    ↓ Python: uv run pytest (affected services)
    ↓ all jobs required → single ci-complete gate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary: Why This Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript dep linking&lt;/td&gt;
&lt;td&gt;pnpm workspaces&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;workspace:*&lt;/code&gt; + symlinks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python dep linking&lt;/td&gt;
&lt;td&gt;uv workspaces&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{ workspace = true }&lt;/code&gt; + editable installs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go dep linking&lt;/td&gt;
&lt;td&gt;go workspaces&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;use&lt;/code&gt; directives (gitignored)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-language task orchestration&lt;/td&gt;
&lt;td&gt;Task (Taskfile.yml)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;includes&lt;/code&gt; + &lt;code&gt;sources&lt;/code&gt;/&lt;code&gt;generates&lt;/code&gt; caching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-language contracts&lt;/td&gt;
&lt;td&gt;buf + Protobuf&lt;/td&gt;
&lt;td&gt;single &lt;code&gt;.proto&lt;/code&gt; → 3 language stubs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript lint + format&lt;/td&gt;
&lt;td&gt;Biome&lt;/td&gt;
&lt;td&gt;root &lt;code&gt;biome.json&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python lint + format&lt;/td&gt;
&lt;td&gt;Ruff&lt;/td&gt;
&lt;td&gt;root &lt;code&gt;pyproject.toml&lt;/code&gt; &lt;code&gt;[tool.ruff]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go lint + format&lt;/td&gt;
&lt;td&gt;golangci-lint + gofumpt&lt;/td&gt;
&lt;td&gt;root &lt;code&gt;.golangci.yml&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-language git hooks&lt;/td&gt;
&lt;td&gt;lefthook&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;glob_matcher: doublestar&lt;/code&gt; + parallel jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI: only run changed languages&lt;/td&gt;
&lt;td&gt;dorny/paths-filter&lt;/td&gt;
&lt;td&gt;per-language path filters + job conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The polyglot monorepo isn't one tool — it's a composition. Three workspace managers run in parallel. Task sits on top as the single command interface. buf provides the shared contract layer between all three languages. lefthook enforces quality at commit time regardless of which language a developer touches.&lt;/p&gt;

&lt;p&gt;The complexity cost is real: you're maintaining three build toolchains instead of one. The payoff is equally real: one &lt;code&gt;git blame&lt;/code&gt;, one pull request for a cross-language feature, one place to define the contract between your TypeScript frontend and your Go and Python backends.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deploying a Python Service from the Polyglot Monorepo
&lt;/h2&gt;

&lt;p&gt;Go compiles to a static binary — Docker is trivial. TypeScript builds to a &lt;code&gt;dist/&lt;/code&gt; directory — straightforward. Python is the tricky one: editable workspace installs point back at source paths and break inside a container without extra work.&lt;/p&gt;

&lt;p&gt;The fix is &lt;code&gt;uv sync --package ml --no-editable&lt;/code&gt;. The build context &lt;strong&gt;must be the repo root&lt;/strong&gt; — not &lt;code&gt;services/ml/&lt;/code&gt; — because &lt;code&gt;uv sync&lt;/code&gt; needs to resolve &lt;code&gt;libs/shared&lt;/code&gt;, &lt;code&gt;gen/python&lt;/code&gt;, and the root &lt;code&gt;pyproject.toml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Without a &lt;code&gt;.dockerignore&lt;/code&gt;, Docker sends the entire monorepo as context including &lt;code&gt;node_modules/&lt;/code&gt;, Go binaries, and the frontend build cache. Add this at the repo root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .dockerignore (at repo root — used by all Python service Dockerfiles)
frontend/
packages/
node_modules/
.turbo/
.task/
.next/
*.go
go.*
go.work
go.work.sum
.venv/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then build from the repo root: &lt;code&gt;docker build -f services/ml/Dockerfile .&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# services/ml/Dockerfile&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.12-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Copy manifests first — dep install layer is cached separately from source&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pyproject.toml uv.lock ./&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; libs/shared/pyproject.toml  libs/shared/pyproject.toml&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; gen/python/pyproject.toml   gen/python/pyproject.toml&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; services/ml/pyproject.toml  services/ml/pyproject.toml&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--package&lt;/span&gt; ml &lt;span class="se"&gt;\ &lt;/span&gt;     &lt;span class="c"&gt;# install only ml + its deps (libs/shared, gen/python, etc.)&lt;/span&gt;
    --no-dev \          # exclude pytest, mypy, ruff
    --frozen \          # fail if uv.lock is stale
    --no-editable       &lt;span class="c"&gt;# copy source into site-packages — no source tree needed at runtime&lt;/span&gt;

&lt;span class="c"&gt;# Copy source after dep install (changes here don't bust dep cache)&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; libs/shared/src  libs/shared/src&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; gen/python        gen/python&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; services/ml/src  services/ml/src&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/.venv /app/.venv&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH="/app/.venv/bin:$PATH"&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["uvicorn", "ml.main:app", "--host", "0.0.0.0", "--port", "8080"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;--no-editable&lt;/code&gt; is the critical flag.&lt;/strong&gt; Without it, &lt;code&gt;libs/shared&lt;/code&gt; installs as an editable install pointing at &lt;code&gt;libs/shared/src/&lt;/code&gt; in the build context. The final image has a &lt;code&gt;.venv&lt;/code&gt; with broken symlinks and imports fail at runtime. &lt;code&gt;--no-editable&lt;/code&gt; copies the package source into &lt;code&gt;.venv/lib/pythonX.Y/site-packages/&lt;/code&gt; — self-contained.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to Watch Out For
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fresh clone requires &lt;code&gt;task go:init&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;go.work&lt;/code&gt; is gitignored; a fresh clone has no workspace file. Run &lt;code&gt;task go:init&lt;/code&gt; once per clone (or once per machine). The &lt;code&gt;status:&lt;/code&gt; guard makes it idempotent — it's safe to run every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;gen/ts/&lt;/code&gt; must be a workspace member, not &lt;code&gt;file:&lt;/code&gt;&lt;/strong&gt; — a &lt;code&gt;file:&lt;/code&gt; reference copies the package into &lt;code&gt;node_modules&lt;/code&gt; at install time. After &lt;code&gt;buf generate&lt;/code&gt;, the copies are stale until &lt;code&gt;pnpm install&lt;/code&gt; runs again. Use &lt;code&gt;workspace:*&lt;/code&gt; (symlinked, always live).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker build context must be repo root&lt;/strong&gt; — &lt;code&gt;uv sync --package ml&lt;/code&gt; resolves workspace deps (&lt;code&gt;libs/shared&lt;/code&gt;, &lt;code&gt;gen/python&lt;/code&gt;). Build with &lt;code&gt;docker build -f services/ml/Dockerfile .&lt;/code&gt; from the repo root. Use &lt;code&gt;.dockerignore&lt;/code&gt; to exclude &lt;code&gt;node_modules/&lt;/code&gt;, Go source, and the frontend build cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;GOWORK=off&lt;/code&gt; in CI must be explicit&lt;/strong&gt; — &lt;code&gt;.gitignore&lt;/code&gt; prevents committing &lt;code&gt;go.work&lt;/code&gt; but doesn't prevent &lt;code&gt;git add -f&lt;/code&gt;. Set &lt;code&gt;GOWORK=off&lt;/code&gt; in every CI Go step. Don't rely on the file not being present.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;go.mod&lt;/code&gt; replace directives are required for CI&lt;/strong&gt; — &lt;code&gt;go.work&lt;/code&gt; resolves in-repo deps locally, but &lt;code&gt;GOWORK=off&lt;/code&gt; requires each module's &lt;code&gt;go.mod&lt;/code&gt; to have &lt;code&gt;replace&lt;/code&gt; directives: &lt;code&gt;replace github.com/your-org/beacon/gen/go =&amp;gt; ../../gen/go&lt;/code&gt;. Without these, CI fails because the module can't find its in-repo dependencies. Run &lt;code&gt;go mod tidy&lt;/code&gt; in each module after adding them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;golangci-lint cannot accept &lt;code&gt;{staged_files}&lt;/code&gt; across packages&lt;/strong&gt; — passing files from multiple packages produces "named files must all be in one directory." Run golangci-lint per-module in pre-push, not per-file in pre-commit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pnpm and uv both need lock enforcement in CI&lt;/strong&gt; — &lt;code&gt;pnpm install --frozen-lockfile&lt;/code&gt; and &lt;code&gt;uv sync --frozen&lt;/code&gt; are the equivalent safety checks. Omitting either means CI silently tests different dep versions than developers have locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;sources&lt;/code&gt; lists must include lockfiles&lt;/strong&gt; — Task caches by hashing declared &lt;code&gt;sources&lt;/code&gt;. If &lt;code&gt;uv.lock&lt;/code&gt;, &lt;code&gt;pnpm-lock.yaml&lt;/code&gt;, or &lt;code&gt;go.sum&lt;/code&gt; changes without any source file changing, Task serves a stale cached build. Add lockfiles to every &lt;code&gt;sources&lt;/code&gt; list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;task dev&lt;/code&gt; log visibility&lt;/strong&gt; — three servers writing to one terminal is unreadable. Run them in separate terminals or use a process manager (&lt;code&gt;overmind&lt;/code&gt;, &lt;code&gt;foreman&lt;/code&gt;, or &lt;code&gt;pnpm turbo run dev&lt;/code&gt; for TypeScript). &lt;code&gt;task ts:dev&lt;/code&gt;, &lt;code&gt;task go:dev&lt;/code&gt;, &lt;code&gt;task py:dev&lt;/code&gt; as separate commands is the pragmatic answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;uv &lt;code&gt;members&lt;/code&gt; and pnpm coexistence&lt;/strong&gt; — uv will error if a &lt;code&gt;members&lt;/code&gt; glob matches a directory that has no &lt;code&gt;pyproject.toml&lt;/code&gt;. Be explicit: list Python-only directories rather than using broad globs like &lt;code&gt;services/*&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;mypy in pre-push, not pre-commit&lt;/strong&gt; — mypy is 10–30s on a real Python codebase. Every commit, on any &lt;code&gt;.py&lt;/code&gt; change. It belongs in &lt;code&gt;pre-push&lt;/code&gt; alongside &lt;code&gt;buf breaking&lt;/code&gt;, not blocking commits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;uv sync --frozen&lt;/code&gt; in CI&lt;/strong&gt; — without &lt;code&gt;--frozen&lt;/code&gt;, uv silently re-resolves if &lt;code&gt;uv.lock&lt;/code&gt; is stale, meaning CI may test different dep versions than developers have locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--no-editable&lt;/code&gt; in Python Docker images&lt;/strong&gt; — editable installs point at source paths in the build context. Use &lt;code&gt;uv sync --package ml --no-editable&lt;/code&gt; in Dockerfiles so the &lt;code&gt;.venv&lt;/code&gt; is self-contained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;jq&lt;/code&gt; is a prerequisite&lt;/strong&gt; — &lt;code&gt;Taskfile.go.yml&lt;/code&gt; uses &lt;code&gt;go work edit -json | jq&lt;/code&gt;. Install with &lt;code&gt;brew install jq&lt;/code&gt; or &lt;code&gt;apt install jq&lt;/code&gt;. Add to your &lt;code&gt;task prereqs&lt;/code&gt; check or README.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;proto changes cascade + deployment order&lt;/strong&gt; — &lt;code&gt;buf breaking&lt;/code&gt; catches compile-time breakage, but not runtime version skew. In production, always deploy consumers (Python ML, TypeScript frontend) before producers (Go API) when adding fields. Never remove or rename fields in a single deployment — deprecate first, remove in a later release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;lefthook &lt;code&gt;glob_matcher: doublestar&lt;/code&gt;&lt;/strong&gt; — without this line, &lt;code&gt;**/*.py&lt;/code&gt; won't match files more than one directory deep. Always verify with &lt;code&gt;lefthook run pre-commit --all-files&lt;/code&gt; after first setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generated code is committed&lt;/strong&gt; — &lt;code&gt;gen/go/&lt;/code&gt;, &lt;code&gt;gen/python/&lt;/code&gt;, &lt;code&gt;gen/ts/&lt;/code&gt; are in git. This keeps CI simple and makes stub changes reviewable. Proto changes produce large diffs — that's the tradeoff.&lt;/p&gt;




&lt;h2&gt;
  
  
  Going Further
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Dependency updates across three package managers&lt;/strong&gt; — use &lt;a href="https://docs.renovatebot.com/" rel="noopener noreferrer"&gt;Renovate&lt;/a&gt; (not Dependabot — Renovate handles pnpm, uv, and Go modules in a single config). For audits: &lt;code&gt;pnpm audit&lt;/code&gt;, &lt;code&gt;uv audit&lt;/code&gt;, &lt;code&gt;govulncheck ./...&lt;/code&gt; per Go module.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding a new service&lt;/strong&gt; — checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python service: add to &lt;code&gt;pyproject.toml&lt;/code&gt; &lt;code&gt;members&lt;/code&gt;, create &lt;code&gt;pyproject.toml&lt;/code&gt; + &lt;code&gt;Taskfile.yml&lt;/code&gt; + &lt;code&gt;dir:&lt;/code&gt; include, add to &lt;code&gt;lefthook.yml&lt;/code&gt; paths, add to CI &lt;code&gt;py&lt;/code&gt; paths-filter&lt;/li&gt;
&lt;li&gt;Go service: add to &lt;code&gt;go.work use&lt;/code&gt;, add to &lt;code&gt;Taskfile.go.yml init&lt;/code&gt; cmd + lint/test, add CI matrix entry&lt;/li&gt;
&lt;li&gt;TypeScript package: add to &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;, add Taskfile include&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cross-language integration tests&lt;/strong&gt; — unit tests per language are not enough. Add a &lt;code&gt;task test:integration&lt;/code&gt; that starts all three services (via Docker Compose or k3s) and runs contract tests against the live stack. &lt;a href="https://pact.io" rel="noopener noreferrer"&gt;Pact&lt;/a&gt; works across Go, Python, and TypeScript.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment config&lt;/strong&gt; — one &lt;code&gt;.env&lt;/code&gt; at repo root, read by all three languages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript: &lt;code&gt;dotenv&lt;/code&gt; or &lt;code&gt;@t3-oss/env-nextjs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Python: &lt;code&gt;pydantic-settings&lt;/code&gt; with &lt;code&gt;env_file = ".env"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Go: &lt;code&gt;godotenv.Load()&lt;/code&gt; or pass via Docker env&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;CODEOWNERS&lt;/strong&gt; — &lt;code&gt;proto/&lt;/code&gt; should require review from both Go and Python leads. Use GitHub CODEOWNERS to enforce this on every proto PR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hot reload across proto changes&lt;/strong&gt; — &lt;code&gt;task dev&lt;/code&gt; doesn't re-run &lt;code&gt;buf generate&lt;/code&gt; when &lt;code&gt;.proto&lt;/code&gt; files change. Add a file-watcher task (e.g., via &lt;code&gt;watchexec&lt;/code&gt; or &lt;code&gt;task --watch&lt;/code&gt;) that runs &lt;code&gt;buf generate&lt;/code&gt; and restarts affected dev servers on &lt;code&gt;.proto&lt;/code&gt; changes.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>polyglot</category>
      <category>coding</category>
    </item>
    <item>
      <title>Go Monorepo Magic: Organize, Build, and Ship Multi-Service Apps</title>
      <dc:creator>M Hossein</dc:creator>
      <pubDate>Thu, 04 Jun 2026 07:49:00 +0000</pubDate>
      <link>https://dev.to/_mh/go-monorepo-magic-organize-build-and-ship-multi-service-apps-55i1</link>
      <guid>https://dev.to/_mh/go-monorepo-magic-organize-build-and-ship-multi-service-apps-55i1</guid>
      <description>&lt;h2&gt;
  
  
  What is a Monorepo?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;monorepo&lt;/strong&gt; is a single Git repository that contains multiple distinct services or modules. The alternative is a &lt;strong&gt;polyrepo&lt;/strong&gt;: one repo per service.&lt;/p&gt;

&lt;p&gt;This repo is &lt;code&gt;forge-monorepo&lt;/code&gt;. It contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;forge-monorepo/
├── services/
│   ├── api/         ← HTTP API service
│   ├── worker/      ← Background job processor
│   └── notifier/    ← Notification service
├── pkg/
│   ├── shared/      ← Shared types + domain logic
│   ├── events/      ← Event definitions
│   └── proto/       ← Protobuf definitions
└── gen/
    └── go/          ← Generated proto stubs (own go.mod)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The core idea:&lt;/strong&gt; code needed by multiple services lives in &lt;code&gt;pkg/&lt;/code&gt;, and all services import it directly — no private module proxy, no vendoring ceremony.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 1: Go Workspaces (&lt;code&gt;go work&lt;/code&gt;) — The Foundation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Go workspaces&lt;/strong&gt; are the monorepo primitive built into the Go toolchain since 1.18. They replace &lt;code&gt;replace&lt;/code&gt; directives in &lt;code&gt;go.mod&lt;/code&gt; for local multi-module development.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it's declared
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;go.work&lt;/code&gt; at the repo root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="m"&gt;1.22&lt;/span&gt;

&lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;gen&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;
    &lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;
    &lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;
    &lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;
    &lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;
    &lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;notifier&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells the Go toolchain: &lt;em&gt;treat every listed directory as a module, and resolve imports between them on disk&lt;/em&gt;. That's it — one file defines the entire workspace.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this enables
&lt;/h3&gt;

&lt;p&gt;When you run any &lt;code&gt;go&lt;/code&gt; command inside the workspace, the toolchain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads every &lt;code&gt;go.mod&lt;/code&gt; in every listed directory&lt;/li&gt;
&lt;li&gt;Resolves inter-module imports to local disk paths automatically&lt;/li&gt;
&lt;li&gt;Lets every module keep its own &lt;code&gt;go.mod&lt;/code&gt; and version independently&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Internal imports
&lt;/h3&gt;

&lt;p&gt;Each module has a normal &lt;code&gt;go.mod&lt;/code&gt; with a full module path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// pkg/shared/go.mod&lt;/span&gt;
&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;yourorg&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;forge&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;

&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="m"&gt;1.22&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any other workspace module imports it by that exact path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// services/api/main.go&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/yourorg/forge/pkg/shared"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/yourorg/forge/pkg/events"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No special syntax. The workspace resolves &lt;code&gt;github.com/yourorg/forge/pkg/shared&lt;/code&gt; to &lt;code&gt;./pkg/shared&lt;/code&gt; on disk automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Critical: do NOT commit &lt;code&gt;go.work&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;go.work&lt;/code&gt; is a local development convenience. It is &lt;strong&gt;not&lt;/strong&gt; a source of truth for module dependencies. Add both files to &lt;code&gt;.gitignore&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Go workspace (local dev only)
go.work
go.work.sum

# Task cache
.task/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CI validates each module independently with &lt;code&gt;GOWORK=off&lt;/code&gt; — more on that in Tool 2.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why &lt;code&gt;go work&lt;/code&gt; over &lt;code&gt;replace&lt;/code&gt; directives?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;replace&lt;/code&gt; directives in &lt;code&gt;go.mod&lt;/code&gt; are messy: they leak into the module graph, they break when you &lt;code&gt;go get&lt;/code&gt;, and you have to touch every module's &lt;code&gt;go.mod&lt;/code&gt; to wire things up. &lt;code&gt;go work&lt;/code&gt; is additive — the workspace file lives outside the modules and doesn't pollute their dependency declarations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 2: Task (Taskfile.yml) — Task Orchestration and Caching
&lt;/h2&gt;

&lt;p&gt;Go workspaces handle &lt;em&gt;module resolution&lt;/em&gt;. &lt;strong&gt;Task&lt;/strong&gt; handles &lt;em&gt;build orchestration&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The problem Task solves
&lt;/h3&gt;

&lt;p&gt;If you need to build 6 modules in the right order, you have to figure out the sequence yourself. And if nothing in &lt;code&gt;pkg/shared&lt;/code&gt; changed, you shouldn't rebuild everything that depends on it.&lt;/p&gt;

&lt;p&gt;Task solves both: &lt;strong&gt;explicit task dependency ordering&lt;/strong&gt; + &lt;strong&gt;file-based output caching&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Taskfile.yml&lt;/code&gt; — the pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;

&lt;span class="na"&gt;includes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gen&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./gen/go/Taskfile.yml&lt;/span&gt;
  &lt;span class="na"&gt;shared&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./pkg/shared/Taskfile.yml&lt;/span&gt;
  &lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./pkg/events/Taskfile.yml&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./services/api/Taskfile.yml&lt;/span&gt;
  &lt;span class="na"&gt;worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./services/worker/Taskfile.yml&lt;/span&gt;
  &lt;span class="na"&gt;notifier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./services/notifier/Taskfile.yml&lt;/span&gt;

&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build:all&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build all modules in dependency order&lt;/span&gt;
    &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gen:build&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared:build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;events:build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api:build&lt;/span&gt;
        &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worker:build&lt;/span&gt;
        &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;notifier:build&lt;/span&gt;
        &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="na"&gt;test:all&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test all modules&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared:test&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;events:test&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api:test&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worker:test&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;notifier:test&lt;/span&gt;

  &lt;span class="na"&gt;lint:all&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run golangci-lint across all modules&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go work edit -json | jq -r '.Use[].DiskPath' | xargs -P4 -I{} sh -c 'cd {} &amp;amp;&amp;amp; golangci-lint run ./...'&lt;/span&gt;

  &lt;span class="na"&gt;tidy:all&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run go mod tidy in every module&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go work edit -json | jq -r '.Use[].DiskPath' | xargs -I{} sh -c 'cd {} &amp;amp;&amp;amp; go mod tidy'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A per-module Taskfile (e.g., &lt;code&gt;pkg/shared/Taskfile.yml&lt;/code&gt;) looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;

&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*.go'&lt;/span&gt;
    &lt;span class="na"&gt;generates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.task/shared.built'&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go build ./...&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;touch .task/shared.built&lt;/span&gt;

  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*.go'&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go test ./...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key concepts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;deps:&lt;/code&gt; — tasks listed here run in parallel before the current task starts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cmds:&lt;/code&gt; — commands run sequentially in order&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sources&lt;/code&gt; / &lt;code&gt;generates&lt;/code&gt; — Task hashes these paths. Cache hit → task skipped entirely&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;async: true&lt;/code&gt; — run the task concurrently with other commands in the same &lt;code&gt;cmds:&lt;/code&gt; block&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The build graph
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gen/go (buf generate)
    ↓
pkg/shared (go build)
    ↓
pkg/events (go build)
    ↓
services/api     services/worker     services/notifier
  (go build)       (go build)           (go build)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Affected-only in CI
&lt;/h3&gt;

&lt;p&gt;Task has no &lt;code&gt;--affected&lt;/code&gt; flag. Implement it with a git diff script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# scripts/affected.sh&lt;/span&gt;
&lt;span class="c"&gt;# Outputs a newline-separated list of changed module paths&lt;/span&gt;

&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;/main&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;CHANGED_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;...HEAD&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; AFFECTED

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; file&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c"&gt;# Match each workspace module by its directory prefix&lt;/span&gt;
  go work edit &lt;span class="nt"&gt;-json&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.Use[].DiskPath'&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; mod&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nv"&gt;mod_clean&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;#./&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$mod_clean&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$mod_clean&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;fi
  done
done&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHANGED_FILES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run affected tests&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;AFFECTED=$(bash scripts/affected.sh origin/main)&lt;/span&gt;
    &lt;span class="s"&gt;for mod in $AFFECTED; do&lt;/span&gt;
      &lt;span class="s"&gt;(cd "$mod" &amp;amp;&amp;amp; GOWORK=off go test ./...)&lt;/span&gt;
    &lt;span class="s"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Tool 3: Shared Modules — The Internal Module Pattern
&lt;/h2&gt;

&lt;p&gt;Each &lt;code&gt;pkg/&lt;/code&gt; module is a normal Go module with its own &lt;code&gt;go.mod&lt;/code&gt;. It doesn't need to be published anywhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pkg/shared/
├── go.mod          ← module github.com/yourorg/forge/pkg/shared
├── types.go
├── domain.go
└── errors.go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;pkg/shared&lt;/code&gt; is listed in &lt;code&gt;go.work&lt;/code&gt;, any service imports it by its declared module path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/yourorg/forge/pkg/shared"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/yourorg/forge/pkg/events"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"alice"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewUserCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The key insight:&lt;/strong&gt; the workspace resolves that import to disk at development time. In CI, with &lt;code&gt;GOWORK=off&lt;/code&gt;, each module must declare real &lt;code&gt;require&lt;/code&gt; entries in its own &lt;code&gt;go.mod&lt;/code&gt;. This forces you to be explicit about dependencies while keeping the local dev loop frictionless.&lt;/p&gt;

&lt;h3&gt;
  
  
  Proto and generated code
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pkg/proto/&lt;/code&gt; holds &lt;code&gt;.proto&lt;/code&gt; files. &lt;code&gt;gen/go/&lt;/code&gt; holds the generated stubs as its own module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// gen/go/go.mod&lt;/span&gt;
&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;yourorg&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;forge&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;gen&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;

&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="m"&gt;1.22&lt;/span&gt;

&lt;span class="n"&gt;require&lt;/span&gt; &lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;golang&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;protobuf&lt;/span&gt; &lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="m"&gt;.34.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Services import generated types the same way — no magic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;forgepb&lt;/span&gt; &lt;span class="s"&gt;"github.com/yourorg/forge/gen/go"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Tool 4: Root &lt;code&gt;.golangci.yml&lt;/code&gt; — Shared Config
&lt;/h2&gt;

&lt;p&gt;Centralize lint rules instead of duplicating them across every module.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.golangci.yml&lt;/code&gt; at repo root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2"&lt;/span&gt;

&lt;span class="na"&gt;linters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;errcheck&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gosimple&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;govet&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ineffassign&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;staticcheck&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unused&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gofumpt&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;goimports&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;revive&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gosec&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;exhaustive&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;wrapcheck&lt;/span&gt;

&lt;span class="na"&gt;linters-settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gofumpt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;extra-rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;goimports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;local-prefixes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.com/yourorg/forge&lt;/span&gt;
  &lt;span class="na"&gt;revive&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;exported&lt;/span&gt;
        &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warning&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;error-return&lt;/span&gt;
        &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt;

&lt;span class="na"&gt;issues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;exclude-rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;_test\.go&lt;/span&gt;
      &lt;span class="na"&gt;linters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gosec&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;errcheck&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;golangci-lint v2 walks &lt;strong&gt;upward&lt;/strong&gt; from the directory where it's invoked to find the nearest config file. Running &lt;code&gt;golangci-lint run ./...&lt;/code&gt; inside &lt;code&gt;services/api/&lt;/code&gt; picks up the root &lt;code&gt;.golangci.yml&lt;/code&gt; automatically — no per-module config needed unless you want to override.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; change one rule and it propagates to every service. No drift, no &lt;code&gt;eslint.config.js&lt;/code&gt; archaeology.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 5: golangci-lint + gofumpt — Unified Linting and Formatting
&lt;/h2&gt;

&lt;p&gt;One &lt;code&gt;.golangci.yml&lt;/code&gt; at the root. Each module runs &lt;code&gt;golangci-lint run ./...&lt;/code&gt; but rules are defined once. &lt;strong&gt;One source of truth, many consumers.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  golangci-lint v2
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;version: "2"&lt;/code&gt; key at the top of &lt;code&gt;.golangci.yml&lt;/code&gt; is required for the v2 config schema. The v1 schema is silently different — don't mix them.&lt;/p&gt;

&lt;p&gt;Run across all modules in parallel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go work edit &lt;span class="nt"&gt;-json&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.Use[].DiskPath'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  xargs &lt;span class="nt"&gt;-P4&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt;&lt;span class="o"&gt;{}&lt;/span&gt; sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'cd {} &amp;amp;&amp;amp; golangci-lint run ./...'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  gofumpt
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;gofumpt&lt;/strong&gt; is a stricter superset of &lt;code&gt;gofmt&lt;/code&gt;. It enforces additional rules: blank lines between imports and first declaration, grouping of related declarations, trailing newlines. Configure it under &lt;code&gt;formatters:&lt;/code&gt; in the v2 schema (or as a linter via &lt;code&gt;gofumpt: true&lt;/code&gt; in linters-settings as shown above).&lt;/p&gt;

&lt;p&gt;Install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;mvdan.cc/gofumpt@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run standalone (useful in hooks):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gofumpt &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why not just &lt;code&gt;gofmt&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;gofmt&lt;/code&gt; is the baseline. &lt;code&gt;gofumpt&lt;/code&gt; is what you actually want in a team — it eliminates the formatting debates &lt;code&gt;gofmt&lt;/code&gt; leaves open. Add it to the linter pipeline once and you never argue about blank lines again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 6: lefthook — Git Hooks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;lefthook&lt;/strong&gt; is a fast, language-agnostic Git hooks manager written in Go. It ships as a single binary, runs hooks in parallel by default, and integrates natively with staged files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/evilmartians/lefthook@latest
&lt;span class="c"&gt;# or: brew install lefthook&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialize in the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lefthook &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;lefthook.yml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;pre-commit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;parallel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;go-lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.go"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;golangci-lint run {staged_files}&lt;/span&gt;
      &lt;span class="na"&gt;stage_fixed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="na"&gt;go-fmt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.go"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gofumpt -l -w {staged_files}&lt;/span&gt;
      &lt;span class="na"&gt;stage_fixed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="na"&gt;proto-lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.proto"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buf lint {staged_files}&lt;/span&gt;

    &lt;span class="na"&gt;mod-tidy-check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;go work edit -json | jq -r '.Use[].DiskPath' | while read -r mod; do&lt;/span&gt;
          &lt;span class="s"&gt;(cd "$mod" &amp;amp;&amp;amp; go mod tidy &amp;amp;&amp;amp; git diff --exit-code go.mod go.sum) || exit 1&lt;/span&gt;
        &lt;span class="s"&gt;done&lt;/span&gt;

&lt;span class="na"&gt;commit-msg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;conventional&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;msg=$(cat {1})&lt;/span&gt;
        &lt;span class="s"&gt;if ! echo "$msg" | grep -qE '^(feat|fix|chore|docs|refactor|test|ci)(\(.+\))?: .+'; then&lt;/span&gt;
          &lt;span class="s"&gt;echo "Commit message must follow Conventional Commits format"&lt;/span&gt;
          &lt;span class="s"&gt;exit 1&lt;/span&gt;
        &lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key concepts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;{staged_files}&lt;/code&gt; — lefthook's template variable; expands to the list of files currently staged for this commit&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stage_fixed: true&lt;/code&gt; — if &lt;code&gt;gofumpt&lt;/code&gt; rewrites a file, lefthook re-stages it automatically. You don't have to &lt;code&gt;git add&lt;/code&gt; again.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;parallel: true&lt;/code&gt; — go-lint and go-fmt run concurrently; the hook only blocks as long as the slowest command&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The key insight:&lt;/strong&gt; it doesn't lint the whole repo on every commit — only the files currently staged. &lt;code&gt;golangci-lint run {staged_files}&lt;/code&gt; is dramatically faster than &lt;code&gt;golangci-lint run ./...&lt;/code&gt; across every module. This keeps commits fast while still enforcing quality.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Picture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer commits
    ↓
lefthook pre-commit
    ↓ golangci-lint on staged .go files (fast, per-file)
    ↓ buf lint on staged .proto files
    ↓ go mod tidy check
    ↓ (fails → abort; passes → continue)
    ↓
go work resolves internal imports to local disk paths
    ↓
task build:all
    ↓ reads Taskfile.yml includes
    ↓ builds modules in order: gen/go → pkg/shared → pkg/events → services/* (parallel)
    ↓ caches outputs in .task/
    ↓
CI: GOWORK=off — each module tested independently
    ↓ git diff → affected modules → test only those + dependents
    ↓ golangci-lint matrix across modules
    ↓ .golangci.yml config inherited from repo root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary: Why This Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Share code without publishing&lt;/td&gt;
&lt;td&gt;Go Workspaces&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;go.work&lt;/code&gt; + disk path resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run tasks in dependency order&lt;/td&gt;
&lt;td&gt;Task&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;deps:&lt;/code&gt; + &lt;code&gt;cmds:&lt;/code&gt; ordering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Don't rebuild unchanged modules&lt;/td&gt;
&lt;td&gt;Task cache&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sources&lt;/code&gt;/&lt;code&gt;generates&lt;/code&gt; hashing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI: only changed modules&lt;/td&gt;
&lt;td&gt;git diff script&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GOWORK=off&lt;/code&gt; per-module testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consistent lint config&lt;/td&gt;
&lt;td&gt;Root &lt;code&gt;.golangci.yml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;upward config file walk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One linter + formatter&lt;/td&gt;
&lt;td&gt;golangci-lint + gofumpt&lt;/td&gt;
&lt;td&gt;single &lt;code&gt;.golangci.yml&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quality before commits&lt;/td&gt;
&lt;td&gt;lefthook&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{staged_files}&lt;/code&gt; pre-commit hook&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Clone and initialize workspace&lt;/span&gt;
git clone https://github.com/yourorg/forge-monorepo
&lt;span class="nb"&gt;cd &lt;/span&gt;forge-monorepo
go work &lt;span class="nb"&gt;sync&lt;/span&gt;          &lt;span class="c"&gt;# sync all module dependencies&lt;/span&gt;

&lt;span class="c"&gt;# 2. Install tools&lt;/span&gt;
go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/go-task/task/v3/cmd/task@latest
go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/golangci/golangci-lint/cmd/golangci-lint@latest
go &lt;span class="nb"&gt;install &lt;/span&gt;mvdan.cc/gofumpt@latest
go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/evilmartians/lefthook@latest
brew &lt;span class="nb"&gt;install &lt;/span&gt;bufbuild/buf/buf

&lt;span class="c"&gt;# 3. Install git hooks&lt;/span&gt;
lefthook &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# 4. Build everything&lt;/span&gt;
task build:all

&lt;span class="c"&gt;# 5. Test everything&lt;/span&gt;
task &lt;span class="nb"&gt;test&lt;/span&gt;:all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What to Watch Out For
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;go.work&lt;/code&gt; in CI.&lt;/strong&gt; Set &lt;code&gt;GOWORK=off&lt;/code&gt; in your CI environment variables. Each module's &lt;code&gt;go.mod&lt;/code&gt; must have correct &lt;code&gt;require&lt;/code&gt; entries — the workspace won't paper over missing dependencies in CI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Module path discipline.&lt;/strong&gt; Your &lt;code&gt;go.mod&lt;/code&gt; module paths must be globally valid (i.e., &lt;code&gt;github.com/yourorg/forge/...&lt;/code&gt;), even if you never publish. The workspace resolves them locally, but the paths need to be stable and consistent across all modules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;golangci-lint v1 vs v2.&lt;/strong&gt; The config schema changed significantly. If you're migrating from v1, the &lt;code&gt;version: "2"&lt;/code&gt; key is mandatory and several linter names moved. Run &lt;code&gt;golangci-lint migrate&lt;/code&gt; to auto-upgrade your config.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Task and &lt;code&gt;go generate&lt;/code&gt;.&lt;/strong&gt; Add a &lt;code&gt;generate&lt;/code&gt; task that runs &lt;code&gt;buf generate&lt;/code&gt; before any &lt;code&gt;go build&lt;/code&gt; task that depends on generated types. Generated code in &lt;code&gt;gen/go/&lt;/code&gt; is a real module with its own &lt;code&gt;go.mod&lt;/code&gt; — it needs to be built first, just like any other dependency.&lt;/p&gt;

</description>
      <category>go</category>
      <category>tutorial</category>
      <category>code</category>
    </item>
    <item>
      <title>Python Monorepo Magic: Organize, Build, and Ship Multi-Service Apps</title>
      <dc:creator>M Hossein</dc:creator>
      <pubDate>Wed, 03 Jun 2026 07:10:00 +0000</pubDate>
      <link>https://dev.to/_mh/python-monorepo-magic-organize-build-and-ship-multi-service-apps-3al3</link>
      <guid>https://dev.to/_mh/python-monorepo-magic-organize-build-and-ship-multi-service-apps-3al3</guid>
      <description>&lt;h2&gt;
  
  
  What is a Monorepo?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;monorepo&lt;/strong&gt; is a single Git repository that contains multiple distinct packages or applications. The alternative is a &lt;strong&gt;polyrepo&lt;/strong&gt;: one repo per app or package.&lt;/p&gt;

&lt;p&gt;This repo is &lt;code&gt;nest-monorepo&lt;/code&gt;. It contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nest-monorepo/
├── apps/
│   ├── api/         ← FastAPI backend
│   ├── worker/      ← Celery async worker
│   └── cli/         ← Internal tooling CLI
├── packages/
│   ├── shared/      ← Pydantic schemas + SQLAlchemy models
│   ├── events/      ← Event type definitions
│   └── proto/       ← Protobuf definitions + generated stubs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The core idea:&lt;/strong&gt; code that is needed by multiple apps lives in &lt;code&gt;packages/&lt;/code&gt;, and all apps can import it directly — no PyPI publishing required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 1: uv Workspaces — The Foundation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;uv&lt;/strong&gt; is the package manager. Workspaces are its monorepo feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it's declared
&lt;/h3&gt;

&lt;p&gt;Root &lt;code&gt;pyproject.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.uv.workspace]&lt;/span&gt;
&lt;span class="py"&gt;members&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"apps/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"packages/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Three lines define the entire workspace. uv reads every &lt;code&gt;pyproject.toml&lt;/code&gt; under those globs and treats each directory as a workspace member.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this enables
&lt;/h3&gt;

&lt;p&gt;When you run &lt;code&gt;uv sync&lt;/code&gt; at the repo root, uv:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads every &lt;code&gt;pyproject.toml&lt;/code&gt; in every workspace directory&lt;/li&gt;
&lt;li&gt;Resolves all external dependencies into a single &lt;code&gt;uv.lock&lt;/code&gt; file at the root&lt;/li&gt;
&lt;li&gt;Installs internal packages as editable installs so they can import each other directly&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;{ workspace = true }&lt;/code&gt; source
&lt;/h3&gt;

&lt;p&gt;Look at &lt;code&gt;apps/api/pyproject.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"api"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;requires-python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.12&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="py"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"shared"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"events"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;"fastapi&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.115&lt;/span&gt;&lt;span class="s"&gt;"]&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;tool.uv.sources&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;workspace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;workspace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;{ workspace = true }&lt;/code&gt; means: &lt;em&gt;don't fetch this from PyPI — link it from this workspace instead&lt;/em&gt;. When &lt;code&gt;api&lt;/code&gt; imports &lt;code&gt;shared&lt;/code&gt;, Python resolves it to &lt;code&gt;packages/shared/src/&lt;/code&gt; on disk via an editable install. No publishing, no version pinning against PyPI, no symlink juggling.&lt;/p&gt;

&lt;p&gt;This is the mechanism that makes internal sharing work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why uv over pip/poetry?
&lt;/h3&gt;

&lt;p&gt;uv uses a &lt;strong&gt;global content-addressable cache&lt;/strong&gt; at &lt;code&gt;~/.cache/uv&lt;/code&gt;. Every version of every package is stored once globally across all projects on your machine. Workspaces get hard links to the cache, not copies. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10–100x faster installs than pip&lt;/li&gt;
&lt;li&gt;A single &lt;code&gt;uv.lock&lt;/code&gt; committed at the repo root — one lockfile, all packages&lt;/li&gt;
&lt;li&gt;Strict resolution — packages can't accidentally import things they didn't declare&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;uv.lock&lt;/code&gt; is committed to version control. Unlike &lt;code&gt;go.work.sum&lt;/code&gt;, it is not gitignored. It is the canonical record of every resolved dependency across the entire workspace.&lt;/p&gt;

&lt;h3&gt;
  
  
  One virtualenv to rule them all
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;uv sync&lt;/code&gt; creates a &lt;strong&gt;single &lt;code&gt;.venv&lt;/code&gt; at the repo root&lt;/strong&gt;. All workspace members are installed into it as editable installs. There is no per-package virtualenv.&lt;/p&gt;

&lt;p&gt;This has two important consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No version skew.&lt;/strong&gt; &lt;code&gt;api&lt;/code&gt; and &lt;code&gt;worker&lt;/code&gt; cannot depend on different versions of &lt;code&gt;shared&lt;/code&gt; — they share the same installed environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IDE setup is trivial.&lt;/strong&gt; Point your editor's Python interpreter at &lt;code&gt;.venv/bin/python&lt;/code&gt; once, at the repo root. Every package is immediately importable — no per-project interpreter switching.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt;                  &lt;span class="c"&gt;# creates .venv, installs everything&lt;/span&gt;
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate  &lt;span class="c"&gt;# or let your IDE detect it&lt;/span&gt;
python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import shared"&lt;/span&gt;  &lt;span class="c"&gt;# works from anywhere in the repo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;.venv/&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 2: Task (Taskfile.yml) — Task Orchestration and Caching
&lt;/h2&gt;

&lt;p&gt;uv workspaces handle &lt;em&gt;dependencies&lt;/em&gt;. &lt;strong&gt;Task&lt;/strong&gt; handles &lt;em&gt;tasks&lt;/em&gt; (build, test, lint, etc.).&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing Task
&lt;/h3&gt;

&lt;p&gt;Task is a language-agnostic binary — it has nothing to do with Go and requires no Go installation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS / Linux via Homebrew (recommended)&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;go-task

&lt;span class="c"&gt;# Linux: direct install script (no Go needed)&lt;/span&gt;
sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;--location&lt;/span&gt; https://taskfile.dev/install.sh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; ~/.local/bin

&lt;span class="c"&gt;# Or grab a binary directly from https://github.com/go-task/task/releases&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify with &lt;code&gt;task --version&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The problem Task solves
&lt;/h3&gt;

&lt;p&gt;If you run &lt;code&gt;uv run pytest&lt;/code&gt; across 6 packages, you have to figure out the order yourself. &lt;code&gt;shared&lt;/code&gt; must be importable before &lt;code&gt;events&lt;/code&gt;, which must be importable before &lt;code&gt;api&lt;/code&gt; and &lt;code&gt;worker&lt;/code&gt;. &lt;code&gt;proto&lt;/code&gt; must run &lt;code&gt;buf generate&lt;/code&gt; before anything imports the generated stubs. Do it wrong and you get import errors or stale generated code.&lt;/p&gt;

&lt;p&gt;Also, if nothing in &lt;code&gt;packages/shared&lt;/code&gt; changed, you shouldn't rebuild it.&lt;/p&gt;

&lt;p&gt;Task solves both: &lt;strong&gt;explicit task ordering&lt;/strong&gt; + &lt;strong&gt;file-based caching via &lt;code&gt;sources&lt;/code&gt;/&lt;code&gt;generates&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Taskfile.yml&lt;/code&gt; — the pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;

&lt;span class="na"&gt;includes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;shared&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;taskfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./packages/shared/Taskfile.yml&lt;/span&gt;
    &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./packages/shared&lt;/span&gt;        &lt;span class="c1"&gt;# without dir:, commands run from repo root&lt;/span&gt;
  &lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;taskfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./packages/events/Taskfile.yml&lt;/span&gt;
    &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./packages/events&lt;/span&gt;
  &lt;span class="na"&gt;proto&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;taskfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./packages/proto/Taskfile.yml&lt;/span&gt;
    &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./packages/proto&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;taskfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./apps/api/Taskfile.yml&lt;/span&gt;
    &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./apps/api&lt;/span&gt;
  &lt;span class="na"&gt;worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;taskfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./apps/worker/Taskfile.yml&lt;/span&gt;
    &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./apps/worker&lt;/span&gt;
  &lt;span class="na"&gt;cli&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;taskfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./apps/cli/Taskfile.yml&lt;/span&gt;
    &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./apps/cli&lt;/span&gt;

&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build:all&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build all packages in dependency order&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared:build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;events:build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proto:generate&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api:build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worker:build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cli:build&lt;/span&gt;

  &lt;span class="na"&gt;test:all&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run all tests&lt;/span&gt;
    &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;shared&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;proto&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;generate&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api:test&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worker:test&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cli:test&lt;/span&gt;

  &lt;span class="na"&gt;lint:all&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared:lint&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;events:lint&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proto:lint&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api:lint&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worker:lint&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cli:lint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;packages/proto/Taskfile.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;

&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate Python stubs from .proto files&lt;/span&gt;
    &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*.proto'&lt;/span&gt;
    &lt;span class="na"&gt;generates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;generated/**/*.py'&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;buf generate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key concepts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sources&lt;/code&gt; — glob patterns of input files. Task fingerprints these.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;generates&lt;/code&gt; — files produced by the task. Task checks if they still exist and match the fingerprint.&lt;/li&gt;
&lt;li&gt;If inputs haven't changed and outputs still exist, Task skips the task entirely (cache hit). Results are stored in &lt;code&gt;.task/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;deps:&lt;/code&gt; runs dependencies &lt;strong&gt;in parallel&lt;/strong&gt; before the task executes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cmds:&lt;/code&gt; runs commands &lt;strong&gt;sequentially&lt;/strong&gt;, in order.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How the build graph flows
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages/shared (uv sync)
    ↓
packages/events (uv sync)     packages/proto (buf generate)
         ↓                              ↓
    apps/api                     (generates stubs)
    apps/worker                          ↓
    apps/cli               apps/api, apps/worker (import proto stubs)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running individual apps
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run just the API&lt;/span&gt;
task api:dev

&lt;span class="c"&gt;# Run just the worker&lt;/span&gt;
task worker:dev

&lt;span class="c"&gt;# Build a single package&lt;/span&gt;
task shared:build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Affected-only in CI
&lt;/h3&gt;

&lt;p&gt;Task has no &lt;code&gt;--affected&lt;/code&gt; flag. The naive approach — looping over changed package names — is wrong: if &lt;code&gt;shared&lt;/code&gt; changes, &lt;code&gt;api&lt;/code&gt; and &lt;code&gt;worker&lt;/code&gt; (which depend on it) must also be tested. You need reverse-dependency propagation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# ci/affected.sh — tests changed packages AND their dependents&lt;/span&gt;
&lt;span class="nv"&gt;CHANGED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; origin/main...HEAD&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Reverse dependency map: if X changes, also test these&lt;/span&gt;
&lt;span class="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nv"&gt;RDEPS&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;shared]&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"api worker cli"&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;events]&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"api worker"&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;proto]&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"api worker"&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;PACKAGES&lt;/span&gt;&lt;span class="o"&gt;=()&lt;/span&gt;
mark&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;pkg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
  &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;" &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PACKAGES&lt;/span&gt;&lt;span class="p"&gt;[*]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; "&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ &lt;span class="s2"&gt;" &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pkg&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; "&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;   &lt;span class="c"&gt;# skip if already queued&lt;/span&gt;
  PACKAGES+&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pkg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;dep &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RDEPS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$pkg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;mark &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dep&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;   &lt;span class="c"&gt;# propagate to dependents&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHANGED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"^packages/shared/"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mark &lt;span class="s2"&gt;"shared"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHANGED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"^packages/events/"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mark &lt;span class="s2"&gt;"events"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHANGED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"^packages/proto/"&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mark &lt;span class="s2"&gt;"proto"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHANGED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"^apps/api/"&lt;/span&gt;        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mark &lt;span class="s2"&gt;"api"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHANGED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"^apps/worker/"&lt;/span&gt;     &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mark &lt;span class="s2"&gt;"worker"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHANGED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"^apps/cli/"&lt;/span&gt;        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mark &lt;span class="s2"&gt;"cli"&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;pkg &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PACKAGES&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;task &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pkg&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:test"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A change to &lt;code&gt;packages/shared&lt;/code&gt; marks &lt;code&gt;shared&lt;/code&gt; → then propagates to &lt;code&gt;api&lt;/code&gt;, &lt;code&gt;worker&lt;/code&gt;, &lt;code&gt;cli&lt;/code&gt;. A change to &lt;code&gt;apps/api&lt;/code&gt; only marks &lt;code&gt;api&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest alternative:&lt;/strong&gt; skip this complexity and run &lt;code&gt;task test:all&lt;/code&gt; every time. Task's &lt;code&gt;sources&lt;/code&gt;/&lt;code&gt;generates&lt;/code&gt; caching skips packages whose inputs haven't changed. You get the same outcome with far less script maintenance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key insight:&lt;/strong&gt; Task knows nothing about Python — it just tracks file fingerprints and runs shell commands in the order you declare. That's exactly the level of abstraction you want.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 3: Shared Packages — The Internal Packages Pattern
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Source-level packages (no build step)
&lt;/h3&gt;

&lt;p&gt;Most packages in this repo point directly at Python source:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;packages/shared/pyproject.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"shared"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;requires-python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.12&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="py"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="py"&gt;"pydantic&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  &lt;span class="py"&gt;"sqlalchemy&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[build-system]&lt;/span&gt;
&lt;span class="py"&gt;requires&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"hatchling"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;build-backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"hatchling.build"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;packages/shared/src/shared/__init__.py&lt;/code&gt; exports the Pydantic models and SQLAlchemy base classes directly. No compilation, no &lt;code&gt;dist/&lt;/code&gt; folder. When &lt;code&gt;apps/api&lt;/code&gt; imports &lt;code&gt;shared&lt;/code&gt;, it reaches straight into &lt;code&gt;packages/shared/src/&lt;/code&gt; via the editable install.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;internal packages pattern&lt;/strong&gt; — source is the distribution artifact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Packages that do need a build step
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;packages/proto&lt;/code&gt; generates Python stubs from &lt;code&gt;.proto&lt;/code&gt; files:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;packages/proto/pyproject.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"proto"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;requires-python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.12&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="py"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;["grpcio&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.60&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="py"&gt;protobuf&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="s"&gt;"]&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;
&lt;span class="nn"&gt;[build-system]&lt;/span&gt;
&lt;span class="py"&gt;requires&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"hatchling"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;build-backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"hatchling.build"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;packages/proto/Taskfile.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*.proto'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;generates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;generated/**/*.py'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;buf generate&lt;/span&gt;   &lt;span class="c1"&gt;# buf handles all codegen — don't mix with raw protoc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Task's &lt;code&gt;sources&lt;/code&gt;/&lt;code&gt;generates&lt;/code&gt; fingerprinting ensures &lt;code&gt;proto:generate&lt;/code&gt; runs before anything that imports the stubs — and skips if the &lt;code&gt;.proto&lt;/code&gt; files haven't changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consuming internal packages in apps
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;apps/api/pyproject.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"api"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;requires-python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.12&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="py"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s"&gt;"shared"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"events"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"proto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="py"&gt;"fastapi&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.115&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  &lt;span class="py"&gt;"uvicorn&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.30&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[tool.uv.sources]&lt;/span&gt;
&lt;span class="py"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;workspace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;workspace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;proto&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;workspace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;apps/worker/pyproject.toml&lt;/code&gt; declares the same &lt;code&gt;[tool.uv.sources]&lt;/code&gt; block. Because uv resolves everything into a single &lt;code&gt;uv.lock&lt;/code&gt;, both &lt;code&gt;api&lt;/code&gt; and &lt;code&gt;worker&lt;/code&gt; are guaranteed to use the exact same version of &lt;code&gt;shared&lt;/code&gt; — no version skew between apps in the same repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; you can refactor a Pydantic model in &lt;code&gt;packages/shared&lt;/code&gt; and immediately see type errors in both &lt;code&gt;apps/api&lt;/code&gt; and &lt;code&gt;apps/worker&lt;/code&gt; without publishing anything. The workspace is the registry.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 4: Root &lt;code&gt;pyproject.toml&lt;/code&gt; — Shared Config
&lt;/h2&gt;

&lt;p&gt;Rather than duplicating &lt;code&gt;[tool.mypy]&lt;/code&gt; and &lt;code&gt;[tool.pytest.ini_options]&lt;/code&gt; across 6 packages, this repo centralizes all tool configuration in the root &lt;code&gt;pyproject.toml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Root &lt;code&gt;pyproject.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"nest-monorepo"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.0.0"&lt;/span&gt;
&lt;span class="py"&gt;requires-python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.12&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;
&lt;span class="nn"&gt;[tool.uv.workspace]&lt;/span&gt;
&lt;span class="py"&gt;members&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"apps/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"packages/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c"&gt;# Dev tools live here — not in subpackage pyproject.toml files&lt;/span&gt;
&lt;span class="nn"&gt;[dependency-groups]&lt;/span&gt;
&lt;span class="py"&gt;dev&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="py"&gt;"pytest&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"pytest-asyncio&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="s"&gt;",   # asyncio_mode = "&lt;/span&gt;&lt;span class="err"&gt;auto&lt;/span&gt;&lt;span class="s"&gt;" requires this&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"pytest-cov&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"mypy&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.15&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"ruff&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"pydantic[mypy]&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="s"&gt;",    # provides the pydantic.mypy plugin&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="s"&gt;"types-requests"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[tool.mypy]&lt;/span&gt;
&lt;span class="py"&gt;python_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"3.12"&lt;/span&gt;
&lt;span class="py"&gt;strict&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;disallow_untyped_defs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;no_implicit_optional&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;warn_redundant_casts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;warn_unused_ignores&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;plugins&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"pydantic.mypy"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c"&gt;# Paths where mypy finds internal package source&lt;/span&gt;
&lt;span class="py"&gt;mypy_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"packages/shared/src:packages/events/src:packages/proto/src"&lt;/span&gt;

&lt;span class="nn"&gt;[tool.pytest.ini_options]&lt;/span&gt;
&lt;span class="py"&gt;testpaths&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"apps"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"packages"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c"&gt;# no top-level tests/ dir — each package has its own&lt;/span&gt;
&lt;span class="py"&gt;asyncio_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"auto"&lt;/span&gt;
&lt;span class="py"&gt;addopts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"-v --tb=short"&lt;/span&gt;

&lt;span class="nn"&gt;[tool.coverage.run]&lt;/span&gt;
&lt;span class="py"&gt;branch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"apps"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"packages"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c"&gt;# not "src" — there's no top-level src/&lt;/span&gt;

&lt;span class="nn"&gt;[tool.coverage.report]&lt;/span&gt;
&lt;span class="py"&gt;fail_under&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
&lt;span class="py"&gt;show_missing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install dev tools with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--group&lt;/span&gt; dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How mypy finds this config
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;mypy does not walk up the directory tree.&lt;/strong&gt; Unlike Ruff, mypy reads config from the directory where it's invoked — not from the file being type-checked. If you run &lt;code&gt;uv run mypy src/&lt;/code&gt; from inside &lt;code&gt;apps/api/&lt;/code&gt;, mypy finds no &lt;code&gt;[tool.mypy]&lt;/code&gt; there, silently falls back to non-strict defaults, and your &lt;code&gt;strict = true&lt;/code&gt; is ignored.&lt;/p&gt;

&lt;p&gt;The correct pattern: &lt;strong&gt;always run mypy from the repo root, passing the directories to check:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run mypy apps/ packages/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is different from Ruff, which discovers config per-file by walking upward. With mypy, the invocation location is what matters — not the file location.&lt;/p&gt;

&lt;p&gt;There is no &lt;code&gt;[tool.mypy]&lt;/code&gt; in subpackages. &lt;strong&gt;Do not add one.&lt;/strong&gt; mypy does not merge configs — a subpackage &lt;code&gt;[tool.mypy]&lt;/code&gt; completely replaces the root config rather than extending it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; change &lt;code&gt;strict = true&lt;/code&gt; in one place and it applies to all 6 packages — but only when mypy is invoked from the root. Enforce this in your Taskfile and CI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 5: Ruff — Unified Linting and Formatting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ruff&lt;/strong&gt; replaces Flake8 + Black + isort with a single fast tool written in Rust. A single &lt;code&gt;[tool.ruff]&lt;/code&gt; section in the root &lt;code&gt;pyproject.toml&lt;/code&gt; applies to the entire repo.&lt;/p&gt;

&lt;p&gt;Root &lt;code&gt;pyproject.toml&lt;/code&gt; (continued):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.ruff]&lt;/span&gt;
&lt;span class="py"&gt;target-version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"py312"&lt;/span&gt;
&lt;span class="py"&gt;line-length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="py"&gt;src&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"apps"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"packages"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c"&gt;# not "src" — tells Ruff where source roots are&lt;/span&gt;

&lt;span class="nn"&gt;[tool.ruff.lint]&lt;/span&gt;
&lt;span class="py"&gt;select&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s"&gt;"E"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c"&gt;# pycodestyle errors&lt;/span&gt;
  &lt;span class="s"&gt;"W"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c"&gt;# pycodestyle warnings&lt;/span&gt;
  &lt;span class="s"&gt;"F"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c"&gt;# pyflakes&lt;/span&gt;
  &lt;span class="s"&gt;"I"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c"&gt;# isort&lt;/span&gt;
  &lt;span class="s"&gt;"UP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c"&gt;# pyupgrade&lt;/span&gt;
  &lt;span class="s"&gt;"B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c"&gt;# flake8-bugbear&lt;/span&gt;
  &lt;span class="s"&gt;"SIM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# flake8-simplify&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;ignore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"E501"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c"&gt;# line length handled by formatter&lt;/span&gt;

&lt;span class="nn"&gt;[tool.ruff.lint.per-file-ignores]&lt;/span&gt;
&lt;span class="py"&gt;"**/tests/**"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"S101"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c"&gt;# allow assert in tests&lt;/span&gt;

&lt;span class="nn"&gt;[tool.ruff.lint.isort]&lt;/span&gt;
&lt;span class="py"&gt;known-first-party&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"shared"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"events"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"proto"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c"&gt;# without this, internal packages&lt;/span&gt;
                                                     &lt;span class="c"&gt;# sort as third-party — wrong grouping&lt;/span&gt;

&lt;span class="nn"&gt;[tool.ruff.format]&lt;/span&gt;
&lt;span class="py"&gt;quote-style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"double"&lt;/span&gt;
&lt;span class="py"&gt;indent-style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"space"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Config discovery: how subpackages inherit
&lt;/h3&gt;

&lt;p&gt;Ruff discovers config by walking up the directory tree from the file being linted until it finds a &lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;ruff.toml&lt;/code&gt;, or &lt;code&gt;.ruff.toml&lt;/code&gt;. In &lt;code&gt;nest-monorepo&lt;/code&gt;, every file in &lt;code&gt;apps/&lt;/code&gt; and &lt;code&gt;packages/&lt;/code&gt; walks up and hits the root &lt;code&gt;pyproject.toml&lt;/code&gt;. One config, all packages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-package overrides
&lt;/h3&gt;

&lt;p&gt;If &lt;code&gt;apps/cli&lt;/code&gt; has looser requirements (it's internal tooling), add an &lt;code&gt;extend&lt;/code&gt; in &lt;code&gt;apps/cli/pyproject.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.ruff]&lt;/span&gt;
&lt;span class="py"&gt;extend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"../../pyproject.toml"&lt;/span&gt;

&lt;span class="nn"&gt;[tool.ruff.lint]&lt;/span&gt;
&lt;span class="py"&gt;ignore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"E501"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"T201"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c"&gt;# allow print() in CLI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;extend&lt;/code&gt; inherits all parent settings and overrides only what you specify. This is the right pattern — not a full &lt;code&gt;[tool.ruff]&lt;/code&gt; block that silently drops all parent rules.&lt;/p&gt;

&lt;p&gt;Each package runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run ruff check src/
uv run ruff format src/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the rules are defined once. This is the same pattern as the mypy config: &lt;strong&gt;one source of truth, many consumers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key insight:&lt;/strong&gt; Ruff running at 10–100x the speed of Flake8 is not just a nice-to-have in a monorepo. When pre-commit runs on every commit across 6 packages, linting speed is the difference between a 2-second hook and a 30-second one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 6: pre-commit + Git Hooks — Enforcing Quality at Commit Time
&lt;/h2&gt;

&lt;p&gt;Install pre-commit as a uv tool (not a project dependency):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install &lt;/span&gt;pre-commit
pre-commit &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; at the repo root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/astral-sh/ruff-pre-commit&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v0.9.0&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruff&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;--fix&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruff-format&lt;/span&gt;

  &lt;span class="c1"&gt;# mypy CANNOT use the standard pre-commit mirror hook here.&lt;/span&gt;
  &lt;span class="c1"&gt;# pre-commit/mirrors-mypy runs mypy in an isolated virtualenv that has no&lt;/span&gt;
  &lt;span class="c1"&gt;# access to your uv workspace's editable installs — `from shared import ...`&lt;/span&gt;
  &lt;span class="c1"&gt;# produces false ImportError failures. Run mypy via the local repo instead:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mypy&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mypy&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;           &lt;span class="c1"&gt;# uses the active .venv, not an isolated env&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv run mypy&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;apps/&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;packages/&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;--config-file&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pyproject.toml&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;python&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;      &lt;span class="c1"&gt;# mypy needs whole-package context, not individual files&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The key insight:&lt;/strong&gt; pre-commit automatically passes only staged files to Ruff — no custom script needed. &lt;code&gt;ruff check --fix&lt;/code&gt; runs only against the files you're about to commit. mypy, however, needs the full package context to resolve imports correctly, so &lt;code&gt;pass_filenames: false&lt;/code&gt; is set and it receives the full &lt;code&gt;apps/ packages/&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;The flow on &lt;code&gt;git commit&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;pre-commit intercepts the commit&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ruff&lt;/code&gt; checks and auto-fixes staged &lt;code&gt;.py&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ruff-format&lt;/code&gt; formats staged &lt;code&gt;.py&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mypy&lt;/code&gt; type-checks all of &lt;code&gt;apps/&lt;/code&gt; and &lt;code&gt;packages/&lt;/code&gt; (fast due to incremental cache)&lt;/li&gt;
&lt;li&gt;If any hook fails — commit is aborted, fixed files are left staged for review&lt;/li&gt;
&lt;li&gt;If all hooks pass — commit proceeds&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Installing consistently across the team
&lt;/h3&gt;

&lt;p&gt;Add to root &lt;code&gt;Taskfile.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bootstrap the repo for local development&lt;/span&gt;
    &lt;span class="na"&gt;cmds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;uv sync&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;uv tool install pre-commit&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pre-commit install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every developer runs &lt;code&gt;task setup&lt;/code&gt; once after cloning. No tribal knowledge required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; a linting problem caught at commit time costs 3 seconds to fix. The same problem caught in CI costs 5 minutes of context switching. The same problem caught in code review costs someone else's time too.&lt;/p&gt;




&lt;h2&gt;
  
  
  CI: GitHub Actions
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/ci.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;astral-sh/setup-uv@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;enable-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;          &lt;span class="c1"&gt;# caches ~/.cache/uv between runs&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv sync --frozen --group dev&lt;/span&gt;
        &lt;span class="c1"&gt;# --frozen: fail if uv.lock is out of date (never auto-update in CI)&lt;/span&gt;
        &lt;span class="c1"&gt;# --group dev: includes pytest, mypy, ruff&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv run ruff check apps/ packages/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv run ruff format --check apps/ packages/&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv run mypy apps/ packages/&lt;/span&gt;
        &lt;span class="c1"&gt;# always run from repo root — mypy does not walk up per-file&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv run pytest apps/ packages/ -x --cov&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;PYTHONDONTWRITEBYTECODE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv cache prune --ci&lt;/span&gt;
        &lt;span class="c1"&gt;# trims downloaded wheels, keeps source-built cache for next run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--frozen&lt;/code&gt; flag is the CI safety net. If a developer forgot to commit an updated &lt;code&gt;uv.lock&lt;/code&gt; after adding a dependency, the CI job fails immediately rather than silently installing a different package version than the rest of the team has.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deploying One App from a Monorepo
&lt;/h2&gt;

&lt;p&gt;The hardest Python monorepo question isn't the build — it's Docker. You need an image for &lt;code&gt;apps/api&lt;/code&gt; that includes its internal deps (&lt;code&gt;packages/shared&lt;/code&gt;, &lt;code&gt;packages/events&lt;/code&gt;) without shipping the entire repo or the dev toolchain.&lt;/p&gt;

&lt;p&gt;uv makes this clean with &lt;code&gt;--package&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# apps/api/Dockerfile&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.12-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Copy the workspace manifests first (cache layer for dep installation)&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pyproject.toml uv.lock ./&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; packages/shared/pyproject.toml packages/shared/pyproject.toml&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; packages/events/pyproject.toml packages/events/pyproject.toml&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; apps/api/pyproject.toml        apps/api/pyproject.toml&lt;/span&gt;

&lt;span class="c"&gt;# Install only api's deps — no dev tools, no other apps&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--package&lt;/span&gt; api &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--frozen&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--no-editable&lt;/span&gt;    &lt;span class="c"&gt;# copies source into site-packages instead of editable links&lt;/span&gt;

&lt;span class="c"&gt;# Now copy the actual source (separate layer so code changes don't bust dep cache)&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; packages/shared/src packages/shared/src&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; packages/events/src packages/events/src&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; apps/api/src        apps/api/src&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/.venv /app/.venv&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/apps/api/src /app/src&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH="/app/.venv/bin:$PATH"&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key flags:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--package api&lt;/code&gt; — installs only &lt;code&gt;api&lt;/code&gt; and its transitive deps (including &lt;code&gt;shared&lt;/code&gt; and &lt;code&gt;events&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--no-dev&lt;/code&gt; — excludes pytest, mypy, ruff&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--frozen&lt;/code&gt; — fails if &lt;code&gt;uv.lock&lt;/code&gt; is stale&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--no-editable&lt;/code&gt; — copies package source into &lt;code&gt;.venv/lib/&lt;/code&gt; so the final image doesn't need the source tree&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; without &lt;code&gt;--no-editable&lt;/code&gt;, the editable installs point back at the source paths in the build context. The final image would need all of &lt;code&gt;packages/shared/src/&lt;/code&gt; mapped at the same absolute path. &lt;code&gt;--no-editable&lt;/code&gt; severs that dependency — the &lt;code&gt;.venv&lt;/code&gt; is self-contained.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Picture: How These Tools Interact
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer commits
    ↓
pre-commit hook
    ↓ ruff-check (staged files only, --fix)
    ↓ ruff-format (staged files only)
    ↓ (fails → abort commit; passes → continue)
    ↓
uv workspace resolves internal deps via { workspace = true } editable installs
    ↓
task build:all
    ↓ reads Taskfile.yml includes
    ↓ builds packages in order: shared → events → api/worker/cli
    ↓ caches outputs in .task/
    ↓
CI: git diff → affected packages → test only those + dependents
    ↓ diffs against main branch
    ↓ runs only impacted packages
    ↓ Ruff config inherited from root pyproject.toml
    ↓ mypy config inherited from root pyproject.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary: Why This Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Share code without publishing to PyPI&lt;/td&gt;
&lt;td&gt;uv workspaces&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{ workspace = true }&lt;/code&gt; + editable installs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run tasks in dependency order&lt;/td&gt;
&lt;td&gt;Task&lt;/td&gt;
&lt;td&gt;explicit &lt;code&gt;cmds:&lt;/code&gt; ordering + &lt;code&gt;deps:&lt;/code&gt; for parallel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Don't rebuild unchanged packages&lt;/td&gt;
&lt;td&gt;Task cache&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sources&lt;/code&gt;/&lt;code&gt;generates&lt;/code&gt; fingerprinting in &lt;code&gt;.task/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run tasks on only changed code in CI&lt;/td&gt;
&lt;td&gt;git diff script&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git diff --name-only origin/main...HEAD&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consistent mypy/pytest config&lt;/td&gt;
&lt;td&gt;root &lt;code&gt;pyproject.toml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;single &lt;code&gt;[tool.mypy]&lt;/code&gt;; always invoke from repo root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One linter/formatter config&lt;/td&gt;
&lt;td&gt;Ruff root &lt;code&gt;[tool.ruff]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;config discovery walks up per-file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-package lint overrides&lt;/td&gt;
&lt;td&gt;Ruff &lt;code&gt;extend&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;inherits parent rules, adds/removes specific ones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enforce quality before commits&lt;/td&gt;
&lt;td&gt;pre-commit hooks&lt;/td&gt;
&lt;td&gt;staged-file Ruff; mypy via &lt;code&gt;language: system&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy one app without full repo&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uv sync --package api --no-editable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;installs app + internal deps, no dev tools&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The monorepo isn't one tool — it's these tools composing together. uv handles what exists and how it's linked, Task handles when things run and in what order, and the tooling layer in &lt;code&gt;pyproject.toml&lt;/code&gt; handles how everything is configured.&lt;/p&gt;

</description>
      <category>python</category>
      <category>architecture</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>TypeScript Monorepo Magic: Organize, Build, and Ship Multi-Package Apps</title>
      <dc:creator>M Hossein</dc:creator>
      <pubDate>Tue, 02 Jun 2026 12:16:54 +0000</pubDate>
      <link>https://dev.to/_mh/typescript-monorepo-magic-organize-build-and-ship-multi-package-apps-4cgf</link>
      <guid>https://dev.to/_mh/typescript-monorepo-magic-organize-build-and-ship-multi-package-apps-4cgf</guid>
      <description>&lt;h2&gt;
  
  
  What is a Monorepo?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;monorepo&lt;/strong&gt; is a single Git repository that contains multiple distinct packages or applications. The alternative is a &lt;strong&gt;polyrepo&lt;/strong&gt;: one repo per app or package.&lt;/p&gt;

&lt;p&gt;This repo is &lt;code&gt;shrimp-monorepo&lt;/code&gt;. It contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shrimp-monorepo/
├── apps/
│   ├── api/            ← Node.js backend (Elysia + gRPC)
│   ├── client-user/    ← React frontend for end users
│   └── client-admin/   ← React frontend for admins
├── packages/
│   ├── shared-types/   ← TypeScript types used everywhere
│   ├── ui/             ← Shared React components
│   ├── proto/          ← Protobuf definitions + generated code
│   ├── grpc-client/    ← gRPC client wrapper
│   └── db-schema/      ← Drizzle ORM schema
└── tooling/
    └── typescript/     ← Shared tsconfig files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The core idea:&lt;/strong&gt; code that is needed by multiple apps lives in &lt;code&gt;packages/&lt;/code&gt;, and all apps can import it directly — no npm publishing required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 1: pnpm Workspaces — The Foundation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;pnpm&lt;/strong&gt; is the package manager. Workspaces are its monorepo feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it's declared
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;apps/*'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;packages/*'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tooling/*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells pnpm: &lt;em&gt;treat every directory in these globs as a package&lt;/em&gt;. That's it — three lines define the entire workspace.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this enables
&lt;/h3&gt;

&lt;p&gt;When you run &lt;code&gt;pnpm install&lt;/code&gt; at the repo root, pnpm:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads every &lt;code&gt;package.json&lt;/code&gt; in every workspace directory&lt;/li&gt;
&lt;li&gt;Hoists shared external dependencies into a single &lt;code&gt;node_modules&lt;/code&gt; at the root&lt;/li&gt;
&lt;li&gt;Creates symlinks for internal packages so they can import each other&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;workspace:*&lt;/code&gt; protocol
&lt;/h3&gt;

&lt;p&gt;Look at &lt;code&gt;apps/api/package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@shrimp/db-schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"workspace:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@shrimp/proto"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"workspace:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@shrimp/shared-types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"workspace:*"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;workspace:*&lt;/code&gt; means: &lt;em&gt;don't fetch this from npm — link it from this workspace instead&lt;/em&gt;. When &lt;code&gt;api&lt;/code&gt; imports &lt;code&gt;@shrimp/db-schema&lt;/code&gt;, Node resolves it to &lt;code&gt;packages/db-schema/src/index.ts&lt;/code&gt; on disk via a symlink in &lt;code&gt;node_modules/.pnpm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is what makes internal sharing work without publishing packages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why pnpm over npm/yarn?
&lt;/h3&gt;

&lt;p&gt;pnpm uses a &lt;strong&gt;content-addressable store&lt;/strong&gt; (the &lt;code&gt;.pnpm-store/&lt;/code&gt; directory in this repo). Every version of every package is stored once globally. Workspaces get hard links to the store, not copies. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Faster installs&lt;/li&gt;
&lt;li&gt;Significantly less disk space&lt;/li&gt;
&lt;li&gt;Strict by default — packages can't accidentally import things they didn't declare&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Tool 2: Turborepo — Task Orchestration and Caching
&lt;/h2&gt;

&lt;p&gt;pnpm workspaces handle &lt;em&gt;dependencies&lt;/em&gt;. &lt;strong&gt;Turbo&lt;/strong&gt; handles &lt;em&gt;tasks&lt;/em&gt; (build, test, lint, etc.).&lt;/p&gt;

&lt;h3&gt;
  
  
  The problem Turbo solves
&lt;/h3&gt;

&lt;p&gt;If you run &lt;code&gt;pnpm run build&lt;/code&gt; in a repo with 8 packages, you have to figure out the order yourself. &lt;code&gt;proto&lt;/code&gt; must build before &lt;code&gt;grpc-client&lt;/code&gt;, which must build before &lt;code&gt;api&lt;/code&gt;. Do it wrong and you get stale or missing types.&lt;/p&gt;

&lt;p&gt;Also, if nothing in &lt;code&gt;packages/ui&lt;/code&gt; changed, you shouldn't need to rebuild it.&lt;/p&gt;

&lt;p&gt;Turbo solves both: &lt;strong&gt;dependency-aware task ordering&lt;/strong&gt; + &lt;strong&gt;task result caching&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;turbo.json&lt;/code&gt; — the pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tasks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"^build"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"outputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"dist/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".output/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"generated/**"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cache"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"persistent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"^build"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"typecheck"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"^build"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"^build"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"outputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"coverage/**"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key concepts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;"dependsOn": ["^build"]&lt;/code&gt; — the &lt;code&gt;^&lt;/code&gt; prefix means &lt;em&gt;build all packages that this package depends on first&lt;/em&gt;. So when building &lt;code&gt;@shrimp/api&lt;/code&gt;, Turbo automatically builds &lt;code&gt;@shrimp/proto&lt;/code&gt;, &lt;code&gt;@shrimp/db-schema&lt;/code&gt;, and &lt;code&gt;@shrimp/shared-types&lt;/code&gt; beforehand, in the right order.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;"dependsOn": ["build"]&lt;/code&gt; (no &lt;code&gt;^&lt;/code&gt;) — run the same package's &lt;code&gt;build&lt;/code&gt; task first. Used by &lt;code&gt;test:e2e&lt;/code&gt;: build the app before running E2E tests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;"cache": false&lt;/code&gt; — never cache this task. &lt;code&gt;dev&lt;/code&gt; is persistent/interactive, so caching makes no sense.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;"persistent": true&lt;/code&gt; — this task runs forever (a dev server). Turbo knows not to treat it as something that finishes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;"outputs"&lt;/code&gt; — Turbo hashes these paths to know what "done" looks like. If inputs haven't changed and outputs still exist, Turbo skips the task entirely (cache hit).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How the build graph flows
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;proto:generate
    ↓
@shrimp/proto (build)
    ↓
@shrimp/grpc-client (build)
    ↓
apps/api (build)
apps/client-user (build)
apps/client-admin (build)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@shrimp/shared-types&lt;/code&gt;, &lt;code&gt;@shrimp/db-schema&lt;/code&gt;, and &lt;code&gt;@shrimp/ui&lt;/code&gt; also build in parallel before their consumers, since they have no inter-dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Filtering — run tasks for specific packages
&lt;/h3&gt;

&lt;p&gt;The root &lt;code&gt;package.json&lt;/code&gt; uses &lt;code&gt;--filter&lt;/code&gt; for targeted dev:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"dev:user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"turbo run dev --filter=@shrimp/client-user"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"dev:admin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"turbo run dev --filter=@shrimp/client-admin"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"dev:api"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s2"&gt;"turbo run dev --filter=@shrimp/api"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--filter&lt;/code&gt; accepts package names, directory globs, or git-based expressions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Affected-only in CI
&lt;/h3&gt;

&lt;p&gt;The CI workflow uses the most powerful filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm turbo run typecheck lint test:unit build --affected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--affected&lt;/code&gt; uses the git diff against the base branch to determine which packages changed, then runs tasks only on those packages (and their dependents). A PR that only touches &lt;code&gt;packages/ui&lt;/code&gt; won't re-run &lt;code&gt;api&lt;/code&gt; tests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 3: Shared Packages — How Internal Code is Shared
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Source-level packages (no compilation step)
&lt;/h3&gt;

&lt;p&gt;Most packages in this repo point directly at TypeScript source:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;packages/shared-types/package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@shrimp/shared-types"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src/index.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src/index.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src/index.ts"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no &lt;code&gt;build&lt;/code&gt; script. When &lt;code&gt;@shrimp/api&lt;/code&gt; imports &lt;code&gt;@shrimp/shared-types&lt;/code&gt;, it imports &lt;code&gt;.ts&lt;/code&gt; files directly. The consuming app's bundler or TypeScript compiler handles the compilation.&lt;/p&gt;

&lt;p&gt;This is sometimes called the &lt;strong&gt;"internal packages" pattern&lt;/strong&gt; — no &lt;code&gt;dist/&lt;/code&gt; folder, no compile step, just source. It works because all consumers in the monorepo are TypeScript themselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Packages that do need a build step
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;@shrimp/proto&lt;/code&gt; generates TypeScript from &lt;code&gt;.proto&lt;/code&gt; files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"proto:generate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm exec protoc --ts_out ./generated ..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm run proto:generate"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turbo's &lt;code&gt;"dependsOn": ["^build"]&lt;/code&gt; ensures &lt;code&gt;proto:generate&lt;/code&gt; runs before anything that consumes &lt;code&gt;@shrimp/proto&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 4: Shared TypeScript Config — &lt;code&gt;tooling/typescript&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Rather than duplicating 25 lines of &lt;code&gt;compilerOptions&lt;/code&gt; across 8 packages, this repo centralizes TypeScript config in &lt;code&gt;tooling/typescript/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tooling/typescript/tsconfig.base.json&lt;/code&gt; — strict settings shared by all packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noUnusedLocals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noUncheckedIndexedAccess"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bundler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tooling/typescript/tsconfig.react.json&lt;/code&gt; — extends base, adds JSX:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./tsconfig.base.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"jsx"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"react-jsx"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each package inherits via &lt;code&gt;extends&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;apps/api/tsconfig.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"../../tooling/typescript/tsconfig.base.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rootDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;packages/ui/tsconfig.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"../../tooling/typescript/tsconfig.react.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"src/**/*.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/**/*.tsx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tests/**/*.ts"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; change one setting in &lt;code&gt;tsconfig.base.json&lt;/code&gt; and it propagates to every package in the repo. No drift, no "why is strict mode off in this one package" surprises.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 5: Biome — Unified Linting and Formatting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Biome&lt;/strong&gt; replaces ESLint + Prettier with a single fast tool. A single &lt;code&gt;biome.json&lt;/code&gt; at the root applies to the entire repo.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;biome.json&lt;/code&gt; key config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"linter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"correctness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"noUnusedVariables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"noUnusedImports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"style"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"useConst"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"formatter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"indentStyle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tab"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"indentWidth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lineWidth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each package runs &lt;code&gt;biome check .&lt;/code&gt; as its &lt;code&gt;lint&lt;/code&gt; script, but the rules are defined once. This is the same pattern as TypeScript config inheritance: &lt;strong&gt;one source of truth, many consumers&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 6: Git Hooks — Enforcing Quality at Commit Time
&lt;/h2&gt;

&lt;p&gt;The repo uses a &lt;strong&gt;custom git hooks directory&lt;/strong&gt; instead of the default &lt;code&gt;.git/hooks&lt;/code&gt;. This means hooks can be committed and versioned.&lt;/p&gt;

&lt;p&gt;Setup (runs on &lt;code&gt;pnpm install&lt;/code&gt; via the &lt;code&gt;prepare&lt;/code&gt; lifecycle):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"prepare"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git config core.hooksPath .githooks"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.githooks/pre-commit&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env sh&lt;/span&gt;
pnpm precommit:staged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;scripts/biome-staged.mjs&lt;/code&gt; — runs Biome only on staged files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;staged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawnSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;git&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;diff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--cached&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--name-only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]);&lt;/span&gt;
&lt;span class="c1"&gt;// ... filters to .ts/.tsx/.js/.json files&lt;/span&gt;
&lt;span class="nf"&gt;spawnSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pnpm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exec&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;biome&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;check&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--no-errors-on-unmatched&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The key insight:&lt;/strong&gt; it doesn't lint the whole repo on every commit — only the files currently staged. This keeps commits fast while still enforcing quality.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Picture: How These Tools Interact
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer commits
    ↓
git pre-commit hook (.githooks/pre-commit)
    ↓ runs biome-staged.mjs
    ↓ Biome checks only staged files
    ↓ (fails → abort commit; passes → continue)
    ↓
pnpm workspace resolves internal deps via workspace:* symlinks
    ↓
turbo run build
    ↓ reads turbo.json pipeline
    ↓ resolves dependency graph from package.json deps
    ↓ builds packages in order: proto → grpc-client → api/clients
    ↓ caches outputs in .turbo/
    ↓
CI: turbo run ... --affected
    ↓ diffs against main
    ↓ runs only impacted packages
    ↓ TypeScript config inherited from tooling/typescript/
    ↓ Biome config inherited from root biome.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary: Why This Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Share code without publishing to npm&lt;/td&gt;
&lt;td&gt;pnpm workspaces&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;workspace:*&lt;/code&gt; + symlinks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run tasks in dependency order&lt;/td&gt;
&lt;td&gt;Turbo&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"dependsOn": ["^build"]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Don't rebuild unchanged packages&lt;/td&gt;
&lt;td&gt;Turbo cache&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;outputs&lt;/code&gt; hashing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run tasks on only changed code in CI&lt;/td&gt;
&lt;td&gt;Turbo &lt;code&gt;--affected&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;git diff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consistent TypeScript config&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tooling/typescript&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;extends&lt;/code&gt; inheritance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One linter/formatter config&lt;/td&gt;
&lt;td&gt;Biome root &lt;code&gt;biome.json&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;single config, all packages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enforce quality before commits&lt;/td&gt;
&lt;td&gt;Git hooks (&lt;code&gt;.githooks/&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;staged-file Biome check&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The monorepo isn't one tool — it's these tools composing together. pnpm handles what exists, Turbo handles when things run, and the tooling layer handles how they're configured.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>tutorial</category>
      <category>architecture</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
