<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Reviews | AkitaOnRails.com</title><link>https://akitaonrails.github.io/en/tags/reviews/</link><description>Fabio Akita's blog — tech, career, and assorted geek topics. English edition.</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 19 Oct 2026 23:20:52 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/en/tags/reviews/index.xml" rel="self" type="application/rss+xml"/><item><title>Intel's Comeback: ARM64 vs X86-64 Is Not What You Think</title><link>https://akitaonrails.github.io/en/2026/08/11/intels-comeback-arm64-vs-x86-64-is-not-what-you-think/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/08/11/intels-comeback-arm64-vs-x86-64-is-not-what-you-think/</guid><pubDate>Tue, 11 Aug 2026 10:00:00 -0300</pubDate><description>&lt;p&gt;Since January, when the first Panther Lake laptops hit store shelves, reviews keep repeating a sentence that would have sounded absurd five years ago: an Intel x86 chip going toe to toe with the Apple M5. Not in everything — I&amp;rsquo;ll get to the caveats — but in the category that matters most day to day, battery life, the game has flipped. And that gives me the perfect excuse to write about a belief I see programmers repeat without checking: that true efficiency only comes with ARM64, because x86 is old, heavy, and full of 1978 baggage.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Since January, when the first Panther Lake laptops hit store shelves, reviews keep repeating a sentence that would have sounded absurd five years ago: an Intel x86 chip going toe to toe with the Apple M5. Not in everything — I&rsquo;ll get to the caveats — but in the category that matters most day to day, battery life, the game has flipped. And that gives me the perfect excuse to write about a belief I see programmers repeat without checking: that true efficiency only comes with ARM64, because x86 is old, heavy, and full of 1978 baggage.</p>
<p>My thesis: that was true generations ago, when decoding x86 cost a relevant slice of the die. Today x86 is, in practice, a translation layer into micro-instructions — and what separates Apple, Qualcomm, and Intel was never the instruction set. Intel&rsquo;s crisis was managerial and manufacturing, not architectural. Which is exactly why its comeback, now, makes sense.</p>
<h2>What the reviews show<span class="hx:absolute hx:-mt-20" id="what-the-reviews-show"></span>
    <a href="#what-the-reviews-show" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The Panther Lake laptops (Core Ultra series 3, launched at CES on January 5 and on sale since January 27) deliver numbers the Meteor/Arrow Lake era couldn&rsquo;t dream of. Starting with battery — Dell XPS 14 2026 (Core Ultra X7 358H) versus MacBook Air 15 (M5):</p>
<table>
  <thead>
      <tr>
          <th>Battery test</th>
          <th>Dell XPS 14 (Panther Lake)</th>
          <th>MacBook Air 15 (M5)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Web browsing (<a href="https://www.notebookcheck.net/43-hours-battery-life-Dell-XPS-14-2026-lasts-almost-3x-longer-vs-MacBook-Air-15-M5-in-web-browsing-test.1262947.0.html"target="_blank" rel="noopener">Hardware Canucks</a>, VRR on)</td>
          <td><strong>43 hours</strong></td>
          <td>14h30</td>
      </tr>
      <tr>
          <td>Web browsing (<a href="https://www.notebookcheck.net/Dell-XPS-14-2026-with-Intel-Panther-Lake-delivers-55-longer-battery-life-vs-2025-Dell-14-Premium.1225329.0.html"target="_blank" rel="noopener">Notebookcheck</a>, different methodology)</td>
          <td>16h45 (+55% vs 2025 model)</td>
          <td>17h12</td>
      </tr>
      <tr>
          <td>4K YouTube</td>
          <td><strong>20h21</strong></td>
          <td>14h</td>
      </tr>
      <tr>
          <td>Heavy load (gaming)</td>
          <td>2h30</td>
          <td><strong>4h10</strong></td>
      </tr>
  </tbody>
</table>
<p>The honest reading: in light use, the Dell ties or wins; under sustained load, Apple remains unbeatable. And DHH posted that his XPS 14 running Omarchy Linux gets over 16 hours of real use, with a 1.4W idle draw — Dell made a point of <a href="https://www.dell.com/en-us/blog/year-of-the-linux-laptop-omarchy-on-xps/"target="_blank" rel="noopener">day-one Linux support</a>.</p>
<p>On CPU, the flagship Core Ultra X9 388H against the M5 (<a href="https://www.notebookcheck.net/Intel-Panther-Lake-Core-Ultra-X9-388H-performance-analysis-Outpaces-Arrow-Lake-and-exceeds-Zen-5-in-efficiency.1212583.0.html"target="_blank" rel="noopener">Notebookcheck data</a>):</p>
<table>
  <thead>
      <tr>
          <th>Metric</th>
          <th>Core Ultra X9 388H</th>
          <th>Apple M5</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cinebench 2024 multi-core</td>
          <td>~1,162</td>
          <td>~1,172 (<strong>statistical tie</strong>)</td>
      </tr>
      <tr>
          <td>Cinebench 2024 single-core</td>
          <td>~130</td>
          <td><strong>200</strong> (~30% ahead)</td>
      </tr>
      <tr>
          <td>Single-core efficiency</td>
          <td>5.17 pts/watt</td>
          <td><strong>13.1 pts/watt</strong></td>
      </tr>
      <tr>
          <td>Multi-core efficiency capped at 20W</td>
          <td>24.6 pts/watt</td>
          <td>24.8 (M4)</td>
      </tr>
  </tbody>
</table>
<p>Single-core remains Apple&rsquo;s territory — it does the same work <strong>on a third of the energy</strong>. But in moderate multi-core, Intel caught up. Against AMD (Ryzen AI 9 465) the 388H wins across the board; against the first-gen Snapdragon X Elite too; the X2 Elite Extreme, which arrived in 2026, already handed the Windows CPU crown back to Qualcomm (+24% in single-core). Honesty above all.</p>
<p>On GPU, the verdict is more mixed than Intel&rsquo;s marketing suggests:</p>
<table>
  <thead>
      <tr>
          <th>Arc B390 matchup</th>
          <th>Result</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>vs Radeon 890M (AMD)</td>
          <td><a href="https://videocardz.com/newz/intel-arc-b390-beats-amds-mainstream-igpus-and-nears-rtx-4050-level-performance-in-some-tests"target="_blank" rel="noopener">+63 to 80%</a></td>
      </tr>
      <tr>
          <td>vs laptop RTX 4050</td>
          <td>dead heat</td>
      </tr>
      <tr>
          <td>vs Strix Halo (AMD) at 15-20W</td>
          <td><a href="https://www.notebookcheck.net/No-chance-for-AMD-Intel-Panther-Lake-Core-Ultra-X9-388H-trounces-AMD-Strix-Halo-at-low-power-signaling-handheld-gaming-domination-in-2026.1213244.0.html"target="_blank" rel="noopener">wins</a></td>
      </tr>
      <tr>
          <td>vs base M5 GPU</td>
          <td>wins on performance, loses on fps/watt</td>
      </tr>
      <tr>
          <td>vs M5 Pro / M5 Max</td>
          <td><a href="https://nanoreview.net/en/gpu-compare/intel-arc-b390-vs-apple-m5-max-gpu-40-core"target="_blank" rel="noopener">17 against 24 and 44</a> — no chance</td>
      </tr>
  </tbody>
</table>
<p><a href="https://arstechnica.com/gadgets/2026/02/intel-panther-lake-core-ultra-review-intels-best-laptop-cpu-in-a-very-long-time/"target="_blank" rel="noopener">Ars Technica&rsquo;s</a> summary is the fairest: &ldquo;Intel&rsquo;s best laptop CPU in a very long time&rdquo; — with the caveat that Intel must prove this is the new normal, not an aberration.</p>
<blockquote>
  <p><strong>Remember this:</strong> MacBook battery life in an x86 laptop happened — in light use. In single-core and under heavy load, Apple still sets the pace.</p>

</blockquote>
<h2>The belief: &ldquo;to be efficient you have to be ARM&rdquo;<span class="hx:absolute hx:-mt-20" id="the-belief-to-be-efficient-you-have-to-be-arm"></span>
    <a href="#the-belief-to-be-efficient-you-have-to-be-arm" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Every programmer has heard the explanation: ARM has a simple, fixed-length, elegant instruction set. x86 is a chimera accumulated since 1978, with variable-length instructions, real mode, segmentation, decades of mess. &ldquo;Therefore&rdquo; ARM is inherently more efficient, and the way for anyone to reach Apple- or Qualcomm-class efficiency is to move to ARM64.</p>
<p>There&rsquo;s truth in there: x86 does carry historical baggage, and decoding variable-length instructions is objectively more annoying than decoding fixed 32-bit ones. The error is in the conclusion. That difference mattered when decode logic occupied a significant fraction of the chip. That was a long time ago.</p>
<blockquote>
  <p><strong>Remember this:</strong> the ISA difference mattered when decoding occupied a relevant fraction of the die. That ended generations ago.</p>

</blockquote>
<h2>x86 became a translation layer in 1995<span class="hx:absolute hx:-mt-20" id="x86-became-a-translation-layer-in-1995"></span>
    <a href="#x86-became-a-translation-layer-in-1995" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The Pentium Pro, from November 1995, already didn&rsquo;t execute x86 directly: it translated each CISC instruction into internal RISC-like micro-operations and executed those. AMD did the same with the K5 in 1996 (its &ldquo;ROPs&rdquo;). In other words: for <strong>thirty years</strong>, &ldquo;executing x86&rdquo; has meant &ldquo;translate it into something else and execute the something else.&rdquo; x86 is a compatibility interface, a translation layer over a RISC engine. And the numbers show how cheap that layer became:</p>
<table>
  <thead>
      <tr>
          <th>Measurement</th>
          <th>Result</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Micro-ops per x86 instruction (Pentium Pro, Intel data)</td>
          <td>1.2 to 1.7</td>
      </tr>
      <tr>
          <td>Micro-op cache hit rate (since Sandy Bridge, 2011)</td>
          <td>~80% overall, ~100% in hot loops</td>
      </tr>
      <tr>
          <td>Decoder cost on Haswell (<a href="https://research.aalto.fi/en/publications/empirical-study-of-the-power-consumption-of-the-x86-64-instructio/"target="_blank" rel="noopener">Hirki et al., 2016</a>)</td>
          <td>3 to 10% of package power, worst case</td>
      </tr>
      <tr>
          <td>Cost of disabling the micro-op cache (<a href="https://chipsandcheese.com/p/how-zen-2s-op-cache-affects-performance"target="_blank" rel="noopener">Zen 2, Chips and Cheese</a>)</td>
          <td>+4 to 10% in the core, +0.5 to 6% at the package</td>
      </tr>
  </tbody>
</table>
<p>When the micro-op cache hits, the fetch and decode hardware is literally switched off. The Hirki study&rsquo;s conclusion is dry: &ldquo;the x86-64 instruction set is not a major hindrance in producing an energy-efficient processor.&rdquo; And the definitive academic study — <a href="https://research.cs.wisc.edu/vertical/papers/2013/hpca13-isa-power-struggles.pdf"target="_blank" rel="noopener">Blem, Menon, and Sankaralingam, HPCA 2013</a>, actually measuring ARM against x86 — concluded: instruction count and mix are ISA-independent to first order, performance differences come from microarchitecture, and &ldquo;the energy consumption is again ISA-independent.&rdquo;</p>
<p>Jim Keller — the guy who designed AMD&rsquo;s Zen and Apple&rsquo;s A4/A5 chips, someone who has lived on both sides — put it even more bluntly in an AnandTech interview: variable-length decode &ldquo;isn&rsquo;t dominating the die, so it doesn&rsquo;t matter that much.&rdquo; What limits performance today, in his words, is branch predictability and data locality.</p>
<p>And here&rsquo;s the detail that kills the myth for good: modern ARM does the same thing. The Cortex-A77 has a micro-op cache. Samsung added one to the Exynos M5 explicitly to save fetch and decode power. Fujitsu&rsquo;s A64FX — the ARM inside the Fugaku supercomputer — decodes the SVE <code>FADDA</code> instruction into <strong>63 micro-ops</strong>. Sixty-three. The fantasy of &ldquo;ARM is one instruction per cycle, simple and pure&rdquo; hasn&rsquo;t existed in any high-performance ARM for a long time.</p>
<p>One honest nuance before moving on: x86&rsquo;s variable length really does make very wide decoders harder to build — Apple decodes <strong>up to twice as many</strong> instructions per cycle as an x86. That&rsquo;s a real engineering difficulty. But you pay for it in logic area, which is cheap, not in consumption proportional to the work — which is what defines battery life.</p>
<blockquote>
  <p><strong>Remember this:</strong> for thirty years, no x86 has executed x86 — everything becomes RISC-style micro-ops. The ISA became a compatibility interface.</p>

</blockquote>
<h2>The transistor math<span class="hx:absolute hx:-mt-20" id="the-transistor-math"></span>
    <a href="#the-transistor-math" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To understand why the decode burden evaporated, look at the evolution:</p>
<table>
  <thead>
      <tr>
          <th>Year</th>
          <th>Chip</th>
          <th>Process</th>
          <th>Transistors</th>
          <th>Milestone</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2000</td>
          <td>Pentium 4 Willamette</td>
          <td>180nm</td>
          <td>42 million</td>
          <td>the rising-clock era</td>
      </tr>
      <tr>
          <td>2006</td>
          <td>Core 2 Duo</td>
          <td>65nm</td>
          <td>291 million</td>
          <td>the post-Tejas multicore pivot</td>
      </tr>
      <tr>
          <td>2007</td>
          <td>Penryn</td>
          <td>45nm</td>
          <td>410 million</td>
          <td>industry&rsquo;s first high-k metal gate</td>
      </tr>
      <tr>
          <td>2011</td>
          <td>Ivy Bridge</td>
          <td>22nm</td>
          <td>1.4 billion</td>
          <td>3D FinFET, -50% power at the same performance</td>
      </tr>
  </tbody>
</table>
<p>In 2000, every block of logic was a tight budget, and x86&rsquo;s mess was expensive. As transistors became infinite for all practical purposes, the decoder&rsquo;s fixed cost became pocket change. Along the way, two important shifts:</p>
<ul>
<li><strong>Dennard scaling died around 2005.</strong> Leakage current stopped clocks from climbing, Intel canceled Tejas in 2004, and the world went multicore. Clocks stalled in the 1-4GHz range and never left.</li>
<li><strong>Moore&rsquo;s law became economics, not physics.</strong> Cost per transistor stopped falling at 28nm: a leading-edge fab today costs US$20 to 30 billion, and an ASML High-NA EUV machine costs US$350 million.</li>
</ul>
<p>And what occupies die and consumes energy in a modern chip? Giant caches, branch predictors, dozens of execution ports, GPU, NPU, media engines. The x86 decoder doesn&rsquo;t even show up in the bread line.</p>
<p>And history hands us the perfect empirical proof: from 2015 to 2021, Intel was stuck at 14nm — Skylake and its derivatives, six years of stagnant process from manufacturing failure. Even so, those old 14nm cores traded blows with AMD&rsquo;s Zen 2, built on TSMC 7nm. If x86 were the problem, that would be impossible. The bottleneck was the fab. It was the fab all along.</p>
<blockquote>
  <p><strong>Remember this:</strong> six years stuck at 14nm, and Intel still competed with TSMC 7nm chips. The bottleneck was the fab, never the ISA.</p>

</blockquote>
<h2>What actually makes Apple efficient<span class="hx:absolute hx:-mt-20" id="what-actually-makes-apple-efficient"></span>
    <a href="#what-actually-makes-apple-efficient" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The M1 isn&rsquo;t efficient &ldquo;because it&rsquo;s ARM.&rdquo; Apple has held an ARM architectural license since the A6, in 2012: it designs its own cores from scratch, and only the instruction set is ARM&rsquo;s. When AnandTech dissected the M1&rsquo;s Firestorm core, the contrast with contemporary x86 was about engineering choices, not instructions:</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Apple Firestorm (M1, 2020)</th>
          <th>Intel Sunny Cove (2019)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Decode per cycle</td>
          <td>8</td>
          <td>4</td>
      </tr>
      <tr>
          <td>Reorder buffer</td>
          <td>~630 entries</td>
          <td>352 entries</td>
      </tr>
      <tr>
          <td>L1 instruction cache</td>
          <td>192KB</td>
          <td>32KB</td>
      </tr>
  </tbody>
</table>
<p>In practice: twice the decode width, nearly twice the reorder buffer, <strong>six times</strong> the instruction cache. Add unified memory soldered into the package and a cutting-edge TSMC process. That&rsquo;s aggressive microarchitecture, enormous caches, vertical integration, and a leading-edge process — all expensive, all deliberate, and none of it comes free with the ISA.</p>
<p>The reciprocal is also true, by the way: you can make bad ARM. The market is full of mediocre ARM. Efficiency is an engineering and process choice, not an ISA birth certificate.</p>
<blockquote>
  <p><strong>Remember this:</strong> the M1 is efficient because of microarchitecture, cache, process, and vertical integration — not &ldquo;because it&rsquo;s ARM.&rdquo;</p>

</blockquote>
<h2>Your favorite instructions aren&rsquo;t x86 or ARM<span class="hx:absolute hx:-mt-20" id="your-favorite-instructions-arent-x86-or-arm"></span>
    <a href="#your-favorite-instructions-arent-x86-or-arm" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>And there&rsquo;s something else almost nobody mentions in this debate: much of what a modern CPU executes doesn&rsquo;t belong to either side&rsquo;s &ldquo;classic&rdquo; instruction set. Look at what runs outside it:</p>
<ul>
<li><strong>Video.</strong> On Intel, decoding lives in Quick Sync, fixed-function hardware that has existed since Sandy Bridge — a separate block with nothing to do with the x86 legacy. It&rsquo;s because of it that Frandroid measured the Snapdragon X2 <strong>58% slower</strong> than Panther Lake in video export.</li>
<li><strong>Cryptography.</strong> AES-NI exists since 2010, SHA extensions since 2016 — dedicated instructions added decades after the &ldquo;old x86.&rdquo;</li>
<li><strong>AI and matrices.</strong> AVX-512, then AVX10, and AMX, a matrix tile accelerator. The ARM side has the equivalents: NEON, SVE, SME.</li>
</ul>
<p>Chips and Cheese ran the perfect experiment: in the same 4K HEVC encode, an Ampere ARM took <strong>more than twelve times</strong> as long as a Zen 2 with stock ffmpeg; using NEON assembly cut the ARM time by more than 60%. The difference was never ARM versus x86 — it was well-used vector extensions versus ignored vector extensions. In the real world, the heavy lifting lives in the extensions and accelerators, and those are orthogonal to the base ISA.</p>
<blockquote>
  <p><strong>Remember this:</strong> modern heavy lifting — video, crypto, AI — runs on dedicated extensions and accelerators, orthogonal to the base ISA.</p>

</blockquote>
<h2>The fall was managerial, not architectural<span class="hx:absolute hx:-mt-20" id="the-fall-was-managerial-not-architectural"></span>
    <a href="#the-fall-was-managerial-not-architectural" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Here&rsquo;s the part that convinces me completely. Look at the timeline and try to find &ldquo;x86 was a fundamental limitation&rdquo; anywhere in it:</p>
<ul>
<li><strong>2005-2006</strong>: Intel passes on making the iPhone&rsquo;s chip — Otellini admitted the regret in his exit interview with The Atlantic: &ldquo;the world would have been a lot different.&rdquo; And the tragicomic detail: Intel <strong>had</strong> an ARM division (XScale) — and sold it to Marvell in 2006 for US$600 million. It wasn&rsquo;t a lack of technology; it was a lack of vision.</li>
<li><strong>2013-2021</strong>: three CEOs. Krzanich left in 2018 over an internal scandal. In came Bob Swan, a finance CFO, to run an engineering company at the most delicate moment in its history.</li>
<li><strong>2018-2020</strong>: 10nm became a joke (Cannon Lake only in limited release) and in July 2020 Intel announced a 7nm delay — the stock dropped 16% in a day. In April 2019, it abandoned the 5G smartphone modem business; Apple bought the modem unit for US$1 billion. In November 2020, the M1.</li>
<li><strong>2021-2024</strong>: Gelsinger returned with the IDM 2.0 plan, &ldquo;five nodes in four years.&rdquo; In December 2024 he was pushed into &ldquo;retirement&rdquo; — a board ultimatum, per Reuters and Bloomberg. The year closed with a <strong>US$18.8 billion</strong> loss, 15,000 layoffs, a suspended dividend, and Intel expelled from the Dow Jones after 25 years — replaced by Nvidia, which at that point was worth <strong>more than 30 times</strong> Intel. By October 2025, the cumulative layoff count had reached 35,500.</li>
</ul>
<p>Nothing on that list is architecture. It&rsquo;s missed products, delayed fabs, wrong decision after wrong decision. x86 was there, competent, while the company dismantled itself around it.</p>
<blockquote>
  <p><strong>Remember this:</strong> the iPhone pass, the XScale sale, the broken 10nm, three CEOs, a US$18.8 billion loss — Intel&rsquo;s fall was decision after decision, not an x86 limitation.</p>

</blockquote>
<h2>The unlikely rescue<span class="hx:absolute hx:-mt-20" id="the-unlikely-rescue"></span>
    <a href="#the-unlikely-rescue" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>And then 2025 happened. Lip-Bu Tan took over in March. In August, Trump demanded his head on Truth Social over China ties — and just days later, after a White House meeting, became a fan. On August 22, the US government <a href="https://newsroom.intel.com/corporate/intel-and-trump-administration-reach-historic-agreement"target="_blank" rel="noopener">bought 9.9% of Intel</a>: 433.3 million shares at US$20.47, US$8.9 billion total, converting CHIPS Act grants into equity, with no board seat. SoftBank put in <a href="https://newsroom.intel.com/corporate/softbank-group-and-intel-corporation-sign-2b-investment-agreement"target="_blank" rel="noopener">US$2 billion</a>. And the surreal cherry on top: <strong>Nvidia</strong> — the same company that replaced it on the Dow — <a href="http://nvidianews.nvidia.com/news/nvidia-and-intel-to-develop-ai-infrastructure-and-personal-computing-products"target="_blank" rel="noopener">invested US$5 billion</a> and signed a partnership to put RTX chiplets into x86 CPUs.</p>
<p>The financial results started showing: Q1 2026 revenue of US$13.6 billion (+7% year over year), Q2 at US$16.1 billion (+25%), seventh consecutive quarter above projections. The stock the government paid US$20.47 for reached <strong>more than six times</strong> that price in May, when Bloomberg reported talks for Intel to manufacture Apple chips — preliminary reporting, production years away, but the market went wild. Even after cooling off, the US government&rsquo;s position is still worth <strong>five times</strong> what it cost. The American taxpayer is, today, a profitable Intel shareholder. Go figure.</p>
<blockquote>
  <p><strong>Remember this:</strong> the US government, SoftBank, and Nvidia as shareholders, and the stock five times above the government&rsquo;s entry price — Intel became a national cause, and the market bought the comeback.</p>

</blockquote>
<h2>The comeback, feet on the ground<span class="hx:absolute hx:-mt-20" id="the-comeback-feet-on-the-ground"></span>
    <a href="#the-comeback-feet-on-the-ground" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Panther Lake is the first big product on 18A — RibbonFET (gate-all-around transistors) plus PowerVia (backside power delivery), coming out of Fab 52 in Chandler, Arizona. There&rsquo;s delicious irony here: the flagship&rsquo;s 12-core GPU tile is still manufactured by TSMC. And 18A yields (the fraction of good chips per wafer), per Tom&rsquo;s Hardware, should only reach industry-standard levels in 2027 — the ramp is slow, and Ars Technica asks the right question: is this the new normal or an aberration?</p>
<p>But the point of this article isn&rsquo;t cheerleading. It&rsquo;s that Panther Lake empirically closes a debate that was theological. An x86-64 on a competitive process delivers MacBook battery life, ties the M5 in multi-core, and beats the Windows competition in several scenarios. If the ISA were the decisive factor, that would never happen, on any process. What brought Intel down was management; what brings it back is fabs and focus; and what separates Apple, Qualcomm, AMD, and Intel is microarchitecture, cache, process, and integration — exactly as Jim Keller and the literature always said.</p>
<p>I&rsquo;m rooting for the comeback. Not out of nostalgia from someone who built PCs with Pentiums, but because the efficient-laptop market was settling into a comfortable duopoly — and a comfortable duopoly is the enemy of price and innovation. Let the fight go on.</p>
<blockquote>
  <p><strong>Remember this:</strong> Panther Lake closes a theological debate: competitive x86 exists when the fab is competitive. It was never about instructions.</p>

</blockquote>
]]></content:encoded><category>hardware</category><category>reviews</category></item><item><title>First Impressions Using Oh-My-Pi and OpenCode</title><link>https://akitaonrails.github.io/en/2026/05/25/first-impressions-using-oh-my-pi-and-opencode/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/05/25/first-impressions-using-oh-my-pi-and-opencode/</guid><pubDate>Mon, 25 May 2026 01:00:00 -0300</pubDate><description>&lt;p&gt;&lt;img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/26/oh-my-pi-opencode/omp-hero.webp" alt="Official Oh-My-Pi image: “A coding agent with the IDE wired in”, with the Pi logo in a purple and blue gradient." loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;Every time I mention coding agent harnesses, somebody shows up asking: &amp;ldquo;but why don&amp;rsquo;t you use Pi?&amp;rdquo; or &amp;ldquo;why not Oh-My-Pi?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;I hate that question when it comes like that.&lt;/p&gt;
&lt;p&gt;Not because I have anything against Pi. I don&amp;rsquo;t. The problem is that almost nobody explains the &lt;strong&gt;why&lt;/strong&gt;. What is the concrete benefit? What problem does it solve better than Claude Code or OpenCode? Which use case is clearly better there? Which limitation does it remove?&lt;/p&gt;</description><content:encoded><![CDATA[<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/26/oh-my-pi-opencode/omp-hero.webp" alt="Official Oh-My-Pi image: “A coding agent with the IDE wired in”, with the Pi logo in a purple and blue gradient."  loading="lazy" /></p>
<p>Every time I mention coding agent harnesses, somebody shows up asking: &ldquo;but why don&rsquo;t you use Pi?&rdquo; or &ldquo;why not Oh-My-Pi?&rdquo;</p>
<p>I hate that question when it comes like that.</p>
<p>Not because I have anything against Pi. I don&rsquo;t. The problem is that almost nobody explains the <strong>why</strong>. What is the concrete benefit? What problem does it solve better than Claude Code or OpenCode? Which use case is clearly better there? Which limitation does it remove?</p>
<p>Usually, nothing comes back. Just a tool name. That smells like one of two things: fanboy wants validation for the choice he already made, or somebody wants to outsource the homework. &ldquo;Akita, test this for me and tell me if my favorite tool is good.&rdquo; Not exactly the kind of pitch that makes me drop what I&rsquo;m doing.</p>
<p>But I tested it anyway. And, as expected, it did not blow my mind.</p>
<p>Before getting into Pi, OpenCode and Oh-My-Pi, we need to align vocabulary. I already explained this in previous posts, especially in <a href="/en/2026/02/08/rant-ai-killed-programmers/">&ldquo;RANT: Did AI Kill Programmers?&rdquo;</a>, but it is worth repeating because people still mix everything together.</p>
<h2>What is a coding agent harness<span class="hx:absolute hx:-mt-20" id="what-is-a-coding-agent-harness"></span>
    <a href="#what-is-a-coding-agent-harness" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A raw LLM does not edit your project. It receives text and returns text. That&rsquo;s it.</p>
<p>When you open Claude Code, Codex, OpenCode, Oh-My-Pi, Crush or anything similar, you are not talking directly to &ldquo;the model&rdquo; in a raw form. You are talking to a program in the middle. That program sets up the LLM&rsquo;s work environment.</p>
<p>That program is what I call a <strong>harness</strong>. In English, harness is the thing you use to strap and steer a horse. The metaphor works: the raw model is strong, but without a harness it runs wherever.</p>
<p>The harness does several things:</p>
<ul>
<li>builds the <strong>system prompt</strong>, with behavior rules;</li>
<li>reads context files like <code>AGENTS.md</code>, <code>CLAUDE.md</code>, local rules, skills and project docs;</li>
<li>defines which <strong>tools</strong> the LLM can call;</li>
<li>executes those tools: read files, search text, edit, run shell, run tests, open a browser, query LSP, call MCP;</li>
<li>collects tool results and sends them back to the model as new context;</li>
<li>manages permissions, sandboxing, confirmations for dangerous commands;</li>
<li>controls the execution plan, task list, parallel subagents;</li>
<li>detects when context is filling up and triggers compaction;</li>
<li>tries to preserve memory across turns or sessions;</li>
<li>decides how to interrupt, resume or steer the agent when you change your mind mid-run.</li>
</ul>
<p>So two CLIs using the same model can behave differently. Forget &ldquo;GPT 5.5 vs Opus 4.7&rdquo; for a second. It is model <strong>plus</strong> harness. The harness decides what information the model sees, which tools it knows about, which patch format it uses, how it confirms actions, when it summarizes, when it calls a subagent, when it stops.</p>
<p>That layer turns a chat with manual copy-paste into something that can actually work on a real project.</p>
<h2>The problem with subsidized plans<span class="hx:absolute hx:-mt-20" id="the-problem-with-subsidized-plans"></span>
    <a href="#the-problem-with-subsidized-plans" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I have said this many times: if you use agents for many hours a day, Anthropic and OpenAI&rsquo;s subsidized plans are much more attractive than paying per token.</p>
<p>Direct API usage is expensive. Very expensive. If you do real Agile Vibe Coding, letting the agent run tests, read code, refactor, compile, rewrite and review, it burns tokens like a weekend barbecue.</p>
<p>A monthly plan changes the math. You pay a fixed amount, get a large quota, and can work without treating every tool call like a taxi meter.</p>
<p>The problem is that Anthropic is strict about where those plans can be used. The <a href="https://code.claude.com/docs/en/legal-and-compliance"target="_blank" rel="noopener">Claude Code Legal and compliance docs</a> say OAuth for Free, Pro and Max plans is meant for Claude Code and Anthropic&rsquo;s native applications, and that third-party developers should use API keys through Claude Console or a cloud provider. They also explicitly say Anthropic does not allow third parties to offer Claude.ai login or route requests using users&rsquo; Free, Pro or Max credentials.</p>
<p>So yes, are there hacks to make OpenCode, OpenClaw and other tools use Anthropic plans? Yes. The Claude Code leak helped a lot of people understand how authentication and transport worked. I wrote about that in <a href="/en/2026/03/31/claude-code-source-code-leaked-what-we-found-inside/">&ldquo;Claude Code&rsquo;s Source Code Leaked. Here&rsquo;s What We Found Inside.&rdquo;</a>.</p>
<p>But I do not recommend it.</p>
<p>From what I have seen, Anthropic keeps enforcing the policy. You may get it working today and get blocked tomorrow. Worse, you may lose an account you use every day. Not worth it for me. If you want Opus through an Anthropic plan, use Claude Code. Full stop.</p>
<p>And to be fair, Claude Code is one of the best harnesses around. After hundreds of hours using it, I can praise it without hesitation:</p>
<ul>
<li>the planning mode is good;</li>
<li>the task list is clear and the agent follows it step by step;</li>
<li>you can interrupt and redirect without destroying the session;</li>
<li>you can inject prompts while it is working;</li>
<li>memory and compaction are much more sophisticated than the alternatives;</li>
<li>after the leak, we could see several layers of context management, not a single dumb summary.</li>
</ul>
<p>I explained the memory part in more detail in <a href="/en/2026/05/18/ai-agent-memory-karpathy-llm-wiki-agentmemory/">&ldquo;AI Agent Memory: Karpathy LLM Wiki and agentmemory in Practice&rdquo;</a>. Claude Code spends real effort preserving what matters when context fills up. Codex and OpenCode do enough. Claude Code goes further.</p>
<h2>Codex is the problem, not GPT<span class="hx:absolute hx:-mt-20" id="codex-is-the-problem-not-gpt"></span>
    <a href="#codex-is-the-problem-not-gpt" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>OpenAI has Codex CLI to use with its models. I will be blunt: Codex is an inferior harness.</p>
<p>I am not saying GPT 5.5 is bad. Quite the opposite. The model is excellent. The problem is the harness around it.</p>
<p>Codex&rsquo;s planning mode is weak compared to Claude Code. Interruption is awkward: many times it means stopping everything, losing the flow, and continuing manually. Task organization is looser. More than once it did not finish what it promised until I explicitly told it, &ldquo;finish this properly.&rdquo; It feels like a tool that has not decided whether it wants to be a chat, an executor, a planner or a terminal assistant.</p>
<p>Is it usable? Sure. Can you work with it? Yes. But the experience is worse than it should be.</p>
<p>The good news is that we are not forced to use Codex to use GPT. OpenCode exists. In practice, using GPT 5.5 through OpenCode feels much less nerfed. The harness lets the model work better.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/26/oh-my-pi-opencode/opencode.webp" alt="OpenCode 1.15.10 running GPT-5.5 on a code review, with the sidebar showing context, ai-memory MCP connected, task list and modified files."  loading="lazy" /></p>
<p>OpenCode also documents that it can connect providers and use subscriptions in some cases. In the <a href="https://opencode.ai/docs/providers/"target="_blank" rel="noopener">Providers</a> page, they warn that Anthropic prohibits Claude Pro/Max usage in third-party tools, but list subscriptions like ChatGPT Plus, GitHub Copilot and GitLab Duo as supported in OpenCode. The interface changes between versions, sometimes you see <code>/connect</code>, sometimes people say <code>/login</code>, but the idea is the same: plan OAuth is different from an API key. API key is token credit. OAuth uses the subscription.</p>
<p>So my practical recommendation today is simple:</p>
<ul>
<li>Want subsidized Opus 4.7? Use Claude Code.</li>
<li>Want subsidized GPT 5.5? Use OpenCode.</li>
<li>Want GPT 5.5 through Codex? You can, but in my opinion you are nerfing the model with a worse harness.</li>
</ul>
<p>If OpenAI were smart, I would honestly drop Codex CLI and adopt OpenCode as the base. Or at least copy what OpenCode does well without shame. Codex has no concrete advantage for me today.</p>
<h2>So where does Pi fit?<span class="hx:absolute hx:-mt-20" id="so-where-does-pi-fit"></span>
    <a href="#so-where-does-pi-fit" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now, Pi.</p>
<p>Pi is a minimalist harness. You can use it by itself, but to me it is like opening NeoVim with no plugins. Yes, it works. Yes, some people love building everything from scratch. Yes, you learn a lot doing it.</p>
<p>But I do not recommend it as the starting point for someone who just wants to work.</p>
<p>Oh-My-Pi is a Bun-based fork with &ldquo;batteries included.&rdquo; And of course there is already polarization: Pi extremists say Oh-My-Pi is bloated, that the right thing is pure Pi, building your own tools, composing your own distro, configuring everything by hand.</p>
<p>This reminds me of NeoVim. I like NeoVim. But I do not want to spend my life choosing a statusline plugin, configuring LSP by hand, tweaking file tree keymaps and comparing 12 fuzzy finders. I install LazyVim and move on. When I want to study internals, I study. When I want to deliver work, I use the package that works.</p>
<p>For me, Oh-My-Pi occupies that spot. If you are curious about Pi, start with Oh-My-Pi. If you like it, if you feel the itch, then go to minimalist Pi and build your own distribution. Go for it. Just do not pretend it is objectively better for everyone.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/26/oh-my-pi-opencode/omp.webp" alt="Oh-My-Pi v15.2.4 opening with GPT-5.5, detected LSP servers and the initial prompt pasted in the terminal."  loading="lazy" /></p>
<p>My first impression of Oh-My-Pi is that it works. It is not bad. It also did not make me say, &ldquo;how did I live without this?&rdquo;</p>
<p>Out of the box I found it too verbose. I am sure you can customize it, but that is the rabbit hole. I do not want to spend the afternoon tuning prompt, theme, panel size, tool policy and whatever else. I left it default.</p>
<p>In normal use, it feels a lot like OpenCode. Both can work with planning, tasks, tools, subagents, LSP, compaction, interruption and steering. Both are better harnesses than Codex. That is enough for me to take it seriously.</p>
<p>But OpenCode feels more cohesive. More product. More polished. Oh-My-Pi feels more like a lab built by people who enjoy powerful tools full of knobs. That can be praise or criticism, depending on what kind of person you are.</p>
<h2>What I checked in the code<span class="hx:absolute hx:-mt-20" id="what-i-checked-in-the-code"></span>
    <a href="#what-i-checked-in-the-code" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To avoid relying only on visual impressions, I asked for an analysis comparing OpenCode, Pi and Oh-My-Pi, then checked parts of the source code myself.</p>
<p>The result matches the intuition.</p>
<h3>Upstream Pi<span class="hx:absolute hx:-mt-20" id="upstream-pi"></span>
    <a href="#upstream-pi" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The original Pi is small. The built-in toolset is basically:</p>
<ul>
<li><code>read</code></li>
<li><code>bash</code></li>
<li><code>edit</code></li>
<li><code>write</code></li>
<li><code>grep</code></li>
<li><code>find</code></li>
<li><code>ls</code></li>
</ul>
<p>That is clean. I like clean code. But as a modern harness for heavy programming, by itself, it does not compete with OpenCode or Oh-My-Pi.</p>
<p>Again: that does not mean it is useless. It means it is a minimalist base. If you want to build your own agent distribution, great. If you want to open a tool and work today, it is thin.</p>
<h3>OpenCode<span class="hx:absolute hx:-mt-20" id="opencode"></span>
    <a href="#opencode" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>OpenCode has the normal competent kit for programming:</p>
<ul>
<li>shell;</li>
<li>read;</li>
<li>glob;</li>
<li>grep;</li>
<li>edit/write or apply_patch;</li>
<li>task/subagents;</li>
<li>todo;</li>
<li>web fetch/search;</li>
<li>skills;</li>
<li>optional LSP;</li>
<li>plugins and MCP.</li>
</ul>
<p>Its <code>read</code> covers what I expect in a code project: source file, directory, supported image and PDF. It has LSP. It has task. It has todo. It does not try to be the Swiss Army knife for every possible artifact.</p>
<p>For normal software, that is enough. For a project mixing SQLite databases, spreadsheets, notebooks, PDFs, archives and web pages, the difference starts to show.</p>
<h3>Oh-My-Pi<span class="hx:absolute hx:-mt-20" id="oh-my-pi"></span>
    <a href="#oh-my-pi" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Oh-My-Pi comes with more tools. A lot more. In its registry you find things like:</p>
<ul>
<li><code>read</code></li>
<li><code>bash</code></li>
<li><code>edit</code></li>
<li><code>ast_grep</code></li>
<li><code>ast_edit</code></li>
<li><code>ask</code></li>
<li><code>debug</code></li>
<li><code>eval</code></li>
<li><code>find</code></li>
<li><code>search</code></li>
<li><code>lsp</code></li>
<li><code>inspect_image</code></li>
<li><code>browser</code></li>
<li><code>task</code></li>
<li><code>job</code></li>
<li><code>recipe</code></li>
<li><code>irc</code></li>
<li><code>todo_write</code></li>
<li><code>web_search</code></li>
<li><code>write</code></li>
<li>memory/hindsight tools</li>
</ul>
<p>The clearest advantage is <code>read</code>.</p>
<p>In Oh-My-Pi, a single <code>read</code> tool understands files, directories, archives (<code>.tar</code>, <code>.tar.gz</code>, <code>.tgz</code>, <code>.zip</code>), SQLite, images, documents (<code>PDF</code>, <code>DOCX</code>, <code>PPTX</code>, <code>XLSX</code>, <code>RTF</code>, <code>EPUB</code>), Jupyter notebooks (<code>.ipynb</code>), URLs in reader mode, and internal URIs like <code>skill://</code>, <code>agent://</code>, <code>artifact://</code>, <code>memory://</code>, <code>rule://</code>, <code>local://</code>, <code>mcp://</code>.</p>
<p>That is where it really wins when the work depends on those formats.</p>
<p>Example: a Rails project with a local SQLite database. In OpenCode, the agent will probably try to use <code>sqlite3</code> through shell, or write a script, or you will have to instruct it. In Oh-My-Pi, the tool prompt itself teaches:</p>
<ul>
<li><code>file.db</code> lists tables and row counts;</li>
<li><code>file.db:table</code> shows schema and samples;</li>
<li><code>file.db:table:key</code> fetches a row by primary key;</li>
<li><code>file.db:table?limit=50&amp;offset=100</code> paginates;</li>
<li><code>file.db:table?where=status='active'&amp;order=created:desc</code> filters;</li>
<li><code>file.db?q=SELECT ...</code> runs a read-only <code>SELECT</code>.</li>
</ul>
<p>That matters. Of course a human can do this with shell. But the model does not need to invent the command, does not need <code>sqlite3</code> installed, does not need to get escaping wrong, does not need to dump 30 thousand lines into context. The right path is already described inside the tool, visible to the model.</p>
<p>Same for ZIP, PDF, spreadsheet, notebook. If your project is just TypeScript, Ruby, Go and Markdown, whatever. If your project is data science, investigation, audit, document analysis, database migration, artifact parsing, Oh-My-Pi starts making more sense.</p>
<h2>The advantage in pure source code is smaller<span class="hx:absolute hx:-mt-20" id="the-advantage-in-pure-source-code-is-smaller"></span>
    <a href="#the-advantage-in-pure-source-code-is-smaller" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>For ordinary source code, the difference exists, but it is not huge.</p>
<p>Oh-My-Pi has some good conveniences:</p>
<ul>
<li>code reads with structural summaries when the file is parseable;</li>
<li>multiple ranges in a single read call;</li>
<li>line-hash anchors that make text edits safer;</li>
<li>more aggressive LSP routing;</li>
<li>AST tools (<code>ast_grep</code> and <code>ast_edit</code>);</li>
<li>a system prompt that insists on using the specialized tool instead of falling back to shell.</li>
</ul>
<p>The AST part is worth explaining.</p>
<p><code>grep</code> and <code>rg</code> search text. AST searches syntax structure. Instead of searching the string <code>console.log</code>, you can search for a call <code>console.log($$$)</code> and ignore spacing, line breaks, comments and formatting. Instead of doing a textual replace of an import, you can rewrite an import node. Instead of hunting <code>foo &amp;&amp; foo()</code> as text, you can structurally transform it into <code>foo?.()</code> while ensuring the same metavariable appears on both sides.</p>
<p>This is useful in codemods. Useful when you want to change function calls, imports, declarations, error patterns, without catching false positives in strings, comments or docs.</p>
<p>But it is not magic. AST patterns are sensitive. The model can get the shape wrong. Sometimes you need to adjust the query. For a big refactor, LSP rename is still more correct than AST if the language supports it. AST is one more tool, not a religion.</p>
<p>OpenCode is not helpless without it. It has grep, edit, apply_patch, optional LSP, task, todo. For 90% of ordinary coding tasks, that is enough.</p>
<p>My read after checking the code is simple: when the work involves SQLite, notebooks, documents, archives, images, URLs or internal artifacts, Oh-My-Pi has a real advantage. In pure source code, the advantage shows up more in large refactors, type navigation, structural search and codemods. For normal code editing, OpenCode remains just as good and often more pleasant.</p>
<h2>Prompt is also a tool<span class="hx:absolute hx:-mt-20" id="prompt-is-also-a-tool"></span>
    <a href="#prompt-is-also-a-tool" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>One thing people ignore: it is not enough for a tool to exist. The LLM needs to know when to use it.</p>
<p>Oh-My-Pi&rsquo;s prompt is explicit. It tells the model to use tools when they improve correctness, parallelize calls when appropriate, prefer LSP for symbol operations, prefer AST for structural search and codemods, prefer <code>read</code>/<code>search</code>/<code>find</code> over shell, delegate to subagents when the work can be decomposed.</p>
<p>That helps big models. Opus and GPT 5.5 follow this kind of instruction well when the tool description is clear.</p>
<p>OpenCode does not fail at the basics. It teaches <code>Read</code>, <code>Glob</code>, <code>Grep</code>, <code>Edit</code>, <code>Write</code>, <code>Bash</code>, <code>Task</code>, patch. It handles the normal flow. The difference is that it does not push LSP and advanced tools with the same insistence as Oh-My-Pi.</p>
<p>Upstream Pi is also minimalist in the prompt. It lists tools, gives guidelines, but does not do heavy routing. Again: good if you want a small base. Less good if you want a harness full of ready-made conveniences.</p>
<h2>Memory, planning, interruption<span class="hx:absolute hx:-mt-20" id="memory-planning-interruption"></span>
    <a href="#memory-planning-interruption" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Here Oh-My-Pi and OpenCode are more similar.</p>
<p>Both have context compaction. Both have some plan/todo mode. Both can spawn subagents. Both allow steering, meaning you do not have to treat the task list like scripture. If the agent starts doing nonsense, you interrupt, inject a new prompt, change direction.</p>
<p>That is mandatory for Agile Vibe Coding. Without decent steering, the tool becomes a conveyor belt: starts wrong, keeps going wrong until it hits a wall. Claude Code is very good at this. Oh-My-Pi and OpenCode get close enough. Codex, in my experience, does not.</p>
<p>In Oh-My-Pi, looking at configuration, there are explicit knobs for <code>steeringMode</code>, <code>interruptMode</code> and <code>compaction.strategy</code>. That is its personality: expose harness behavior as configuration. Powerful, but also one more rabbit hole.</p>
<p>OpenCode tends to hide more of that and deliver a more closed product. I like that trade-off for daily work.</p>
<h2>But doesn&rsquo;t Pi have more agents?<span class="hx:absolute hx:-mt-20" id="but-doesnt-pi-have-more-agents"></span>
    <a href="#but-doesnt-pi-have-more-agents" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Another argument that shows up: &ldquo;but in Pi you can spawn way more agents.&rdquo;</p>
<p>So what?</p>
<p>Having more agents available does not mean the task became more parallelizable. Programming is hard to parallelize for the same reason managing teams of programmers is hard: there is almost always a critical path. You can split some of it, sure, but quickly you hit the point where one decision depends on the previous one, which depends on a detail that only appeared after running the test, which changes the architecture, which invalidates half the original plan.</p>
<p>LLMs with subagents suffer the same physics. Agent A needs to know what Agent B decided. Agent C implements on top of an API Agent A is still changing. The main agent needs to consolidate everything, detect conflicts, request fixes, reread diffs, run tests, ask for another patch. Past a certain point, you are not parallelizing. You are inventing management.</p>
<p>I already demonstrated this in <a href="/en/2026/04/25/llm-benchmarks-vale-a-pena-misturar-2-modelos/">&ldquo;LLM Benchmarks: Is It Worth ($$) Mixing 2 Models?&rdquo;</a>. I ran variations with a strong planner, cheap executor, free delegation, forced delegation, manual orchestration. Boring result: in continuous coding-agent work, coordination overhead eats the gain. When I did not force delegation, Tier A models simply did not delegate. When I forced it, quality dropped or time increased. In the best case, it tied with more coordination.</p>
<p>That is not a bug. That is the model having common sense.</p>
<p>In a cohesive task, a good model realizes it needs to keep reasoning in the same context. Design and implementation mix. You plan, write, test, discover a premise was wrong, adjust, go back, simplify. Artificially splitting that across 10 agents only creates more edges to break.</p>
<p>Truly parallelizable tasks exist. Translating a huge strings file into 10 languages? Fine. One agent per language. Converting 200 images to WebP? Parallelize it. Applying the same mechanical refactor to 50 independent files? Maybe. The result of one item does not change the other.</p>
<p>How many tasks in your day actually look like that?</p>
<p>I did not see Oh-My-Pi parallelize more than OpenCode or Claude Code in normal programming. And that is correct. If it started opening agents for everything, I would be suspicious. It would spend time, context and tokens consolidating duplicated work.</p>
<p>So no: mixing models, mixing ad hoc agents, increasing the magic worker count, none of that solves hard programming. It solves independent batches. Independent batches are the easy part.</p>
<h2>But doesn&rsquo;t Pi save more tokens?<span class="hx:absolute hx:-mt-20" id="but-doesnt-pi-save-more-tokens"></span>
    <a href="#but-doesnt-pi-save-more-tokens" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Another myth: because Pi is more customizable, you control the system prompt of every component, so you can minimize everything and save tokens compared to OpenCode, Codex or Claude Code, which start with bigger prompts.</p>
<p>You are saving cents and losing dollars.</p>
<p>The initial prompt is not where most of the spend happens in a serious coding agent. The spend comes from the work: reading code, writing code, running tests, receiving stack traces, reading logs, rereading files, applying patches, running tests again. One or two verbose logs already eat the entire window of a small local model with 30K tokens. A <code>bundle exec</code>, an <code>npm test</code>, a frontend build spitting warnings, done: there goes your heroic system-prompt savings.</p>
<p>My personal rule: I would not try to use a serious coding agent with less than 200K tokens of context. Below that, the harness has to compact memory constantly. Compaction helps, but it always loses detail. It loses the warning that appeared before the error. It loses the decision made half an hour ago. It loses the chunk of stdout that looked like noise but was the clue.</p>
<p>Below 100K, for me, it is basically unusable for a long programming session. You cannot let the agent read a large log, test output, build stdout, a long diff. You are back to the dumb job of manually choosing what fits in context. That is the opposite of productivity. You become the assistant&rsquo;s assistant.</p>
<p>That is why I recommend frontier models. Opus and GPT have windows around 1 million tokens, and that matters in long sessions. You can let the harness work without treating every log like contraband. Recent details stay around longer before the next compaction. The answer improves because the model still has the facts in front of it.</p>
<p>This is a big reason I do not recommend local open source LLMs for serious programming. Model quality is only half of it. The other half is KV cache. There is no consumer GPU with enough spare VRAM for giant context on a large model. Even on a beefy Mac Studio, in practice, you barely get near something like 300K before you start sacrificing memory and performance. Still far from 1 million.</p>
<p>At that point, the price comparison changes. Even paying per token can make sense. And if you want to save money, use a previous-generation cloud model, like Kimi K2.6. In <a href="/en/2026/04/24/llm-benchmarks-parte-3-deepseek-kimi-mimo/">my benchmark</a>, it is still much better than any local open source model I tested. It is cheap, and you do not have to pretend 30K context is a serious development environment.</p>
<p>Worse: if you try to save tokens manually, you break the right workflow.</p>
<p>The wrong way is to let the harness suggest a command, copy it to your terminal, run it yourself, grab only a slice of the output, paste it back into chat, and hope you picked the relevant slice. That saves tokens and destroys context. The model goes blind exactly where it matters: the full error, event order, previous warning, real path, stdout vs stderr, the exact command that ran.</p>
<p>The right way is to let the harness run. Yes, it will send output into context. Yes, it costs. But that is how the model gets enough information to choose the next step. The more real, verifiable context it has, the better the chance it fixes the right thing on the next iteration.</p>
<p>That is why I insist on subsidized plans. If you pay per token, you are tempted to drive with the handbrake on. If you are on a Pro/Max/Plus plan, use the tool properly. Let it run tests. Let it read logs. Let it open files. Let the agent work.</p>
<p>And please, do not waste time trying to turn local Qwen into your main programmer because &ldquo;it is free.&rdquo; I already tested this in the canonical benchmark <a href="/en/2026/04/24/llm-benchmarks-parte-3-deepseek-kimi-mimo/">&ldquo;LLM Coding Benchmark: DeepSeek, Kimi, MiMo, GPT&rdquo;</a>. Local open source models came in Tier C or worse. Local Qwen is fine for lab work, demos, curiosity, study. For serious programming with long tool-calling, large context, tests and refactors, it is still not a replacement for Claude or GPT.</p>
<p>If you cannot pay US$100 a month for a tool that can multiply your productivity by 5 or 10, the problem is not tokens. It is ROI. It is career. It is the kind of work you are doing.</p>
<p>Tweaking Pi to save crumbs of token is not your ticket to better pay. Delivering more, with better quality, using good tools, is.</p>
<h2>Is Pi better than OpenCode?<span class="hx:absolute hx:-mt-20" id="is-pi-better-than-opencode"></span>
    <a href="#is-pi-better-than-opencode" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I tried a practical comparison. I took the same PR, <a href="https://github.com/akitaonrails/ai-memory/pull/10"target="_blank" rel="noopener">akitaonrails/ai-memory#10</a>, and asked three combinations to review it:</p>
<ul>
<li>Oh-My-Pi with GPT 5.5;</li>
<li>OpenCode with GPT 5.5;</li>
<li>Claude Code with Opus 4.7.</li>
</ul>
<p>Same PR. Same kind of request. Same intention: find real problems before merge.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/26/oh-my-pi-opencode/review-omp.webp" alt="Oh-My-Pi with GPT-5.5 reviewing PR #10 and concluding it caught only some of the problems: temperature on GPT-5/o-series models, a required comment mismatch and the GPT-4 Turbo cap, while leaving other divergences out."  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/26/oh-my-pi-opencode/review-opencode.webp" alt="OpenCode with GPT-5.5 reviewing the same PR #10, agreeing to request changes and adding a problem the first pass missed about OpenAiCompatProvider reusing OpenAiProvider behavior."  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/05/26/oh-my-pi-opencode/review-claude.webp" alt="Claude Code with Opus 4.7 reviewing PR #10, listing problems around temperature, additionalProperties, OpenAI behavior leaking into compat providers and empty dry-runs."  loading="lazy" /></p>
<p>None of them found everything.</p>
<p>To simplify, imagine the PR had problems A, B, C and D. Oh-My-Pi found A and B. OpenCode found B and C. Claude found C and D. That was not the exact matrix, but it is the shape of the result: each one caught part of it. Each one missed part of it.</p>
<p>I keep repeating this because people keep learning it the hard way: do not trust a single LLM to audit anything important. It will find <strong>some</strong> problems. Maybe good problems. Maybe problems you missed. But complete coverage? No. It depends on the context it read, how you asked, which files it opened, which tool it chose, where the reasoning path went.</p>
<p>The interesting part: the same GPT 5.5 gave different answers in two different harnesses. Oh-My-Pi and OpenCode did not become clones just because they used the same model. The harness changes the prompt, tools, context format, ordering, pressure to use LSP or AST, everything. That changes the path.</p>
<p>In this particular case, I would call Oh-My-Pi and OpenCode tied. Both found real problems. Both missed real problems. Claude was a little worse because in one pass it came closer to saying the PR was mergeable while issues were still missing. But that also does not prove Claude is worse. A second pass with a different prompt could flip it.</p>
<p>I am not crowning a winner. I am killing the fantasy.</p>
<p>Pi does not have hidden magic that turns review into perfect audit. Oh-My-Pi is a very good harness. So is OpenCode. So is Claude Code. All of them fail. All of them depend on the model, context and request. That is why I do not know if I would prefer Oh-My-Pi over OpenCode or Claude Code as my main tool. It sits on the same shelf: excellent, useful, but not miraculous.</p>
<p>One more anecdote, not proof of anything. While editing this very post in Oh-My-Pi, I asked for a localized change. With Claude Code and OpenCode, when I do that in a bilingual post, they usually edit the PT-BR section and the matching EN section, only in the requested range. Oh-My-Pi got half of it right: it edited PT-BR in the right place. But for EN it decided to retranslate the whole file from scratch. I do not know why.</p>
<p>It is one case. It proves nothing by itself. But it is funny because it punctures the idea that Pi always saves tokens. In that round, it spent far more. I have never seen Claude Code or OpenCode retranslate the whole post when I asked for a small edit; they are usually good at touching only the piece I asked for.</p>
<p>My first impression is that OMP really does have more tools, and it tries hard to use them. Maybe too hard. Sometimes it feels like it is forcing tooling onto tasks where OpenCode and Claude Code would be more direct, and that starts getting close to making the answer worse instead of better. Maybe that is only a first impression, but that is the feeling I have so far: OMP sometimes tries a little too hard for its own good.</p>
<h2>So which one would I use?<span class="hx:absolute hx:-mt-20" id="so-which-one-would-i-use"></span>
    <a href="#so-which-one-would-i-use" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>For normal programming, I would keep alternating between Claude Code and OpenCode.</p>
<p>Claude Code when I want Opus and the Anthropic plan. OpenCode when I want GPT 5.5 and do not want to suffer through Codex. Simple.</p>
<p>I would keep Oh-My-Pi in the toolbox, not as a mandatory replacement. I would pick Oh-My-Pi when the project had a lot of stuff outside source code:</p>
<ul>
<li>PDFs that need to be read;</li>
<li>spreadsheets;</li>
<li>SQLite;</li>
<li>Jupyter notebooks;</li>
<li>ZIPs and tarballs;</li>
<li>images;</li>
<li>web pages that need to become text;</li>
<li>internal artifacts from previous runs;</li>
<li>frequent need for AST codemods.</li>
</ul>
<p>Maybe it shines more in data science projects. Maybe in data investigation. Maybe in audits, migrations, document research, internal tools full of SQLite and CSV and XLSX. In those scenarios, the universal <code>read</code> tool is very convenient.</p>
<p>For Rails CRUD, Go backend, TypeScript frontend, Rust CLI, Hugo blog? OpenCode is just as good and more polished.</p>
<p>And pure Pi? I would only use it if my fun were building the harness itself. Nothing wrong with that. I just will not pretend it is the path for someone who wants to finish work today.</p>
<h2>My current recommendation<span class="hx:absolute hx:-mt-20" id="my-current-recommendation"></span>
    <a href="#my-current-recommendation" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If you want the short answer:</p>
<ul>
<li>Use <strong>Claude Code</strong> with <strong>Opus 4.7</strong> if you want the best proprietary harness and can stay inside Anthropic&rsquo;s ecosystem.</li>
<li>Use <strong>OpenCode</strong> with <strong>GPT 5.5</strong> if you want to use OpenAI&rsquo;s plan without being stuck with Codex.</li>
<li>Use <strong>Oh-My-Pi</strong> if you want a more flexible harness, with more tools, especially for projects with varied files and artifacts.</li>
<li>Avoid <strong>Codex</strong> if you have an alternative. The model is good. The harness gets in the way.</li>
<li>Use pure <strong>Pi</strong> if you enjoy building your own distro. As an immediate work tool, I would start with Oh-My-Pi.</li>
</ul>
<p>The Linux analogy helps. I do not use Linux because it is &ldquo;free.&rdquo; I use it because, for me, it is the best tool for the job. The alternatives do not come close in my workflow. That does not mean every beginner should drop Windows or macOS tomorrow and hand-roll Arch.</p>
<p>Same with editors. NeoVim is technically fantastic. I love it. But there is a reason it is not as mainstream as VS Code: it is not productive on day one unless you already have experience and learned it gradually. If you need productivity now, use the easiest tool that works out of the box. VS Code, Cursor, Windsurf, JetBrains, whatever. Deliver first. Tweak later.</p>
<p>And look, I am not the guy ricing NeoVim all day. I prefer NeoVim because its editing is better than VS Code&rsquo;s, and because I like staying 100% in the terminal. A GUI editor like VS Code or Zed does not give me that flow. I use the same editor locally or over SSH, combine it with Tmux or Zellij, run everything in a modern terminal like Kitty, Ghostty or Alacritty, and I am productive that way.</p>
<p>Here, LLM harnesses fit me well. Oh-My-Pi, OpenCode and Claude Code are already terminal-based. Any of them fits my workflow.</p>
<p>Pi falls into that category. It is NeoVim. Technically great, elegant for people who like building their own workshop, but not the right choice for everyone. It is a tool for people who enjoy tweaking, understand the cost, and accept spending time tuning the harness as part of the hobby or the work. Most people should start with the package that works.</p>
<p>So: Oh-My-Pi and OpenCode are good. Pick one and work. There is no religion here.</p>
<p>What irritates me is fanboys selling a tool name as if it were an argument. &ldquo;Use Pi&rdquo; means nothing. Tell me what it solves better. Tell me where it saves time. Tell me which real bug it avoids. Tell me which workflow is impossible in Claude Code or OpenCode and trivial in Pi.</p>
<p>Then we can talk.</p>
<p>So far, my read is boring: for normal software development, Oh-My-Pi and OpenCode perform similarly when using the same model. Oh-My-Pi has more tools and is more flexible. OpenCode is more cohesive and polished. Claude Code remains the bar for Opus. Codex remains the option I would only use if I had no choice.</p>
<p>Tools help. Harnesses matter. But process still wins: small prompt, clear scope, tests running, careful review, frequent commits, actual engineering.</p>
]]></content:encoded><category>coding-agents</category><category>reviews</category></item><item><title>Omarchy on the Thinkpad T14 Gen 6: Mini-Review and Full Setup</title><link>https://akitaonrails.github.io/en/2026/04/18/omarchy-on-thinkpad-t14-gen-6/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/04/18/omarchy-on-thinkpad-t14-gen-6/</guid><pubDate>Sat, 18 Apr 2026 08:30:00 -0300</pubDate><description>&lt;p&gt;&lt;img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/closed-lid.jpg" alt="Thinkpad T14 Gen 6 closed, next to its USB-C charger" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;I bought a Lenovo Thinkpad T14 Gen 6 and installed Omarchy on it. It&amp;rsquo;s not my main machine, and it&amp;rsquo;s not supposed to be. It&amp;rsquo;s a companion: a notebook I can open on the 3D printing office desk, SSH into the desktop, fire up Claude Code, access files on the NAS, debug the network through ethernet without hunting for a USB dongle and a long cable. This article covers the hardware choice, the Omarchy setup on top, the customizations specific to a laptop and to this Thinkpad in particular, and the architecture decisions that might not be obvious.&lt;/p&gt;</description><content:encoded><![CDATA[<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/closed-lid.jpg" alt="Thinkpad T14 Gen 6 closed, next to its USB-C charger"  loading="lazy" /></p>
<p>I bought a Lenovo Thinkpad T14 Gen 6 and installed Omarchy on it. It&rsquo;s not my main machine, and it&rsquo;s not supposed to be. It&rsquo;s a companion: a notebook I can open on the 3D printing office desk, SSH into the desktop, fire up Claude Code, access files on the NAS, debug the network through ethernet without hunting for a USB dongle and a long cable. This article covers the hardware choice, the Omarchy setup on top, the customizations specific to a laptop and to this Thinkpad in particular, and the architecture decisions that might not be obvious.</p>
<h2>Mini-review of the Thinkpad T14 Gen 6<span class="hx:absolute hx:-mt-20" id="mini-review-of-the-thinkpad-t14-gen-6"></span>
    <a href="#mini-review-of-the-thinkpad-t14-gen-6" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/open-lid-turned-off.jpg" alt="Thinkpad T14 Gen 6 open with the screen off, showing the keyboard and the Intel Core Ultra 5 sticker"  loading="lazy" /></p>
<p>Let&rsquo;s get this out of the way: this isn&rsquo;t the notebook of my dreams. If I were picking by looks, I&rsquo;d grab an Asus Zenbook S 14 with an OLED screen. If I were picking by portability, the Thinkpad T14s with the aluminum shell, which is lighter and much better looking. The regular T14 has a 14&quot; 1920x1200 IPS panel, 400 nits, 60Hz, no HDR. It gets the job done, but it&rsquo;s a far cry from the Zenbook&rsquo;s screen. The shell is plastic with a rubberized finish, which makes it scratch-resistant but not premium. Reviews from <a href="https://www.notebookcheck.net/AMD-Ryzen-AI-meets-classic-ThinkPad-Lenovo-ThinkPad-T14-Gen-6-AMD-laptop-review.1222690.0.html"target="_blank" rel="noopener">NotebookCheck</a> and <a href="https://www.xda-developers.com/lenovo-thinkpad-t14-gen-6-review/"target="_blank" rel="noopener">XDA Developers</a> land on the same verdict: good price, good connectivity, fine performance for office work, but the screen is dated.</p>
<p>What makes up for it:</p>
<ul>
<li>Port selection. Full-size HDMI. Gigabit ethernet. USB-A, USB-C Thunderbolt 4, charges over USB-C (not a proprietary charger). For a debug companion, this is exactly what I wanted. The Zenbook S 14, being thinner, cuts ports.</li>
<li>Fingerprint sensor that actually works on Linux (Goodix MOC, works with libfprint on kernel 6.11+). I use this, details later.</li>
<li>Thinkpad keyboard. 1.5mm key travel, trackpoint, classic layout. It&rsquo;s not the best keyboard in the world in 2026, but it&rsquo;s reliable and durable.</li>
<li>Rugged shell. It will fall, it will scratch, it will travel in a backpack. If I put a Zenbook OLED or a Macbook Pro on the 3D printing office desk next to the printer with PLA dust in the air, I&rsquo;d be nervous. The Thinkpad can take a beating.</li>
</ul>
<p>If I wanted a gaming machine, I&rsquo;d get an Asus Zephyrus G14, my favorite gaming notebook. If I wanted a creative work machine, I&rsquo;d get the <a href="https://www.asus.com/laptops/for-home/zenbook/zenbook-duo-ux8406/"target="_blank" rel="noopener">Asus Zenbook Duo (UX8406)</a> with two vertical OLED screens, great for video editing and 3D modeling. I can afford any Macbook Pro or Mac Studio, and I chose not to go that way. I&rsquo;ll explain in the next section.</p>
<h2>My use case<span class="hx:absolute hx:-mt-20" id="my-use-case"></span>
    <a href="#my-use-case" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/open-lid-fastfetch.jpg" alt="Thinkpad open on a 3D printing office bench, with fastfetch showing Omarchy in the terminal"  loading="lazy" /></p>
<p>My main PC is a desktop with Ryzen 9 7950X3D, 96 GB of RAM, RTX 5090 with 32 GB. That&rsquo;s where I work, experiment with local models, run containers, edit the blog. For gaming, I have a separate mini-PC with an RTX 4090. Those two cover 100% of what I need to do at home.</p>
<p>The notebook exists to cover the remaining 1%: sitting on the couch, taking it to the 3D printing office, taking it to the kitchen, short trips. It&rsquo;s not meant to replace the desktop. It&rsquo;s meant to give me remote access to the desktop when I&rsquo;m away from it.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/ssh-to-remote-desktop-on-left.png" alt="Hyprland with two terminals side by side: left is an SSH session on the main desktop (96 GB, RTX 5090), right is the Thinkpad itself. Same UI, two machines."  loading="lazy" /></p>
<p>In practice it looks like this: open the notebook, split the Hyprland layout, left pane SSHs into the main desktop, right pane is the notebook itself. Same Omarchy on both, same keybindings, same bash. The notebook becomes an extension of the desktop, not a parallel environment I have to re-learn in my head every time I switch.</p>
<h3>SSH from outside the house: Tailscale<span class="hx:absolute hx:-mt-20" id="ssh-from-outside-the-house-tailscale"></span>
    <a href="#ssh-from-outside-the-house-tailscale" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Inside the local network, SSH is trivial, the notebook talks to the desktop through the internal IP. Outside the house is another story. My home IP is dynamic, opening port 22 to the internet is a terrible idea, and even with DDNS and port forwarding you&rsquo;re putting SSH on the public internet for any scanner to find.</p>
<p>The solution I use is <a href="https://tailscale.com/"target="_blank" rel="noopener">Tailscale</a>. For those who don&rsquo;t know: Tailscale is a mesh VPN built on WireGuard that creates a private network between your devices (the &ldquo;tailnet&rdquo;). Each machine runs the agent, authenticates once, and gets a fixed IP on the private network (something like 100.x.y.z). Traffic between your own devices goes peer-to-peer, encrypted by WireGuard. It doesn&rsquo;t route through Tailscale&rsquo;s central server, they only coordinate NAT traversal. Result: from my notebook in a café anywhere in the world, I run <code>ssh hal9000</code> and land on my home desktop as if I were on the same network.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/tailscale-machines.png" alt="Tailscale admin panel showing my two tailnet machines, hal9000 (desktop) and hal9666 (thinkpad), both with SSH enabled"  loading="lazy" /></p>
<p>More sophisticated options exist: <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/"target="_blank" rel="noopener">Cloudflare Tunnel</a> with Zero Trust to expose services publicly with SSO auth, self-hosted headscale, raw WireGuard with manual config, Nebula, OpenVPN. Each has its use case. If you need to expose services to third parties, control granular per-identity access, or run the whole infrastructure at home without depending on anyone, those options win. In my case, it&rsquo;s just notebook talking to desktop, for short periods (it&rsquo;s not a full week of work, it&rsquo;s a weekend debug session), so free Tailscale covers it. The free tier accepts up to 100 devices and 3 users, way more than I need.</p>
<p>Setup is as simple as it gets:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># On Arch/Omarchy</span>
</span></span><span class="line"><span class="cl">sudo pacman -S tailscale
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now tailscaled.service
</span></span><span class="line"><span class="cl">sudo tailscale up --ssh</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The <code>--ssh</code> flag enables <a href="https://tailscale.com/kb/1193/tailscale-ssh/"target="_blank" rel="noopener">Tailscale SSH</a>, which authenticates via tailnet identity instead of a local SSH key. Once you log in through the browser to tailscale, each registered machine can SSH into the others based on ACL policy defined in the admin panel. Zero key management.</p>
<p>I repeat the same on the desktop, log in with the same account, done. Both machines show up in the panel (hal9000 and hal9666 in the screenshot above, both with SSH enabled). From the notebook: <code>ssh hal9000</code>. From the desktop to the notebook: <code>ssh hal9666</code>. No port forwarding, no public IP, no port 22 exposed to the internet. If the notebook is stolen, I remove it from the tailnet with one click.</p>
<p>A practical detail: since the tailnet gives stable names, I added entries to <code>~/.ssh/config</code> to use these short names:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Host hal9000
  HostName hal9000
  User akitaonrails</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now <code>ssh hal9000</code> works from anywhere that has Tailscale connected. It&rsquo;s the closest thing to &ldquo;it just works&rdquo; I&rsquo;ve seen for remote SSH.</p>
<h3>Why not a Mac<span class="hx:absolute hx:-mt-20" id="why-not-a-mac"></span>
    <a href="#why-not-a-mac" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I have no use for macOS. As a dev, I live better in native Linux. Every tool I need has a first-class Linux version, and on macOS I&rsquo;d get a second-class version via Homebrew. I don&rsquo;t do iOS, so I don&rsquo;t need XCode. For mobile I use Flutter or Hotwire Native, which run on any OS. iTerm2 and Ghostty on Mac are fine, but Alacritty, Kitty and Ghostty itself on Linux work just as well. Every good piece of software lands on Linux first and gets ported afterwards. Arch with AUR covers everything in a single <code>yay</code>.</p>
<p>For creative work, I haven&rsquo;t done it professionally in years. DaVinci Resolve Studio on Linux is better than Final Cut Pro. Krita or Affinity Photo replace Photoshop for most cases. Clip Studio Paint on Android is better than Procreate. I simply don&rsquo;t have a workflow that depends on Apple, and the App Store annoys me.</p>
<p>For gaming, Mac is terrible. Apple Silicon has a decent GPU for some things, but the native macOS game library is pathetic compared to Windows or Linux. Game Porting Toolkit exists, CrossOver exists, but for anyone who games seriously, it doesn&rsquo;t cut it. I won&rsquo;t be gaming on the Thinkpad, I have the main desktop and the mini-PC with the RTX 4090 for that. But if I ever feel like running Hollow Knight or some indie on a train, I just install Steam and let Proton handle it. Linux became a real gaming platform in the last few years, with Proton/DXVK running almost the entire Steam catalog (check <a href="https://www.protondb.com/"target="_blank" rel="noopener">ProtonDB</a>). I recently wrote about <a href="/en/2026/04/11/emulation-distrobox-with-claude-code/">running my emulation library in distrobox</a> without polluting the host system. With Mac, those options don&rsquo;t exist.</p>
<p>Another argument that always shows up: &ldquo;but a Mac Mini M4 or a Mac Studio with 128GB of unified memory runs large models locally, it&rsquo;s a ChatGPT replacement.&rdquo; I already tested that thesis and wrote a <a href="/en/2026/04/05/testing-llms-open-source-and-commercial-can-anyone-beat-claude-opus/">detailed benchmark</a>. The conclusion: expensive local hardware to run LLMs is a weekend hobby, not a production tool. The open source models that fit don&rsquo;t deliver the quality Claude Opus delivers. I have a <a href="/en/2026/03/31/migrating-my-home-server-with-claude-code/">home server with AMD Strix Halo and 96 GB of unified RAM</a>, I ran models for dozens of hours, and in my real coding flow they&rsquo;re fine for simple tasks. For complex tasks, Claude Opus. Before spending $4000 on a Mac Studio justifying it with local models, actually test it first. You&rsquo;ll probably end up paying for Opus again.</p>
<h2>Why Omarchy<span class="hx:absolute hx:-mt-20" id="why-omarchy"></span>
    <a href="#why-omarchy" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I&rsquo;ve been using Omarchy on the desktop for months and documented the path in a <a href="/en/tags/omarchy/">series of articles</a>. <a href="/en/2025/08/29/new-omarchy-2-0-install/">Omarchy 2.0</a> has its own installer with LUKS, Btrfs, Limine, snapper and SDDM already configured. I wrote about <a href="/en/2025/09/12/omarchy-2-0-install-with-the-omarchy-iso/">using the official ISO</a>, about <a href="/en/2025/09/07/omarchy-2-0-zsh-configs/">ZSH customizations</a> with atuin, starship, secrets properly organized, about <a href="/en/2025/09/07/omarchy-2-0-mise-to-organize-dev-environments/">Mise for multiple languages</a>, about <a href="/en/2025/09/07/omarchy-2-0-lazyvim-lazyextras/">LazyVim and LazyExtras</a>, about <a href="/en/2025/09/09/omarchy-2-0-understanding-ssh-and-yubikeys/">SSH and Yubikeys</a>, about <a href="/en/2025/09/09/omarchy-2-0-tuis/">modern TUIs</a>. There&rsquo;s also <a href="/en/2026/01/21/omarchy-3-dual-gpu-setup-with-amd-and-nvidia/">Omarchy 3 with dual GPU AMD + NVIDIA</a> and <a href="/en/2026/01/09/omarchy-3-one-of-the-best-coding-agents-crush/">Crush</a> as a coding agent.</p>
<p>For those who don&rsquo;t know: Omarchy is plain Arch Linux with a cosmetic layer on top of Hyprland/Wayland. Pre-configured with sane defaults. I could build all of it from scratch, I&rsquo;ve done it several times in my life, but why redo work someone already did well? I install Omarchy, I stack the tweaks that are mine on top, and I have customized Arch in a fraction of the time.</p>
<p>A point I raise every time I recommend Omarchy: the documentation is excellent. There&rsquo;s an <a href="https://manuals.omamix.org/2/the-omarchy-manual"target="_blank" rel="noopener">official manual</a> covering everything from installation to theme customization, keybindings, Hyprland, Waybar, the works. If you&rsquo;re coming from Ubuntu or Fedora and are wary of Arch, this manual handles most of the doubts. If you&rsquo;ve never touched Hyprland, open it and read around. It&rsquo;s not a README dumped on GitHub, it&rsquo;s a real manual with chapters and an index.</p>
<p>The story of this article starts in a previous thread. I recently migrated my home server, swapping an old Ubuntu box for a <a href="/en/2026/03/31/migrating-my-home-server-with-claude-code/">Minisforum MS-S1 running openSUSE MicroOS configured with Claude Code</a>. That was the first serious experiment of letting Claude Code drive an entire infra migration, with containers, NFS, services, networking. It worked. And it left an idea in the air: why not use the same approach to configure a new notebook?</p>
<p>That&rsquo;s what I did. I grabbed the Thinkpad, downloaded the latest Omarchy ISO, burned it to a flash drive, installed it over Windows. From zero to working desktop in under an hour. From there on, it&rsquo;s all tweaking, which I documented as I went.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/open-lid-claude-terminal.jpg" alt="Claude Code running on the Thinkpad terminal, ready to start configuring the notebook"  loading="lazy" /></p>
<p>I&rsquo;ll detail the two layers of customization: what any notebook needs, and what&rsquo;s specific to this Thinkpad.</p>
<h2>Laptop-specific configs<span class="hx:absolute hx:-mt-20" id="laptop-specific-configs"></span>
    <a href="#laptop-specific-configs" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A notebook has problems a desktop doesn&rsquo;t: battery, suspend, lid, brightness, trackpad. Those are the parts Omarchy default doesn&rsquo;t cover the way I want.</p>
<h3>Power management with TLP<span class="hx:absolute hx:-mt-20" id="power-management-with-tlp"></span>
    <a href="#power-management-with-tlp" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Omarchy defaults to <code>power-profiles-daemon</code>. I swapped it for TLP, which gives granular control over CPU scaling, battery thresholds and dynamic profiles based on AC vs battery.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pacman -S tlp tlp-rdw
</span></span><span class="line"><span class="cl">sudo systemctl mask power-profiles-daemon.service
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now tlp.service</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The <code>mask</code> is necessary because upower pulls <code>power-profiles-daemon</code> back in if you just <code>disable</code> it.</p>
<p>Charge thresholds: 60% to start, 85% to stop. The notebook spends most of its time plugged in on the office desk, and keeping the battery at 100% 24/7 wrecks capacity over time. With 60/85, the battery spends most of its time in the healthy lithium range and still has decent usable capacity.</p>
<p>Profiles: <code>balanced</code> with <code>balance_power</code> on battery, <code>performance</code> on AC. TLP&rsquo;s <code>low-power</code> was too aggressive on the Core Ultra 5 235U, window and terminal response became noticeable. Balanced gives the best consumption/responsiveness ratio for normal use.</p>
<p>There&rsquo;s a gotcha: <code>tlp auto</code> after <code>tlp ac</code> doesn&rsquo;t always re-apply the battery profile. I wrote a small script bound to <code>Super+Ctrl+P</code> that reads the current state and calls <code>tlp bat</code> or <code>tlp ac</code> explicitly:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">profile</span><span class="o">=</span><span class="k">$(</span>cat /sys/firmware/acpi/platform_profile 2&gt;/dev/null <span class="o">||</span> <span class="nb">echo</span> unknown<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">on_ac</span><span class="o">=</span><span class="k">$(</span>cat /sys/class/power_supply/AC*/online 2&gt;/dev/null <span class="p">|</span> head -1 <span class="o">||</span> <span class="nb">echo</span> 0<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$profile</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;performance&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$on_ac</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;1&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    sudo /usr/bin/tlp auto &gt;/dev/null
</span></span><span class="line"><span class="cl">    <span class="nv">label</span><span class="o">=</span><span class="s2">&#34;Plugged in — back to AC auto&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    sudo /usr/bin/tlp bat &gt;/dev/null
</span></span><span class="line"><span class="cl">    <span class="nv">label</span><span class="o">=</span><span class="s2">&#34;On battery — normal profile&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  notify-send -t <span class="m">2000</span> <span class="s2">&#34;Performance mode: off&#34;</span> <span class="s2">&#34;</span><span class="nv">$label</span><span class="s2">&#34;</span> 2&gt;/dev/null <span class="o">||</span> <span class="nb">true</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  sudo /usr/bin/tlp ac &gt;/dev/null
</span></span><span class="line"><span class="cl">  notify-send -t <span class="m">2000</span> <span class="s2">&#34;Performance mode: on&#34;</span> <span class="s2">&#34;Forcing AC profile&#34;</span> 2&gt;/dev/null <span class="o">||</span> <span class="nb">true</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In waybar, a custom module <code>custom/perf</code> shows the current state (󰓅 PERF, 󰌪 ECO, or empty for balanced) and accepts a click to toggle. The output script is tiny:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="nv">p</span><span class="o">=</span><span class="k">$(</span>cat /sys/firmware/acpi/platform_profile 2&gt;/dev/null<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="k">case</span> <span class="s2">&#34;</span><span class="nv">$p</span><span class="s2">&#34;</span> in
</span></span><span class="line"><span class="cl">  performance<span class="o">)</span> <span class="nb">printf</span> <span class="s1">&#39;󰓅 PERF&#39;</span> <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  low-power<span class="o">)</span>   <span class="nb">printf</span> <span class="s1">&#39;󰌪 ECO&#39;</span>  <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  balanced<span class="p">|</span><span class="s2">&#34;&#34;</span><span class="o">)</span> <span class="nb">printf</span> <span class="s1">&#39;&#39;</span>       <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  *<span class="o">)</span>           <span class="nb">printf</span> <span class="s1">&#39;%s&#39;</span> <span class="s2">&#34;</span><span class="nv">$p</span><span class="s2">&#34;</span> <span class="p">;;</span>
</span></span><span class="line"><span class="cl"><span class="k">esac</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Suspend, hibernate, and the lid<span class="hx:absolute hx:-mt-20" id="suspend-hibernate-and-the-lid"></span>
    <a href="#suspend-hibernate-and-the-lid" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is what changed compared to the notebooks I used in 2015. The T14 Gen 6 closes the lid, suspends, and wakes up when you open it. No bug, no weird delay, no needing to log in again mid graphical session. Hyprlock kicks in after the suspend, accepts fingerprint or password, and brings me back to the desktop in seconds. This is the behavior Apple has had for years and that on Linux used to be an adventure. In 2026, on modern hardware with kernel 6.11+, it just works.</p>
<p>Mem sleep mode is s2idle (no deep sleep). Hibernation is enabled via a ~30 GB Btrfs swapfile and <code>resume=/dev/mapper/root resume_offset=...</code> on the Limine kernel cmdline. I rarely use it, but it&rsquo;s there.</p>
<p>Hypridle has aggressive timeouts for a laptop:</p>
<ul>
<li>2.5 min → screensaver</li>
<li>5 min → lock</li>
<li>5.5 min → DPMS off + keyboard backlight off</li>
</ul>
<p>After lock, another 5 min and the screen turns off completely. On unlock, it restores screen and keyboard brightness to the previous level. These timings are much shorter than on the desktop (20-40 min there). The battery difference over a day is measurable.</p>
<h3>Brightness and keyboard backlight<span class="hx:absolute hx:-mt-20" id="brightness-and-keyboard-backlight"></span>
    <a href="#brightness-and-keyboard-backlight" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><code>brightnessctl</code> controls both. Fn+Space toggles the keyboard backlight across three levels. Hypridle saves the current level before turning off and restores it on resume. Command I use in the script:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brightnessctl -sd <span class="s1">&#39;*::kbd_backlight&#39;</span> <span class="nb">set</span> <span class="m">0</span>    <span class="c1"># save and turn off</span>
</span></span><span class="line"><span class="cl">brightnessctl -rd <span class="s1">&#39;*::kbd_backlight&#39;</span>          <span class="c1"># restore</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Touchpad<span class="hx:absolute hx:-mt-20" id="touchpad"></span>
    <a href="#touchpad" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In <code>hypr/input.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>touchpad {
    natural_scroll = true
    clickfinger_behavior = true
    disable_while_typing = true
    scroll_factor = 0.4
}
gesture = 3, horizontal, workspace</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>clickfinger_behavior</code> swaps the 2-finger click as right-click (more comfortable than hitting the lower-right zone). <code>disable_while_typing</code> is basic palm rejection. Three-finger horizontal swipes switch workspaces, the most useful gesture in Hyprland.</p>
<h2>Thinkpad-specific configs<span class="hx:absolute hx:-mt-20" id="thinkpad-specific-configs"></span>
    <a href="#thinkpad-specific-configs" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is what&rsquo;s specific to this model. Some things modern Linux handles on its own, others need explicit configuration.</p>
<h3>Fingerprint sensor<span class="hx:absolute hx:-mt-20" id="fingerprint-sensor"></span>
    <a href="#fingerprint-sensor" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Goodix MOC <code>27c6:6594</code>, works with libfprint 1.94.9+ on kernel 6.11+. Package is <code>fprintd</code>.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pacman -S fprintd
</span></span><span class="line"><span class="cl">fprintd-enroll                        <span class="c1"># right index finger by default</span>
</span></span><span class="line"><span class="cl">fprintd-enroll -f left-index-finger   <span class="c1"># other finger</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>To make <code>sudo</code> accept fingerprint, I added <code>auth sufficient pam_fprintd.so</code> above the <code>pam_unix.so</code> line in <code>/etc/pam.d/sudo</code>. With <code>sufficient</code>, if the fingerprint passes, it authenticates directly. If it fails or I hit ESC, it falls back to the password prompt. This is genuinely worth it: dozens of times a day, <code>sudo pacman -Syu</code> or <code>sudo systemctl restart something</code>, and I just touch the sensor.</p>
<p>On hyprlock, I use hyprlock&rsquo;s native configuration, not PAM:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>auth {
    fingerprint {
        enabled = true
        ready_message = Scan fingerprint or type password
        present_message = Scanning...
        retry_delay = 250
    }
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The PAM path gives a double prompt. Configured natively, hyprlock accepts fingerprint or password, whichever succeeds first unlocks.</p>
<p>On SDDM (initial login), I kept password only. The reason: the login password unlocks the GNOME keyring, and the fingerprint can&rsquo;t provide plaintext for that. Once the keyring is unlocked, hyprlock can use fingerprint without issues.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/sudo-asks-for-fingerprinting.png" alt="sudo prompt asking for fingerprint read"  loading="lazy" /></p>
<p>In practice, sudo looks like that. Touch the sensor, authenticate, move on.</p>
<h3>Brazilian Thinkpad keyboard<span class="hx:absolute hx:-mt-20" id="brazilian-thinkpad-keyboard"></span>
    <a href="#brazilian-thinkpad-keyboard" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The Brazilian Thinkpad keyboard has an annoying quirk. The <code>/?</code> key sits where the right Ctrl would be (keycode 97), not in the traditional ABNT2 AB11 position (keycode 89). If you use the standard <code>br(abnt2)</code> layout, that key is inaccessible. It literally prints nothing.</p>
<p>The solution is the <code>br(thinkpad)</code> variant that exists in <code>/usr/share/X11/xkb/symbols/br</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>xkb_symbols &#34;thinkpad&#34; {
    include &#34;br(abnt2)&#34;
    name[Group1]=&#34;Portuguese (Brazil, IBM/Lenovo ThinkPad)&#34;;
    key &lt;RCTL&gt; { [ slash, question, degree, questiondown ] };
};</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In <code>hypr/input.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kb_layout = br
kb_variant = thinkpad
kb_model = thinkpad60</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And system-wide for TTY/X11:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo localectl set-keymap br-abnt2
</span></span><span class="line"><span class="cl">sudo localectl set-x11-keymap br thinkpad thinkpad60</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>I wrote a small Python that reads raw scancodes from <code>/dev/input/event*</code> to diagnose these quirks. Useful when a key decides not to work and you need to find out if it&rsquo;s hardware, kernel, or xkb.</p>
<h3>Input method: fcitx5<span class="hx:absolute hx:-mt-20" id="input-method-fcitx5"></span>
    <a href="#input-method-fcitx5" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Alongside the layout I install fcitx5. Input method, for those who don&rsquo;t know, is the layer that turns a key sequence into characters. It handles deadkeys (tilde for nasalization, acute accent, circumflex), composing characters that aren&rsquo;t on the keyboard (ç, Ç, uppercase accented letters), emoji support. In Qt or GTK apps, the input method also drives context menus for cedilla and accents.</p>
<p>Packages:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pacman -S --needed fcitx5 fcitx5-configtool fcitx5-gtk fcitx5-qt</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And the environment variables for toolkits to find fcitx5. I created <code>~/.config/environment.d/fcitx.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>INPUT_METHOD=fcitx
QT_IM_MODULE=fcitx
XMODIFIERS=@im=fcitx
SDL_IM_MODULE=fcitx</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>systemd&rsquo;s <code>environment.d</code> is loaded before graphical sessions, so Brave, Alacritty, VS Code and any GTK/Qt app pick it up automatically. I enable autostart in Hyprland:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>exec-once = fcitx5 -d</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Typing <code>~a</code> produces <code>ã</code>, <code>'e</code> produces <code>é</code>, <code>ç</code> works as it should in every app. On a Brazilian Thinkpad keyboard, this is the difference between typing Portuguese naturally or hunting for each character.</p>
<h3>SOF audio<span class="hx:absolute hx:-mt-20" id="sof-audio"></span>
    <a href="#sof-audio" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Realtek ALC3306/ALC287 codec via Sound Open Firmware. Without <code>sof-firmware</code>, the kernel module loads but the DSP never boots and PipeWire silently falls back to <code>auto_null</code>. Result: you think the speaker is on mute, but actually PipeWire has no device at all.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pacman -S --needed sof-firmware alsa-ucm-conf pipewire pipewire-pulse wireplumber</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you need to force SOF instead of legacy HDA:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;options snd-intel-dspcfg dsp_driver=3&#34;</span> <span class="p">|</span> sudo tee /etc/modprobe.d/alsa.conf</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Reload without reboot:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo modprobe -r snd_sof_pci_intel_mtl
</span></span><span class="line"><span class="cl">sudo modprobe snd_sof_pci_intel_mtl
</span></span><span class="line"><span class="cl">systemctl --user restart wireplumber pipewire pipewire-pulse</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Firmware updates via fwupd<span class="hx:absolute hx:-mt-20" id="firmware-updates-via-fwupd"></span>
    <a href="#firmware-updates-via-fwupd" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/bios-update.jpg" alt="Lenovo firmware update (ME Corp) running on boot"  loading="lazy" /></p>
<p><code>fwupdmgr update</code> works, but with Limine there&rsquo;s a gotcha: fwupd tries to write to <code>/boot/EFI/systemd/</code> or <code>/boot/EFI/arch/</code>, which don&rsquo;t exist. The workaround:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo mkdir -p /boot/EFI/arch
</span></span><span class="line"><span class="cl">sudo fwupdmgr update -y --no-reboot-check
</span></span><span class="line"><span class="cl">fwupdmgr get-history  <span class="c1"># should show &#34;Success&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>HiDPI / fractional scaling<span class="hx:absolute hx:-mt-20" id="hidpi--fractional-scaling"></span>
    <a href="#hidpi--fractional-scaling" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>14&quot; panel at 1920x1200, Hyprland&rsquo;s free resolution. Omarchy&rsquo;s auto 1.5x felt too chunky. I pinned it at 1.333x:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>env = GDK_SCALE,1
monitor=,preferred,auto,1.3333,vrr,2</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Effective 1440x900. GTK with <code>GDK_SCALE=1</code> renders 1:1 with Hyprland (no double magnification). VRR mode 2 only in fullscreen, because LCD panels tend to flicker on static content with VRR active.</p>
<p>Brave and Chromium flags to render well at this scale:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>--ozone-platform=wayland
--enable-features=WaylandFractionalScaleV1,UseOzonePlatform,VaapiVideoDecoder,VaapiVideoEncoder
--enable-gpu-rasterization</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>VAAPI makes a real difference on YouTube battery life.</p>
<h2>Tuning Omarchy to be mine<span class="hx:absolute hx:-mt-20" id="tuning-omarchy-to-be-mine"></span>
    <a href="#tuning-omarchy-to-be-mine" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Omarchy comes with good defaults. What I adjust stacks on top of them, without touching <code>~/.local/share/omarchy/</code> (which is clobbered by <code>omarchy-update</code>). All customization lives in <code>~/.config/</code>.</p>
<h3>Infra: Btrfs, snapshots, Snapper<span class="hx:absolute hx:-mt-20" id="infra-btrfs-snapshots-snapper"></span>
    <a href="#infra-btrfs-snapshots-snapper" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Omarchy already ships with Btrfs and separate subvolumes:</p>
<table>
  <thead>
      <tr>
          <th>Subvolume</th>
          <th>Mount</th>
          <th>In snapshots?</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>@</code></td>
          <td><code>/</code></td>
          <td>yes</td>
      </tr>
      <tr>
          <td><code>@home</code></td>
          <td><code>/home</code></td>
          <td>no</td>
      </tr>
      <tr>
          <td><code>@log</code></td>
          <td><code>/var/log</code></td>
          <td>no</td>
      </tr>
      <tr>
          <td><code>@pkg</code></td>
          <td><code>/var/cache/pacman/pkg</code></td>
          <td>no</td>
      </tr>
  </tbody>
</table>
<p><code>@home</code> separated means <code>~/.cache</code>, <code>~/.config/BraveSoftware</code>, etc. don&rsquo;t bloat root snapshots. Snapshot is for system, not profile.</p>
<p>Swap: 4 GB zram at priority 100 (hit first), plus a 30 GB swapfile at priority 0 (enables hibernation, sized to match RAM).</p>
<p>The snapshot stack: Snapper takes a snapshot before and after every <code>pacman -Syu</code> via snap-pac. <code>limine-snapper-sync</code> writes those snapshots into the Limine menu, so you can boot into a previous snapshot to roll back. If something breaks after an update, you hold a key on boot, pick the pre-update snapshot, boot read-only to verify, and if it&rsquo;s good, run <code>snapper rollback</code>.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/btrfs-snapshots-config.png" alt="Btrfs subvolumes separated so root snapshots don’t balloon with user cache"  loading="lazy" /></p>
<p>Omarchy leaves <code>snapper-cleanup.timer</code> and <code>snapper-boot.timer</code> disabled by default. I enabled both and configured retention to fit on a 1 TB SSD without blowing up:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>TIMELINE_LIMIT_HOURLY=10
TIMELINE_LIMIT_DAILY=0
TIMELINE_LIMIT_WEEKLY=1
TIMELINE_LIMIT_MONTHLY=1
NUMBER_LIMIT=50</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A detail that costs debug time if you forget: Docker and Ollama write gigabytes into <code>/var/lib/docker</code> and <code>/var/lib/ollama</code>. If that falls inside <code>@</code>, snapshots go catastrophic. Each Docker image or Ollama model downloaded triples in size. I created nested subvolumes for both, with <code>chattr +C</code> to disable CoW:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo btrfs subvolume create /var/lib/docker
</span></span><span class="line"><span class="cl">sudo chattr +C /var/lib/docker
</span></span><span class="line"><span class="cl">sudo btrfs subvolume create /var/lib/ollama</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This has to be done BEFORE Docker or Ollama writes any data. If they already have data there, you need to migrate.</p>
<h3>NFS to the Synology NAS<span class="hx:absolute hx:-mt-20" id="nfs-to-the-synology-nas"></span>
    <a href="#nfs-to-the-synology-nas" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>My Synology exposes three volumes. On the desktop, I mount them the usual way. On the notebook, it has to be more defensive. The notebook moves around, forgets networks, connects to public WiFi. Notebook&rsquo;s fstab:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>nfs4  _netdev,noauto,nofail,x-systemd.automount,x-systemd.idle-timeout=10min,x-systemd.mount-timeout=15s,noatime,nodiratime,nconnect=4,actimeo=10,soft,timeo=30,retrans=2</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Critical difference vs the desktop: <code>soft</code> with short timeouts (doesn&rsquo;t hang forever), <code>x-systemd.idle-timeout=10min</code> (auto-unmount when idle), no <code>network-online.target</code> in the require (doesn&rsquo;t slow boot). Practical result: <code>cd /mnt/gigachad</code> at home mounts lazily, away from home it fails fast without locking the shell.</p>
<p>Another important detail: my user on the notebook has UID 1026, which matches the share permissions on the Synology. Linux defaults to creating users at 1000, the Synology enforces identity via UID on the wire. If the UIDs don&rsquo;t match, you can&rsquo;t read the files, or worse, you write as nobody. I ran the <code>usermod</code>/<code>groupmod</code> from a TTY (with the user logged out) to remap the user to 1026/1026 and did <code>chown -R</code> on <code>/home</code>.</p>
<h3>Public WiFi hardening<span class="hx:absolute hx:-mt-20" id="public-wifi-hardening"></span>
    <a href="#public-wifi-hardening" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The notebook will leave the house. In an airport or café, I don&rsquo;t want to announce hostname, don&rsquo;t want a trackable MAC, don&rsquo;t want a service listening on an open port.</p>
<p><code>/etc/NetworkManager/conf.d/00-macrandomize.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[device]
wifi.scan-rand-mac-address=yes

[connection]
wifi.cloned-mac-address=stable
ethernet.cloned-mac-address=stable
connection.stable-id=${CONNECTION}/${BOOT}

ipv6.ip6-privacy=2
ipv6.addr-gen-mode=stable-privacy</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Random MAC per scan (passive anti-fingerprinting). Stable cloned MAC per SSID (captive portals don&rsquo;t re-prompt you every time) but different MACs between networks. IPv6 with temporary addresses and interface ID derived from a stable secret, not from the MAC (no EUI-64 leak).</p>
<p><code>/etc/systemd/resolved.conf.d/hardening.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[Resolve]
LLMNR=no
MulticastDNS=no</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Kills hostname broadcast on LLMNR and mDNS. I also disable avahi:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl disable --now avahi-daemon.service avahi-daemon.socket</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>UFW firewall:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo ufw default deny incoming
</span></span><span class="line"><span class="cl">sudo ufw default allow outgoing
</span></span><span class="line"><span class="cl">sudo ufw --force enable</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>SSH server is already off by default on Omarchy. Nothing listening. Even if the firewall leaks, there&rsquo;s no surface.</p>
<p>All of it rolls into a single initial setup ritual.</p>
<h3>SSH agent persistence (keyring + keychain)<span class="hx:absolute hx:-mt-20" id="ssh-agent-persistence-keyring--keychain"></span>
    <a href="#ssh-agent-persistence-keyring--keychain" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If you use SSH seriously, you want to type the passphrase once per boot and have <code>ssh</code>, <code>git push</code>, <code>scp</code> stay silent for the rest of the day. On current Arch there&rsquo;s a catch: <code>gnome-keyring</code> 50 dropped its SSH component, and the replacement (<code>gcr-ssh-agent</code>) is a plain in-memory agent with no passphrase persistence. The &ldquo;remember this key&rdquo; checkbox you saw in old guides simply doesn&rsquo;t exist anymore.</p>
<p>The combination that works has three pieces:</p>
<ol>
<li><code>gcr-ssh-agent.socket</code> managed by systemd serves <code>SSH_AUTH_SOCK</code> at <code>$XDG_RUNTIME_DIR/gcr/ssh</code></li>
<li><code>pam_gnome_keyring</code> on SDDM login unlocks the keyring with the login password (used by GUI apps like Brave, not by SSH)</li>
<li><code>keychain</code> (wrapper) keeps an <code>ssh-agent</code> alive across logouts, overrides <code>SSH_AUTH_SOCK</code> to point at that persistent agent, and caches the PID in <code>~/.keychain/&lt;host&gt;-sh</code></li>
</ol>
<p>First, install the keyring:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pacman -S --needed gnome-keyring seahorse keychain</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Then edit <code>/etc/pam.d/sddm</code> so the login password unlocks the keyring automatically. Add at the top:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>-auth      optional    pam_gnome_keyring.so</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And at the end:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>-session   optional    pam_gnome_keyring.so    auto_start</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The leading hyphen (<code>-auth</code>, <code>-session</code>) marks it optional, so if the module fails to load, login doesn&rsquo;t break.</p>
<p>Pin <code>SSH_AUTH_SOCK</code> for graphical and TTY sessions via <code>~/.config/environment.d/ssh-agent.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>SSH_AUTH_SOCK=%t/gcr/ssh</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>%t</code> resolves to <code>$XDG_RUNTIME_DIR</code>, something like <code>/run/user/1026</code>. Enable the gcr socket and user linger (so user daemons survive logout):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl --user <span class="nb">enable</span> --now gcr-ssh-agent.socket
</span></span><span class="line"><span class="cl">sudo loginctl enable-linger <span class="nv">$USER</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Finally, in <code>~/.config/bash/init.sh</code>, keychain starts or reuses the agent:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">if</span> <span class="nb">command</span> -v keychain <span class="p">&amp;</span>&gt;/dev/null <span class="o">&amp;&amp;</span> <span class="o">[[</span> -r ~/.ssh/id_ed25519 <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">eval</span> <span class="s2">&#34;</span><span class="k">$(</span>keychain --eval --quiet ~/.ssh/id_ed25519<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>--eval</code> emits shell assignments (<code>SSH_AUTH_SOCK</code>, <code>SSH_AGENT_PID</code>) pointing to keychain&rsquo;s agent, overriding the gcr path set by environment.d. <code>--quiet</code> silences the banner once the key is loaded.</p>
<p>Flow in practice: boot → first terminal → keychain prompts for the passphrase once → key stays loaded. Logout → login again → new shells reattach to the same agent (via <code>~/.keychain/&lt;host&gt;-sh</code>) → zero prompts. <code>ssh-add -l</code> confirms the key is there, <code>echo $SSH_AUTH_SOCK</code> confirms it&rsquo;s at keychain&rsquo;s path.</p>
<p>When you&rsquo;ll re-enter the passphrase: reboot, <code>ssh-add -D</code>, or <code>keychain --clear</code>. In normal use, once a day.</p>
<h3>Bash instead of ZSH<span class="hx:absolute hx:-mt-20" id="bash-instead-of-zsh"></span>
    <a href="#bash-instead-of-zsh" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>On the desktop I use ZSH. On the notebook I went with Bash to align with Omarchy&rsquo;s default, without having to maintain a parallel stack of modular layers. <code>~/.bashrc</code> is a symlink to <code>~/.config/bash/bashrc</code>, which sources Omarchy&rsquo;s defaults first and then stacks my customizations:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">source</span> ~/.local/share/omarchy/default/bash/rc
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.config/bash/envs.sh
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.config/bash/aliases.sh
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.config/bash/mounts.sh
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.config/bash/init.sh
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.config/bash/secrets    <span class="c1"># gitignored, chmod 600</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>envs.sh</code> has what&rsquo;s mine: OpenRouter base URL, Ollama pointing to the LAN GPU box (192.168.0.14), AWS region, Hugo analytics, zoxide and SSH agent configs. <code>aliases.sh</code> has TLP shortcuts, an alias for <code>shell-gpt</code> via Docker, and functions to harden PATH when running <code>makepkg</code> or <code>yay</code> (prevents binary injection via a malicious user config).</p>
<p><code>init.sh</code> does the integration work. Atuin with a manual bind for Ctrl-R (so the up arrow keeps bash&rsquo;s default history-search, which I use more). Keychain loading <code>~/.ssh/id_ed25519</code> once per boot and reusing it across shells (no need to re-authenticate SSH every time). Blesh if installed (ZSH-style autosuggestions for Bash). And a function that has PROMPT_COMMAND set the window title to the current pwd and the running command:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">__title_idle<span class="o">()</span> <span class="o">{</span> <span class="nb">printf</span> <span class="s1">&#39;\033]2;%s\007&#39;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">PWD</span><span class="p">/#</span><span class="nv">$HOME</span><span class="p">/~</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">__title_busy<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">cmd</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">BASH_COMMAND</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$cmd</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;__title_&#34;</span>* <span class="o">||</span> <span class="s2">&#34;</span><span class="nv">$cmd</span><span class="s2">&#34;</span> <span class="o">==</span> *<span class="s2">&#34;PROMPT_COMMAND&#34;</span>* <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">return</span>
</span></span><span class="line"><span class="cl">  <span class="nb">printf</span> <span class="s1">&#39;\033]2;%s — %s\007&#39;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">PWD</span><span class="p">/#</span><span class="nv">$HOME</span><span class="p">/~</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$cmd</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="si">${</span><span class="nv">PROMPT_COMMAND</span><span class="p">-</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nv">PROMPT_COMMAND</span><span class="o">=</span><span class="s2">&#34;__title_idle; </span><span class="si">${</span><span class="nv">PROMPT_COMMAND</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  <span class="nv">PROMPT_COMMAND</span><span class="o">=</span><span class="s2">&#34;__title_idle&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="nb">trap</span> <span class="s1">&#39;__title_busy&#39;</span> DEBUG</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Idle, the title shows just the pwd. With a command running, <code>trap DEBUG</code> catches the <code>BASH_COMMAND</code> and updates the title. Waybar&rsquo;s <code>hyprland/window</code> picks that up and displays it.</p>
<p>For the modern Rust toolbelt, the list:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pacman -S --needed <span class="se">\
</span></span></span><span class="line"><span class="cl">  eza bat fd ripgrep sd git-delta dust procs bottom duf tokei hyperfine <span class="se">\
</span></span></span><span class="line"><span class="cl">  zoxide atuin tldr starship</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>eza</code> replaces <code>ls</code>. <code>bat</code> replaces <code>cat</code> with syntax highlighting. <code>fd</code> replaces <code>find</code>. <code>ripgrep</code> replaces <code>grep</code>. <code>sd</code> replaces <code>sed</code>. <code>delta</code> plugs into git for colored side-by-side diff. <code>dust</code> for visual <code>du</code>. <code>procs</code> for <code>ps</code>. <code>btm</code> (bottom) for <code>top</code>. <code>duf</code> for <code>df</code>. <code>tokei</code> counts lines of code. <code>hyperfine</code> for command benchmarking. <code>zoxide</code> is <code>cd</code> with memory (very useful). <code>atuin</code> is shell history with encrypted sync (I point it to my home server via <code>sync_address = &quot;http://192.168.0.90:8888&quot;</code>). <code>starship</code> is the prompt. <code>tldr</code> is a man page in 10 lines.</p>
<p>The <code>atuin key</code> has to be backed up offline. If you lose it, you lose the encrypted history. I saved mine in my self-hosted Bitwarden (Vaultwarden), documented in the <a href="/en/2025/09/10/omarchy-2-0-bitwarden-self-hosted-vaultwarden/">Bitwarden self-hosted article</a>.</p>
<p>Git pipes diff through delta:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[core]        pager = delta
[interactive] diffFilter = delta --color-only
[delta]       navigate side-by-side line-numbers hyperlinks, light=false
[merge]       conflictstyle = zdiff3</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Hyprland and Waybar<span class="hx:absolute hx:-mt-20" id="hyprland-and-waybar"></span>
    <a href="#hyprland-and-waybar" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Visual and UX customizations on top of the default.</p>
<p>In <code>hypr/looknfeel.conf</code>, smaller gaps (2/5 vs the default 5/10), slide animation between workspaces, VFR on (reduces consumption when the screen is static), <code>allow_session_lock_restore</code> (if hyprlock crashes, it goes back to the lock screen instead of dumping you on the desktop).</p>
<p>In <code>hypr/bindings.conf</code>:</p>
<table>
  <thead>
      <tr>
          <th>Binding</th>
          <th>Action</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>Super+B</code></td>
          <td>Brave</td>
      </tr>
      <tr>
          <td><code>Super+L</code></td>
          <td>Lock screen (default was layout toggle)</td>
      </tr>
      <tr>
          <td><code>Super+Ctrl+L</code></td>
          <td>Layout toggle (moved here)</td>
      </tr>
      <tr>
          <td><code>Super+Ctrl+P</code></td>
          <td>Toggle TLP perf mode</td>
      </tr>
  </tbody>
</table>
<p>On Waybar, three custom modules I recommend:</p>
<p><strong>Window title</strong>: the <code>hyprland/window</code> module shows what&rsquo;s focused. In bash, PROMPT_COMMAND updates the title to something like &ldquo;~/Projects/blog — hugo server&rdquo;. So in waybar the directory and running command show up, without me needing to look at the terminal.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/hypr---window-title-on-waybar.png" alt="Waybar showing workspaces on the left and the focused window title with the pwd and the running command"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/hypr---another-window-title-on-waybar.png" alt="Another example of the window title on waybar, this time showing “Claude Code”"  loading="lazy" /></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonc" data-lang="jsonc"><span class="line"><span class="cl"><span class="s2">&#34;hyprland/window&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;format&#34;</span><span class="p">:</span> <span class="s2">&#34;{title}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;max-length&#34;</span><span class="p">:</span> <span class="mi">50</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;rewrite&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;(.{47}).+&#34;</span><span class="p">:</span> <span class="s2">&#34;$1…&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>Perf mode</strong>: <code>custom/perf</code> runs a script that reads TLP state and shows an icon:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonc" data-lang="jsonc"><span class="line"><span class="cl"><span class="s2">&#34;custom/perf&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;exec&#34;</span><span class="p">:</span> <span class="s2">&#34;~/.config/scripts/perf-waybar.sh&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;interval&#34;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;format&#34;</span><span class="p">:</span> <span class="s2">&#34;{}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;on-click&#34;</span><span class="p">:</span> <span class="s2">&#34;~/.config/scripts/perf-toggle.sh&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Empty for balanced, 󰓅 PERF for performance, 󰌪 ECO for low-power. Click toggles. Useful to quickly see what mode the machine is in.</p>
<p><strong>Claudebar</strong>: I integrated <a href="https://github.com/alfredopiquet/claudebar"target="_blank" rel="noopener">claudebar</a> as a custom module that shows the Claude Code session usage % and extra spend inline in waybar. No need to open another window.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/hypr---power-profile-and-claudebar-next-to-traybar.png" alt="Claudebar showing 9%, 1h16m remaining and $1120.74 extra spend, followed by the PERF indicator and the tray"  loading="lazy" /></p>
<p>The clock:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonc" data-lang="jsonc"><span class="line"><span class="cl"><span class="s2">&#34;clock&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;format&#34;</span><span class="p">:</span> <span class="s2">&#34;{:L%A %d %B - %H:%M}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;format-alt&#34;</span><span class="p">:</span> <span class="s2">&#34;{:L%A W%V %Y - %H:%M}&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Shows &ldquo;Thursday 17 April - 14:32&rdquo; by default, click toggles to &ldquo;Thursday W16 2026 - 14:32&rdquo; with the ISO week number, useful for planning articles and tasks.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/hypr---better-date-time-on-waybar-center.png" alt="Waybar clock in the center with date and time"  loading="lazy" /></p>
<h3>Self-hosted Atuin<span class="hx:absolute hx:-mt-20" id="self-hosted-atuin"></span>
    <a href="#self-hosted-atuin" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Atuin runs on my home server at 192.168.0.90:8888. I made a separate account for the notebook (<code>akitaonrails-thinkpad</code>), I don&rsquo;t mix history with the desktop on purpose. <code>atuin sync</code> runs every 5 minutes and all history is encrypted before going to the server. The server doesn&rsquo;t see the commands, only encrypted bytes.</p>
<h2>Conclusion: picking a notebook for Linux<span class="hx:absolute hx:-mt-20" id="conclusion-picking-a-notebook-for-linux"></span>
    <a href="#conclusion-picking-a-notebook-for-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>One thing I learned on this journey: picking a notebook to run Linux comfortably is not trivial. You have to check the <a href="https://wiki.archlinux.org/title/Laptop"target="_blank" rel="noopener">ArchWiki</a> for your specific model before buying. The <a href="https://wiki.archlinux.org/title/Lenovo_ThinkPad_T14s_%28AMD%29_Gen_6"target="_blank" rel="noopener">T14s Gen 6 AMD page</a>, for example, catalogs every hardware quirk and how to work around it. Without that reference, you discover the problems during install.</p>
<p>Rule I follow: never buy a freshly-released model. Let 6 to 12 months go by after launch. That gives the community time to iron out bugs, drivers to land mainline, the ArchWiki to have a decent page, libfprint to include the fingerprint sensor, and the kernel to cover WiFi and the IR camera. Buying on day one means signing up as a beta tester.</p>
<p>Brands with a decent Linux track record: Lenovo (especially Thinkpad, they ship Ubuntu and Fedora pre-loaded), Dell (the XPS and Latitude lines), Asus (Zenbook and ROG have reasonable support), Framework (built for Linux from the factory). Any recent Mac is cut off from Linux mainline or runs through Asahi with caveats.</p>
<p>The Thinkpad T14 Gen 6 isn&rsquo;t the prettiest notebook I could have bought. But it&rsquo;s rugged, it has the right ports, the fingerprint sensor works, and the plastic shell lets it fall, get scratched, travel in a backpack without making me nervous. To serve as a remote debug companion, it&rsquo;s what I needed. If that&rsquo;s your use case too, I recommend it. If you want OLED and metal, go Zenbook or T14s. Every premium comes with a compromise, no Linux notebook in 2026 is perfect.</p>
<p>Everything I described here is versioned in a private repo of mine. If I need to reinstall tomorrow, I run half a dozen steps in order and I&rsquo;m back at the same place. That&rsquo;s the whole point of keeping config in Git: notebook is commodity, config is mine.</p>
]]></content:encoded><category>omarchy</category><category>linux</category><category>hardware</category><category>reviews</category></item><item><title>Seedance 2.0 Is Finally Out: First Impressions</title><link>https://akitaonrails.github.io/en/2026/04/15/seedance-2-0-public-launch-first-impressions/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/04/15/seedance-2-0-public-launch-first-impressions/</guid><pubDate>Wed, 15 Apr 2026 10:00:00 -0300</pubDate><description>&lt;p&gt;ByteDance opened up the public release of &lt;a href="https://seed.bytedance.com/en/blog/official-launch-of-seedance-2-0"target="_blank" rel="noopener"&gt;Seedance 2.0&lt;/a&gt; today, April 15. They had promised the model late last year, but access stayed locked to commercial partners for months, with delay after delay. As of today, anyone can try it.&lt;/p&gt;
&lt;p&gt;The pitch is strong. It&amp;rsquo;s a multimodal video generation model that takes text, up to 9 reference images, 3 reference video clips, and 3 audio tracks all in the same call. On top of that, it produces up to 15 seconds of output with native synced audio (dialogue, sound effects, ambient music), supports video extension, surgical editing of characters and objects, and prompt-driven camera planning. ByteDance is pitching it at commercial advertising, explainer videos, film production, e-commerce, and gaming. Translation: for content creators trying to climb out of the &amp;ldquo;random meme&amp;rdquo; tier and start producing things that look professional.&lt;/p&gt;</description><content:encoded><![CDATA[<p>ByteDance opened up the public release of <a href="https://seed.bytedance.com/en/blog/official-launch-of-seedance-2-0"target="_blank" rel="noopener">Seedance 2.0</a> today, April 15. They had promised the model late last year, but access stayed locked to commercial partners for months, with delay after delay. As of today, anyone can try it.</p>
<p>The pitch is strong. It&rsquo;s a multimodal video generation model that takes text, up to 9 reference images, 3 reference video clips, and 3 audio tracks all in the same call. On top of that, it produces up to 15 seconds of output with native synced audio (dialogue, sound effects, ambient music), supports video extension, surgical editing of characters and objects, and prompt-driven camera planning. ByteDance is pitching it at commercial advertising, explainer videos, film production, e-commerce, and gaming. Translation: for content creators trying to climb out of the &ldquo;random meme&rdquo; tier and start producing things that look professional.</p>
<p>If you want a quick visual tour before going further, here&rsquo;s a great walkthrough from Stefan, of the <a href="https://www.youtube.com/channel/UCRW08KcTVjXEmBzBsVl7XjA"target="_blank" rel="noopener">Stefan 3D AI Lab</a> channel, which I already plugged in the <a href="/en/2026/01/23/ai-3d-can-you-actually-model-3d-with-prompts-now/">post on AI 3D modeling</a>:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/fv9vA6RCNHU"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>The main interface is Dreamina&rsquo;s &ldquo;Photo Studio,&rdquo; where you pick the generation mode and stack your references. So far I&rsquo;ve only tested the standard multi-reference mode.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/studio.png" alt="The Seedance 2.0 Photo Studio interface, with a reference upload panel and prompt configuration"  loading="lazy" /></p>
<h2>Test 1: lip sync with podcast audio<span class="hx:absolute hx:-mt-20" id="test-1-lip-sync-with-podcast-audio"></span>
    <a href="#test-1-lip-sync-with-podcast-audio" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>My first test was simple. I grabbed a few seconds of the opening bumper from my podcast <a href="/en/tags/the-m-akita-chronicles/">The M.Akita Chronicles</a>, which is generated by AI through an <a href="/en/2026/04/09/how-elevenlabs-was-not-killed-by-qwen3-tts/">ElevenLabs v3 pipeline</a>, passed in my new anime-style avatar as a reference image, and asked Seedance to make the character lip-sync and gesture along with that audio.</p>
<div style="max-width: 100%; margin: 1em 0;">
  <video controls playsinline style="width: 100%; border-radius: 8px;">
    <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/test1.mp4" type="video/mp4">
  </video>
  <em>Anime avatar lip-syncing to the M.Akita Chronicles bumper</em>
</div>
<p>The result is decent. All of that came from a single still image. The lip sync follows the speech reasonably well, the expression has some life to it, and the hand gesture lands at the right beat. But this is a YouTube short or Instagram clip at best. Not the kind of animation you&rsquo;d open a serious video with.</p>
<h2>The feature that actually matters: video reference<span class="hx:absolute hx:-mt-20" id="the-feature-that-actually-matters-video-reference"></span>
    <a href="#the-feature-that-actually-matters-video-reference" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>And here&rsquo;s what I think is the only real use of these models for actual work: feeding in <strong>video as a reference</strong>. Text alone is never going to be precise enough. You can describe a scene with whatever vocabulary you want, swap adjectives, specify lens, angle, framing, and every generation will still hand you back something subtly different. It&rsquo;s a slot machine. Great for memes, terrible for production.</p>
<p>Most amateurs see one random clip that comes out nice on the first try and go straight to flooding their X and Instagram feeds with it. That&rsquo;s not control. That&rsquo;s luck. For film, for ads, for show openers, for anything where the next shot has to match the previous one, you have to be able to tell the model: &ldquo;do exactly this motion, with exactly this camera.&rdquo; That&rsquo;s where reference video changes the game, because it replaces hours of 3D modeling, rigging, animation, and rendering that would cost a fortune in a studio.</p>
<p>For my test, I recycled the model I&rsquo;d already generated in the <a href="/en/2026/01/23/ai-3d-can-you-actually-model-3d-with-prompts-now/">post on AI 3D with Hunyuan and Nano Banana</a>, where I tested how far you can take 3D modeling from a prompt and even hit &ldquo;print&rdquo; on the result. But for something more cinematic I wanted a model with a proper animation cycle. So I grabbed the <a href="https://sketchfab.com/3d-models/darth-talon-4338105e09704f359c6afcab7e5fce10"target="_blank" rel="noopener">Darth Talon</a> model from Sketchfab, which has a short, well-made motion sequence, just because it looked cool.</p>
<p>I imported the FBX into Blender, set up a basic camera and lighting, and did a quick Cycles render. Nothing pretty about it, just a sketch to feed in as motion and framing reference.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/blender.png" alt="The Darth Talon model imported into Blender with camera and lighting set up for a rough Cycles render"  loading="lazy" /></p>
<div style="max-width: 100%; margin: 1em 0;">
  <video controls playsinline style="width: 100%; border-radius: 8px;">
    <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/blender-render.mp4" type="video/mp4">
  </video>
  <em>The quick Cycles render that's about to become motion reference</em>
</div>
<p>I uploaded that render to Seedance as a video reference along with three t-pose images of the character (front, back, left side) for visual consistency:</p>
<div style="display: flex; gap: 8px; flex-wrap: wrap; margin: 1em 0; justify-content: center;">
  <img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/front.jpg" alt="Front t-pose" style="flex: 1 1 30%; min-width: 140px; max-width: 32%; border-radius: 8px;" />
  <img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/back.jpg" alt="Back t-pose" style="flex: 1 1 30%; min-width: 140px; max-width: 32%; border-radius: 8px;" />
  <img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/left.jpg" alt="Left side t-pose" style="flex: 1 1 30%; min-width: 140px; max-width: 32%; border-radius: 8px;" />
</div>
<p>First attempt, with a generic prompt:</p>
<div style="max-width: 100%; margin: 1em 0;">
  <video controls playsinline style="width: 100%; border-radius: 8px;">
    <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/test2.mp4" type="video/mp4">
  </video>
  <em>First generation: the character stayed consistent, but the motion and camera missed the reference</em>
</div>
<p>Not bad. The character keeps its identity, the lighting got a serious upgrade over my raw render, and the clip is coherent. But the motion and camera position drifted noticeably from the reference. The model basically read it as &ldquo;there&rsquo;s a character here that looks like that doing something like that,&rdquo; and improvised the rest.</p>
<p>Then I remembered Stefan&rsquo;s tutorial: the trick is to literally drop something like <code>Follow exact motion and camera from reference video</code> into the prompt. Tried again:</p>
<div style="max-width: 100%; margin: 1em 0;">
  <video controls playsinline style="width: 100%; border-radius: 8px;">
    <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/test3.mp4" type="video/mp4">
  </video>
  <em>Second generation: explicit instruction to follow the reference's motion and camera</em>
</div>
<p>A lot better. It&rsquo;s clearly following the reference choreography, the camera angle matches in several beats, and the character looks more polished than anything I could hand-render in a quick Cycles sketch. But it&rsquo;s still not a frame-for-frame match. In some shots the model is still taking liberties with timing, position, and framing. There are other prompting tricks and parameters to play with, but I stopped here to write this up while the launch is still fresh.</p>
<h2>ComfyUI, Runway, and the rest of the ecosystem<span class="hx:absolute hx:-mt-20" id="comfyui-runway-and-the-rest-of-the-ecosystem"></span>
    <a href="#comfyui-runway-and-the-rest-of-the-ecosystem" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Seedance 2.0 also already shipped <a href="https://blog.comfy.org/p/seedance-20-is-now-available-in-comfyui"target="_blank" rel="noopener">as a ComfyUI cloud node</a>, which is great for anyone who wants to drop the model into a bigger pipeline without bouncing between web UIs. If you&rsquo;ve already got an image-gen workflow built in ComfyUI, this is genuinely handy.</p>
<p>And for anyone who wants volume without doing the per-generation credit math, Runway is offering Seedance 2.0 as part of their <a href="https://www.mindstudio.ai/blog/seedance-2-0-runway-unlimited-plan"target="_blank" rel="noopener">Unlimited plan</a>, which runs around $76/month annual or $95/month monthly. &ldquo;Unlimited&rdquo; means no per-generation charge, but you do trade for lower queue priority at peak hours and an output storage cap. If you iterate a lot, the subscription model makes way more sense than buying credits one at a time.</p>
<h2>The price question<span class="hx:absolute hx:-mt-20" id="the-price-question"></span>
    <a href="#the-price-question" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Seedance&rsquo;s own pricing is credit-based. The Standard plan is $24.90/month annual or $49.90/month monthly, and gets you 2,500 credits a month. Every 10 seconds of generated video burns 6 credits.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/15/seedance/pricing.png" alt="Seedance 2.0 pricing table, with plans from Standard up to the top tier and per-second credit costs"  loading="lazy" /></p>
<p>The math is brutal. With 2,500 credits on Standard, you get about 416 ten-second generations a month. Sounds like a lot, until you remember my own test: it took at least two tries to get close to what I wanted, and even then it wasn&rsquo;t a one-to-one match. In practice, plan on 3 to 5 regenerations per usable scene. That drops your real number to something like 80 to 140 short clips per month. Maybe enough for a solo creator doing short-form content. Nowhere near enough for production that needs minutes of polished, continuous video.</p>
<p>The higher tiers scale credits roughly linearly, but the same problem holds: at today&rsquo;s price, Seedance 2.0 is still a tool for short clips and well-made memes, or for pre-vis sketches before you send the real thing to an actual studio. It doesn&rsquo;t replace continuous professional production. For that use case, Runway with Unlimited is honestly the cheaper option in practice. <a href="https://seedance2.ai/pricing"target="_blank" rel="noopener">Full pricing table here</a>.</p>
<h2>What nobody wants to talk about: restrictions and deepfakes<span class="hx:absolute hx:-mt-20" id="what-nobody-wants-to-talk-about-restrictions-and-deepfakes"></span>
    <a href="#what-nobody-wants-to-talk-about-restrictions-and-deepfakes" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Two things I don&rsquo;t want to leave out. First: Seedance 2.0 took its sweet time getting to the public partly because ByteDance is being a lot more conservative than the competition when it comes to moderation. The model blocks generation that uses real people as reference (celebrities, politicians, public figures) and filters strong-IP characters from Disney, Marvel/DC, Nintendo, and registered trademarks generally. If you want the details of what passes, what doesn&rsquo;t, and why ByteDance went that way, <a href="https://www.mindstudio.ai/blog/seedance-2-0-content-restrictions-workarounds"target="_blank" rel="noopener">this MindStudio piece breaks it down</a>. It makes sense: ByteDance operates across dozens of jurisdictions, lives under heavy regulatory scrutiny worldwide, and has the elephant in the room of Hollywood pushing the entire industry with copyright suits over training data and generation. The release got delayed and arrived with heavier filters precisely because of that.</p>
<p>Second point, and this is the one that matters more: for you, the average reader, the message is that <strong>deepfakes are no longer hypothetical</strong>. Photos on the internet stopped counting as proof a long time ago, because image models like Nano Banana, Hunyuan, and the rest deliver realism that fools anyone. Now video sits in the same bucket. What we just saw above, where a couple of references and two prompts get you a coherent animated scene in minutes, is just the start. The average person, with no training, isn&rsquo;t going to be able to tell generated video from real video at a glance anymore. And Seedance&rsquo;s filters only block above-board, legitimate use. Anyone with bad intent will always find a path through open source models, workarounds, or less strict platforms. That&rsquo;s the world we&rsquo;re in now, and the sooner we collectively accept it, the better off we are.</p>
<h2>No, this doesn&rsquo;t replace real artists<span class="hx:absolute hx:-mt-20" id="no-this-doesnt-replace-real-artists"></span>
    <a href="#no-this-doesnt-replace-real-artists" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Let me cut off the usual speech before it shows up in the comments. No, Seedance doesn&rsquo;t replace VFX artists, directors, editors, none of that. This is a hammer. The tool is the same for everybody. What comes out the other end depends entirely on who&rsquo;s swinging it.</p>
<p>Anyone who&rsquo;s actually studied framing, pacing, continuity, art direction, editing, color, composition is going to use Seedance to 10x the work they were already capable of delivering. Anyone who hasn&rsquo;t studied any of that is going to crank out the same Instagram meme they were already cranking out, just 10x faster. <strong>The AI reflects who you are.</strong> If you&rsquo;re a real artist, the tool multiplies you. If you only think you are, the tool multiplies the mediocrity. There&rsquo;s no miracle here.</p>
<p>This is exactly why the whole &ldquo;now everyone can be a film director with AI&rdquo; wave is the same song we&rsquo;ve heard with the digital camera, with Photoshop, with After Effects, with Premiere, and with every powerful tool that&rsquo;s shown up in the last 30 years. The technical bar drops. The bar of taste, reference, and study sits exactly where it always has. Good people get more productive. Everyone else still needs to hire real ones to do real work.</p>
<h2>Where this goes next<span class="hx:absolute hx:-mt-20" id="where-this-goes-next"></span>
    <a href="#where-this-goes-next" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Seedance 2.0 is clearly a step in the right direction, but it&rsquo;s not the tool that replaces professional production yet. Text alone stays imprecise, video reference helps but still drifts, and the price per minute of polished output is high relative to what you actually get. The good news is that this story always rhymes: Google has Veo, Runway has its own model and is also reselling Seedance, Kling and Hailuo from China keep pushing, and the open source models are gaining ground. OpenAI had Sora, but they shut it down. As competition tightens, quality goes up and price comes down. A year or two from now we&rsquo;ll be looking back at this post and finding what I just showed pretty quaint. Same as it ever was.</p>
]]></content:encoded><category>generative-media</category><category>reviews</category><category>security</category></item><item><title>How ElevenLabs Was Not Killed by Qwen3 TTS</title><link>https://akitaonrails.github.io/en/2026/04/09/how-elevenlabs-was-not-killed-by-qwen3-tts/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/04/09/how-elevenlabs-was-not-killed-by-qwen3-tts/</guid><pubDate>Thu, 09 Apr 2026 08:30:00 -0300</pubDate><description>&lt;p&gt;&lt;strong&gt;TL;DR — Listen to this and keep reading:&lt;/strong&gt;&lt;/p&gt;
&lt;audio controls preload="metadata" style="width: 100%; max-width: 640px;"&gt;
&lt;source src="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3" type="audio/mpeg"&gt;
Your browser doesn't support the audio element. &lt;a href="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3"&gt;Download the mp3 here.&lt;/a&gt;
&lt;/audio&gt;
&lt;p&gt;That&amp;rsquo;s the April 6th episode of the &lt;a href="https://akitaonrails.github.io/en/tags/the-m-akita-chronicles/"&gt;The M.Akita Chronicles&lt;/a&gt; podcast, already generated with the new ElevenLabs v3 pipeline. Subscribe to the show on &lt;a href="https://open.spotify.com/show/7MzG2UB7IAkC3GAwEXEIVD"target="_blank" rel="noopener"&gt;Spotify&lt;/a&gt; so you don&amp;rsquo;t miss new episodes like this one.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;When Qwen3 TTS dropped, back around January this year, everyone on Twitter/X and on the AI newsletter circuit was shouting &amp;ldquo;ElevenLabs killer&amp;rdquo;. There&amp;rsquo;s a &lt;a href="https://medium.com/@warpie/qwen3-tts-is-the-first-real-open-source-threat-to-elevenlabs-56ba200ab5ee"target="_blank" rel="noopener"&gt;Medium piece&lt;/a&gt; claiming it&amp;rsquo;s the first real open source threat to ElevenLabs. There&amp;rsquo;s a &lt;a href="https://byteiota.com/qwen3-tts-3-second-voice-cloning-beats-elevenlabs/"target="_blank" rel="noopener"&gt;post on byteiota&lt;/a&gt; saying 3-second voice cloning beats ElevenLabs. There&amp;rsquo;s an &lt;a href="https://www.analyticsvidhya.com/blog/2025/12/qwen3-tts-flash-review/"target="_blank" rel="noopener"&gt;analysis on Analytics Vidhya&lt;/a&gt; calling it the most realistic open source TTS released so far. The enthusiast internet consensus was: we finally have open source that can go toe-to-toe with ElevenLabs, the tables have turned, it&amp;rsquo;s just a matter of time.&lt;/p&gt;</description><content:encoded><![CDATA[<p><strong>TL;DR — Listen to this and keep reading:</strong></p>
<audio controls preload="metadata" style="width: 100%; max-width: 640px;">
  <source src="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3" type="audio/mpeg">
  Your browser doesn't support the audio element. <a href="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3">Download the mp3 here.</a>
</audio>
<p>That&rsquo;s the April 6th episode of the <a href="/en/tags/the-m-akita-chronicles/">The M.Akita Chronicles</a> podcast, already generated with the new ElevenLabs v3 pipeline. Subscribe to the show on <a href="https://open.spotify.com/show/7MzG2UB7IAkC3GAwEXEIVD"target="_blank" rel="noopener">Spotify</a> so you don&rsquo;t miss new episodes like this one.</p>
<hr>
<p>When Qwen3 TTS dropped, back around January this year, everyone on Twitter/X and on the AI newsletter circuit was shouting &ldquo;ElevenLabs killer&rdquo;. There&rsquo;s a <a href="https://medium.com/@warpie/qwen3-tts-is-the-first-real-open-source-threat-to-elevenlabs-56ba200ab5ee"target="_blank" rel="noopener">Medium piece</a> claiming it&rsquo;s the first real open source threat to ElevenLabs. There&rsquo;s a <a href="https://byteiota.com/qwen3-tts-3-second-voice-cloning-beats-elevenlabs/"target="_blank" rel="noopener">post on byteiota</a> saying 3-second voice cloning beats ElevenLabs. There&rsquo;s an <a href="https://www.analyticsvidhya.com/blog/2025/12/qwen3-tts-flash-review/"target="_blank" rel="noopener">analysis on Analytics Vidhya</a> calling it the most realistic open source TTS released so far. The enthusiast internet consensus was: we finally have open source that can go toe-to-toe with ElevenLabs, the tables have turned, it&rsquo;s just a matter of time.</p>
<p>I decided to test it on my own production pipeline, as usual. I built out a whole pipeline on top of Qwen3 TTS 1.7B to generate the weekly podcast for <a href="/en/tags/the-m-akita-chronicles/">The M.Akita Chronicles</a>, and I documented the behind-the-scenes on the <a href="/en/2026/02/18/serving-ai-in-the-cloud-my-personal-tts-behind-the-m-akita-chronicles/">serving-AI-in-the-cloud post</a>. Anyone who wants the detail on cold starts, voice cloning, and sampling parameters that change from one mode to another, that&rsquo;s the place. I&rsquo;m not repeating it all here.</p>
<p>This post asks a different question. After almost two months running that setup in production, shipping an episode every Monday, last night I finally pulled the plug on Qwen3 and switched the whole thing to ElevenLabs v3. Let me tell you why.</p>
<h2>What didn&rsquo;t work on Qwen3<span class="hx:absolute hx:-mt-20" id="what-didnt-work-on-qwen3"></span>
    <a href="#what-didnt-work-on-qwen3" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Between February 15 and March 30, I made dozens of commits fine-tuning the podcast flow: prompts, sampling parameters, generation order, leading silence on the voice clone sample, volume normalization, pronunciation of tech acronyms. Fixing Marvin&rsquo;s voice, which was clipping the first syllable because the reference audio started without any lead-in silence. Tuning Akita&rsquo;s voice to sound more confident and assertive. Adding crossfades between section jingles. Sorting out the &ldquo;podcast&rdquo; pronunciation so it didn&rsquo;t come out as &ldquo;pódcast&rdquo;. Making the script generator prefer Portuguese over gratuitous anglicisms so the TTS wouldn&rsquo;t choke. Each of those fixes was a multi-hour session of listening to audio, regenerating, tweaking a parameter.</p>
<p>The result came out acceptable. &ldquo;Acceptable&rdquo; in the sense that I managed to ship every episode without re-recording anything by hand. But if you listen closely, the Qwen3 voice has that unmistakable AI-generating-audio quality. Flat intonation, uniform rhythm. Over long stretches you can feel it&rsquo;s a machine talking. Good enough to ship, but miles away from what you&rsquo;d hear on a professional podcast made by humans.</p>
<p>The worst problem was English pronunciation. My podcast covers tech news, so terms like &ldquo;MCP&rdquo;, &ldquo;RAG&rdquo;, &ldquo;Claude Opus&rdquo;, &ldquo;GPT-5&rdquo;, &ldquo;open source&rdquo; show up in every conversation. Qwen3 took those terms and pronounced them with a Brazilian accent, something like &ldquo;oh-pen-ee-sohrss-eh&rdquo;, that kind of thing. Unlistenable for the audience. The workaround I had to implement was manually mapping in the script-generation LLM prompt which English words to swap for Portuguese equivalents. The prompt today has a whole section split between &ldquo;keep in English&rdquo; (proper nouns, brands, terms already adopted into Brazilian tech lingo) and &ldquo;translate to Portuguese&rdquo; (gratuitous anglicisms), looking roughly like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">**REPLACE with Portuguese** (common English words that have natural
</span></span><span class="line"><span class="cl">Brazilian Portuguese equivalents):
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;update&#34; → &#34;atualização&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;release&#34; → &#34;lançamento&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;feature&#34; → &#34;recurso/funcionalidade&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;deploy&#34; → &#34;implantação&#34; or just &#34;colocar em produção&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;trade-off&#34; → &#34;dilema&#34; or &#34;escolha&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;performance&#34; → &#34;desempenho&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;default&#34; → &#34;padrão&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;insight&#34; → &#34;percepção/sacada&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;skills&#34; → &#34;habilidades&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;approach&#34; → &#34;abordagem&#34;
</span></span><span class="line"><span class="cl">- &#34;highlights&#34; → &#34;destaques&#34;</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>I call this &ldquo;scrubbing anglicisms so the TTS doesn&rsquo;t choke&rdquo;. The funny part is I didn&rsquo;t want that level of restriction on my script. I wanted the voice to just pronounce &ldquo;update&rdquo; when &ldquo;update&rdquo; was the natural word. Since the model couldn&rsquo;t, I had to mutilate the podcast&rsquo;s vocabulary so the final result was actually listenable. It&rsquo;s a band-aid, the kind you add hoping to rip it off later when the tech grows up.</p>
<h2>The experience with ElevenLabs v3<span class="hx:absolute hx:-mt-20" id="the-experience-with-elevenlabs-v3"></span>
    <a href="#the-experience-with-elevenlabs-v3" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Yesterday afternoon I opened an ElevenLabs account, bought the Pro plan ($99/month), and started experimenting with the <code>eleven_v3</code> model, which shipped in February of this year. Thirty minutes later I had a proof of concept running, and about two hours later the whole podcast system was migrated. The effort gap is an abyss.</p>
<p>The technical details of the migration ended up in an internal project doc, so here&rsquo;s the summary comparison table that actually matters:</p>
<table>
  <thead>
      <tr>
          <th>Dimension</th>
          <th>Qwen3 TTS 1.7B (old)</th>
          <th>ElevenLabs <code>eleven_v3</code> (current)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Quality (Akita)</td>
          <td>Good, cloned from real audio</td>
          <td>Better, same clone but more natural prosody</td>
      </tr>
      <tr>
          <td>Inline emotion in script</td>
          <td>Not supported</td>
          <td><code>[sighs]</code>, <code>[sarcastically]</code>, <code>[excited]</code>, <code>[laughs]</code>, works in pt-BR</td>
      </tr>
      <tr>
          <td>Cold start</td>
          <td>5 to 15 min spinning up a RunPod GPU before each run</td>
          <td>Zero, HTTPS call with immediate response</td>
      </tr>
      <tr>
          <td>Throughput</td>
          <td>~1× real time (serialized)</td>
          <td>~6× real time with concurrency 4</td>
      </tr>
      <tr>
          <td>Wall clock for a 28-min episode</td>
          <td>~25 to 30 min</td>
          <td><strong>~4 min</strong></td>
      </tr>
      <tr>
          <td>Operational surface</td>
          <td>RunPod, Docker, FastAPI, Qwen weights, GPU billing</td>
          <td>One env var (<code>ELEVENLABS_API_KEY</code>)</td>
      </tr>
      <tr>
          <td>Cost per episode</td>
          <td>~$0.08 of GPU</td>
          <td>~$2.70 in ElevenLabs credits</td>
      </tr>
  </tbody>
</table>
<p>Look at the last line. Qwen3 costs less than ten cents of a dollar per episode. ElevenLabs costs almost thirty times more. And it&rsquo;s still worth it. The other lines on the table solve problems that were eating hours of my week. I no longer need to write code to scale GPUs in the cloud, I don&rsquo;t need to wait for the machine to spin up every time, I don&rsquo;t need to babysit a model or rebuild a Docker image when the weights change. The operation became a single line of configuration.</p>
<p>And here&rsquo;s the interesting part: the inline emotion tags. The v3 model accepts markers like <code>[sighs]</code>, <code>[sarcastically]</code>, <code>[dryly]</code>, <code>[excited]</code>, and changes the delivery accordingly. This works in more than 70 languages, including Brazilian Portuguese. It transformed script generation, because now I can ask the LLM that writes the script to drop emotional tags at the right moments, which gives a liveliness Qwen3 couldn&rsquo;t deliver in a million years. A concrete example of what comes out of the script later:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>AKITA: Isso é simples. [dismissive] Quem ainda acredita que Bitcoin
vai morrer não tá prestando atenção.
MARVIN: [sighs] Mais uma semana, mais uma leva de devs confiando
cegamente em pacotes npm. Previsível.</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>I even have two separate tag palettes, one for Akita (expressive but controlled, uses <code>[excited]</code>, <code>[dismissive]</code>, <code>[emphatic]</code>) and another for Marvin (stoic, just <code>[sighs]</code>, <code>[sarcastically]</code>, <code>[tired]</code>, <code>[dryly]</code>). That&rsquo;s all encoded in the script generation prompts so the LLM knows which character is allowed to use what.</p>
<h2>About Marvin&rsquo;s voice<span class="hx:absolute hx:-mt-20" id="about-marvins-voice"></span>
    <a href="#about-marvins-voice" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>For listeners who are already used to Marvin&rsquo;s voice, don&rsquo;t worry: I cloned him on ElevenLabs using the same audio sample I had used to train on Qwen3. It&rsquo;s the same voice. It just sounds even better now, because the ElevenLabs model captures nuance and prosody that Qwen3 couldn&rsquo;t deliver.</p>
<h2>Listen to it and tell me<span class="hx:absolute hx:-mt-20" id="listen-to-it-and-tell-me"></span>
    <a href="#listen-to-it-and-tell-me" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To prove the talk is real, here&rsquo;s Monday&rsquo;s episode, April 6th, already generated with the new pipeline:</p>
<audio controls preload="metadata" style="width: 100%; max-width: 640px;">
  <source src="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3" type="audio/mpeg">
  Your browser doesn't support the audio element. <a href="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3">Download the mp3 here.</a>
</audio>
<p>If you&rsquo;re already a podcast listener on <a href="https://open.spotify.com/show/7MzG2UB7IAkC3GAwEXEIVD"target="_blank" rel="noopener">Spotify</a> and you&rsquo;ve heard previous episodes made with Qwen3, compare them and tell me in the comments whether you notice the difference or whether it&rsquo;s the same to you. I&rsquo;m genuinely curious how much of this is my trained ear after hours of listening to TTS audio and how much is an obvious difference for a casual listener.</p>
<p>Starting next week, every episode of the podcast will be generated by ElevenLabs v3. The newsletter is already plugged into the new pipeline, the scheduled jobs to pre-warm the RunPod GPU have been disabled, and the legacy Qwen3 code stays in the repo as plan B in case v3 starts acting up chronically. Two file edits and I can switch back. I probably never will.</p>
<h2>The part about dubbing the YouTube videos<span class="hx:absolute hx:-mt-20" id="the-part-about-dubbing-the-youtube-videos"></span>
    <a href="#the-part-about-dubbing-the-youtube-videos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now the hook for the second half of this post. On the <a href="/en/2026/04/09/20-years-of-blogging-ai-finally-translated-everything/">anniversary piece I published earlier today</a>, I told the story of how Claude Code translated nearly half my blog to English over a weekend. In the same spirit, I went after dubbing the videos on the <a href="https://www.youtube.com/@Akitando"target="_blank" rel="noopener">Akitando</a> channel.</p>
<p>The channel has 146 episodes, around 96 hours of technical content in Portuguese, and more than 500k subscribers. I already had the subtitles translated (one curated <code>.srt</code> per episode), and YouTube even offers automatic English dubbing. But the result is like Google Translate circa 2015: gets the idea across, nobody wants to listen for very long.</p>
<p>I tested ElevenLabs&rsquo; three voice approaches and only one worked. Speech-to-Speech converts a voice but doesn&rsquo;t translate. The Dubbing API does translate but creates the voice on its own, with no way to force a specific clone. Only Text-to-Speech could solve this: take the English <code>.srt</code> I already had, send each block to the TTS endpoint with my cloned voice, and assemble the audio aligned to the original video.</p>
<p>But having a translated <code>.srt</code> is not enough. Raw subtitle translation doesn&rsquo;t survive TTS. Passages with source code, URLs, hex hashes, lists of shell commands — the model switches to spelling mode and the audio comes out at twice the expected duration. Translations that run too long blow past the time window and need to be condensed so the voice doesn&rsquo;t sound like an auctioneer. Truncated SRTs need to be completed. And after each fix, you re-run the pipeline, listen, find the next problem, fix it, re-run. The whole thing was a cycle of interruptions, manual corrections, and re-runs — a far cry from the &ldquo;press a button and get a dub&rdquo; fantasy.</p>
<p>The big challenge was the accent. My cloned voice was trained on Brazilian Portuguese audio, so when it tries to speak English, the thick accent comes through. ElevenLabs has an <code>[American accent]</code> tag that works on v3, but on top of a voice trained in another language it&rsquo;s weak — the Brazilian accent still bleeds through. The fix was to train a second voice of mine, English only. I recorded a few minutes in my best American accent, uploaded it as a separate Instant Voice Clone in my account, named it &ldquo;Akita English&rdquo;, and set the pipeline to use that voice by default. The result comes out more natural, no tag needed, and the voice identity is still mine.</p>
<h3>The honest ceiling on my English voice quality<span class="hx:absolute hx:-mt-20" id="the-honest-ceiling-on-my-english-voice-quality"></span>
    <a href="#the-honest-ceiling-on-my-english-voice-quality" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Let&rsquo;s be frank. I&rsquo;m a Brazilian who speaks English, not a native speaker, and I don&rsquo;t have the time or patience to drill pronunciation for hours. A clone can only ever be as good as the source sample — if the source is a non-native reading a script, the clone inherits every imperfection. No clone is going to make me sound like &ldquo;Fabio speaking perfect English&rdquo; because that sound doesn&rsquo;t exist for the model to learn from.</p>
<p>To get to the version that&rsquo;s running today, I recorded five different training samples throughout the day, each with a different rhythm and script, and ran an A/B battery against the original Portuguese. None sounded native, and that was never the goal. The realistic bar was more modest: sound recognizably like me, read technical content without stumbling, and carry a full one-hour episode without wearing the listener out. The winner ended up being the first iteration, the original &ldquo;Akita English&rdquo;. Far from perfect, but good enough to ship. Swapping it out later is literally one line in the config file.</p>
<p>What surprised me most along the way was realizing that the rhythm of the training sample matters more than the timbre. The clone learns the cadence of whoever recorded it: iteration 5 came out 25% slower than iteration 2 reading the exact same text, purely because I recorded that particular sample more slowly. For dubbing tech videos, the right target is the rhythm of a &ldquo;YouTube tech explainer&rdquo; — brisk, energetic, around 150 words per minute. No audiobook pacing.</p>
<h3>How I align audio to the original video<span class="hx:absolute hx:-mt-20" id="how-i-align-audio-to-the-original-video"></span>
    <a href="#how-i-align-audio-to-the-original-video" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A non-obvious point: spoken English tends to be longer than equivalent Portuguese. If you just generate the audio for each subtitle and paste it at the original timestamp, the dub drifts later and later. With 30 minutes of video you&rsquo;re already several seconds out of sync.</p>
<p>My approach was to attack the problem in layers. The first version split the SRT into smaller blocks, called &ldquo;chunks&rdquo;. Each chunk maxed out at 700 characters (so v3 wouldn&rsquo;t hallucinate) and could only cut at a sentence boundary, never mid-sentence. A simple algorithm accumulated subtitles into a buffer until it was about to overflow, then walked back looking for the last cue whose text ended with <code>.</code>, <code>!</code>, or <code>?</code>, and flushed only up to there. The cues still in the buffer got rolled into the next chunk. Each chunk also stored the start and end timestamps of the subtitles it covered, so the assembly step knew exactly where to drop that piece of audio later.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># For every incoming cue, check whether adding it</span>
</span></span><span class="line"><span class="cl"><span class="c1"># would push the buffer past the char cap.</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">cue</span> <span class="ow">in</span> <span class="n">cues</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">projected</span> <span class="o">=</span> <span class="n">buffer_char_len</span><span class="p">(</span><span class="n">buf</span><span class="p">)</span> <span class="o">+</span> <span class="nb">len</span><span class="p">(</span><span class="n">cue</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">projected</span> <span class="o">&gt;</span> <span class="n">max_chars</span> <span class="ow">and</span> <span class="n">buf</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Walk back for the last cue in the buffer that</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># ends in &#39;.&#39;, &#39;!&#39;, &#39;?&#39;, or &#39;…&#39;.</span>
</span></span><span class="line"><span class="cl">        <span class="n">sentence_end</span> <span class="o">=</span> <span class="n">last_sentence_end_index</span><span class="p">(</span><span class="n">buf</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">sentence_end</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">buf</span> <span class="o">=</span> <span class="n">flush</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="n">sentence_end</span><span class="p">,</span> <span class="n">chunks</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># Run-on sentence longer than max_chars with no</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># terminator. Rare, but happens. Warn and cut.</span>
</span></span><span class="line"><span class="cl">            <span class="n">log</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">&#34;run-on sentence &gt; </span><span class="si">%d</span><span class="s2"> chars — &#34;</span>
</span></span><span class="line"><span class="cl">                        <span class="s2">&#34;splitting mid-sentence as a last resort&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">max_chars</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">buf</span> <span class="o">=</span> <span class="n">flush</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">buf</span><span class="p">),</span> <span class="n">chunks</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">buf</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">cue</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Soft-break: if the buffer is already at least 60% full</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># AND the current cue ended a sentence, flush now. Keeps</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># chunks balanced around 60-100% of max.</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">cue</span><span class="o">.</span><span class="n">ends_sentence</span> <span class="ow">and</span> <span class="n">buffer_char_len</span><span class="p">(</span><span class="n">buf</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">max_chars</span> <span class="o">*</span> <span class="mf">0.6</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">buf</span> <span class="o">=</span> <span class="n">flush</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">buf</span><span class="p">),</span> <span class="n">chunks</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Technically, this was the turning point: instead of letting the dub follow the visual structure of the subtitle file, the chunker started following the real paragraph structure of the script. I won&rsquo;t retell the whole story of how I got there and why it eventually forced a mass rerender, because I come back to that later in the section about the problems that only showed up after the first upload. The technical point here is simple: once the window represented a real spoken paragraph, the audio started sounding natural, and the stretch target could move from 92% to 95%.</p>
<p>Once the chunking is done, there&rsquo;s still the problem that English runs longer than the Portuguese equivalent when spoken. The trick here is predicting whether a chunk is going to blow past its target window <strong>before</strong> generating the audio. If the ratio <code>characters / 16 chars-per-second</code> already exceeds the target duration by a margin, the pipeline passes the <code>speed</code> parameter directly to the ElevenLabs API, asking the model to natively generate faster speech. This preserves prosody way better than compressing afterwards. The ceiling is 1.15× (beyond that the voice starts sounding rushed).</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># before calling the API, predict if it will overrun</span>
</span></span><span class="line"><span class="cl"><span class="n">expected_sec</span> <span class="o">=</span> <span class="n">char_count</span> <span class="o">/</span> <span class="n">EXPECTED_CHARS_PER_SEC</span>  <span class="c1"># 16 chars/s</span>
</span></span><span class="line"><span class="cl"><span class="n">predicted_ratio</span> <span class="o">=</span> <span class="n">expected_sec</span> <span class="o">/</span> <span class="n">target_sec</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">voice_speed</span> <span class="o">=</span> <span class="mf">1.0</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">predicted_ratio</span> <span class="o">&gt;</span> <span class="n">PREEMPTIVE_SPEED_THRESHOLD</span><span class="p">:</span>   <span class="c1"># 1.05</span>
</span></span><span class="line"><span class="cl">    <span class="n">voice_speed</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="n">predicted_ratio</span> <span class="o">*</span> <span class="mf">0.98</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                      <span class="n">PREEMPTIVE_SPEED_MAX</span><span class="p">)</span>         <span class="c1"># cap at 1.15</span>
</span></span><span class="line"><span class="cl">    <span class="n">voice_settings</span><span class="p">[</span><span class="s2">&#34;speed&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">voice_speed</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Even with preemptive <code>speed</code> turned on, sometimes the generated audio still comes out a tiny bit longer than the target window. That&rsquo;s what the second safety net is for: measure the actual audio with <code>ffprobe</code> after it&rsquo;s generated, compare to the target window, and apply the <code>ffmpeg</code> <code>atempo</code> filter to compress in post if it exceeded the 2% tolerance, capped at 1.20×. Combining native <code>speed</code> (1.15×) with <code>atempo</code> (1.20×) gives an effective compression ceiling of 1.38×, enough to fit naturally even in the worst cases without breaking quality.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># after generating the chunk</span>
</span></span><span class="line"><span class="cl"><span class="n">actual</span> <span class="o">=</span> <span class="n">ffprobe_duration</span><span class="p">(</span><span class="n">chunk_path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">ratio</span> <span class="o">=</span> <span class="n">actual</span> <span class="o">/</span> <span class="n">target_sec</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">ratio</span> <span class="o">&gt;</span> <span class="n">FIT_TOLERANCE</span><span class="p">:</span>       <span class="c1"># 1.02</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># compress the audio without touching pitch</span>
</span></span><span class="line"><span class="cl">    <span class="n">ffmpeg_atempo</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="n">ratio</span><span class="p">,</span> <span class="n">FIT_MAX_ATEMPO</span><span class="p">)</span>   <span class="c1"># 1.20</span>
</span></span><span class="line"><span class="cl">    <span class="n">apply_atempo</span><span class="p">(</span><span class="n">chunk_path</span><span class="p">,</span> <span class="n">ffmpeg_atempo</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>When a generated chunk comes out shorter than the target window, the pipeline slightly stretches the audio (without affecting pitch) to reduce the ugly silence after it ends. The goal isn&rsquo;t to fill the whole window — a natural pause between sentences is fine — just to smooth over the worst offenders that would feel like &ldquo;cut audio&rdquo;.</p>
<p>On a big episode, 95 minutes, 194 chunks, the total cumulative drift came out to -0.7%. About 43 seconds of drift across the entire episode, imperceptible while you&rsquo;re watching.</p>
<p>What saved the budget on this architecture is that each chunk lives on disk as its own <code>.mp3</code>. The pipeline keeps a manifest with the normalized text of every chunk, and before calling the ElevenLabs API, it compares the current text against the cache. If the text hasn&rsquo;t changed, it reuses the existing audio without spending a single credit. If I rewrite a problematic cue, only the chunks affected by that cue get regenerated — the rest of the episode stays untouched.</p>
<p>This is what made the iterations viable. I&rsquo;d run the batch, listen to sections, spot a problem (translation too long, a code snippet the TTS couldn&rsquo;t pronounce, a chunk boundary that landed at a bad spot), fix the SRT or tweak the chunker parameters, and re-run. Each re-run consumed a fraction of the credits and time of the original run, because only the changed chunks were regenerated. Without this cache, every iteration would have cost nearly as much as the first pass, and the total batch cost would have been two or three times higher.</p>
<p>That chunking change also had a heavy cache and cost impact, but I&rsquo;ll leave that part for the later section where I explain the actual retracing and rerender cycle. Here, the important point is just that it was the right technical move.</p>
<h3>When <code>atempo</code> can&rsquo;t save you: rewriting cues before TTS<span class="hx:absolute hx:-mt-20" id="when-atempo-cant-save-you-rewriting-cues-before-tts"></span>
    <a href="#when-atempo-cant-save-you-rewriting-cues-before-tts" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Not everything is about window alignment. There&rsquo;s a kind of cue that breaks the whole pipeline, where neither preemptive <code>speed</code> nor post-process <code>atempo</code> can save it, and I only ran into it once I started running the batch over the more technical episodes of the channel.</p>
<p>The problem is this. The v3 reads at about 16 characters per second when you hand it normal conversational text. But when you hand it a cue stuffed with a literal URL, a hex hash, a binary string, a long sorted number list, or a shell code block, the model shifts into &ldquo;spelling it out letter by letter&rdquo; mode and drops to about 9 characters per second. A 500-character cue that was supposed to turn into 30 seconds of audio comes out at 55. The sanity check rejects it (because it&rsquo;s past 1.8×), the automatic retry tries the same 500 characters again, the five retries all fail in a row, and the chunk gets stuck.</p>
<p>And this did not happen by accident. I used to write those passages that way because I knew the same scripts would later become blog posts, so it made sense to leave the raw URL, the full command, the complete hash, the whole technical detail there for the reader. In Portuguese that worked fine because it matched my original workflow exactly: record the video, then publish the matching text on the blog. What I had never prepared for was the next step, turning that same material into automated English dubbing. I only realized that conflict when the pipeline started failing for real.</p>
<p>I hit this first on ep052, the Ubuntu beginner&rsquo;s guide, where two cues carried <code>github.com</code> URLs, <code>hkp://keyserver.ubuntu.com</code> URLs, and a 40-character GPG hash. Trying to fix that in post-processing is a waste of time. The 1.20× <code>atempo</code> ceiling will never compress 55 seconds into 30, and even if it did, the result would be an unlistenable chipmunk. The fix has to land upstream, in the text itself.</p>
<p>The solution was to rewrite the cue so it describes what the command does instead of showing the literal command:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">- Run: apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 \
</span></span></span><span class="line"><span class="cl"><span class="gd">-   --recv-keys 0A6A3E7F79F93EF8AAB9E92BAEBB74C8B5A1E44D
</span></span></span><span class="line"><span class="cl"><span class="gi">+ Run the full command in the video description to import the
</span></span></span><span class="line"><span class="cl"><span class="gi">+ signing key from the Ubuntu keyserver.
</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The on-screen English caption still shows the full command, so anyone reading the caption sees exactly what they need to type. The dubbed audio describes the intent in spoken language, which is what the TTS can deliver without choking. The instruction to the viewer stays intact, the characters-per-second ratio goes back to the expected 16, and the generated audio fits the original window without needing any stretch or compression in post.</p>
<p>I went hunting for cues like this across the whole batch. I scanned from ep052 through ep146 looking for cues with a high density of &ldquo;hard&rdquo; characters (digits, brackets, operators, long URLs, binary, hex) and ended up rewriting around 30 cues across 13 episodes:</p>
<ul>
<li>ep052 Ubuntu for devs: GitHub URLs and a 40-character GPG hash</li>
<li>ep091 Hello World in C: binary examples</li>
<li>ep095 640kB memory: address wrap, segment-offset</li>
<li>ep106 CodeMiner: spelled-out email address</li>
<li>ep113 Compression: pure binary strings, 75% hard characters</li>
<li>ep115 SQL Server: long sorted number list</li>
<li>ep120 Internet: dotted IP addresses</li>
<li>ep121 Sockets: two JavaScript code blocks</li>
<li>ep122 Proxies: Chrome User-Agent headers</li>
<li>ep123 Secure networking: shell one-liners, Docker flags</li>
<li>ep126 Gentoo: C <code>chroot</code> demo</li>
<li>ep136 Containers: GitHub release URL</li>
<li>ep144 Cryptography: signing key address</li>
</ul>
<p>The pattern was always the same: keep the caption showing the code, URL, or hash verbatim, and rewrite the spoken text so the narrator describes what it does. I could have automated the rewrite by letting an LLM read each cue and propose a substitution on the fly, but it felt safer to review them all by hand. It&rsquo;s exactly the kind of thing where the model decides to &ldquo;improve&rdquo; a command to make it cleaner and ships with broken shell. The manual sweep took about two hours. The automated version would have cost the same time in review, with the bonus anxiety of having shipped a mangled command by accident.</p>
<p>There was also one weird structural case on ep146, about Docker Compose. Two cues had glued themselves together because of a missing blank line in the SRT, and <code>pysrt</code> was treating them as one giant cue. The TTS never even got to process it — the chunker choked first. I fixed it by hand, adding the missing blank line. One-character fix, half an hour to track down.</p>
<h3>Reconstructing truncated SRTs<span class="hx:absolute hx:-mt-20" id="reconstructing-truncated-srts"></span>
    <a href="#reconstructing-truncated-srts" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Another trap showed up when I went to run the full batch: three episodes had English captions that were too short relative to the actual video length. Ep056, the Rails episode, was the most absurd case. Nineteen minutes of caption for an 80-minute video. Sixty-two minutes of content with no caption at all. Ep057 (WSL 2) was missing 14 minutes, and ep068 (Git Direito) was missing 2.</p>
<p>This is a leftover from my old translation workflow. At some point I started hand-revising the caption, stopped partway through, and the <code>.en.srt</code> file got saved truncated at the spot where I stopped. You can&rsquo;t dub an 80-minute video with a 19-minute caption. The chunker produces audio up to where it can read and then simply has no idea what to do with the rest of the video.</p>
<p>The fix became a new script. It diffs the truncated <code>.en.srt</code> against the <code>.pt-orig.srt</code> (the raw YouTube auto-caption, which always covers the whole video), picks up the cues that only exist in Portuguese, sends them to Claude Sonnet 4.6 with a strict JSON schema asking for cue-by-cue translation, and pastes the result back at the tail of the English file. The schema is the detail that matters. When I tried asking for the response as plain text in <code>N|text</code> format, Claude was dropping about 20% of the cues along the way. With a strict JSON schema, the drop rate fell to zero across the three repairs.</p>
<p>The numbers:</p>
<ul>
<li>ep068 Git Direito: +50 cues / 2 minutes reconstructed</li>
<li>ep057 WSL 2: +368 cues / 14 minutes</li>
<li>ep056 Rails: +1445 cues / 62 minutes (plus dropping 5 fake cues the old translator had invented at the tail to plug the gap)</li>
</ul>
<p>After the repairs, the three episodes joined the batch normally and got dubbed like any other. The kind of tool you hope you never need, but when you need it, it&rsquo;s worth writing once and closing the problem for good.</p>
<h3>Automatic emotion tags<span class="hx:absolute hx:-mt-20" id="automatic-emotion-tags"></span>
    <a href="#automatic-emotion-tags" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>While I was testing, I decided to add one more step to the pipeline: an &ldquo;emotion tagger&rdquo; that reads the English SRT before it goes to TTS and inserts tags like <code>[sarcastic]</code>, <code>[thoughtful]</code>, <code>[emphatic]</code>, <code>[deadpan]</code> at spots where a human narrator would naturally shift tone. The idea is to mimic what a professional voice actor would do — hit the emphasis at the right moments — without turning the video into a theater of overblown emotions.</p>
<p>This part is tricky for two reasons. First: letting an LLM loose on your SRT runs a real risk of it &ldquo;improving&rdquo; the text (swapping a word here, rewriting a sentence there) and you ending up with a dub that doesn&rsquo;t match the caption. Second: LLMs love to overdo it. Ask them to tag emotion and they&rsquo;ll put a tag on every other line. To guard against both, I pinned a small allow-list of tags and run a round-trip validation after Claude&rsquo;s response comes back:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">ALLOWED_TAGS</span><span class="p">:</span> <span class="nb">frozenset</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="nb">frozenset</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;[sarcastic]&#34;</span><span class="p">,</span> <span class="s2">&#34;[thoughtful]&#34;</span><span class="p">,</span> <span class="s2">&#34;[emphatic]&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;[deadpan]&#34;</span><span class="p">,</span> <span class="s2">&#34;[serious]&#34;</span><span class="p">,</span> <span class="s2">&#34;[amused]&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;[sighs]&#34;</span><span class="p">,</span> <span class="s2">&#34;[exasperated]&#34;</span><span class="p">,</span> <span class="s2">&#34;[confident]&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;[matter-of-fact]&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">validate_tagged</span><span class="p">(</span><span class="n">original</span><span class="p">,</span> <span class="n">tagged</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Ensure the tagged SRT preserves the original with no drift.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">original</span><span class="p">)</span> <span class="o">!=</span> <span class="nb">len</span><span class="p">(</span><span class="n">tagged</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="n">TagValidationError</span><span class="p">(</span><span class="s2">&#34;cue count mismatch&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">o</span><span class="p">,</span> <span class="n">t</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">original</span><span class="p">,</span> <span class="n">tagged</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Index and timestamp must match byte for byte.</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">o</span><span class="o">.</span><span class="n">index</span> <span class="o">!=</span> <span class="n">t</span><span class="o">.</span><span class="n">index</span> <span class="ow">or</span> <span class="n">o</span><span class="o">.</span><span class="n">timestamp</span> <span class="o">!=</span> <span class="n">t</span><span class="o">.</span><span class="n">timestamp</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="n">TagValidationError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;cue </span><span class="si">{</span><span class="n">o</span><span class="o">.</span><span class="n">index</span><span class="si">}</span><span class="s2">: header drift&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Any tag outside the allow-list invalidates the whole response.</span>
</span></span><span class="line"><span class="cl">        <span class="n">bad</span> <span class="o">=</span> <span class="n">find_disallowed_tags</span><span class="p">(</span><span class="n">t</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">bad</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="n">TagValidationError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;cue </span><span class="si">{</span><span class="n">o</span><span class="o">.</span><span class="n">index</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">bad</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># The text with the tags stripped must be byte-identical to</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># the original. If the LLM swapped a word, rewrote anything,</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># validation fails and the response is rejected.</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">strip_tags</span><span class="p">(</span><span class="n">o</span><span class="o">.</span><span class="n">text</span><span class="p">)</span> <span class="o">!=</span> <span class="n">strip_tags</span><span class="p">(</span><span class="n">t</span><span class="o">.</span><span class="n">text</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="n">TagValidationError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;cue </span><span class="si">{</span><span class="n">o</span><span class="o">.</span><span class="n">index</span><span class="si">}</span><span class="s2">: text drift&#34;</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>When validation fails, the pipeline throws the response away and re-requests (or, if it keeps failing, ships the SRT untagged). On tag density, the prompt sent to Claude explicitly says: aim for <code>N/10</code> tags in a SRT with N cues, hard floor of 1 tag per 12 cues, hard ceiling of 1 tag per 7 cues, distribution balanced across the four quarters of the episode, and no two tagged cues within 4 cues of each other. That rule set was the sixth iteration — the five before it either blanketed everything in tags or loaded them all into the first quarter.</p>
<h3>Don&rsquo;t let v3 hallucinate<span class="hx:absolute hx:-mt-20" id="dont-let-v3-hallucinate"></span>
    <a href="#dont-let-v3-hallucinate" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A critical detail that only shows up in production: ElevenLabs v3 tends to hallucinate when you send very long blocks of text. I&rsquo;ve seen blocks where the input text was around 1,500 characters and the model generated <strong>nine minutes</strong> of audio when the expected duration was a minute and a half. The model just decides to keep talking on its own, inventing content.</p>
<p>The official ElevenLabs docs recommend keeping each call under 800 characters to avoid this. I went more conservative and cut at 700, always at sentence end (never mid-sentence, because a mid-sentence split sounds horrible when concatenated). On top of that, I bumped <code>stability</code> to 0.9 (Robust mode, more stable but less responsive to emotion tags), turned on <code>apply_text_normalization</code> so the model pronounces numbers and acronyms correctly, and added a sanity check that rejects any generated audio longer than 1.8× the expected duration.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">DEFAULT_VOICE_SETTINGS</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;stability&#34;</span><span class="p">:</span> <span class="mf">0.9</span><span class="p">,</span>          <span class="c1"># Robust mode</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;similarity_boost&#34;</span><span class="p">:</span> <span class="mf">0.95</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;style&#34;</span><span class="p">:</span> <span class="mf">0.0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;use_speaker_boost&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">EXPECTED_CHARS_PER_SEC</span> <span class="o">=</span> <span class="mf">16.0</span>
</span></span><span class="line"><span class="cl"><span class="n">CHUNK_SANITY_MAX_FACTOR</span> <span class="o">=</span> <span class="mf">1.8</span>   <span class="c1"># reject if actual &gt; 1.8× expected</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>With those mitigations in place, a 95-minute episode (194 blocks) ran start to finish with zero hallucinations. Exactly one block failed mid-run on a transient 502 error from the API, which the automatic retry caught on the next attempt.</p>
<h3>Mastering for YouTube<span class="hx:absolute hx:-mt-20" id="mastering-for-youtube"></span>
    <a href="#mastering-for-youtube" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>YouTube doesn&rsquo;t publish an official number, but industry consensus is that its playback loudness normalization targets -14 LUFS. Deliver audio louder than that and YouTube turns it down; deliver it quieter and it leaves it alone. To land on the target exactly, the pipeline runs <code>ffmpeg</code>&rsquo;s <code>loudnorm</code> filter in two passes. The first pass measures the entire audio and prints the stats (<code>input_i</code>, <code>input_tp</code>, <code>input_lra</code>, <code>input_thresh</code>) as a JSON blob on stderr. The second pass reads those stats back in and applies a linear static gain to land precisely at -14 LUFS, -1.5 dBTP:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Pass 1: measure</span>
</span></span><span class="line"><span class="cl">ffmpeg -i final_en.mp3 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -af <span class="s2">&#34;highpass=f=80,loudnorm=I=-14:TP=-1.5:LRA=9:print_format=json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -f null -
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pass 2: apply linear gain with the measured values</span>
</span></span><span class="line"><span class="cl">ffmpeg -i final_en.mp3 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -af <span class="s2">&#34;highpass=f=80,loudnorm=I=-14:TP=-1.5:LRA=9\
</span></span></span><span class="line"><span class="cl"><span class="s2">:measured_I=-18.3:measured_TP=-3.1:measured_LRA=5.4\
</span></span></span><span class="line"><span class="cl"><span class="s2">:measured_thresh=-28.7:offset=1.2:linear=true&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -ar <span class="m">48000</span> -ac <span class="m">2</span> -c:a pcm_s16le final_en_mastered.wav</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Two passes instead of one because single-pass mode runs in dynamic compression and ends up &ldquo;pumping&rdquo; the gain during silent stretches. With <code>linear=true</code> on pass 2, the gain stays static on top of the pass-1 measurements, so no pumping. The result lands within ±0.1 LU of the target, which is effectively inaudible. The <code>highpass=f=80</code> filter in front kills HVAC rumble and mains hum below 80 Hz, which the human ear doesn&rsquo;t catch but will move peak measurements around.</p>
<h3>The point where I thought it was done. And it wasn&rsquo;t.<span class="hx:absolute hx:-mt-20" id="the-point-where-i-thought-it-was-done-and-it-wasnt"></span>
    <a href="#the-point-where-i-thought-it-was-done-and-it-wasnt" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is where the most annoying, and most educational, part of the project starts.</p>
<p>I really thought I had nailed it on the first big pass. The pipeline closed all 146 episodes, I uploaded everything, and it looked done. The durations matched, the files were all there, and the whole process had finally crossed the complete batch of nearly 96 hours of video. But it was exactly after that upload that I started noticing problems I simply had not anticipated.</p>
<p>The first scare was the jingle situation. I already knew many Akitando episodes begin with that short instrumental intro before I start talking, but I underestimated how much trouble it would cause. YouTube&rsquo;s <code>.srt</code> is almost useless here, because a jingle is not speech. Sometimes it marks <code>[Music]</code>, sometimes it gives you nothing useful, sometimes the window is just wrong. In the first assembly pass, some episodes lost the jingle entirely, some got it doubled, and in others the source-fill from the original audio already contained the right jingle and my splice came in on top of it, pushing everything else forward. This was the kind of bug you only catch when you sit down and actually listen to the final mastered file, not when you just look at total duration.</p>
<p>The fix was to stop hoping the subtitle would tell me where the jingle was and go straight to the source. I built a detector using the original video audio itself plus reference jingles, with an audit and repair pass on top of the mastered outputs already generated. That eventually became a small subsystem of its own: detector, audit, cached reassembly, exact-window repair. At the end of that detour, the set of changed episodes collapsed into a well-defined reupload bucket and the jingle problem finally came under control. It was one of those classic production moments: the first solution looked good enough until it hit the whole archive.</p>
<p>Then came the second punch, and this one was more expensive: I had forgotten a basic fact about YouTube subtitles. SRT is built to be read on screen, not to become spoken script. So an idea that was a full paragraph in my original text showed up split into several small cues, each with its own micro time window. In the first version of the pipeline I was using those cues as the generation unit for ElevenLabs. The result was TTS audio that was coherent locally, but the final assembly inserted artificial silences in the middle of sentences. Technically aligned, humanly weird.</p>
<p>Fixing that hurt because it meant walking several steps back. Instead of respecting the cue structure of the SRT, I had to go back to my original scripts and rebuild chunking around the paragraphs I had actually read when recording the videos. Almost every Akitando episode has a matching blog post with a <code>## Script</code> section, and that is what saved the project. Because I had been meticulous back when I made those videos — writing the scripts first, reading exactly that on camera, and archiving both the videos and the scripts — I had a source of truth. That made it possible to map the script paragraphs onto the timings in the <code>.pt-BR.srt</code>, regroup the <code>.en.srt</code> on top of that, and regenerate the chunks in the right structure.</p>
<p>The problem is that this destroys the previous cache. Even when the text barely changes, if the chunk boundary changes then the TTS call changes too: different context, different prosody, different beginning, different ending. So this was not a touch-up. It was a full regeneration of everything again. That was the point where the cost started escaping my original estimate.</p>
<p>And just when I thought now it really was over, the third hit landed: external clips. Many videos on the channel include inserted footage from other sources. The Ruby on Rails episode, for example, has 37signals clips, Apple material, commercials, external inserts in the middle of the narrative. YouTube&rsquo;s <code>.srt</code> did not know what to do with that. In some places it simply hallucinated text, in others the timings were completely wrong, and in others the original speech had nothing to do with the subtitle I was using as ground truth. Again: if you only look at the automation, it looks fine; if you actually listen to the final master, it isn&rsquo;t.</p>
<p>The fix, again, was to leave the subtitle behind and go back to the original material. I built an external-clip window detector based on alignment gaps between transcript, <code>.pt-BR.srt</code>, speechless cues and chunks that crossed those windows. Then, instead of trying to dub what made no sense to dub, the process started recovering the original audio from the source video exactly in those stretches and filling the master with the correct source audio there. That&rsquo;s what fixed the 37signals, Apple Education, Mac vs PC commercial and other inserted segments I had forgotten were even there.</p>
<p>That was the biggest lesson from the second half of this project: automation takes you very far, but the full archive always has more dark corners than you imagine on the first pass.</p>
<h2>The dubbing batch numbers (and why it hurt more than I expected)<span class="hx:absolute hx:-mt-20" id="the-dubbing-batch-numbers-and-why-it-hurt-more-than-i-expected"></span>
    <a href="#the-dubbing-batch-numbers-and-why-it-hurt-more-than-i-expected" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I need to correct myself again. I didn&rsquo;t just miscalculate the first bill. I also overestimated how confident I should have been after the first full run.</p>
<p>When I upgraded to ElevenLabs Business, I thought I would finally have plenty of headroom. In practice, I didn&rsquo;t. The latest dashboard screenshot shows <strong>10,046,279 credits consumed out of a limit of 11,000,000</strong>, leaving <strong>953,721</strong>:</p>
<p><img src="/2026/04/09/como-a-elevenlabs-nao-foi-morta-pelo-qwen3-tts/elevenlabs-credits-dashboard.png" alt="ElevenLabs dashboard showing 10,046,279 credits consumed out of 11,000,000"  loading="lazy" /></p>
<p>In plain English: I burned through more than 91% of the Business workspace limit. And that helps explain what &ldquo;10 million credits&rdquo; really means in this context. It does not mean &ldquo;I rendered 146 episodes once&rdquo;. It means rendering 146 episodes, discovering a structural problem, going back, fixing it, regenerating chunks, discovering another structural problem, going back again, rebuilding masters, auditing jingles, repairing external clip windows, uploading again. Every extra round costs money. And when the change invalidates the cache, it costs a lot.</p>
<p>The cost also doesn&rsquo;t stop at ElevenLabs. All the subtitle curation, problematic cue rewrites, truncated SRT reconstruction, jingle audits, external clip detector work and the rest ran through Claude Code on the Claude Max 20× plan. Combined with the mass blog translation to English that I described in the <a href="/en/2026/04/09/20-years-of-blogging-ai-finally-translated-everything/">anniversary post</a>, the Claude Max 20× weekly limit hit 100%, with more than BRL 300 in extra usage on top. If you think generative AI becomes free once you&rsquo;ve signed up for the plan, the invoices disagree.</p>
<p>Even so, I still think it&rsquo;s worth it. We&rsquo;re talking about 146 episodes, almost 96 hours of technical content, more than 5 years of channel archive. Doing that manually in a studio, with an actor, direction, pickups and human review on everything, would cost a completely different order of magnitude.</p>
<p>What changed was how I think about the project. At the start I was looking at this as &ldquo;one very large batch&rdquo;. In the end it was something else. It was digging through a whole archive, finding hidden exceptions, fixing old rot, rebuilding masters, rerendering, relistening.</p>
<p>The first full batch took a little under two days and made me think I had solved everything. I hadn&rsquo;t. The second pass, with paragraph-aware chunking, was already expensive. The third, with jingle repairs and mastered-output fixes, cost more again. And the fourth hit came from the external clips I had not modeled correctly in the first automation pass.</p>
<p>But now, finally, I think it&rsquo;s done. Or rather: I hope it&rsquo;s done. Everything has been uploaded again, for the last time. It isn&rsquo;t perfect in the absolute sense of the word. It&rsquo;s as good as I can get it with scripts, audits and automation, without descending into manual-intervention hell episode by episode.</p>
<p>And if all of this was possible, it&rsquo;s because years ago I had the discipline to produce the videos the right way: write first, read the script when recording, and archive the materials afterward. Without the original videos and without the original scripts, I would have had no way to discover where the <code>.srt</code> was lying, where the timing was wrong, where the external clip started, where the original paragraph began and ended. That old organization is what made it possible to repair the project now.</p>
<p>So yes: the dub for all 146 episodes is done. Not perfect. But closed. At least, I sincerely hope this time it really is.</p>
<h2>The first test, watch it<span class="hx:absolute hx:-mt-20" id="the-first-test-watch-it"></span>
    <a href="#the-first-test-watch-it" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I made a test video to prove the thing works. Two caveats up front: first, the embed below already loads with English captions on by default, so that side is solvable via URL. Second, the audio is annoying: YouTube removed the audio track selector from embedded players back around March of this year. So inside the embed you won&rsquo;t find that option in the gear menu at all, it just plays in Portuguese. To actually hear the English dub, click <strong><a href="https://www.youtube.com/watch?v=QNLd8TZ_JQc"target="_blank" rel="noopener">watch directly on YouTube</a></strong> — the main YouTube watch page still has the audio track switcher in the gear menu.</p>
<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/QNLd8TZ_JQc?cc_load_policy=1&amp;cc_lang_pref=en&amp;hl=en"
    title="Akitando dubbed in English (test)"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>
<p>If you&rsquo;re used to YouTube&rsquo;s automatic dubbing or to the AI dubbing on TikTok, compare them. The difference is glaring. The voice is mine, with a worked-over American accent, and the sync with the original video stays within 1 to 2% cumulative drift, practically imperceptible over 95 minutes of video.</p>
<h2>And the missing piece: translating the thumbnails<span class="hx:absolute hx:-mt-20" id="and-the-missing-piece-translating-the-thumbnails"></span>
    <a href="#and-the-missing-piece-translating-the-thumbnails" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>While I was closing this post, the penny dropped: I&rsquo;d left a loose end. All 146 thumbnails on my channel are in Portuguese, many of them with big all-caps titles like &ldquo;9 DICAS PARA PALESTRANTES&rdquo; or &ldquo;7 RECOMENDAÇÕES DE SHOWS PARA PESSOAS DE TECH&rdquo;. Perfect English audio doesn&rsquo;t help if the image in the search results is an illegible block of text to anyone who doesn&rsquo;t read Portuguese. YouTube lets you upload an alternative thumbnail per language, so I needed a way to generate English versions, keeping the rest of the art identical and swapping only the text.</p>
<p>The tool uses two pieces. <strong><code>yt-dlp</code></strong> is the modern fork of the old <code>youtube-dl</code>, a Python CLI that downloads anything from YouTube (videos, audio, subtitles, thumbnails) without needing an API key. <strong>Nano Banana Pro</strong> (<code>nano-banana-pro-preview</code> on the API) is Google Gemini&rsquo;s latest image-editing model — you feed it an input image with a prompt and it returns an edited image, preserving the rest of the composition when you ask it to touch only a specific part.</p>
<p>The pipeline has two steps. Step 1: <code>yt-dlp</code> grabs the thumbnail as a <code>.jpg</code>, no video download:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">yt-dlp --skip-download <span class="se">\
</span></span></span><span class="line"><span class="cl">       --write-thumbnail <span class="se">\
</span></span></span><span class="line"><span class="cl">       --convert-thumbnails jpg <span class="se">\
</span></span></span><span class="line"><span class="cl">       -o <span class="s2">&#34;thumbnails/originals/&lt;slug&gt;/&lt;video_id&gt;&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">       <span class="s2">&#34;https://www.youtube.com/watch?v=&lt;video_id&gt;&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Step 2: send each image to Gemini Nano Banana Pro with a prompt that&rsquo;s a rigid contract of hard requirements, not just a &ldquo;translate this&rdquo;:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">TASK:
</span></span><span class="line"><span class="cl">Detect every piece of Portuguese text visible on this image and
</span></span><span class="line"><span class="cl">translate it to clear, natural American English. Replace the
</span></span><span class="line"><span class="cl">Portuguese text with the English translation in the same visual
</span></span><span class="line"><span class="cl">position.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">STRICT REQUIREMENTS — follow every one:
</span></span><span class="line"><span class="cl">- Preserve EVERY non-text element identically: face, pose,
</span></span><span class="line"><span class="cl">  expression, background, color palette, lighting, icons, logos,
</span></span><span class="line"><span class="cl">  decorative shapes, borders, layout. Only the text changes.
</span></span><span class="line"><span class="cl">- The English translation must be IDIOMATIC and CONFIDENT — not
</span></span><span class="line"><span class="cl">  a literal word-for-word rewrite. It&#39;s a YouTube thumbnail for a
</span></span><span class="line"><span class="cl">  tech audience, so use punchy phrasing a native English-speaking
</span></span><span class="line"><span class="cl">  tech YouTuber would write.
</span></span><span class="line"><span class="cl">- Match the original text&#39;s font family, weight, size, color,
</span></span><span class="line"><span class="cl">  stroke outline, drop shadow, and any decorative treatment.
</span></span><span class="line"><span class="cl">- If there is no Portuguese text at all on the image, return the
</span></span><span class="line"><span class="cl">  original image unchanged.</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The &ldquo;return the original image unchanged&rdquo; clause is what saves the earliest episodes, which don&rsquo;t have overlay text, just my face. Without it, the model would invariably try to &ldquo;improve&rdquo; the composition, swap the lighting, and so on.</p>
<p>Here&rsquo;s the result on two examples (episodes 10 and 11):</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 16px 0;">
<div><strong>Original (PT)</strong><br><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_thumb_ep010_pt.jpg" alt="Original Portuguese thumbnail: 7 Recomendações de Shows para pessoas de Tech" style="width: 100%;"></div>
<div><strong>Translated (EN)</strong><br><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_thumb_ep010_en.jpg" alt="Translated English thumbnail: 7 TV Shows You Must Watch If You're In Tech" style="width: 100%;"></div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 16px 0;">
<div><strong>Original (PT)</strong><br><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_thumb_ep011_pt.jpg" alt="Original Portuguese thumbnail: 9 Dicas para Palestrantes: venda sua caneta" style="width: 100%;"></div>
<div><strong>Translated (EN)</strong><br><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_thumb_ep011_en.jpg" alt="Translated English thumbnail: 9 Tips for Speakers: sell your pen" style="width: 100%;"></div>
</div>
<p>Notice the detail on episode 10. The model didn&rsquo;t translate &ldquo;7 Recomendações de Shows para Pessoas de Tech&rdquo; literally into &ldquo;7 Show Recommendations for Tech People&rdquo; the way Google Translate would. It rewrote it as &ldquo;7 TV Shows You Must Watch If You&rsquo;re In Tech&rdquo;, the way an actual American tech YouTuber would headline the video. The rest of the image stays pixel-for-pixel identical. On episode 11, &ldquo;9 DICAS PARA PALESTRANTES: venda sua caneta&rdquo; becomes &ldquo;9 TIPS FOR SPEAKERS: sell your pen&rdquo;, with the font, all-caps treatment and position preserved. If you didn&rsquo;t know the original was in Portuguese, you couldn&rsquo;t guess the second image is an automatic AI edit.</p>
<p>Cost on the Gemini side: a few cents per image, a handful of dollars total for the whole batch. Trivial next to the $1,500+ on the audio dub. With the thumbnail sorted, the channel&rsquo;s English conversion is finally buttoned up — ElevenLabs v3 cloning the voice over hand-curated English <code>.srt</code> files, and Nano Banana Pro editing the image so the title matches the audio.</p>
<h2>The conclusion, which is the title of this post<span class="hx:absolute hx:-mt-20" id="the-conclusion-which-is-the-title-of-this-post"></span>
    <a href="#the-conclusion-which-is-the-title-of-this-post" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>When Qwen3 TTS dropped, the hype was calling it an &ldquo;ElevenLabs killer&rdquo;. I spent weeks trying to live with that premise in practice, with real content shipping every week. And what I found is that open source TTS is still miles behind ElevenLabs. The gap is large, and it&rsquo;s a gap that shows up precisely when you step off the 5-second demo tweet and put the model to work on a real 30-minute weekly podcast.</p>
<p>In practice, Qwen3 doesn&rsquo;t even beat ElevenLabs&rsquo; v2 model. v3, which is the current one, is still a step above v2. The prosody comes out better, the inline emotion tags work in Portuguese and English without effort, and the API stays up without you maintaining a server. The cost per character is a bit higher, but for my current volume it sits comfortably within budget and gives me back the hours I was spending on GPU babysitting.</p>
<p>The lesson here is the same one the LLM crowd is learning slowly. A 30-second demo tweet is one thing, production is a completely different thing. Open source has its niche, mainly when you have sensitive data that can&rsquo;t leave the house, or when you have tons of idle GPU and little recurring budget. But for serious TTS use in a commercial product, here in April 2026, ElevenLabs is still untouchable. Qwen3 didn&rsquo;t kill anyone.</p>
<p>And don&rsquo;t forget: subscribe to <a href="https://open.spotify.com/show/7MzG2UB7IAkC3GAwEXEIVD"target="_blank" rel="noopener">The M.Akita Chronicles on Spotify</a> so you don&rsquo;t miss new episodes like this one, made with the new pipeline.</p>
]]></content:encoded><category>generative-media</category><category>local-models</category><category>reviews</category></item><item><title>Review: Minisforum MS-S1 Max | AMD AI Max+ 395 with 96GB of VRAM</title><link>https://akitaonrails.github.io/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/</guid><pubDate>Tue, 31 Mar 2026 15:00:00 -0300</pubDate><description>&lt;p&gt;If you&amp;rsquo;ve been following my &lt;a href="https://akitaonrails.github.io/en/2024/04/03/my-personal-netflix-with-docker-compose/"&gt;home server posts&lt;/a&gt;, you know I used to run everything on an Intel NUC Core i7 with 32GB of RAM. It worked. But as open source AI models grew, the NUC became the bottleneck. Without a dedicated GPU, any LLM inference would fall back to the CPU and turn unusable.&lt;/p&gt;
&lt;p&gt;I bought a Minisforum MS-S1 Max with the new AMD Ryzen AI Max+ 395 chip for one specific reason: this chip supports up to 128GB of unified RAM, and I can allocate 96GB of it as VRAM for the iGPU. That gives me more VRAM than any consumer gaming card, including the RTX 5090 (32GB). And that changes what I can run locally.&lt;/p&gt;</description><content:encoded><![CDATA[<p>If you&rsquo;ve been following my <a href="/en/2024/04/03/my-personal-netflix-with-docker-compose/">home server posts</a>, you know I used to run everything on an Intel NUC Core i7 with 32GB of RAM. It worked. But as open source AI models grew, the NUC became the bottleneck. Without a dedicated GPU, any LLM inference would fall back to the CPU and turn unusable.</p>
<p>I bought a Minisforum MS-S1 Max with the new AMD Ryzen AI Max+ 395 chip for one specific reason: this chip supports up to 128GB of unified RAM, and I can allocate 96GB of it as VRAM for the iGPU. That gives me more VRAM than any consumer gaming card, including the RTX 5090 (32GB). And that changes what I can run locally.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/31/minisforum-desk.jpg" alt="Minisforum MS-S1 Max on the desk"  loading="lazy" /></p>
<h2>Why ditch the Intel NUC<span class="hx:absolute hx:-mt-20" id="why-ditch-the-intel-nuc"></span>
    <a href="#why-ditch-the-intel-nuc" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The NUC was a fine Docker server for two years. But the limitation was clear: without a GPU with enough VRAM, I couldn&rsquo;t run LLMs locally in any usable way. <a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">Frank Yomik</a>, my automatic manga translation system, needed CPU-based OCR (slow) and would connect remotely to the Ollama running on my desktop (AMD 7950X3D + RTX 5090) for translation. It worked, but it meant my desktop had to be on for the server to do its job.</p>
<p><img src="https://raw.githubusercontent.com/akitaonrails/FrankYomik/master/docs/sample_translate.png" alt="Frank Yomik - automatic manga translation"  loading="lazy" /></p>
<p>With the Minisforum, Frank Yomik now runs entirely on the server. The worker uses ROCm for OCR on the iGPU, and Ollama runs locally with 96GB of VRAM. Zero dependency on the desktop.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/31/minisforum-nuc-compare.jpg" alt="Comparison: Intel NUC (left) vs Minisforum MS-S1 Max (right)"  loading="lazy" /></p>
<p>You can get a sense of the size from the photo. The NUC is the tiny cube on the left. The Minisforum is bigger but it&rsquo;s still a mini-PC. It fits on the rack shelf under my Synology NAS without any trouble.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/31/minisforum-shelf.jpg" alt="Minisforum installed on the shelf, next to the NAS"  loading="lazy" /></p>
<h2>The specs<span class="hx:absolute hx:-mt-20" id="the-specs"></span>
    <a href="#the-specs" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/31/minisforum-fastfetch.png" alt="fastfetch on the Minisforum"  loading="lazy" /></p>
<p>The chip is the AMD Ryzen AI Max+ 395: 16 cores / 32 threads Zen 5, with an integrated Radeon 8060S iGPU and 128GB of unified LPDDR5X. In the BIOS, I set the UMA Frame Buffer Size to 96GB, which leaves ~30GB of RAM for the operating system and containers. Plus the kernel parameters for TTM (without them, ROCm only sees 15.5GB even with the BIOS allocation in place).</p>
<p>The operating system is openSUSE MicroOS (more on that in the <a href="/en/2026/03/31/migrating-my-home-server-with-claude-code/">next post</a>). The whole machine pulls under 100W, which is absurd if you&rsquo;re used to dedicated GPUs that draw 450W+ on their own.</p>
<h2>Minisforum vs my desktop: benchmarks<span class="hx:absolute hx:-mt-20" id="minisforum-vs-my-desktop-benchmarks"></span>
    <a href="#minisforum-vs-my-desktop-benchmarks" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I ran a <a href="https://github.com/akitaonrails/homelab-docs/tree/master/benchmarks"target="_blank" rel="noopener">set of benchmarks</a> comparing the Minisforum against my desktop (AMD 7950X3D, 96GB DDR5, RTX 5090 32GB GDDR7). The results are clear.</p>
<h3>CPU<span class="hx:absolute hx:-mt-20" id="cpu"></span>
    <a href="#cpu" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Test</th>
          <th>7950X3D</th>
          <th>AI Max+ 395</th>
          <th>Winner</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Prime sieve (single-core)</td>
          <td>0.021s</td>
          <td>0.018s</td>
          <td>Strix Halo +14%</td>
      </tr>
      <tr>
          <td>Float pi (single-core)</td>
          <td>1.335s</td>
          <td>1.706s</td>
          <td>7950X3D +28%</td>
      </tr>
      <tr>
          <td>Multi-core sieve (32 threads)</td>
          <td>0.181s</td>
          <td>0.118s</td>
          <td>Strix Halo +53%</td>
      </tr>
      <tr>
          <td>SHA-256 throughput</td>
          <td>2.714 MB/s</td>
          <td>2.488 MB/s</td>
          <td>7950X3D +9%</td>
      </tr>
      <tr>
          <td>AES-256-CBC throughput</td>
          <td>1.613 MB/s</td>
          <td>1.410 MB/s</td>
          <td>7950X3D +14%</td>
      </tr>
  </tbody>
</table>
<p>Mixed results. The AI Max+ 395 is better at pure parallelism (multi-core sieve), probably thanks to lower latency in the unified memory architecture. The 7950X3D wins at float and crypto because of its higher clocks and the 3D V-Cache.</p>
<h3>LLM inference (models that fit on both)<span class="hx:absolute hx:-mt-20" id="llm-inference-models-that-fit-on-both"></span>
    <a href="#llm-inference-models-that-fit-on-both" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is where it gets interesting. For models that fit in the 32GB of the RTX 5090, the comparison is purely about memory bandwidth:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Size</th>
          <th>RTX 5090 (tok/s)</th>
          <th>Strix Halo (tok/s)</th>
          <th>5090 advantage</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>phi4</td>
          <td>9.1 GB</td>
          <td>155.1</td>
          <td>23.2</td>
          <td>6.7x</td>
      </tr>
      <tr>
          <td>qwen3:14b</td>
          <td>9.3 GB</td>
          <td>138.9</td>
          <td>22.6</td>
          <td>6.1x</td>
      </tr>
      <tr>
          <td>phi4-reasoning</td>
          <td>11.1 GB</td>
          <td>130.2</td>
          <td>19.1</td>
          <td>6.8x</td>
      </tr>
      <tr>
          <td>qwen3:32b</td>
          <td>20.2 GB</td>
          <td>66.9</td>
          <td>10.0</td>
          <td>6.7x</td>
      </tr>
  </tbody>
</table>
<p>The RTX 5090 is ~7x faster. The explanation is simple: GDDR7 has ~1,792 GB/s of bandwidth. LPDDR5X has ~256 GB/s. The ratio (7x) lines up almost exactly with the measured speed difference (6.7x). LLM inference is a problem dominated by memory bandwidth. Whoever reads weights faster, generates tokens faster.</p>
<h3>And what about prompt processing?<span class="hx:absolute hx:-mt-20" id="and-what-about-prompt-processing"></span>
    <a href="#and-what-about-prompt-processing" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Model</th>
          <th>RTX 5090 (tok/s)</th>
          <th>Strix Halo (tok/s)</th>
          <th>5090 advantage</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>phi4</td>
          <td>~1,933</td>
          <td>~212</td>
          <td>9.1x</td>
      </tr>
      <tr>
          <td>qwen3:14b</td>
          <td>~1,474</td>
          <td>~155</td>
          <td>9.5x</td>
      </tr>
      <tr>
          <td>qwen3:32b</td>
          <td>~767</td>
          <td>~68</td>
          <td>11.3x</td>
      </tr>
  </tbody>
</table>
<p>Prompt processing is even worse: 7-11x slower. That makes sense, because the prompt has to be processed in full before generating the first token, and it&rsquo;s an even more bandwidth-intensive operation.</p>
<h3>Where the Strix Halo wins: large models<span class="hx:absolute hx:-mt-20" id="where-the-strix-halo-wins-large-models"></span>
    <a href="#where-the-strix-halo-wins-large-models" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Now we get to the reason I bought this PC. Models that don&rsquo;t fit in the RTX 5090:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Size</th>
          <th>Strix Halo (tok/s)</th>
          <th>Notes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>gpt-oss:20b</td>
          <td>13.8 GB MXFP4</td>
          <td>48.9</td>
          <td>MoE, faster than expected</td>
      </tr>
      <tr>
          <td>qwen3.5:35b</td>
          <td>23.9 GB</td>
          <td>43.2</td>
          <td>MoE, only ~4B active params</td>
      </tr>
      <tr>
          <td>qwen3-coder-next</td>
          <td>51.7 GB</td>
          <td>29.5</td>
          <td>MoE, 50GB+</td>
      </tr>
      <tr>
          <td>qwen3.5:122b</td>
          <td>81.4 GB Q4_K_M</td>
          <td>19.2</td>
          <td>122B params, MoE</td>
      </tr>
      <tr>
          <td>glm-4.7-flash:bf16</td>
          <td>59.9 GB</td>
          <td>17.9</td>
          <td>Full precision bf16</td>
      </tr>
      <tr>
          <td>qwen2.5:72b</td>
          <td>47.4 GB Q4_K_M</td>
          <td>4.5</td>
          <td>Dense 72B, bandwidth-limited</td>
      </tr>
  </tbody>
</table>
<p>qwen3.5:122b with 81GB of weights running at 19 tok/s. On a mini-PC. That&rsquo;s simply not possible on an RTX 5090. On the NVIDIA card, that model would have to offload layers to system RAM, dropping to 2-3 tok/s. In practice, unusable.</p>
<p>The difference between MoE and dense models is brutal. qwen3.5:35b runs at 43 tok/s because, despite having 35B total parameters, only ~4B are active per token. A dense 72B model like qwen2.5:72b has to read 40GB+ of weights per token, and at 256 GB/s of bandwidth, the theoretical maximum is ~6.7 tok/s. The 4.5 measured represent ~67% efficiency, which is what you&rsquo;d expect for an iGPU (overhead from shared bus and drivers).</p>
<h3>Summary: when to use which machine<span class="hx:absolute hx:-mt-20" id="summary-when-to-use-which-machine"></span>
    <a href="#summary-when-to-use-which-machine" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Use case</th>
          <th>Best machine</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Interactive chat/coding (models &lt;32GB)</td>
          <td>RTX 5090 (6-7x faster)</td>
      </tr>
      <tr>
          <td>Large models (50GB+)</td>
          <td>Strix Halo (only option)</td>
      </tr>
      <tr>
          <td>Dense 70B+ models</td>
          <td>Strix Halo (only option)</td>
      </tr>
      <tr>
          <td>Full-precision bf16</td>
          <td>Strix Halo (only option)</td>
      </tr>
      <tr>
          <td>Batch processing with long context</td>
          <td>Strix Halo (more VRAM for KV cache)</td>
      </tr>
      <tr>
          <td>API serving with low latency</td>
          <td>RTX 5090 (sub-150ms TTFT)</td>
      </tr>
  </tbody>
</table>
<h3>A ROCm bug that&rsquo;s still around<span class="hx:absolute hx:-mt-20" id="a-rocm-bug-thats-still-around"></span>
    <a href="#a-rocm-bug-thats-still-around" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Not everything works. Models like deepseek-r1:70b, llama3.3:70b and llama4:scout crash with a ggml bug (<code>GGML_ASSERT(ggml_nbytes(src0) &lt;= INT_MAX) failed</code>). The embedding tensor of these models exceeds 2GB and the ROCm copy kernel uses a 32-bit integer for the size. On CUDA (NVIDIA) it&rsquo;s already been fixed, but on ROCm it hasn&rsquo;t. Waiting for the fix in Ollama 0.20.0+.</p>
<h2>LPDDR5X vs GDDR7: why this difference exists<span class="hx:absolute hx:-mt-20" id="lpddr5x-vs-gddr7-why-this-difference-exists"></span>
    <a href="#lpddr5x-vs-gddr7-why-this-difference-exists" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The next question is: why is LPDDR5X so much slower?</p>
<p>GDDR7 is dedicated GPU memory. It&rsquo;s soldered onto the graphics card, connected by a wide bus (384 or 512 bits on the RTX 5090) at high clocks. Its only job is to feed data to the GPU. LPDDR5X is unified memory that serves everything: the operating system, applications, and the GPU all at once. The bus is narrower and shared.</p>
<p>In practice: GDDR7 delivers ~1,792 GB/s dedicated to the GPU. LPDDR5X delivers ~256 GB/s that still need to be split between CPU and GPU. LLM inference is basically &ldquo;read all the model weights from memory, multiply by the current token, generate the next token, repeat.&rdquo; Whoever reads faster, generates faster. There&rsquo;s no shortcut.</p>
<p>The Strix Halo&rsquo;s advantage isn&rsquo;t speed. It&rsquo;s capacity. 96GB of VRAM in a 100W chip that costs a fraction of a professional GPU. The RTX 5090 is 7x faster, but it&rsquo;s stuck at 32GB. Models that don&rsquo;t fit, don&rsquo;t run.</p>
<h2>The alternatives: who else does this?<span class="hx:absolute hx:-mt-20" id="the-alternatives-who-else-does-this"></span>
    <a href="#the-alternatives-who-else-does-this" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If 96GB isn&rsquo;t enough or you want more speed, the options are limited.</p>
<p>The Framework Desktop uses the same AI Max+ 395 chip with up to 128GB of RAM. Same platform, same performance, but with the differential of being modular and repairable (it&rsquo;s Framework, after all). In practice it&rsquo;s equivalent to the Minisforum on specs and price.</p>
<p>Above that, the alternative is a Mac Studio with M3 Ultra. The M3 Ultra chip supports up to 512GB of unified memory, with ~819 GB/s of bandwidth (more than 3x the Strix Halo). Apple manufactures the memory chips on the package, so latency and bandwidth are superior. You could potentially allocate ~400GB as VRAM and run models that don&rsquo;t fit anywhere outside of professional GPU servers.</p>
<p>Apple&rsquo;s internal NVMe is also another level: ~7.4 GB/s of sequential read on the M3 Ultra, compared with ~14 GB/s on the Crucial T700 (PCIe 5.0). The T700 is faster on raw throughput, but Apple&rsquo;s NVMe latency tends to be lower on random I/O thanks to the SoC integration.</p>
<table>
  <thead>
      <tr>
          <th>Spec</th>
          <th>Minisforum MS-S1 Max</th>
          <th>Mac Studio M3 Ultra (max)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Max RAM</td>
          <td>128 GB LPDDR5X</td>
          <td>512 GB unified</td>
      </tr>
      <tr>
          <td>Allocatable VRAM</td>
          <td>~96 GB</td>
          <td>~400 GB</td>
      </tr>
      <tr>
          <td>Memory bandwidth</td>
          <td>~256 GB/s</td>
          <td>~819 GB/s</td>
      </tr>
      <tr>
          <td>CPU</td>
          <td>Zen 5, 16C/32T</td>
          <td>Apple M3 Ultra, 32C</td>
      </tr>
      <tr>
          <td>GPU compute</td>
          <td>ROCm (gfx1151, experimental)</td>
          <td>Metal (mlx, mature)</td>
      </tr>
      <tr>
          <td>Power draw</td>
          <td>~100W</td>
          <td>~135W</td>
      </tr>
      <tr>
          <td>NVMe</td>
          <td>PCIe 5.0 (standard slot)</td>
          <td>Custom Apple (~7.4 GB/s)</td>
      </tr>
      <tr>
          <td>Price (US)</td>
          <td>~$1,500-2,000</td>
          <td>~$9,999 (512GB config)</td>
      </tr>
      <tr>
          <td>Estimated price (Brazil)</td>
          <td>~R$ 12,000-15,000</td>
          <td>~R$ 110,000+ (imported)</td>
      </tr>
  </tbody>
</table>
<p>The Brazilian price is the elephant in the room. The Mac Studio&rsquo;s max config costs $9,999 in the US. With import taxes (~60% + state ICMS), it goes past R$ 110,000. The Minisforum with 128GB lands at R$ 12,000-15,000. A nearly 8x price gap buys you a lot.</p>
<p>If you need more than 96GB of VRAM for truly enormous models (DeepSeek-V3 with 671B parameters fits in ~400GB Q4, for example), the Mac Studio with 512GB is the only consumer option. The alternative would be professional NVIDIA A6000 GPUs (48GB VRAM, ~$6,000 each, and you&rsquo;d need several in NVLink). For everything that fits in 96GB, the Minisforum gets the job done at a fraction of the cost.</p>
<h2>And projects that promise to run big LLMs on small GPUs?<span class="hx:absolute hx:-mt-20" id="and-projects-that-promise-to-run-big-llms-on-small-gpus"></span>
    <a href="#and-projects-that-promise-to-run-big-llms-on-small-gpus" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>There&rsquo;s a concept called &ldquo;layer offloading&rdquo; that projects like llama.cpp already support. The idea: if the model doesn&rsquo;t fit fully in VRAM, keep some layers on the GPU and the rest in system RAM. The GPU processes the layers it has, hands off to the CPU to process the rest, and back.</p>
<p>In practice, it doesn&rsquo;t work well. The bottleneck is PCIe: transfer speed between system RAM and GPU VRAM is ~32 GB/s (PCIe 5.0 x16). Each generated token needs to transfer data back and forth. The result is that you drop from 150 tok/s (everything in VRAM) to 2-8 tok/s (partial offload). It&rsquo;s too slow for interactive use.</p>
<p>VRAM is the fundamental limitation because LLM inference is memory-bandwidth-bound, not compute-bound. The GPU has compute to spare. What&rsquo;s missing is the ability to read the model weights fast enough. When part of the weights live in system RAM through PCIe, the entire pipeline waits on the transfer.</p>
<p>That&rsquo;s why unified memory (like in the Strix Halo or Apple Silicon) makes a difference. There&rsquo;s no PCIe in the middle. CPU and GPU access the same physical memory. The Strix Halo&rsquo;s 256 GB/s is slow compared to GDDR7, but it&rsquo;s 8x faster than offloading through PCIe.</p>
<h2>Advances in LLM optimization (up to 2026)<span class="hx:absolute hx:-mt-20" id="advances-in-llm-optimization-up-to-2026"></span>
    <a href="#advances-in-llm-optimization-up-to-2026" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To understand why some models run so much better than others on the Strix Halo, you have to understand what&rsquo;s changed in the ecosystem over the last two years.</p>
<h3>Mixture of Experts (MoE)<span class="hx:absolute hx:-mt-20" id="mixture-of-experts-moe"></span>
    <a href="#mixture-of-experts-moe" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If you run local models, MoE is the advance that matters most. An MoE model has high total parameters (e.g. 122B in qwen3.5:122b), but only activates a fraction of them per token (e.g. ~4B). The inactive weights stay in VRAM but aren&rsquo;t read on every token, which drastically reduces the bandwidth needed.</p>
<p>In the Strix Halo benchmarks, MoE models run 3-10x faster than dense models of the same size. qwen3.5:35b (MoE, ~4B active) runs at 43 tok/s while qwen2.5:72b (dense, 72B active) runs at 4.5 tok/s.</p>
<h3>DeepSeek and training optimization<span class="hx:absolute hx:-mt-20" id="deepseek-and-training-optimization"></span>
    <a href="#deepseek-and-training-optimization" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>DeepSeek V3 (December 2024) showed that it was possible to train 671B parameter models at a cost an order of magnitude lower than predicted. They combined MoE with FP8 quantization during training (not just inference), multi-stage training with curriculum learning, and several inter-GPU communication optimizations. The impact: everyone copied. Qwen, GLM, MiniMax, all of them adopted variations of the technique.</p>
<h3>Quantization: from FP16 to Q4 without losing much<span class="hx:absolute hx:-mt-20" id="quantization-from-fp16-to-q4-without-losing-much"></span>
    <a href="#quantization-from-fp16-to-q4-without-losing-much" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Quantization compresses model weights from 16 bits (FP16) to smaller formats: 8 bits (Q8), 4 bits (Q4), or even 2 bits. A 70B model that would take ~140GB in FP16 fits in ~40GB at Q4_K_M. Quality loss exists, but in modern formats (GGUF Q4_K_M, AWQ, EXL2) it&rsquo;s small enough for practical use.</p>
<p>GGUF (the llama.cpp format) became the standard for local inference. AWQ and GPTQ are alternatives with more sophisticated calibration, but the ecosystem converged on GGUF because it works on CPU, CUDA and ROCm without recompilation.</p>
<h3>Distillation: smaller models that know more<span class="hx:absolute hx:-mt-20" id="distillation-smaller-models-that-know-more"></span>
    <a href="#distillation-smaller-models-that-know-more" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Distillation is training a small model using the responses of a large model as the teacher. Microsoft&rsquo;s Phi-4 (14B) was trained with distillation from GPT-4 and competes with 70B models on several benchmarks. Qwen3 did the same: qwen3:14b is surprisingly capable for its size.</p>
<h3>Flash Attention and optimized KV Cache<span class="hx:absolute hx:-mt-20" id="flash-attention-and-optimized-kv-cache"></span>
    <a href="#flash-attention-and-optimized-kv-cache" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Flash Attention (Tri Dao, 2022) changed how attention is computed: instead of materializing the full attention matrix in memory, it processes in blocks keeping the data in the GPU&rsquo;s on-chip SRAM, reducing memory consumption from O(n²) to O(n). Without that, contexts of 128K+ tokens would be impractical. It already went through versions 2 and 3, with optimizations for FP8 and async operations on H100. PagedAttention (vLLM, UC Berkeley) did the same for the KV cache during serving: applies virtual memory concepts to the cache, eliminating fragmentation and improving throughput by 2-4x.</p>
<p>In Ollama, I set <code>OLLAMA_FLASH_ATTENTION=1</code> and <code>OLLAMA_KV_CACHE_TYPE=q8_0</code> on the server. The first activates flash attention, the second uses 8-bit KV cache instead of fp16, cutting the bandwidth needed per token in half. These are zero-hardware-cost optimizations that improve throughput measurably.</p>
<h3>What Qwen, Kimi, MiniMax and GLM are doing<span class="hx:absolute hx:-mt-20" id="what-qwen-kimi-minimax-and-glm-are-doing"></span>
    <a href="#what-qwen-kimi-minimax-and-glm-are-doing" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Qwen (Alibaba) has consistently been the best price/performance in open source models. Qwen3:14b is dense and strong; Qwen3.5:122b is MoE and runs surprisingly well in 96GB. GLM-4.7 (Zhipu AI) is notable for offering bf16 full precision versions that fit in 96GB. MiniMax experimented with long contexts (up to 4M tokens). Kimi (Moonshot AI) focused on large context windows with linear architectures.</p>
<h3>What runs well in 96GB of VRAM<span class="hx:absolute hx:-mt-20" id="what-runs-well-in-96gb-of-vram"></span>
    <a href="#what-runs-well-in-96gb-of-vram" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>With 96GB on the Strix Halo, the models that work well for daily use:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Size</th>
          <th>tok/s</th>
          <th>Use</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>qwen3.5:35b</td>
          <td>24 GB</td>
          <td>43.2</td>
          <td>General purpose, excellent</td>
      </tr>
      <tr>
          <td>qwen3-coder-next</td>
          <td>52 GB</td>
          <td>29.5</td>
          <td>Code, MoE</td>
      </tr>
      <tr>
          <td>qwen3.5:122b</td>
          <td>81 GB</td>
          <td>19.2</td>
          <td>Heavy but usable</td>
      </tr>
      <tr>
          <td>glm-4.7-flash:bf16</td>
          <td>60 GB</td>
          <td>17.9</td>
          <td>Full precision</td>
      </tr>
      <tr>
          <td>qwen2.5-coder:32b</td>
          <td>20 GB</td>
          <td>10.2</td>
          <td>Code, dense</td>
      </tr>
      <tr>
          <td>deepseek-r1:32b</td>
          <td>20 GB</td>
          <td>7.4</td>
          <td>Reasoning</td>
      </tr>
  </tbody>
</table>
<p>Dense 70B+ models (deepseek-r1:70b, llama3.3:70b) are still blocked by the ROCm bug I mentioned. When it gets fixed, they should run at ~4-6 tok/s, usable for batch but not for interactive chat.</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I bought the Minisforum to run models that don&rsquo;t fit in any gaming GPU. For that, it works. It&rsquo;s not fast. 19 tok/s on a 122B model isn&rsquo;t the experience you get with Claude or ChatGPT. But it&rsquo;s local, it&rsquo;s private, and it runs on my shelf consuming less power than an old lightbulb.</p>
<p>For people asking about the Mac Studio: if you have the budget, it&rsquo;s the best machine for running local LLMs. 512GB of unified memory, 819 GB/s of bandwidth, mature Metal/mlx ecosystem. You can run DeepSeek-V3 in full Q4. But in Brazil, with import duties, it crosses R$ 110k. The Minisforum with 128GB at R$ 12-15k is the realistic option.</p>
<p>And for people who think you can work around the VRAM limitation with layer offloading: you can&rsquo;t. PCIe is too slow. The model has to fit fully in VRAM for inference to be usable. It&rsquo;s the reason gaming GPUs with 32GB of ultra-fast GDDR7 are still capped on model size, and why the unified memory of the Strix Halo and Apple Silicon changed the equation.</p>
<p>In the <a href="/en/2026/03/31/migrating-my-home-server-with-claude-code/">next post</a> I tell the story of how I migrated the entire home server to the Minisforum using Claude Code, the problems I ran into, and how openSUSE MicroOS behaves as a Docker server operating system.</p>
]]></content:encoded><category>hardware</category><category>local-models</category><category>performance</category><category>reviews</category></item><item><title>Arch Linux - Best distro ever?</title><link>https://akitaonrails.github.io/en/2017/01/10/arch-linux-best-distro-ever/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2017/01/10/arch-linux-best-distro-ever/</guid><pubDate>Tue, 10 Jan 2017 14:25:00 -0200</pubDate><description>&lt;p&gt;&lt;strong&gt;Update 01/18/2017:&lt;/strong&gt; If you&amp;rsquo;re on old hardware like me, you may want to optimize your installation to be way more responsive, &lt;a href="http://www.akitaonrails.com/en/2017/01/17/otimizando-o-linux-para-computadores-lentos"target="_blank" rel="noopener"&gt;read all about it here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Since I decided to move back from macOS to Linux, I didn&amp;rsquo;t want to just return to old Ubuntu (yes, I get bored of doing the same things for too long). So I tried out Fedora 25 and I was delighted by how Gnome 3.22 evolved nicely.&lt;/p&gt;</description><content:encoded><![CDATA[<p><strong>Update 01/18/2017:</strong> If you&rsquo;re on old hardware like me, you may want to optimize your installation to be way more responsive, <a href="http://www.akitaonrails.com/en/2017/01/17/otimizando-o-linux-para-computadores-lentos"target="_blank" rel="noopener">read all about it here</a>.</p>
<p>Since I decided to move back from macOS to Linux, I didn&rsquo;t want to just return to old Ubuntu (yes, I get bored of doing the same things for too long). So I tried out Fedora 25 and I was delighted by how Gnome 3.22 evolved nicely.</p>
<p>Compared to Ubuntu, Fedora&rsquo;s defaults felt nicer. In practice, you can force any distro to become whatever you want, but I&rsquo;d rather not fight the defaults. Ubuntu is heavily customized for Unity and I really, really dislike it. It feels more like a toy than a serious environment to do work.</p>
<p>Fedora 25 looks good with Gnome 3, but it still gave me a few headaches. One thing that didn&rsquo;t work at all was <a href="https://wiki.gnome.org/Projects/GnomeOnlineAccounts"target="_blank" rel="noopener">Gnome&rsquo;s Online Accounts</a>. GOA collects authentication tokens after you sign in to your social services like Google. Then compatible apps like Evolution and the built-in Calendar can pick them up. But the tokens were getting expired all the time, so the integration was useless. And manually configuring Evolution for Google wasn&rsquo;t pleasant.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/584/Screenshot_from_2017-01-10_14-15-41.png" alt="Arch Linux"  loading="lazy" /></p>
<p>Now the surprise: on Arch Linux, I chose Gnome 3.22 as my desktop and manually installed the <a href="https://www.archlinux.org/packages/extra/i686/gnome-online-accounts/"target="_blank" rel="noopener">gnome-online-account</a> package. I signed in to my Google accounts and I am delighted to report that it&rsquo;s not expiring and it &ldquo;Just Works&rdquo;! This is the kind of polish I expect from a major distro, not the fiasco that&rsquo;s labeled as &ldquo;stable&rdquo; in Fedora.</p>
<h3>Why Arch Linux?<span class="hx:absolute hx:-mt-20" id="why-arch-linux"></span>
    <a href="#why-arch-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Most major distros are divided between &ldquo;stable&rdquo; (but with very old packages) and &ldquo;unstable&rdquo; (but with the cutting-edge goodness). If you install the Long-Term Support (LTS) versions, you&rsquo;re doomed to have only old packages from a couple of years ago. If you install the unstable repositories, you&rsquo;re doomed to have stuff exploding in your face without explanation and losing hours browsing through Stackoverflow.</p>
<p>Now, it seems that Arch figured out the exact &ldquo;right&rdquo; level of confidence between stable and cutting-edge. It keeps pushing the most recent version of software without breaking everything else all the time. So, in Ubuntu 16.04 and Fedora 25, if I want to install Postgresql, I am locked in to 9.4 or 9.5, but in Arch I can access 9.6 from the main Pacman repositories. (By the way, &ldquo;Pac&quot;kage &ldquo;Man&quot;ager is the most obvious name ever).</p>
<p>You can easily <code>pacman -Sy postgresql</code> and you&rsquo;re in business.</p>
<p>And if you&rsquo;re on Ubuntu 14.04 and now you want 16.04, good luck <code>dist-upgrade</code>-ing your way. It&rsquo;s easier to start from scratch.</p>
<p>So, it seems that the philosophy of Arch is to have the real most recent version of all software that won&rsquo;t break your system. There is no such thing as a big bang upgrade every 6 months that breaks everything. Instead, you have a constantly <strong>rolling</strong> upgrade system, where you&rsquo;re always on the latest version, without having to wait another year for the next big LTS.</p>
<p>Every major distro has &ldquo;unsupported&rdquo; repositories for proprietary binaries (codecs for example) or 3rd-party software. Then, there is Arch User Repository (AUR): a collection of small Git repositories from users maintaining simple <code>PKGBUILD</code> text files.</p>
<p>AUR is clever. If you&rsquo;re from macOS and familiar with Homebrew, you will understand it: it feels like Casks and Formulas. A <code>PKGBUILD</code> file can describe a recipe to download an available DEB package or tarball, disassemble it and rebuild it as a Pacman compatible package. It can describe the necessary dependencies and make the installation process super smooth.</p>
<p>For example, Sublime Text only has an option to download a DEB package or a tarball with the binaries. Same goes for Spotify, Franz, etc. Sometimes you can register Personal Package Archives (PPAs) and then <code>apt-get</code> your way in installing them. But you still need someone to build, maintain and distribute those packages properly. It&rsquo;s a lot of work.</p>
<p>Now, maintaining a simple Git repository with a simple PKGBUILD text file is far easier. <code>makepkg</code> does the hard work of building the package you need, in your machine, and then <code>pacman</code> can handle installing it like any other package. No more <code>wget</code>-ing tarballs and configuring everything manually!</p>
<p>Maybe I can finally just do <code>pacman -Syu</code> and have everything &ldquo;really&rdquo; upgraded without having to worry about the next big LTS that will eventually force me to reinstall everything from scratch.</p>
<h3>Arch Linux is perfect for &ldquo;Beginners&rdquo;<span class="hx:absolute hx:-mt-20" id="arch-linux-is-perfect-for-beginners"></span>
    <a href="#arch-linux-is-perfect-for-beginners" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I&rsquo;ve been hearing about Arch for a long while and their users are very enthusiastic in trying to convince other people to join. Whenever you see such a heavily loyal fanbase there must be something interesting hidden under the hood. Rolling upgrades, Pacman, AUR are really valuable reasons.</p>
<p>After just one day using it, I&rsquo;ve come to realize that Arch is good for advanced users, but also for <strong>beginners</strong>. But not because it is easy. On the contrary: it&rsquo;s because it is hard in the right way.</p>
<p>Most &ldquo;Linux users&rdquo; nowadays just get a trivial-to-install distro, such as Ubuntu or Elementary, and they have no idea what goes on underneath. Blindly clicking &ldquo;next&rdquo; in the graphical installers.</p>
<p>Most people have no idea what TTYs are. That you can probe USB devices with command line tools such as <code>lsusb</code> or that you must use tools such as <code>fdisk</code> to partition and then <code>mkfs.ext4</code> to format them. That memory swap files are partitions with a special format. They are not aware of LVM options for flexible partitioning, or even that LVM exists at all. That the &ldquo;thing&rdquo; you choose your kernel from in the boot menu is called Grub and that you can tweak it.</p>
<p>There is a lot going on in assembling a fully functional Linux-based distribution. But graphical installers hide most of it. Arch Linux forces you to go step by step and really feel like you &ldquo;own&rdquo; your machine, not the other way around.</p>
<p>If you&rsquo;re a &ldquo;beginner&rdquo;, I really urge you to install a distro like Arch a few times, in different configurations of machines, to really understand what an operating system really looks like.</p>
<p>The <a href="https://wiki.archlinux.org/"target="_blank" rel="noopener">Arch Wiki</a> is a very comprehensive and detailed repository of information for everything you ever need to know about installing and maintaining every component of a proper Linux system. You will learn a lot in the process.</p>
<p>But if you&rsquo;re like me, and you&rsquo;ve been doing that through all the mid 90&rsquo;s and early 2000&rsquo;s (heck, I had to learn my way through Slackware 1.0, I still remember having to use boot and root floppy disks and screwing up my hard-drives not understanding cylinders and sectors through fdisk), you can skip it. For you, advanced/experienced users, I&rsquo;d recommend you go with <a href="https://arch-anywhere.org/"target="_blank" rel="noopener">Arch Linux Anywhere</a>.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/586/big_20170109_150742.jpg" alt="Arch Anywhere"  loading="lazy" /></p>
<p>It will custom install Arch but will provide you with enough automation to not waste much time in bringing a properly configured Arch installation up and running, without bloatware.</p>
<h3>Pacaur - best way to deal with AUR<span class="hx:absolute hx:-mt-20" id="pacaur---best-way-to-deal-with-aur"></span>
    <a href="#pacaur---best-way-to-deal-with-aur" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Arch users are quick to praise Pacman. For the most part, you can basically do:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo pacman -S chromium</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And that&rsquo;s it. And then you can <code>sudo pacman -Syu</code> to upgrade installed packages. This is the basics you need to know.</p>
<p>If you&rsquo;re a developer I also recommend you to install the basic development packages:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo pacman -Sy --needed base-devel</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now, you can manually install AUR packages. You can go to their website and <a href="https://aur.archlinux.org/packages/?O=0&amp;K=terminix"target="_blank" rel="noopener">search for &ldquo;terminix&rdquo;</a> (a very nice Terminal replacement, similar to Mac&rsquo;s iTerm2) for example. You will end up in <a href="https://aur.archlinux.org/packages/terminix/"target="_blank" rel="noopener">this page</a> and you will have to manually do the following:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>git clone https://github.com/gnunn1/terminix.git
cd terminix
makepkg -si</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Feels simple, but you can do better by installing <a href="https://github.com/rmarquis/pacaur"target="_blank" rel="noopener">Pacaur</a>, a wrapper on top of Pacman. If you&rsquo;re using a graphical terminal such as Terminal or Terminix DO NOT FORGET to edit the profile to &ldquo;Run command as login shell&rdquo;, otherwise there will be a PATH problem and Cower will fail to install
.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo pacman -S expac yajl --noconfirm
gpg --recv-keys --keyserver hkp://pgp.mit.edu 1EB2638FF56C0C53
git clone https://aur.archlinux.org/cower.git
cd cower
makepkg -si
cd ..
git clone https://aur.archlinux.org/pacaur.git
cd pacaur
makepkg -si
cd ..</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In summary, Pacaur can be used not only as a complement to install AUR packages, but also if you want to use a single tool to manage both AUR and official Pacman packages. All commands <code>-S</code> will be Pacman commands. So instead of doing <code>sudo pacman -Syu</code> to upgrade all packages, you can replace it for <code>pacaur -Syu</code>. Everything else mostly &ldquo;just works&rdquo;.</p>
<p>When you try to install a package with <code>-S</code> it will first look into the official repos, if not found then it tries AUR. There&rsquo;s even a nice GUI if you want:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>pacaur -S pamac-pacaur</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now, to install the same Terminix, you can do just this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>pacaur -Sy terminix</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>It will ask you simple yes/no questions such as &ldquo;Do you want to edit the build file?&rdquo; You can answer &ldquo;n&rdquo; to those and confirm &ldquo;y&rdquo; when it asks you if you want to install the dependencies or the generated package.</p>
<p>And that&rsquo;s it! You can search the AUR repositories with:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>pacaur -s spotify</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>It will give you a lot of options, for example:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ pacaur -s spotify
aur/spotify 1.0.47.13-1 (1037, 36.09) [installed]
    A proprietary music streaming service
aur/playerctl 0.5.0-1 (127, 11.33)
    mpris media player controller and lib for spotify, vlc, audacious, bmp, xmms2, and others.
aur/blockify 3.6.3-3 (106, 5.61)
    Mutes Spotify advertisements.
...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Common sense, my friends. Read, interpret, choose. Arch requires you to be a <strong>smart</strong> person, and by &ldquo;smart&rdquo; I mean: knowing how to read! Most people skip reading things and just click stuff like morons.</p>
<p>Now that you know the exact name of the package you want, just install it normally like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>pacaur -S spotify</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/604/Screenshot_from_2017-01-16_14-21-49.png" alt="Pacaur"  loading="lazy" /></p>
<p>Pacaur is one of many <a href="https://wiki.archlinux.org/index.php/AUR_helpers"target="_blank" rel="noopener">AUR Helpers</a>. I was initially drawn into Yaourt, but after research, you figure out that you should only try out aurutils, bauerbill or pacaur. I prefer the latter because it&rsquo;s easier to spell out.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>pacaur -Syua</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This should keep your system up to date, both the official and AUR packages.</p>
<h3>Asdf - the last languages version manager you&rsquo;ll ever need<span class="hx:absolute hx:-mt-20" id="asdf---the-last-languages-version-manager-youll-ever-need"></span>
    <a href="#asdf---the-last-languages-version-manager-youll-ever-need" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If you&rsquo;re a Rubyist, you&rsquo;re familiar with RVM, rbenv, chruby. If you&rsquo;re from Node.js you know the RVM-inspired NVM to manage your different versions of Node. Each new language nowadays needs a version manager as they&rsquo;re evolving quickly and because if you work with client projects you will eventually need to use an old version to deal with legacy software.</p>
<p>So even though you can install the current stable Ruby 2.3.3 by just doing <code>pacman -S ruby</code> or <code>pacaur -S ruby</code> you will eventually need to switch back to Ruby 2.1 or older for a client project, for example.</p>
<p>Should you install RVM? Or rbenv? And how do you deal with different versions of Clojure, Go, Rust, Elixir?</p>
<p>That sounds like yet another maintenance nightmare to deal with. But someone decided to actually solve this problem in an elegant way. Enter <a href="https://github.com/asdf-vm/asdf"target="_blank" rel="noopener">asdf</a> - and if you happen to know <a href="https://github.com/HashNuke"target="_blank" rel="noopener">Akash Manohar</a> give him a big hug.</p>
<p>Let&rsquo;s install it (from the project&rsquo;s README):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.2.1</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Then edit your shell config files:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># For Ubuntu or other linux distros
echo -e &#39;\n. $HOME/.asdf/asdf.sh&#39; &gt;&gt; ~/.bashrc
echo -e &#39;\n. $HOME/.asdf/completions/asdf.bash&#39; &gt;&gt; ~/.bashrc

# OR for Mac OSX
echo -e &#39;\n. $HOME/.asdf/asdf.sh&#39; &gt;&gt; ~/.bash_profile
echo -e &#39;\n. $HOME/.asdf/completions/asdf.bash&#39; &gt;&gt; ~/.bash_profile

# For the Fish shell
echo &#39;source ~/.asdf/asdf.fish&#39; &gt;&gt; ~/.config/fish/config.fish
mkdir -p ~/.config/fish/completions; and cp ~/.asdf/completions/asdf.fish ~/.config/fish/completions

# If, like me, you like ZSH with YADR (you have to install YADR before this)
touch ~/.zsh.after/asdf.zsh
echo -e &#39;\n. $HOME/.asdf/asdf.sh&#39; &gt;&gt; ~/.zsh.after/asdf.zsh
echo -e &#39;\n. $HOME/.asdf/completions/asdf.bash&#39; &gt;&gt; ~/.zsh.after/asdf.zsh</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This tool is very self explanatory. Let&rsquo;s start by installing a bunch of plugins (full table of links in the README file):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>asdf plugin-add clojure https://github.com/vic/asdf-clojure.git
asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git
asdf plugin-add erlang https://github.com/asdf-vm/asdf-erlang.git
asdf plugin-add golang https://github.com/kennyp/asdf-golang.git
asdf plugin-add ruby https://github.com/asdf-vm/asdf-ruby.git
asdf plugin-add rust https://github.com/code-lever/asdf-rust.git
asdf plugin-add nodejs https://github.com/asdf-vm/asdf-nodejs.git</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you&rsquo;re like me, you must be <strong>super excited</strong> because you already know what we will do next:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo pacman -Sy jdk8-openjdk # you need Java for Clojure

asdf install clojure 1.8.0
asdf global clojure 1.8.0
mkdir ~/bin
wget https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein -O ~/bin/lein
chmod &#43;x ~/bin/lein
export PATH=$PATH:~/bin
# echo &#34;PATH=$PATH&#34; &gt; ~/.zsh.after/binpath.zsh # if you use YADR&#43;ZSH
lein

asdf install erlang 19.0
asdf global erlang 19.0

asdf install elixir 1.4.0
asdf global elixir 1.4.0
mix local.hex
mix local.rebar

asdf install golang 1.7.4
asdf global golang 1.7.4

asdf install ruby 2.4.0
asdf global ruby 2.4.0
gem install bundler

asdf install rust 1.14.0
asdf global rust 1.14.0

asdf install nodejs 7.4.0
asdf global nodejs 7.4.0
npm -g install brunch phantomjs</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>That&rsquo;s it! We now have every language we need installed and ready to use! What if I need Ruby 2.3.1 for a client project?</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>asdf install ruby 2.3.1
asdf local ruby 2.3.1</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And now I have 2.3.1 locally (I can change it to be the system default using <code>global</code>).</p>
<p>Most of the maintenance effort summarizes to this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>asdf plugin-update --all # update the individual plugins
asdf list-all [language] # to list all available versions</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And that&rsquo;s basically it! You have almost everything you need to develop software.</p>
<h3>Useful Software to Install<span class="hx:absolute hx:-mt-20" id="useful-software-to-install"></span>
    <a href="#useful-software-to-install" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Now, as usual, let me recommend some software:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># make sure you&#39;re up to date
sudo pacman -Syu

# install multimedia codecs
sudo pacman -Sy gstreamer0.10-plugins
sudo pacman -Sy exfat-utils fuse-exfat a52dec faac faad2 flac jasper lame libdca libdv gst-libav libmad libmpeg2 libtheora libvorbis libxv wavpack x264 xvidcore gstreamer0.10-plugins flashplugin libdvdcss libdvdread libdvdnav gecko-mediaplayer dvd&#43;rw-tools dvdauthor dvgrab pulseaudio-equalizer-

# if you need japanese fonts like me
sudo pacman -Sy adobe-source-han-sans-otc-fonts otf-ipafont

# some components that you will need
sudo pacman -Sy fuse-exfat 

# I personally like the Numix theme and Breeze Icons, change them with the Tweak Tool
sudo pacman -Sy numix-themes breeze-icons 

# Ifnstall more good looking fonts
sudo pacman -Sy ttf-dejavu 
pacaur -S ttf-ms-fonts ttf-vista-fonts ttf-liberation adobe-source-sans-pro-fonts ttf-ubuntu-font-family

# Firefox and Java plugin
sudo pacman -Sy icedtea-web firefox

# for devs
sudo pacman -Sy zsh the_silver_searcher gvim imagemagick htop
pacaur -Sy ttf-hack

# Native wrapper for Web apps such as Slack, Hangout, etc
pacaur -Sy franz-bin

# Best native Twitter client for Linux
pacaur -Sy corebird

# No need to explain
pacaur -Sy spotify
pacaur -Sy sublime-text-dev # install these plugins http://www.hongkiat.com/blog/sublime-text-plugins/

# If you like to read RSS
pacaur -Sy feedreader-beta

# if you need Office-like support
sudo pacman -Sy libreoffice-fresh

# if you need Photoshop-like support
sudo pacman -Sy gimp
sh -c &#34;$(curl -fsSL https://raw.githubusercontent.com/doctormo/GimpPs/master/tools/install.sh)&#34;

# if you want a really good video editor
sudo pacman -Sy frei0r-plugins dvdauthor vlc
pacaur -Sy kdenlive

# this can make CPU-intensive software to behave better and guarantee better user experience
pacaur -Sy ananicy-git

# dropbox is the most horrible piece of software, but you may need it:
pacaur -Sy dropbox dropbox-cli nautilus-dropbox</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>As usual, I like to replace Bash for Zsh and configure Vim with YADR:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sh -c &#34;`curl -fsSL https://raw.githubusercontent.com/skwp/dotfiles/master/install.sh`&#34;
touch ~/.vimrc.before
touch ~/.vimrc.after
echo &#34;let g:yadr_using_unsolarized_terminal = 1&#34; &gt;&gt; ~/.vimrc.before
echo &#34;let g:yadr_disable_solarized_enhancements = 1&#34; &gt;&gt; ~/.vimrc.after
echo &#34;colorscheme gruvbox&#34; &gt;&gt; ~/.vimrc.after</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>To install and configure Postgresql 9.6:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo pacman -Sy postgresql
sudo -u postgres -i
initdb --locale $LANG -E UTF8 -D &#39;/var/lib/postgres/data&#39;
exit

# do not do this in Production machines
sudo sed -i.bak &#39;s/ident/trust/&#39; /var/lib/postgres/data/pg_hba.conf
sudo systemctl start postgresql
sudo systemctl enable postgresql

sudo -u postgres -i
createuser --interactive # create with your username and superuser role
createdb youruser
exit
sudo systemctl restart postgresql</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you want to install Docker:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo pacman -Sy docker
sudo usermod -aG docker $USER
sudo systemctl start docker
sudo systemctl enable docker
logout</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>We always need Redis, Memcached, so let&rsquo;s install them:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo pacman -Sy redis memcached
sudo systemctl start redis
sudo systemctl enable redis
sudo systemctl start memcached
sudo systemctl enable memcached</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>After you install and remove a lot of software, you may end up with unnecessary packages eating up disk space. You can clean it up with:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo pacman -Rns $(pacman -Qtdq)</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And as I said before, the Arch Wiki is super useful for you to keep tweaking your system, so make sure you read articles such as <a href="https://wiki.archlinux.org/index.php/Improving_performance"target="_blank" rel="noopener">this &ldquo;Improving Performance&rdquo; page</a>.</p>
<h3>Desktop Kernels<span class="hx:absolute hx:-mt-20" id="desktop-kernels"></span>
    <a href="#desktop-kernels" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>One thing to keep in mind about most Linux distros is that the kernel is usually compiled to be better optimized for Servers.</p>
<p>Modern hardware, especially with lots of RAM and equipped with an SSD &ldquo;should&rdquo; work well enough. But not always, you may experience some &ldquo;stutters&rdquo; or even total unresponsiveness.</p>
<p>There are many reasons why, but the 2 main culprits are application memory being paged out to disk swap and the I/O scheduler of the Linux kernel.</p>
<p>In a server scenario, you want processes to have a fair share of resources, which is why a process scheduler such as CFS - Completely Fair Scheduler - and CFQ - Complete Fairness Queueing - for I/O, are fantastic.</p>
<p>But in the Desktop the story is totally different. You are willing to trade-off high throughput for lower latency in order to have responsiveness. No one wants to have their UI and mouse pointer frozen while copying large files to USB drives, or while waiting for that nasty <code>make -j9</code> to finish compiling your also nasty gcc-gcj. You may end up with a frozen UI for hours! This is just unacceptable!</p>
<p>What you want for Desktop usage, with dozens of random processes doing random operations, is almost &ldquo;soft real time&rdquo; configuration. A more aggressive preemption where the Kernel gives some control back to the UI so you can do other stuff - albeit slower. Low latency is the key to have a smooth user experience.</p>
<p>To increase responsiveness, the most important first thing you want to do is configure this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo tee -a /etc/sysctl.d/99-sysctl.conf &lt;&lt;-EOF
vm.swappiness=1
vm.vfs_cache_pressure=50
vm.dirty_background_bytes=16777216
vm.dirty_bytes=50331648
EOF</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Reboot. If you want to know what those settings are, <a href="https://rudd-o.com/linux-and-free-software/tales-from-responsivenessland-why-linux-feels-slow-and-how-to-fix-that"target="_blank" rel="noopener">read this</a>.</p>
<p>And you may want to install a customized Kernel with a different Scheduler. There are 3 options nowadays: <a href="https://aur.archlinux.org/packages/linux-zen-git/"target="_blank" rel="noopener">Zen</a>, <a href="https://liquorix.net/"target="_blank" rel="noopener">Liquorix</a> and <a href="https://aur.archlinux.org/packages/linux-ck/"target="_blank" rel="noopener">CK</a>.</p>
<p>I am still not 100% sure which one is the best, they have a few maintenance concerns.</p>
<p>Out of the 3, you will want to stick to Zen (which is <a href="https://github.com/zen-kernel/zen-kernel/issues/30#issuecomment-142787936"target="_blank" rel="noopener">basically Liquorix</a>), as it&rsquo;s maintained in the official repositories in binary format (believe me, you don&rsquo;t want to wait for a custom kernel to compile, it takes forever).</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo pacman -Sy linux-zen
sudo grub-mkconfig -o /boot/grub/grub.cfg</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Reboot.</p>
<p>The main change is probably the I/O Scheduler, upgrading from the standard CFQ to BFQ. If you&rsquo;re using a mechanical hard drive you will want to use the better BFQ (which Zen enables by default). If you&rsquo;re using an SSD you will want &lsquo;deadline&rsquo; instead.</p>
<p>DO NOT INSTALL THOSE KERNELS IN PRODUCTION SERVERS! They are intended for desktop usages only!</p>
<p>For the most part, Zen may have the better balance between stability and tweak set. You may want to use it, especially in older hardware. Modern hardware, as I said, may not notice too much difference.</p>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I am not sure if it is the Arch maintainers that are doing a super job or if it&rsquo;s RedHat and Canonical that are screwing up their distros so badly in comparison.</p>
<p>I mean, Ubuntu, Fedora, OpenSuse, Elementary, are all fair and nice distros that, most of the time, &ldquo;just work&rdquo;.</p>
<p>But for a distro that many consider targeted to &ldquo;advanced users&rdquo;, Arch is way more polished. I can&rsquo;t figure out why.</p>
<p>In the same hardware, the Gnome 3 experience under Arch is noticeably better than the same Gnome 3 over Fedora. Compared to Unity on Ubuntu, it&rsquo;s miles ahead. It&rsquo;s fast, fluid, good looking, the defaults all work out nicely.</p>
<p>And all of a sudden I realize that I don&rsquo;t have to worry about major upgrades. Rolling upgrades to the latest continuously brings me another layer of confidence.</p>
<p>Arch makes me feel like I am in control again without requiring me to waste hours tweaking things to my liking. The defaults are rock solid and I can make small improvements over it whenever I need to.</p>
<p>Kudos to the Arch maintainers, this is the finest Linux distro I&rsquo;ve ever had the pleasure to play with. I hope this feeling goes on as I keep using this as my daily driver. But so far I am convinced that this is the right choice.</p>
]]></content:encoded><category>linux</category><category>reviews</category></item><item><title>Choosing MatterMost over Rocket.chat and Slack</title><link>https://akitaonrails.github.io/en/2016/08/13/choosing-mattermost-over-rocket-chat-and-slack/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2016/08/13/choosing-mattermost-over-rocket-chat-and-slack/</guid><pubDate>Sat, 13 Aug 2016 22:41:00 -0300</pubDate><description>&lt;p&gt;I am a technologist, basically a nerd. So when I obsess over technical stuff, no matter how small, I just can&amp;rsquo;t sleep calmly until I find reasonable closure.&lt;/p&gt;
&lt;p&gt;My team, my clients, we&amp;rsquo;ve been happily using Slack for more than 2 years, as I believe was true for many teams around the world. Although no one ever complained, I was always annoyed by the small things. First of all, as everything of value, it costs. Either I pay USD 6.67/month/user or I live with the restrictions of the free plan. And as most other teams, I lived with the restrictions for as long as I could.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I am a technologist, basically a nerd. So when I obsess over technical stuff, no matter how small, I just can&rsquo;t sleep calmly until I find reasonable closure.</p>
<p>My team, my clients, we&rsquo;ve been happily using Slack for more than 2 years, as I believe was true for many teams around the world. Although no one ever complained, I was always annoyed by the small things. First of all, as everything of value, it costs. Either I pay USD 6.67/month/user or I live with the restrictions of the free plan. And as most other teams, I lived with the restrictions for as long as I could.</p>
<p>For example, to get rid of the warnings to upgrade because we hit the upload limits, I created a small tool called <a href="https://github.com/akitaonrails/slack_cleanup"target="_blank" rel="noopener">slack_cleanup</a> (first in Elixir, and then in <a href="https://github.com/akitaonrails/cr_slack_cleanup"target="_blank" rel="noopener">Crystal</a> just for exercise). This helped for a while.</p>
<p>Our team kept growing, steadily, frequently, as well as clients. The more users we add, the more conversations, the faster we hit the restrictions. History gets lost more frequently, we need to clean up uploads more often. It gets old very fast.</p>
<p>One thing I value above everything is knowledge. As a small example, I keep multiple backups of all my emails, all my projects, all my assets that I produced in the last 20 years. Heck, I have a 6 TB, Raid-5, Drobo system right in my home desk and 3 extra 1TB external drives for backup. I&rsquo;ve lost very little over the years.</p>
<p>It really annoys me when I lose information.</p>
<ul>
<li>
<p>Gmail Business, DropBox, AWS S3 buckets, being external services, don&rsquo;t worry me because I keep copies of everything offline. So if those accounts get busted all of a sudden, I have multiple copies.</p>
</li>
<li>
<p>GitHub annoyed me a bit because although I have multiple copies of the repositories, I would lose all the Pull Request and Issues history. That&rsquo;s one reason I moved to my own GitLab and helped tweak the import process to grab that Pull Request history.</p>
</li>
<li>
<p>Slack annoys me a lot for the aforementioned reasons, which is why last week we tried MatterMost first, and then we deployed Rocket.chat.</p>
</li>
</ul>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/558/big_8_13_16__22_20.png" alt="Mattermost"  loading="lazy" /></p>
<h3>There, and Back Again<span class="hx:absolute hx:-mt-20" id="there-and-back-again"></span>
    <a href="#there-and-back-again" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>We moved from Slack to Rocket.chat and a couple of days later we are moving again, now to Mattermost.</p>
<p>Yes, this was cumbersome. My team was not very happy about leaving Slack. Slack really is slick, full-featured, good-looking, well-rounded, a proper web product for this generation. Any alternative should be at least almost as good-looking, and at least have bug-free features, including webhooks.</p>
<p>Mattermost fits the bill almost perfectly but the open sourced Team Edition lacked one important feature for me: the ability to disallow normal members to rename and/or delete private groups. Yes, we expect grown-ups to behave, but when you have remote teams, remote clients, external users with no commitment to the company, it&rsquo;s a hassle.</p>
<p>Yes, I could and I probably should use the paid Enterprise Edition. But for just that small feature, it felt too expensive. So that triggered me to let it go and install Rocket.chat instead. I moved my entire team there (almost 50 people, because I didn&rsquo;t move the clients yet). That would be the end of the story.</p>
<p>But Rocket.chat has a complex infrastructure to deal with (you must have at the very least 3 boxes or pay extra for a proper Mongodb SaaS). Actually, <a href="http://www.akitaonrails.com/en/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough"target="_blank" rel="noopener">in my previous post</a> I explained why you MEAN guys should not be careless dealing with Mongodb. In a nutshell: Mongodb was not meant to run in a single instance, you must have a replica set. If you have a single instance Mongodb, you&rsquo;re doing it wrong.</p>
<p>And the most problematic: the client-side is just too heavy. It frequently spikes out the CPU in not so powerful machines. It&rsquo;s noticeably and measurably slower to navigate its UI, compared to Slack. MatterMost&rsquo;s UI was much faster and way more responsive.</p>
<p>I was almost willing to overlook the lack of a proper test suite. I was willing to try to live with Meteor and CoffeeScript. I was willing to deal with the complex MongoDB maintenance.</p>
<p>But slow responsiveness across many of the members of my team is a no-go, a big show-stopper. We turned the beta videochat feature off (as it always triggers CPU spikes across all users), but many members still had a bad experience with a UI that was too slow and a resource hog.</p>
<p>The React-based, ES6-written, properly structured - with good enough client-side test suites - MatterMost was a more suitable candidate. So I decided to really think about the original problem and I came up with a simple solution: <a href="http://www.akitaonrails.com/en/2016/08/12/hackeando-o-mattermost-team-edition"target="_blank" rel="noopener">add a simple PLPGSQL function</a> to be triggered whenever someone tried to delete a channel. Sure enough, it worked. And that prompted me to call my team again and propose this new change: I believe everybody was on-board as MatterMost was way faster on their machines.</p>
<p>I know I am sounding really harsh towards Rocket.chat, and it&rsquo;s really not my intention. If we didn&rsquo;t have any other options, we would still move to Rocket.chat. But as Mattermost proved to be the better choice, it was a no-brainer.</p>
<h3>MatterMost Install<span class="hx:absolute hx:-mt-20" id="mattermost-install"></span>
    <a href="#mattermost-install" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>As always, I will not bore you with instructions that are already available online. If you want to install everything manually, follow <a href="https://docs.mattermost.com/install/prod-ubuntu.html"target="_blank" rel="noopener">this tutorial</a>, but a better option would be <a href="https://docs.mattermost.com/install/prod-docker.html"target="_blank" rel="noopener">the Docker-based deployment</a>. You can even install/upgrade it <a href="http://docs.gitlab.com/omnibus/gitlab-mattermost/"target="_blank" rel="noopener">together with GitLab</a>.</p>
<p>Make sure you have NGINX in front of the server and that you have both a properly registered domain or sub-domain, and that you have SSL - use Let&rsquo;s Encrypt.</p>
<p>Because I want to keep tweaking and experimenting with the codebase in a live installation, I installed everything manually and I have this directory structure:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>-rw-r--r--  1 mattermost mattermost     6504 Aug 13 20:11 config.json
drwxrwxr-x  5 mattermost mattermost     4096 Aug 13 20:30 data
lrwxrwxrwx  1 mattermost mattermost       33 Aug 13 20:13 mattermost -&gt; mattermost-team-3.3.0-linux-amd64
drwxr-xr-x  9 mattermost mattermost     4096 Aug 13 20:28 mattermost-team-3.2.0-linux-amd64
-rw-rw-r--  1 mattermost mattermost 19968308 Jul 14 16:37 mattermost-team-3.2.0-linux-amd64.tar.gz
drwxr-xr-x 11 mattermost mattermost     4096 Aug 13 20:28 mattermost-team-3.3.0-linux-amd64
-rw-rw-r--  1 mattermost mattermost 20241448 Aug 12 20:41 mattermost-team-3.3.0-linux-amd64.tar.gz
drwxrwxr-x  3 mattermost mattermost     4096 Aug 13 20:23 platform-dev-3.3.0
-rw-r--r--  1 mattermost mattermost 18060203 Aug 13 20:22 platform-dev.tar.gz</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>So I have a restricted, sudo user <code>mattermost</code> and I have a main <code>mattermost</code> directory pointing to the binary packages you will find in the <a href="https://www.mattermost.org/download/"target="_blank" rel="noopener">official download page</a>.</p>
<p>Notice that I have a copy of <code>mattermost/config/config.json</code> in the home directory. I leave it there so every time I download a new version and redo the symlink to <code>mattermost</code>, I can just do:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>rm -Rf ~/mattermost/config/config.json
cd ~/mattermost/config
ln -s ~/mattermost/config.json</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Also make sure you change at least the following in the config:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
&#34;FileSettings&#34;: {
    &#34;MaxFileSize&#34;: 83886080,
    &#34;DriverName&#34;: &#34;local&#34;,
    &#34;Directory&#34;: &#34;/home/mattermost/data&#34;,
...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you want, you can set file uploads to go to some S3 bucket you have, and just fill in the AWS details. But if you choose to have them locally, change the directory to somewhere outside of the <code>mattermost</code> folder, as on every upgrade you will change the folder. With both AWS EC2 or Digital Ocean you can always choose to add a secondary volume that can outlive the virtual boxes, so even if you get to a point where you have hundreds of concurrent users and you want to scale horizontally, you can have all your boxes pointing to a shared volume (AWS EFS, for example).</p>
<p>Speaking of which, in this configuration, upgrading would be like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo service mattermost stop
wget https://releases.mattermost.com/x.y.z/mattermost-team-x.y.z-linux-amd64.tar.gz
tar xvfz mattermost-team-x.y.z-linux-amd64.tar.gz
mv mattermost mattermost-team-x.y.z-linux-amd64
ln -s mattermost-team-x.y.z-linux-amd64 mattermost
rm -Rf mattermost/config/config.json
rm -Rf mattermost/data
cd mattermost/config
ln -s ~/config.json
cd ..
ln -s ~/data
sudo service mattermost start</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The reason I wanted to have it this way is because I can tweak the code and manually push the changes.</p>
<p>For your development machine you should follow <a href="https://docs.mattermost.com/developer/developer-setup.html"target="_blank" rel="noopener">this instruction</a>. If you&rsquo;re in OS X and you choose to use Docker Toolbox, remember that you don&rsquo;t need VirtualBox anymore as it will use OS X&rsquo;s native hypervisor now. On my machine, I had to add <code>dockerhost</code> manually in my <code>/etc/hosts</code> because <code>boot2docker</code> was failing to get my ip address.</p>
<p>Then you can just clone the code from Github:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mkdir mattermost
cd mattermost
git clone https://github.com/mattermost/platform
git checkout -b v3.3.0 v3.3.0</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Remember to always checkout the correct stable version (v3.3.0 as of the time when I originally posted this article) that you have installed in your server. Again, I will not bore you with what&rsquo;s already documented in the links above, but you must have Go 1.6(.3), Docker, Docker-Composer, Docker-Machine all installed already.</p>
<p>You can execute <code>make run</code> and after it finishes (and as usual, npm will make sure it takes a very long time) you can open <code>http://localhost:8065</code> to play with it locally.</p>
<p>Most importantly: you can tweak the React JSX components from <code>platform/webapp/components</code>, for example, the <code>channel_header.jsx</code> and add stuff like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="err">#</span> <span class="nx">line</span> <span class="mi">493</span> <span class="nx">from</span> <span class="nx">v3</span><span class="p">.</span><span class="mf">3.0</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span><span class="p">(</span><span class="nx">isAdmin</span> <span class="o">||</span> <span class="nx">isSystemAdmin</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">dropdownContents</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="o">&lt;</span><span class="nx">li</span>
</span></span><span class="line"><span class="cl">            <span class="nx">key</span><span class="o">=</span><span class="s1">&#39;rename_channel&#39;</span>
</span></span><span class="line"><span class="cl">            <span class="nx">role</span><span class="o">=</span><span class="s1">&#39;presentation&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="o">&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="o">&lt;</span><span class="nx">a</span>
</span></span><span class="line"><span class="cl">                <span class="nx">role</span><span class="o">=</span><span class="s1">&#39;menuitem&#39;</span>
</span></span><span class="line"><span class="cl">                <span class="nx">href</span><span class="o">=</span><span class="s1">&#39;#&#39;</span>
</span></span><span class="line"><span class="cl">                <span class="nx">onClick</span><span class="o">=</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">showRenameChannelModal</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="o">&gt;</span>
</span></span><span class="line"><span class="cl">                <span class="o">&lt;</span><span class="nx">FormattedMessage</span>
</span></span><span class="line"><span class="cl">                    <span class="nx">id</span><span class="o">=</span><span class="s1">&#39;channel_header.rename&#39;</span>
</span></span><span class="line"><span class="cl">                    <span class="nx">defaultMessage</span><span class="o">=</span><span class="s1">&#39;Rename {term}...&#39;</span>
</span></span><span class="line"><span class="cl">                    <span class="nx">values</span><span class="o">=</span><span class="p">{{</span>
</span></span><span class="line"><span class="cl">                        <span class="nx">term</span><span class="o">:</span> <span class="p">(</span><span class="nx">channelTerm</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="p">}}</span>
</span></span><span class="line"><span class="cl">                <span class="o">/&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="o">&lt;</span><span class="err">/a&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="o">&lt;</span><span class="err">/li&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">ChannelStore</span><span class="p">.</span><span class="nx">isDefault</span><span class="p">(</span><span class="nx">channel</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">dropdownContents</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">deleteOption</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And you know what this will do? Remove the &ldquo;Rename Group&rdquo; and &ldquo;Delete Group&rdquo; options from the channel menu if the user is not a system admin. Now how do you put this in your server?</p>
<p>First of all, you have to edit the <code>Makefile</code> at this section:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>@# Make osx package
@# Copy binary
cp $(GOPATH)/bin/darwin_amd64/platform $(DIST_PATH)/bin
@# Package
tar -C dist -czf $(DIST_PATH)-$(BUILD_TYPE_NAME)-osx-amd64.tar.gz mattermost
@# Cleanup
rm -f $(DIST_PATH)/bin/platform

@# Make windows package
@# Copy binary
cp $(GOPATH)/bin/windows_amd64/platform.exe $(DIST_PATH)/bin
@# Package
tar -C dist -czf $(DIST_PATH)-$(BUILD_TYPE_NAME)-windows-amd64.tar.gz mattermost
@# Cleanup
rm -f $(DIST_PATH)/bin/platform.exe

@# Make linux package
@# Copy binary
cp $(GOPATH)/bin/platform $(DIST_PATH)/bin
@# Package
tar -C dist -czf $(DIST_PATH)-$(BUILD_TYPE_NAME)-linux-amd64.tar.gz mattermost
@# Don&#39;t cleanup linux package so dev machines will have an unziped linux package avalilable
@#rm -f $(DIST_PATH)/bin/platform</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Notice that the developer that made this file is probably using Linux. That&rsquo;s because this file builds the Linux-ELF binary as <code>bin/platform</code>, while the OS X Mach-O binary will be at <code>bin/darwin_amd64/platform</code>. Now, if like me you&rsquo;re on OS X, you have to invert this and make the Linux version point to <code>bin/linux_amd64/platform</code>, otherwise the packages will have the wrong binary.</p>
<p>Then you will notice that there is this section right at the bottom:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>setup-mac:
    echo $$(boot2docker ip 2&gt; /dev/null) dockerhost | sudo tee -a /etc/hosts</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you&rsquo;re on the newest versions of Docker-Toolbox, you don&rsquo;t need boot2docker anymore, it will use docker-machine instead. So this line doesn&rsquo;t work and the easiest way to make <code>make test</code> run is to change the <code>dockerhost</code> to <code>localhost</code> in the <code>config/config.json</code>&rsquo;s PostgreSQL configuration.</p>
<p>If the tests are all passing, you can now run <code>make package</code> to make all packages, then you can send the <code>dist/mattermost-team-edition-linux-amd64.tar.gz</code> to your server and uncompress to the correct location.</p>
<p>As a sidenote for Go noobies like myself, I had my <code>GOPATH</code> pointed to <code>/Users/akitaonrails/.go</code> and my project was at <code>/Users/akitaonrails/codeminer42/mattermost/platform</code>. I was told that this is incorrect and the source of my many hours of frustration. The project MUST BE INSIDE THE GOPATH.</p>
<p>So I changed my <code>GOPATH</code> to <code>/Users/akitaonrails/Sites/go</code> and the project to <code>/Users/akitaonrails/Sites/go/src/github.com/mattermost/platform</code> and now it works. I would probably have symlinked the project path to be inside the GOPATH as well. But now everything compiles and runs just fine.</p>
<p>This is how I will keep experimenting for the time being, until I feel that I am comfortable enough to automate the whole process later. This should work just fine for my team for the time being.</p>
<p>Of course, the snippet above is nothing but a <strong>dirty hack</strong>, don&rsquo;t try to contribute like this. A proper implementation would require me to at least create a new setting into the <code>config.json</code>, for example <code>TeamSettings.MembersCanManageChannel</code> to <code>false</code>, and change the <code>api/context.go</code> around line 347 to be something like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">func</span> <span class="p">(</span><span class="n">c</span> <span class="o">*</span><span class="no">Context</span><span class="p">)</span> <span class="no">HasPermissionsToTeam</span><span class="p">(</span><span class="n">teamId</span> <span class="n">string</span><span class="p">,</span> <span class="n">where</span> <span class="n">string</span><span class="p">)</span> <span class="n">bool</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">c</span><span class="o">.</span><span class="n">IsSystemAdmin</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">!</span><span class="n">utils</span><span class="o">.</span><span class="n">cfg</span><span class="o">.</span><span class="n">TeamSettings</span><span class="o">.</span><span class="n">MembersCanManageChannel</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Then change the <code>webapp/channel_header.jsx</code> component in the React front-end (as well as proper unit tests to <code>webapp/tests/client_channel.test.jsx</code>), make sure the <code>make test</code> passes, and then finally create a feature request to the core team.</p>
<p>But the idea here was just to show that it was not so difficult to solve the show-stopper for my particular scenario, both using the <a href="http://www.akitaonrails.com/en/2016/08/12/hackeando-o-mattermost-team-edition"target="_blank" rel="noopener">SQL trigger</a> to safeguard the database and the hack to fix the Web UI.</p>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>But what about the most important feature of all? RightGIF support? There is nothing as simple as a rightgif slack command just yet. Fortunately you can compensate most missing niceties like this by installing a <a href="https://www.npmjs.com/package/hubot-mattermost"target="_blank" rel="noopener">Hubot server</a>, and linking it to a user so you can chat with the bot and make it do things for you (set an alarm, fetch a gif, translate text, etc). As a caveat, the hubot adapter requires the use of the <a href="https://github.com/loafoe/mattermost-client"target="_blank" rel="noopener">mattermost-client</a> which must be synced with the server platform version releases to work properly, so be careful when you&rsquo;re upgrading.</p>
<p>Overall, Mattermost is a great choice. It&rsquo;s not for amateurs either, the development environment requires you to know your Docker stuff. It requires you to know proper Go-lang configuration. It requires you to follow <a href="https://docs.mattermost.com/developer/contribution-guide.html"target="_blank" rel="noopener">proper procedures to contribute</a>, as they should. The project itself is a single codebase divided into roughly 2 applications, a Go-based HTTP API and a React-based front-end to consume the APIs. Everything about the project is automated through the proper usage of Docker images (for mysql, postgresql, openldap instances) and Makefiles to run the test suite, create packages, etc.</p>
<p>And it has some conveniences from Slack that Rocket.chat doesn&rsquo;t have yet such as a simple shortcut to switch channels (Cmd-K), the ability to reply to messages and organize them as threads, proper and more complete Markdown support. Overall, the features are well-rounded, and not half-baked. What is there is solid and works, it&rsquo;s always bad to have half-finished features.</p>
<p>With these hacks in mind I can strongly recommend that you use Mattermost. And as I said in previous posts, it&rsquo;s not just a matter of cost. If you&rsquo;re a small team, without internal developers or someone that can maintain your own installation, you should definitely pay Mattermost for the Enterprise support, it&rsquo;s affordable and way cheaper than Slack.</p>
<p>For now, it&rsquo;s the better choice, both in terms of overall fit-and-finish, well-rounded features, simple and responsive UI, good code quality and overall care on the engineering.</p>
]]></content:encoded><category>reviews</category><category>containers</category></item><item><title>Moving away from Pivotal Tracker to Fulcrum. Good Enough.</title><link>https://akitaonrails.github.io/en/2016/08/10/moving-away-from-pivotal-tracker-to-fulcrum-good-enough/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2016/08/10/moving-away-from-pivotal-tracker-to-fulcrum-good-enough/</guid><pubDate>Wed, 10 Aug 2016 18:30:00 -0300</pubDate><description>&lt;p&gt;I&amp;rsquo;ve been blogging about how I am building my own internal infrastructure. Already reported on how I moved from &lt;a href="http://www.akitaonrails.com/en/2016/08/03/migrando-para-o-gitlab-sim-vale-a-pena"target="_blank" rel="noopener"&gt;GitHub and Travis to GitLab&lt;/a&gt;, then how we moved from &lt;a href="http://www.akitaonrails.com/en/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough"target="_blank" rel="noopener"&gt;Slack to Rocket.chat&lt;/a&gt;, but the original first move is the subject of this article.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been using Pivotal Tracker since 2010 up to 2015 and this is because I believe it is far superior to Trello and other &lt;a href="http://www.akitaonrails.com/en/2009/12/16/off-topic-cuidado-com-o-kanban-butt"target="_blank" rel="noopener"&gt;glorified to-do lists&lt;/a&gt; out there.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/Codeminer42/cm42-central"target="_blank" rel="noopener"&gt;&lt;img src="https://raw.githubusercontent.com/Codeminer42/cm42-central/master/doc/cm42-central-screenshot.png" alt="Cm42-Central" loading="lazy" /&gt;&lt;/a&gt;&lt;/p&gt;</description><content:encoded><![CDATA[<p>I&rsquo;ve been blogging about how I am building my own internal infrastructure. Already reported on how I moved from <a href="http://www.akitaonrails.com/en/2016/08/03/migrando-para-o-gitlab-sim-vale-a-pena"target="_blank" rel="noopener">GitHub and Travis to GitLab</a>, then how we moved from <a href="http://www.akitaonrails.com/en/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough"target="_blank" rel="noopener">Slack to Rocket.chat</a>, but the original first move is the subject of this article.</p>
<p>I&rsquo;ve been using Pivotal Tracker since 2010 up to 2015 and this is because I believe it is far superior to Trello and other <a href="http://www.akitaonrails.com/en/2009/12/16/off-topic-cuidado-com-o-kanban-butt"target="_blank" rel="noopener">glorified to-do lists</a> out there.</p>
<p><a href="https://github.com/Codeminer42/cm42-central"target="_blank" rel="noopener"><img src="https://raw.githubusercontent.com/Codeminer42/cm42-central/master/doc/cm42-central-screenshot.png" alt="Cm42-Central"  loading="lazy" /></a></p>
<h3>The Cost Conundrum<span class="hx:absolute hx:-mt-20" id="the-cost-conundrum"></span>
    <a href="#the-cost-conundrum" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>As with all the other reports, let me start with the cost argument. Pivotal Tracker is <a href="http://www.pivotaltracker.com/why-tracker/pricing/"target="_blank" rel="noopener">priced</a> based on the number of users.</p>
<p>If you have a small team and not many outside clients, a basic 10-user plan would be enough, and you will pay just USD 420 a year. Just pay it already.</p>
<p>If you have up to 25 users, it&rsquo;s still not so expensive, and you will pay USD 1,800 a year. Well worth it.</p>
<p>Even if you have 50, the maximum plan, you will pay USD 3,600 a year. More than that and you have to contact them directly.</p>
<p>The cost is not prohibitive and if you can pay, I advise you to do so.</p>
<p>In my case, the strategy is to get full control over our data and knowledge, which is why I&rsquo;d rather have ownership of the service.</p>
<p>And because Fulcrum was &ldquo;almost&rdquo; good enough, I decided to try it out and see if we could evolve it over time. And I think that even though we are still very far from the full-featured Tracker, what we have is enough for most of our projects.</p>
<p>Let me say it again: you most probably won&rsquo;t benefit if you&rsquo;re a small team. Pivotal Tracker is, to this day, the best project management tool for software development teams, and you should definitely try it out first, get used to its workflow (as I will explain below), and then decide if you want to take control and install the open source alternative instead.</p>
<p>There is the factor of cost, but it&rsquo;s not the driving incentive for this move.</p>
<h3>The Important Features<span class="hx:absolute hx:-mt-20" id="the-important-features"></span>
    <a href="#the-important-features" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Doing some research I found the Fulcrum project. It&rsquo;s a very simple, very bare-bones Rails project that mimics Tracker&rsquo;s most important features.</p>
<p>Any competent project management tool <strong>must</strong> have at the very least 4 pillars:</p>
<ul>
<li>Stories (with discussion threads, assets uploading, tags, Epics organization, and searchability)</li>
<li>Proper numeric estimation (story points, proportional size for stories)</li>
<li>Time-boxed Sprints (short, fixed periods of time)</li>
<li>Velocity (the actual productivity of a particular team in a particular project)</li>
</ul>
<p>Any tool that has all of the above is useful.</p>
<p>Fulcrum itself has almost everything. We made an internal fork of the project and we started adding some of the missing features. We&rsquo;ve been using it for around 1 year already and it&rsquo;s been good enough for our routine. There are still a few bugs and missing features that we intend to add.</p>
<p>If you&rsquo;re interested, I am making this fork <a href="https://github.com/Codeminer42/cm42-central"target="_blank" rel="noopener">publicly available</a> to conform to the Affero GPL license.</p>
<p>Any contribution is welcome.</p>
<h3>On Proper Agile Management<span class="hx:absolute hx:-mt-20" id="on-proper-agile-management"></span>
    <a href="#on-proper-agile-management" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Now sidetracking a bit, I&rsquo;d like to explain why this kind of tool (Tracker-like) is the best compared to what I called &ldquo;glorified to-do lists&rdquo;.</p>
<p>The principles are very simple:</p>
<ul>
<li>Every project must start with the most complete backlog possible (uncertainty is at its highest in the beginning, and details will be missing, but it&rsquo;s important to have a vision of what &ldquo;minimally complete&rdquo; means and the order of importance of each story, a proper &ldquo;priority&rdquo;).
<ul>
<li>The level of detail doesn&rsquo;t matter. A story must have enough information that any developer in the team can implement the required tasks. Otherwise it must be tagged &ldquo;blocked&rdquo; so the Product Owner knows that (s)he has to add more details and no developer will touch it until the tag is removed.</li>
</ul>
</li>
<li>All stories <strong>must be estimated</strong> (more on that on the next section). The unit really doesn&rsquo;t matter. You can think in hours, in fractions of a day or for what it really is: a proportion. A story sized &ldquo;2&rdquo; should be roughly half the complexity of a Story sized &ldquo;4&rdquo;. That&rsquo;s it. Even &ldquo;blocked&rdquo; stories should receive sizes.</li>
<li>Projects must be divided into time-boxed, fixed-sized Sprints, so the team has the opportunity to reassess frequently. This is where estimation can be refined, where more levels of detail in the stories should be required.</li>
<li>Bugs can&rsquo;t have any numeric estimation. They will serve to slow down the team&rsquo;s velocity and they&rsquo;re an indication of poorly made deliveries.</li>
<li>Velocity is the actual amount of story points delivered by the end of a Sprint.
<ul>
<li>The total amount of story points of all stories in the project backlog divided by the team&rsquo;s velocity will give a roughly estimated &ldquo;End Date&rdquo;. This is important, because this is how you know when certain groups of features will be delivered over time, and this is what the Product Owner needs to know to prioritize stories, simplify stories or even cut down unnecessary features.</li>
<li>The Product Owner&rsquo;s sole responsibility is to be accountable for the Return on Investment of the implemented features. And the only way to measure cost-benefit is to know &ldquo;how much&rdquo; a feature will &ldquo;cost&rdquo; in terms of story points divided by velocity.</li>
</ul>
</li>
</ul>
<p>What the tool will expose:</p>
<ul>
<li>If a backlog has a lot of &ldquo;blocked&rdquo; stories or if it has many stories delivered without acceptance, it&rsquo;s a symptom of a Product Owner not doing the work.</li>
<li>If the velocity varies too much between sprints, it&rsquo;s a symptom of either poorly written stories (and therefore developers having a hard time understanding what to do), or developers not concentrating on their work and delivering inconsistent results.</li>
<li>Inconsistent velocities with many bugs in each sprint is the symptom of developers not doing code with proper quality (automated tests to avoid regressions for example).</li>
<li>A &ldquo;bug&rdquo; is not the same thing as the Product Owner changing their mind on a story and rejecting. The team must not implement undocumented changes. Acceptance is tied to what the story says. The Product Owner must change his mind before the story is implemented or create a new story to add the change (and therefore add more cost to the project, there is no free lunch).</li>
<li>Stories in the &ldquo;Chilly Bin&rdquo; (in Fulcrum, or &ldquo;Ice Box&rdquo; in Tracker) are &ldquo;wish-lists&rdquo; and they will never be implemented until the Product Owner moves the story to the backlog.</li>
<li>Velocity should be roughly the same between sprints. Peaks or valleys in the velocity are symptoms of a problem that needs to be addressed by the next sprint. This is how a project evolves for the better over time.</li>
</ul>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/557/big_Cm-Fulcrum_-_CM42_Central.png" alt="Velocity"  loading="lazy" /></p>
<h3>On Estimation<span class="hx:absolute hx:-mt-20" id="on-estimation"></span>
    <a href="#on-estimation" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Many people worry too much about &ldquo;proper estimation&rdquo; to the point of creating so much tension that developers start screaming about <a href="http://www.akitaonrails.com/en/en/2013/10/07/off-topic-desmontando-o-noestimates/"target="_blank" rel="noopener">&ldquo;no estimates&rdquo;</a>. This is nuts. This is why &ldquo;estimation&rdquo; is a different word than &ldquo;prediction&rdquo;, one is uncertain, a ballpark, the other is exact.</p>
<blockquote>
  <p>There is no such thing as a &ldquo;correct&rdquo; estimation.</p>

</blockquote>
<p>I would argue that in many projects if the stories are minimally described (meaning that a developer is able to implement what is written), we could estimate all of them to be size &ldquo;1&rdquo; and forget about it.</p>
<p>We run 2 or 3 sprints (usually one sprint should be one work-week - 5 days of at most 8 hours, no more, no less) and measure how many stories were delivered. And this is the velocity.</p>
<p>Now we can see how many stories we have until the end of the project and divide by this velocity. And this is how many weeks it will take to finish the project.</p>
<blockquote>
  <p>There is no need for consensus based estimation.</p>

</blockquote>
<p>Consensus on estimation is usually a waste of time. Estimation is merely the realization that it&rsquo;s difficult to write down every story to be the exact same size, so we add proportions to them (A story size 2 is half the size of a story size 4).</p>
<p>But if a project is 3 months or larger, the less the proportions matter (unless you do the anti-practice of having heavily disproportionate stories, some size 1 and many sized 5, 8 or even 10, for example). But if you stay closer to less than size 5 for every story, then the larger the project, the less the individual estimates matter. Velocity will average out the differences over time.</p>
<p>Then the task of the Project Manager, Product Owner, the team in general, is to keep an eye on the total amount of points divided by this velocity, which derives the estimated end date of the project. And everybody can negotiate, beforehand, which stories can be done, which cannot, and change priorities, remove stories or simplify stories.</p>
<blockquote>
  <p>Don&rsquo;t waste time trying to create formulas to derive estimation consensus.</p>

</blockquote>
<p>If you manage the Velocity then you can manage the entire project. This is the core of all proper Agile methodologies. Everything else is an accessory to that end.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/556/big_Heroleads_-_CM42_Central.png" alt="Velocity"  loading="lazy" /></p>
<p>Trying to artificially increase velocity is one of the many sins. You can&rsquo;t. It&rsquo;s better to invest time in adding more details to a story, or to simplify a story or to decide that some stories are just not needed to deliver a proper product in the end.</p>
<p>Monte Carlo simulations, Six Sigma techniques, Kanban concepts, none of them matter. All traditional industry techniques are meant for production lines in factories, where the time of each task is well defined and you manage the variance, the exception. It&rsquo;s a Gaussian, Normal world.</p>
<p>In software projects, or any craft and research, you manage expectations on results based on milestones and cutting excesses and uncertainties. It&rsquo;s a Pareto world, one where there is no such thing as an &ldquo;average&rdquo;, there is no medium, no variance.</p>
<p>In a <strong>Pareto</strong> world everything is interconnected, not statistically independent (a requirement for Gaussians). It&rsquo;s therefore 80/20: where 20% of the project will yield 80% of the results, where 20% of the scope can be cut down.</p>
<p>Anything that is not <strong>&ldquo;statistically independent&rdquo;</strong> (like tossing a coin) can&rsquo;t be averaged out, can&rsquo;t fit in a Normal distribution. It&rsquo;s most possibly a Pareto, Power Law, Zipf, Exponential, or even a Poisson, not a Gaussian. Software parts are all intertwined in a network, they are not independent events, they can&rsquo;t be randomly rearranged, they are not easily interchangeable.</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This article is very short and I can expand each bullet point above into its own, very long, article. Let me know in the comments section if I should, and which points raise more questions.</p>
<p>I&rsquo;ve been refining those techniques for the <a href="http://www.akitaonrails.com/agile"target="_blank" rel="noopener">past 10 years</a>, and at least 5 within the constraints of a tool such as Tracker. And I believe I was able to come up with the minimal set of principles that lead to proper management, not superstition, fantasy or delusion you see everywhere nowadays.</p>
<p>Projects suffer when the recommendations above are not followed:</p>
<ul>
<li>Product Owners don&rsquo;t perform their responsibilities: they fail to add details to stories, they neglect delivered features and don&rsquo;t test them properly, they don&rsquo;t participate.</li>
<li>Backlogs are incomplete, lacking detail, lacking estimation, lacking prioritization.</li>
<li>Developers don&rsquo;t properly start and finish their tasks with proper code quality.</li>
<li>Time is wasted in bikeshedding instead of delivery.</li>
</ul>
<p>Our fork of Fulcrum, called <a href="https://github.com/Codeminer42/cm42-central"target="_blank" rel="noopener">&ldquo;Central&rdquo;</a>, is not perfect by any means, but it works well enough and gives us enough control to keep adding features that make our routine more comfortable, and we are just getting started.</p>
<p>I hope this technique and tool is valuable to more teams and I&rsquo;d love to hear feedback from anyone.</p>
]]></content:encoded><category>agile</category><category>rails</category><category>reviews</category></item><item><title>Moving away from Slack into Rocket.chat. Good Enough.</title><link>https://akitaonrails.github.io/en/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough/</guid><pubDate>Tue, 09 Aug 2016 16:21:00 -0300</pubDate><description>&lt;p&gt;In my quest to create my own private infrastructure I&amp;rsquo;ve already moved away from Pivotal Tracker (saving this for a future post), Bitbucket, Github, Travis and into GitLab and GitLab Runner. There are more services I will move away from in the future, so expect more posts like this.&lt;/p&gt;
&lt;p&gt;In &lt;a href="http://www.akitaonrails.com/en/2016/08/03/migrando-para-o-gitlab-sim-vale-a-pena"target="_blank" rel="noopener"&gt;my previous post&lt;/a&gt; I forgot to mention minimal firewall and SSL configuration, but you can check it here.&lt;/p&gt;
&lt;p&gt;One of the most difficult pieces to move away from is Slack. Don&amp;rsquo;t get me wrong, it&amp;rsquo;s a great tool and it gets the job done. The free-tier is really competent but I really want some of the paid features such as history of all conversations with search capabilities and uploads without limit.&lt;/p&gt;</description><content:encoded><![CDATA[<p>In my quest to create my own private infrastructure I&rsquo;ve already moved away from Pivotal Tracker (saving this for a future post), Bitbucket, Github, Travis and into GitLab and GitLab Runner. There are more services I will move away from in the future, so expect more posts like this.</p>
<p>In <a href="http://www.akitaonrails.com/en/2016/08/03/migrando-para-o-gitlab-sim-vale-a-pena"target="_blank" rel="noopener">my previous post</a> I forgot to mention minimal firewall and SSL configuration, but you can check it here.</p>
<p>One of the most difficult pieces to move away from is Slack. Don&rsquo;t get me wrong, it&rsquo;s a great tool and it gets the job done. The free-tier is really competent but I really want some of the paid features such as history of all conversations with search capabilities and uploads without limit.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/555/big_Franz.png" alt="Codeminer Rocket"  loading="lazy" /></p>
<h2>The Cost Conundrum<span class="hx:absolute hx:-mt-20" id="the-cost-conundrum"></span>
    <a href="#the-cost-conundrum" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>But then you have to commit to pay USD 8/month per user (USD 6.67 if you pay ahead of time). If you&rsquo;re a small team (around a dozen people) it&rsquo;s well worth it and you should definitely pay and be done with it.</p>
<p>If you have more than 20 people then it starts to become a conundrum. On my GitLab cost calculations, some people argued that saving USD 15k yearly is not such a big deal, and they&rsquo;re not wrong.</p>
<p>If you&rsquo;re from outside of the US coastal cities, especially from countries in South America or Asia where currency can be more than 3 times weaker, then it quickly starts to become a big deal.</p>
<p>Apart from that, we are currently offloading a whole lot of our IPs and knowledge into the care of companies we don&rsquo;t really know and where we don&rsquo;t hold any real stakes.</p>
<p>If you&rsquo;re a medium to big company it really starts to make sense to want to have more control over your own property. Of course, it costs some work and maintenance, so it&rsquo;s not for everybody.</p>
<p>What I will argue is that if you have <strong>150 people</strong> or more, and you&rsquo;re still growing, it&rsquo;s not such a big burden to jump into your own infrastructure.</p>
<blockquote>
  <p>You avoid paying around <a href="https://codeminer42.slack.com/pricing"target="_blank" rel="noopener"><strong>USD 12,000 a year</strong></a> or more, and instead you will pay <strong>USD 960 a year</strong> in Digital Ocean boxes and MMS plus a few hours every month in basic maintenance. That&rsquo;s 12 times cheaper.</p>

</blockquote>
<p>Again, you shouldn&rsquo;t do it just for the cost savings as the maintenance burden can easily outweigh the savings if you or your team are not devops savvy and you don&rsquo;t have anyone to keep monitoring it. For most companies, the conversation history is not so important and even the free-tier will be more than enough.</p>
<p>Cost is less relevant depending on your context, and there are many different uses for group chats. For example, if you&rsquo;re an <a href="https://news.ycombinator.com/item?id=9754626"target="_blank" rel="noopener">open source community</a>, you would try <a href="https://gitter.im"target="_blank" rel="noopener">Gitter</a> instead.</p>
<p>There are also <a href="https://www.google.com/search?q=slack&#43;alternatives"target="_blank" rel="noopener">many other alternatives</a> that you may want to try.</p>
<p>For the sake of this article, let&rsquo;s assume you&rsquo;re a small to medium company, in need of private rooms, many external users (for example, your clients), with at least 50 people. Then this can start to make sense. Any other scenario and we have to analyze it differently.</p>
<h2>Why I chose Rocket.chat<span class="hx:absolute hx:-mt-20" id="why-i-chose-rocketchat"></span>
    <a href="#why-i-chose-rocketchat" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>With the cost reasoning out of the way, then it&rsquo;s a matter of choosing which alternative to use.</p>
<p>I tried <a href="https://www.mattermost.org/"target="_blank" rel="noopener">MatterMost</a> first and I really wanted to like it. It has a very similar feature set to Slack and most importantly, it&rsquo;s written in Go. So it&rsquo;s both lightweight, very fast, and not so difficult to contribute. It uses MySQL or PostgreSQL which I like.</p>
<p>Unfortunately, it has an important <strong>show-stopper</strong> for my needs. You can create both public channels or private groups, but anyone can delete a private group. If you want permission control for that you must use their <a href="https://about.mattermost.com/pricing/"target="_blank" rel="noopener">&ldquo;Enterprise&rdquo; offering</a> and pay <strong>USD 20/year per user</strong>.</p>
<blockquote>
  <p>It&rsquo;s still at least <strong>3 times cheaper</strong> than Slack, so it may be a good option for you.</p>

</blockquote>
<p>I really want to have full-control, not just make it cheaper. I could pay Slack already if I didn&rsquo;t care, so I would not just pay an alternative and not have full control (including the ability to tweak and improve the entire code base).</p>
<p>Then, it took me back to <a href="https://rocket.chat/"target="_blank" rel="noopener">Rocket.chat</a>. Its feature set also rivals Slack, it&rsquo;s good looking enough. But, it&rsquo;s made in Meteor. Now, I don&rsquo;t have anything against Meteor and I really think a Slack-clone is exactly the kind of use case where you could use Meteor to its full potential.</p>
<p>Technically I really think it&rsquo;s a downside to be forced to use MongoDB (Meteor requires Mongo). For small installations I really prefer to have PostgreSQL. MongoDB is competent, but for medium to big installations there are caveats, and I will show you why below.</p>
<p>I <strong>strongly dislike</strong> the <a href="https://medium.com/friendship-dot-js/i-peeked-into-my-node-modules-directory-and-you-wont-believe-what-happened-next-b89f63d21558#.8jd3z3n6u"target="_blank" rel="noopener">culture of writing software without <em>minimal care</em></a> such as having a reasonably complete test suite. You browse through the many packages that comprise Rocket.chat and several of them have no tests whatsoever. Some packages do have jasmine tests, but the majority is lacking.</p>
<p>This is <a href="https://github.com/RocketChat/Rocket.Chat/blob/a5cb22bb0017f4c39654bf1e2895ae64acb0339b/packages/rocketchat-katex/tests/jasmine/client/unit/katex.spec.coffee"target="_blank" rel="noopener">one example</a> I picked randomly, out of the few test files I found:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">describe</span> <span class="s1">&#39;rocketchat:katex Client&#39;</span><span class="p">,</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">it</span> <span class="s1">&#39;should exist&#39;</span><span class="p">,</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">expect</span><span class="p">(</span><span class="nx">RocketChat</span><span class="p">.</span><span class="nx">katex</span><span class="p">).</span><span class="nx">toBeDefined</span><span class="p">()</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This is sad, sorry to say that, but it is. Not surprisingly I&rsquo;ve seen katex issues while using the interface. And if you think this was just a poor choice, I picked <a href="https://github.com/RocketChat/Rocket.Chat/blob/a5cb22bb0017f4c39654bf1e2895ae64acb0339b/packages/rocketchat-markdown/tests/jasmine/client/unit/markdown.spec.coffee"target="_blank" rel="noopener">another test file</a>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">describe</span> <span class="s1">&#39;rocketchat:markdown Client&#39;</span><span class="p">,</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">it</span> <span class="s1">&#39;should exist&#39;</span><span class="p">,</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">expect</span><span class="p">(</span><span class="nx">RocketChat</span><span class="p">.</span><span class="nx">Markdown</span><span class="p">).</span><span class="nx">toBeDefined</span><span class="p">()</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Really?</p>
<p>So, the code itself is not so pretty, you have to keep this in mind. I&rsquo;d much rather use Mattermost (which is much better structured and with enough tests), but it&rsquo;s not entirely free, so it&rsquo;s not an option for me. For my scenario, I&rsquo;d rather have sloppy code (that minimally &ldquo;works&rdquo;) that I can tweak than code I can&rsquo;t see, for this particular venture. Many inexperienced developers may argue about the necessity of having automated tests, but not having them also impacts this:</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/552/big_Contributors_to_RocketChat_Rocket_Chat.png" alt="Rocket.chat contribution graph"  loading="lazy" /></p>
<p>If we see how the project is evolving over time, we can see that interest is diminishing. The lack of good automated tests also makes it more difficult for new people to contribute without adding regressions, and makes contribution reviews more difficult and time consuming.</p>
<p>On the other hand the feature set is good enough as it is. There is only so much you can add to a Slack-clone. The upside is that if the code doesn&rsquo;t change too much, I can tweak it more without having many things breaking all the time. It&rsquo;s the complete opposite from the impressive contribution boost over at GitLab, for example.</p>
<p>Doing a few days of testing through the administration settings, user interface, having dozens of users doing real conversation, convinced me that even though the code quality is bad, the feature set is indeed there. I think I can live with that. Your mileage may vary though.</p>
<p>I am using <a href="http://meetfranz.com"target="_blank" rel="noopener">Franz</a> to open Slack, Rocket.chat, Hangouts, Messenger and any number of other communicators in one single app, which is great. And Rocket.chat does provide <a href="https://itunes.apple.com/us/app/rocket.chat/id1028869439?mt=8"target="_blank" rel="noopener">mobile</a> <a href="https://play.google.com/store/apps/details?id=com.konecty.rocket.chat&amp;hl=en"target="_blank" rel="noopener">apps</a> based off PhoneGap. Again, it&rsquo;s good, not great.</p>
<p>Many people have heavy bots or integrations into their Slack configuration. The alternatives such as Rocket.chat do support incoming and outgoing webhooks and most of the integrations should be possible (with some tweaking and hacking such as this <a href="https://github.com/FinndropStudios/GifRocket"target="_blank" rel="noopener">Giphy example</a>), so make a list of all important integrations and do some Google research on that before continuing.</p>
<h2>Planning the Infrastructure<span class="hx:absolute hx:-mt-20" id="planning-the-infrastructure"></span>
    <a href="#planning-the-infrastructure" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>All that having been said, I&rsquo;d recommend you try Rocket.chat&rsquo;s <a href="https://demo.rocket.chat"target="_blank" rel="noopener">demo server</a> first to see if it has the minimal features you need. And also that you install a dry-run in a small Digital Ocean box with the intention of blowing it away later.</p>
<p>For a single-instance, stand-alone installation, temporary dry-run, I recommend you start by installing the <a href="https://www.digitalocean.com/community/tutorials/how-to-use-the-mongodb-one-click-application"target="_blank" rel="noopener">MongoDB One-Click Application</a> and follow <a href="https://www.digitalocean.com/community/tutorials/how-to-install-configure-and-deploy-rocket-chat-on-ubuntu-14-04"target="_blank" rel="noopener">this instruction</a> to install everything in one machine.</p>
<p>This is the worst possible production environment, which is why I am being so repetitive in saying that you should destroy this box after you do your testing.</p>
<p>As I&rsquo;ve said before, I am assuming a scenario where you have 50 or more people, with real clients, important communication going on. You don&rsquo;t want to keep a simple installation like this for production usage.</p>
<p>For a <strong>minimal production installation</strong> you will need at the very least 4 boxes with 1 GB of RAM each. Now, why 4?</p>
<p>This is an important section for most of the beginner Javascript programmers out there playing with the so called &ldquo;MEAN&rdquo; stack (MongoDB, Express, Angularjs, Node.js). I&rsquo;ve seen people deploy single-instance, stand-alone &ldquo;MEAN&rdquo; apps for production usage. And this is super bad.</p>
<blockquote>
  <p><strong>MongoDB is NOT MEANT to function with just one single instance!</strong></p>

</blockquote>
<p>More than that:</p>
<blockquote>
  <p><strong>DO NOT start an even number of MongoDB nodes, or at least install an Arbiter</strong></p>

</blockquote>
<p>So, the minimal setup for MongoDB is a <a href="https://docs.mongodb.com/manual/core/replica-set-architecture-three-members/"target="_blank" rel="noopener">Three-member Replica Set</a>, no less!</p>
<p>The article <a href="https://www.rainforestqa.com/blog/2012-11-05-mongodb-gotchas-and-how-to-avoid-them/"target="_blank" rel="noopener">&ldquo;MongoDB Gotchas &amp; How To Avoid Them&rdquo;</a> is a little bit old but you should be aware of several of the documented gotchas, including adding replica sets and avoiding waiting too long to start sharding your data.</p>
<p>The most important part of making Rocket.chat reliable is to make your MongoDB minimally reliable, and this is a hidden cost you must consider.</p>
<h4>Installing a Three-Member Replica Set<span class="hx:absolute hx:-mt-20" id="installing-a-three-member-replica-set"></span>
    <a href="#installing-a-three-member-replica-set" class="subheading-anchor" aria-label="Permalink for this section"></a></h4><p>For the sake of this article I will assume that you created 3 (three) MongoDB One-Click Applications on Digital Ocean&rsquo;s, at the very least with 1 GB RAM boxes (2 GB RAM machines are a good choice if you&rsquo;re in doubt, but never the 512 Mb) and another bare Ubuntu 14.04 box to be the web server. Create all machines in the same sub-region, all of them with private networking and backup enabled.</p>
<p>Take note of the public IPs and private IPs, for the sake of this article let&rsquo;s say you have this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Public IP     Private IP    Hostname

192.160.10.1  10.0.0.1      mongo1.yourdomain.com
192.160.10.2  10.0.0.2      mongo2.yourdomain.com
192.160.10.3  10.0.0.3      mongo3.yourdomain.com
192.160.10.4  10.0.0.4      yourdomain.com</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In the commands and code snippets below, make sure to replace the fake IPs with your own private IPs.</p>
<p>As I said in <a href="http://www.akitaonrails.com/en/2016/08/03/migrando-para-o-gitlab-sim-vale-a-pena?"target="_blank" rel="noopener">my GitLab post</a>, create a swap file, configure the locale, set it up for automatic security updates, and upgrade the Ubuntu packages.</p>
<p>The next first thing: configure the firewall in all machines (assuming <code>ufw</code> package is already installed):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo ufw reset
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow https
sudo ufw allow ssh
sudo ufw allow in on eth1 to any port 27017
sudo ufw enable</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>We are allowing MongoDB&rsquo;s 27017 port only for machines in the same private networking (through eth1, the public IP goes through eth0). We&rsquo;re also allowing port 22 for SSH and although I am not listing it here in this article you should also consider installing and configuring <a href="https://www.digitalocean.com/community/tutorials/how-to-protect-ssh-with-fail2ban-on-ubuntu-14-04"target="_blank" rel="noopener">fail2ban</a>. Digital Ocean also has a <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-firewall-with-ufw-on-ubuntu-14-04"target="_blank" rel="noopener">good post on UFW</a> for more details.</p>
<p>Next thing to do on the MongoDB machines is to <a href="https://docs.mongodb.com/manual/tutorial/transparent-huge-pages/"target="_blank" rel="noopener">Disable Transparent Huge Pages (THP)</a>. For that create a file <code>/etc/init.d/disable-transparent-hugepages</code>, with this content:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/sh
</span></span></span><span class="line"><span class="cl"><span class="c1">### BEGIN INIT INFO</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Provides:          disable-transparent-hugepages</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Required-Start:    $local_fs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Required-Stop:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># X-Start-Before:    mongod mongodb-mms-automation-agent</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Default-Start:     2 3 4 5</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Default-Stop:      0 1 6</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Short-Description: Disable Linux transparent huge pages</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Description:       Disable Linux transparent huge pages, to improve</span>
</span></span><span class="line"><span class="cl"><span class="c1">#                    database performance.</span>
</span></span><span class="line"><span class="cl"><span class="c1">### END INIT INFO</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">case</span> <span class="nv">$1</span> in
</span></span><span class="line"><span class="cl">  start<span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[</span> -d /sys/kernel/mm/transparent_hugepage <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      <span class="nv">thp_path</span><span class="o">=</span>/sys/kernel/mm/transparent_hugepage
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="o">[</span> -d /sys/kernel/mm/redhat_transparent_hugepage <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      <span class="nv">thp_path</span><span class="o">=</span>/sys/kernel/mm/redhat_transparent_hugepage
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="m">0</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s1">&#39;never&#39;</span> &gt; <span class="si">${</span><span class="nv">thp_path</span><span class="si">}</span>/enabled
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s1">&#39;never&#39;</span> &gt; <span class="si">${</span><span class="nv">thp_path</span><span class="si">}</span>/defrag
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">unset</span> thp_path
</span></span><span class="line"><span class="cl">    <span class="p">;;</span>
</span></span><span class="line"><span class="cl"><span class="k">esac</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now you can register it as a service:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo chmod 755 /etc/init.d/disable-transparent-hugepages
sudo update-rc.d disable-transparent-hugepages defaults
sudo /etc/init.d/disable-transparent-hugepages start</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Stop MongoDB in all 3 machines:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo service mongod stop</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now comes a complicated procedure that you should not do wrong. It&rsquo;s only complicated because you have to do it in all 4 machines minding their correct private IP addresses. If you already have your own DNS server, you can skip this, otherwise follow through carefully.</p>
<p>You will have to edit both <code>/etc/hostname</code> and <code>/etc/hosts</code>. Each machine will have a particular name and they should be able to find the other machines by name. Let&rsquo;s start by editing the <code>/etc/hostname</code> on each machine. Use the table in the beginning of this section, for example, in the machine with public IP <code>192.160.10.1</code> you must edit its hostname to be <code>mongo1.yourdomain.com</code>, and so on for all the machines.</p>
<p>Once you have it done, you have to replace the line <code>127.0.0.1 localhost</code> in the <code>/etc/hosts</code> in each of them for the following:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>127.0.0.1 localhost mongo1.yourdomain.com
10.0.0.1 mongo1.yourdomain.com
10.0.0.2 mongo2.yourdomain.com
10.0.0.3 mongo3.yourdomain.com
10.0.0.4 yourdomain.com</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This snippet assumes you&rsquo;re in the machine with private IP <code>10.0.0.1</code>, of course. Make sure you change <code>mongo1.yourdomain.com</code> in the first line for the hostname of the machine you&rsquo;re in. If this is too difficult for you, you definitely should not try to maintain your own infrastructure, so beware.</p>
<p>Finally, let&rsquo;s edit the <code>/etc/mongod.conf</code> with these changes:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>storage:
  dbPath: /mongodb-data
...
net:
  port: 27017
  bindIp: 0.0.0.0
...
replication:
  replSetName: rs0</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This should be exactly the same in all machines. And you must create this <code>/mongodb-data</code> directory like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo mkdir /mongodb-data
sudo chown -R mongodb:mongodb /mongodb-data</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now go ahead on machine <code>mongo1.yourdomain.com</code>, start up with <code>sudo service mongod start</code> and fire up the <code>mongo</code> command, and you should see something like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># mongo
MongoDB shell version: 3.2.8
connecting to: test
&gt;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Let&rsquo;s now configure the replica set by issuing this command in the MongoDB command-line interface:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>rs.initiate()
rs.add(&#39;mongo2.yourdomain.com&#39;)
rs.add(&#39;mongo3.yourdomain.com&#39;)</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And you can now log in to the secondary machines and <code>sudo service mongod start</code> in all of them. They should all start joining the replica set and syncing.</p>
<p>You can check the status of the replication like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&gt; rs.conf()

{
  &#34;set&#34; : &#34;rs0&#34;,
  &#34;date&#34; : ISODate(&#34;2016-08-09T17:33:10.466Z&#34;),
  &#34;myState&#34; : 1,
  &#34;term&#34; : NumberLong(-1),
  &#34;heartbeatIntervalMillis&#34; : NumberLong(2000),
  &#34;members&#34; : [
    {
      &#34;_id&#34; : 1,
      &#34;name&#34; : &#34;mongo1.yourdomain.com:27017&#34;,
      &#34;health&#34; : 1,
      &#34;state&#34; : 1,
      &#34;stateStr&#34; : &#34;PRIMARY&#34;,
      &#34;uptime&#34; : 11727,
      &#34;optime&#34; : Timestamp(1470763990, 3),
      &#34;optimeDate&#34; : ISODate(&#34;2016-08-09T17:33:10Z&#34;),
      &#34;electionTime&#34; : Timestamp(1470752553, 1),
      &#34;electionDate&#34; : ISODate(&#34;2016-08-09T14:22:33Z&#34;),
      &#34;configVersion&#34; : 6,
      &#34;self&#34; : true
    },
    {
      &#34;_id&#34; : 2,
      &#34;name&#34; : &#34;mongo2.yourdomain.com:27017&#34;,
      &#34;health&#34; : 1,
      &#34;state&#34; : 2,
      &#34;stateStr&#34; : &#34;SECONDARY&#34;,
      &#34;uptime&#34; : 11693,
      &#34;optime&#34; : Timestamp(1470763988, 4),
      &#34;optimeDate&#34; : ISODate(&#34;2016-08-09T17:33:08Z&#34;),
      &#34;lastHeartbeat&#34; : ISODate(&#34;2016-08-09T17:33:10.026Z&#34;),
      &#34;lastHeartbeatRecv&#34; : ISODate(&#34;2016-08-09T17:33:08.887Z&#34;),
      &#34;pingMs&#34; : NumberLong(0),
      &#34;syncingTo&#34; : &#34;mongo3.yourdomain.com:27017&#34;,
      &#34;configVersion&#34; : 6
    },
    {
      &#34;_id&#34; : 3,
      &#34;name&#34; : &#34;mongo3.yourdomain.com:27017&#34;,
      &#34;health&#34; : 1,
      &#34;state&#34; : 2,
      &#34;stateStr&#34; : &#34;SECONDARY&#34;,
      &#34;uptime&#34; : 11693,
      &#34;optime&#34; : Timestamp(1470763988, 4),
      &#34;optimeDate&#34; : ISODate(&#34;2016-08-09T17:33:08Z&#34;),
      &#34;lastHeartbeat&#34; : ISODate(&#34;2016-08-09T17:33:09.994Z&#34;),
      &#34;lastHeartbeatRecv&#34; : ISODate(&#34;2016-08-09T17:33:08.655Z&#34;),
      &#34;pingMs&#34; : NumberLong(0),
      &#34;syncingTo&#34; : &#34;mongo1.yourdomain.com:27017&#34;,
      &#34;configVersion&#34; : 6
    }
  ],
  &#34;ok&#34; : 1
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Each box should be syncing data to another box, one of them will be marked &ldquo;PRIMARY&rdquo; and the others will be &ldquo;SECONDARY&rdquo;. If one of them &ldquo;dies&rdquo; for some reason, one of the others will be elected as PRIMARY until you add a replacement box. Read more details on this procedure in <a href="https://www.digitalocean.com/community/tutorials/how-to-implement-replication-sets-in-mongodb-on-an-ubuntu-vps"target="_blank" rel="noopener">Digital Ocean&rsquo;s post about implementing replication sets</a>.</p>
<h4>Adding your MongoDB environment to MMS<span class="hx:absolute hx:-mt-20" id="adding-your-mongodb-environment-to-mms"></span>
    <a href="#adding-your-mongodb-environment-to-mms" class="subheading-anchor" aria-label="Permalink for this section"></a></h4><p>Another thing beginners will not realize is that it&rsquo;s a good idea to monitor your services. At the very least you should install MongoDB Cloud Manager agents into your servers. Just set up an account at <a href="https://www.mongodb.com/cloud"target="_blank" rel="noopener">MongoDB Atlas</a>.</p>
<p>It&rsquo;s beyond the point of this article to explain Atlas, but you should have no trouble installing the agents with the Group ID and API Key provided. Then install the monitoring agent and have the Manager figure out your existing deployment and replica set.</p>
<p>The basic subscription for monitoring will cost you an extra USD 39 a month, but believe me when I say it&rsquo;s worth it to add all your MongoDB deployments under MMS.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/553/big_Deployment___Cloud_Manager__MongoDB_Cloud_Manager.png" alt="MMS"  loading="lazy" /></p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/554/big_Host___Cloud_Manager__MongoDB_Cloud_Manager.png" alt="Replica Set Graphs"  loading="lazy" /></p>
<h4>Installing a multi-instance Node.js service<span class="hx:absolute hx:-mt-20" id="installing-a-multi-instance-nodejs-service"></span>
    <a href="#installing-a-multi-instance-nodejs-service" class="subheading-anchor" aria-label="Permalink for this section"></a></h4><p>Another mistake beginners in the MEAN stack make is to start up a single Node.js instance. Yes, Node.js&rsquo;s asynchronous I/O nature makes it &ldquo;concurrent enough&rdquo; in 1 single thread. But you do want to maximize the rented machine so you should spin up at least one Node.js instance for each CPU core. A 1 GB RAM machine has just 1 core, but you can spin up at least 2 instances.</p>
<p>First of all, we must set up the <code>yourdomain.com</code> (<code>192.160.10.4</code> or <code>10.0.0.4</code> in the example) to support Node.js, let&rsquo;s do it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo apt-get install -y npm curl graphicsmagick nginx git bc
sudo npm install -g n
sudo npm install -g forever
sudo npm install -g forever-service
sudo n 0.10.40</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you haven&rsquo;t already, <a href="https://www.digitalocean.com/community/tutorials/how-to-create-a-sudo-user-on-ubuntu-quickstart"target="_blank" rel="noopener">create a more restricted sudo user</a>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>adduser rocket # add a password when asked
usermod -aG sudo rocket
su - rocket # and always ssh in with `ssh rocket@yourdomain.com`</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Within rocket&rsquo;s home directory let&rsquo;s install the Rocket.chat codebase:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>cd /home/rocket
curl -L https://rocket.chat/releases/latest/download -o rocket.chat.tgz
tar zxvf rocket.chat.tgz
mv bundle Rocket.Chat
cd Rocket.Chat/programs/server
npm install</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And now let&rsquo;s add a way for the machine to start the Rocket.chat whenever it reboots:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>cd ~/Rocket.Chat
sudo forever-service install -s main.js -e &#34;ROOT_URL=https://rocketchat42.com/ MONGO_URL=mongodb://mongo1.yourdomain.com:27017/rocketchat MONGO_OPLOG_URL=mongodb://mongo1.yourdomain.com:27017/local PORT=3001&#34; rocketchat1
sudo forever-service install -s main.js -e &#34;ROOT_URL=https://rocketchat42.com/ MONGO_URL=mongodb://mongo1.yourdomain.com:27017/rocketchat MONGO_OPLOG_URL=mongodb://mongo1.yourdomain.com:27017/local PORT=3002&#34; rocketchat2

sudo start rocketchat1
sudo start rocketchat2</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Let&rsquo;s also create SSL certificates (and for that you must have a properly registered domain, of course). Again, <a href="https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04"target="_blank" rel="noopener">Digital Ocean&rsquo;s documentation on Let&rsquo;s Encrypt</a> is very good. In summary, all you have to do is:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now edit the <code>/etc/nginx/sites-available/default</code> and add this inside the server block:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>server {
  ...
  location ~ /.well-known {
          allow all;
  }
  ...
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now you can:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo service nginx reload

sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

cd /opt/letsencrypt
./letsencrypt-auto certonly -a webroot --webroot-path=/usr/share/nginx/html -d yourdomain.com -d www.yourdomain.com</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Again, replace <code>yourdomain.com</code> with your own registered domain, of course. The <code>letsencrypt</code> command will prompt you to enter your e-mail address and accept terms of service.</p>
<p><a href="https://letsencrypt.org/"target="_blank" rel="noopener">Let&rsquo;s Encrypt</a> is a fantastic free SSL provider. Their certificates are short lived and are meant to expire in 90 days, so you must set up auto-renewal. Start <code>sudo crontab -e</code> and add the following lines:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>30 2 * * 1 /opt/letsencrypt/letsencrypt-auto renew &gt;&gt; /var/log/le-renew.log
35 2 * * 1 /etc/init.d/nginx reload</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This should take care of SSL. Now you can edit your <code>/etc/nginx/sites-available/default</code> and replace it with this template:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># Upstreams
upstream backend {
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
}

server {
  listen 443 ssl;

  server_name yourdomain.com;

  ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_prefer_server_ciphers on;
  ssl_dhparam /etc/ssl/certs/dhparam.pem;
  ssl_ciphers &#39;ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH&#43;AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA&#39;;
  ssl_session_timeout 1d;
  ssl_session_cache shared:SSL:50m;
  ssl_stapling on;
  ssl_stapling_verify on;
  add_header Strict-Transport-Security max-age=15768000;

  location / {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection &#34;upgrade&#34;;
    proxy_set_header Host $http_host;

    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forward-Proto http;
    proxy_set_header X-Nginx-Proxy true;

    proxy_redirect off;
  }

}

server {
  listen 80;
  server_name yourdomain.com www.yourdomain.com;
  return 301 https://$host$request_uri;
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Just for the sake of completeness, again: replace <code>yourdomain.com</code> with your domain. Usually the <code>/etc/nginx/sites-enabled/default</code> is a symlink to <code>/etc/nginx/sites-available/default</code>, just check that as well. This configuration will load balance between the 2 node.js instances we configured and started before. And you can add as many as you want following the same <code>forever-install</code> procedure and adding the new instances to the <code>upstream</code> section in the nginx configuration above.</p>
<p>If everything is correct, you can restart nginx again: <code>sudo service nginx restart</code>.</p>
<p>And now you should have <code>https://yourdomain.com</code> already up and running. The first user that signs up will be the site administrator and from there you can customize Rocket.chat internally.</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>As you can see, this is not a simple procedure to follow and I am assuming you have experience managing your own infrastructure. If you don&rsquo;t, you definitely should NOT do this by yourself.</p>
<p>If you do it correctly, you should have a functional Slack-clone with minimal reliability (thanks in large part to Digital Ocean) and minimal cost (USD 80/month in the configuration I described that should be enough for more than 100 users). Rocket.chat also offers more <a href="https://rocket.chat/docs/installation/"target="_blank" rel="noopener">documentation</a> for other environments including Docker configurations you might want to try. But the procedure above is sufficient for my needs.</p>
<p>I said it already but it&rsquo;s better to repeat it: don&rsquo;t fall for the trap of installing everything in a single box, with single instance MongoDB, and without proper monitoring. It&rsquo;s asking for trouble.</p>
<p>For now I am in the middle of the rollout. All developers in my company are already in the new deployment and soon half of the clients should also migrate (some will not be able to leave Slack just yet), but with Franz and the ability of full history and searchability this shouldn&rsquo;t be a concern even if a developer stays offline for some period of time.</p>
<p>I also don&rsquo;t advocate that everybody should be online and responding in real-time. It&rsquo;s infeasible, unproductive, creates unnecessary tension. People should participate when they have free time, and they should be able to concentrate without worrying that they are missing something. That&rsquo;s why they should opt-out of being notified in the more busy channels and just enable notification on the private groups that matter.</p>
<p>Coincidentally Jason Fried just posted about concerns over group chats at <a href="https://m.signalvnoise.com/is-group-chat-making-you-sweat-744659addf7d#.3qxe09una"target="_blank" rel="noopener">Signal v. Noise</a>, but the gist is that group chat should be purposeful and not yet another tool to create tension. People should definitely get offline when they need to fully concentrate on their work and have the opportunity to catch up with interesting conversations later. And really important communication should go through e-mail or other traditional ways, a simple <code>@all</code> doesn&rsquo;t cut it for company-wide announcements for example.</p>
<p>I hope this exercise gives you more perspective on what you can have and also raises awareness on the need for companies to regain more control over their own data, particularly knowledge. Erase your communication channels and you&rsquo;re losing years&rsquo; worth of knowledge that can be invaluable.</p>
]]></content:encoded><category>reviews</category><category>devops</category><category>databases</category></item><item><title>Off Topic: "I Am Legend" - TERRIBLE!</title><link>https://akitaonrails.github.io/en/2008/01/19/off-topic-i-am-legend-horrvel/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2008/01/19/off-topic-i-am-legend-horrvel/</guid><pubDate>Sat, 19 Jan 2008 02:46:00 -0200</pubDate><description>&lt;p&gt;&lt;strong&gt;DISCLAIMER:&lt;/strong&gt; This article has &lt;strong&gt;SPOILERS&lt;/strong&gt;; if that bothers you, don&amp;rsquo;t read it. I think it&amp;rsquo;s silly, but consider yourself warned.&lt;/p&gt;
&lt;p&gt;&lt;img src="http://s3.amazonaws.com/akitaonrails/assets/2008/1/19/photo_18.jpg" alt="" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s been a while since my last off-topic, but today I&amp;rsquo;m so indignant I had to write.&lt;/p&gt;
&lt;p&gt;After reading good reviews (I need to find better sources), I took a chance and watched &lt;a href="http://en.wikipedia.org/wiki/I_Am_Legend_%28film%29"target="_blank" rel="noopener"&gt;I Am Legend&lt;/a&gt;, which had just opened here. I really liked Will Smith&amp;rsquo;s previous film, &lt;a href="http://en.wikipedia.org/wiki/Pursuit_of_happyness"target="_blank" rel="noopener"&gt;The Pursuit of Happyness&lt;/a&gt;, and word was he goes dramatic again. That got me to the theater.&lt;/p&gt;</description><content:encoded><![CDATA[<p><strong>DISCLAIMER:</strong> This article has <strong>SPOILERS</strong>; if that bothers you, don&rsquo;t read it. I think it&rsquo;s silly, but consider yourself warned.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2008/1/19/photo_18.jpg" alt=""  loading="lazy" /></p>
<p>It&rsquo;s been a while since my last off-topic, but today I&rsquo;m so indignant I had to write.</p>
<p>After reading good reviews (I need to find better sources), I took a chance and watched <a href="http://en.wikipedia.org/wiki/I_Am_Legend_%28film%29"target="_blank" rel="noopener">I Am Legend</a>, which had just opened here. I really liked Will Smith&rsquo;s previous film, <a href="http://en.wikipedia.org/wiki/Pursuit_of_happyness"target="_blank" rel="noopener">The Pursuit of Happyness</a>, and word was he goes dramatic again. That got me to the theater.</p>
<p>The beginning of the film is very good. Great cinematography, fast and dynamic editing. And even though Will Smith is practically the only actor on screen for most of it, he does a solid job. The technical crew recreated a deserted New York beautifully, and the tense moments work. In short, every trick in the book got used.</p>
<p>But the first time one of the monsters shows up, the disappointment kicks in: it brought back the horrendous special effects of films like The Mummy Returns. In a post-Matrix, post-Lord of the Rings era, there&rsquo;s no excuse left for monsters this bad. Still, that part is forgivable.</p>
<p>The other problem is that the story is only superficially based on <a href="http://en.wikipedia.org/wiki/I_Am_Legend"target="_blank" rel="noopener">Richard Matheson&rsquo;s</a> novel. A shallow adaptation wasn&rsquo;t hard to predict, but the film had been building good tension and was fun. Which is why the third act was doubly disappointing. The entire sequence involving Alice Braga is disposable.</p>
<p>It starts with a joke I still don&rsquo;t get. Alice&rsquo;s character, Anna, says she came from São Paulo, Brazil. Mid-conversation, Robert, Will&rsquo;s character, asks if she knows Bob Marley. And she makes a face like: <em>&ldquo;never heard of him&rdquo;</em>. She says it out loud!</p>
<p>Ok, the story is set in 2012, not that far away. Even I, practically music illiterate, know Bob Marley and the song they played in the film. What are they trying to say? <em>&ldquo;In Brazil there&rsquo;s only jungle, so nobody there has ever heard of Bob Marley?&rdquo;</em> Not important, but I found it strange. I&rsquo;ll give the benefit of the doubt, because maybe I didn&rsquo;t get the joke. Who knows.</p>
<p>Now, the <em>&ldquo;God sent me&rdquo;</em> part &hellip; no, no, so fucking sorry! I couldn&rsquo;t swallow it. What does that have to do with the story?? Worse, the story took it seriously!! This Anna character <em>&ldquo;started listening better to signs from heaven&rdquo;</em>. She showed up to save Robert the instant he was about to die, heard his radio message because she got a divine signal, and she <em>knows</em>, simply <em>knows</em>, that a survivor colony exists, address and all, more efficient than Google Maps, because <em>God told her</em> &hellip; oh well &hellip;</p>
<p>And in the end the guy actually has a <em>divine vision</em> and decides to die! And the colony really exists! Wow. I hate it when a film starts thinking too highly of itself.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2008/1/19/iald-06397.jpg" alt=""  loading="lazy" /></p>
<p>It left a worse taste than another terrible one: Signs, by M. Night Shyamalan. Same deal: the film starts with a serious mood, builds the tension, goes along great until it completely falls apart, also in the third act. The dying woman delivers a message from heaven: <em>&ldquo;oh, tell him to swing the bat &hellip;&rdquo;</em> And they <strong>show</strong> the little green aliens!! And the guy swings the bat and saves the world!</p>
<p>God damn! Ok, I Am Legend has nothing to do with the novel of the same name. The book ends in a completely different way, and the theme is something else entirely, far more interesting. If you go see the film, walk out when Anna shows up. Up to that point it&rsquo;s a decent film; after that, brace for a huge disappointment.</p>
<p>I&rsquo;m shocked by the sheer lack of quality in the storytelling. Akiva Goldsman worked on the screenplay, which explains a lot: the same person who managed to ruin the Da Vinci Code adaptation.</p>
<p>I&rsquo;ve seen bad films by the dozen, but they know they&rsquo;re bad and don&rsquo;t take themselves too seriously. Take Resident Evil: mediocre, aware of it, and at least fun for a lazy Sunday afternoon. This one promises a lot up front and ends like that guy who turns around and goes <em>&ldquo;gotcha!&rdquo;</em></p>
<p>The marketing team behind this film is very good! They managed to squeeze plenty of good reviews out of everywhere. Even on <a href="http://www.rottentomatoes.com/m/i_am_legend/"target="_blank" rel="noopener">Rotten Tomatoes</a> it got 68% approval! Far more than it deserves. Anything above single digits, on a zero-to-one-hundred scale, is already too much.</p>
<p>The next film people are talking up, with some hype already building, is <a href="http://web.archive.org/web/20080224041006/http://www.cloverfieldmovie.com/"target="_blank" rel="noopener">Cloverfield</a>. Whether it&rsquo;s as good as they say remains to be seen.</p>
]]></content:encoded><category>film-and-pop-culture</category><category>reviews</category><category>off-topic</category></item></channel></rss>